Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork3
Yet another simple React SSR solution inspired by vue-server-render
License
un-ts/react-server-renderer
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Yet another simple React SSR solution inspired by vue-server-render with:
- Server bundle with hot reload on development and source map support
- prefetch/preload client injection with ClientManifest, generated by webpack-plugin inside
- server css support withreact-style-loader
- Async component support withreact-async-component andreact-async-bootstrapper
- custom dynamic head management for better SEO
This module is heavily inspired byvue-server-render, it is recommended to read aboutbundle-renderer.
It usesreact-router on server, so you should read aboutServer Rendering.
And also, data injection should be implement withasyncBootstrap.
importwebpackfrom'webpack'importmergefrom'webpack-merge'importnodeExternalsfrom'webpack-node-externals'import{ReactSSRServerPlugin}from'react-server-renderer/server-plugin'import{resolve}from'./config'importbasefrom'./base'exportdefaultmerge.smart(base,{// Point entry to your app's server entry fileentry:resolve('src/entry-server.js'),// This allows webpack to handle dynamic imports in a Node-appropriate// fashion, and also tells `react-style-loader` to emit server-oriented code when// compiling React components.target:'node',output:{path:resolve('dist'),filename:`[name].[chunkhash].js`,// This tells the server bundle to use Node-style exportslibraryTarget:'commonjs2',},// https://webpack.js.org/configuration/externals/#function// https://github.com/liady/webpack-node-externals// Externalize app dependencies. This makes the server build much faster// and generates a smaller bundle file.externals:nodeExternals({// do not externalize dependencies that need to be processed by webpack.// you can add more file types here// you should also whitelist deps that modifies `global` (e.g. polyfills)whitelist:/\.s?css$/,}),plugins:[newwebpack.DefinePlugin({'process.env.REACT_ENV':'"server"',__SERVER__:true,}),// This is the plugin that turns the entire output of the server build// into a single JSON file. The default file name will be// `react-ssr-server-bundle.json`newReactSSRServerPlugin(),],})
importwebpackfrom'webpack'importmergefrom'webpack-merge'// do not need 'html-webpack-plugin' any more because we will render html from server// import HtmlWebpackPlugin from 'html-webpack-plugin'import{ReactSSRClientPlugin}from'react-server-renderer/client-plugin'import{__DEV__,publicPath,resolve}from'./config'importbasefrom'./base'exportdefaultmerge.smart(base,{entry:{app:[resolve('src/entry-client.js')],},output:{ publicPath,path:resolve('dist/static'),filename:`[name].[${__DEV__ ?'hash' :'chunkhash'}].js`,},plugins:[newwebpack.DefinePlugin({'process.env.REACT_ENV':'"client"',__SERVER__:false,}),// This plugins generates `react-ssr-client-manifest.json` in the// output directory.newReactSSRClientPlugin({// path relative to your output path, default to be `react-ssr-client-manifest.json`filename:'../react-ssr-client-manifest.json',}),],})
You can then use the generated client manifest, together with a page template:
importfsfrom'node:fs'import{createBundleRenderer}from'react-server-renderer'importserverBundlefrom'/path/to/react-ssr-server-bundle.json'with{type:'json'}importclientManifestfrom'/path/to/react-ssr-client-manifest.json'with{type:'json'}importtemplate=fs.readFileSync('/path/to/template.html','utf-8')constrenderer=createBundleRenderer(serverBundle,{ template, clientManifest,})
With this setup, your server-rendered HTML for a build with code-splitting will look something like this (everything auto-injected):
<html><head><!-- chunks used for this render will be preloaded --><linkrel="preload"href="/manifest.js"as="script"/><linkrel="preload"href="/main.js"as="script"/><linkrel="preload"href="/0.js"as="script"/><!-- unused async chunks will be prefetched (lower priority) --><linkrel="prefetch"href="/1.js"as="script"/></head><body><!-- app content --><divdata-server-rendered="true"><div>async</div></div><!-- manifest chunk should be first --><scriptsrc="/manifest.js"></script><!-- async chunks injected before main chunk --><scriptsrc="/0.js"></script><scriptsrc="/main.js"></script></body></html>`
All you need to do is for hot reload on development:
- compile server webpack config via node.js API like:
const const serverCompiler = webpack(serverConfig) - watch serverCompiler and replace server bundle on change
Example:https://github.com/JounQin/react-hackernews/blob/master/server/dev.js
Your server bundle entry should export a function with acontext param which return a promise, and it should resolve a react component instance.
Example:https://github.com/JounQin/react-hackernews/blob/master/src/entry-server.js
When you need to redirect on server or an error occurs, you should reject inside promise so that we can handle it.
Since you generate server bundle renderer as above, you can easily callrenderer.renderToString(context) orrenderer.renderToStream(context), wherecontext should be a singloton of every request.
renderToString is very simple, justtry/catch error to handle it.
renderToStream is a tiny complicated to handle, you can rediect or reject request by listeningerror event and handle error param. If you want to render application but change response status, you can listenafterRender event and handle with your owncontext, for example maybe you want to render 404 Not Found page via React Component but respond with 404 status.
If you setcontext.state on server, it will auto inject a script containswindow.__INITIAL_STATE__ in output, so that you can resue server state on client.
Without SSR, we can easily usestyle-loader, however we need to collect rendered components with their styles together on runtime, so we choose to usereact-style-loader which forkedvue-style-loader indeed.
Let's create a simple HOC for server style, title management and http injection.
importaxiosfrom'axios'importhoistStaticsfrom'hoist-non-react-statics'importPropTypesfrom'prop-types'importReactfrom'react'import{withRouter}from'react-router'// custom dynamic title for better SEO both on server and clientconstsetTitle=(title,self)=>{title=typeoftitle==='function' ?title.call(self,self) :titleif(!title){return}if(__SERVER__){self.props.staticContext.title=`React Server Renderer |${title}`}else{// `title` here on client can be promise, but you should not and do not need to do that on server,// because on server async data will be fetched in asyncBootstrap first and set into store,// then title function will be called again when you call `renderToString` or `renderToStream`.// But on client, when you change route, maybe you need to fetch async data first// Example: https://github.com/JounQin/react-hackernews/blob/master/src/views/UserView/index.js#L18// And also, you need put `@withSsr` under `@connect` with `react-redux` for get store injected in your title functionPromise.resolve(title).then(title=>{if(title){document.title=`React Server Renderer |${title}`}})}}exportconstwithSsr=(styles,router=true,title)=>{if(typeofrouter!=='boolean'){title=routerrouter=true}returnComponent=>{classSsrComponentextendsReact.PureComponent{staticdisplayName=`Ssr${Component.displayName||Component.name||'Component'}`staticpropTypes={staticContext:PropTypes.object,}componentWillMount(){// `styles.__inject__` will only be exist on server, and inject into `staticContext`if(styles.__inject__){styles.__inject__(this.props.staticContext)}setTitle(title,this)}render(){return(<Component{...this.props}// use different axios instance on server to handle different user client headershttp={__SERVER__ ?this.props.staticContext.axios :axios}/>)}}returnhoistStatics(router ?withRouter(SsrComponent) :SsrComponent,Component,)}}
Then use it:
importPropTypesfrom'prop-types'importReactfrom'react'import{connect}from'react-redux'import{setCounter,increase,decrease}from'store'import{withSsr}from'utils'importstylesfrom'./styles'@connect(({ counter})=>({ counter}),dispatch=>({setCounter:counter=>dispatch(setCounter(counter)),increase:()=>dispatch(increase),decrease:()=>dispatch(decrease),}),)@withSsr(styles,false,({ props})=>props.counter)exportdefaultclassHomeextendsReact.PureComponent{staticpropTypes={counter:PropTypes.number.isRequired,setCounter:PropTypes.func.isRequired,increase:PropTypes.func.isRequired,decrease:PropTypes.func.isRequired,}asyncBootstrap(){if(this.props.counter){returntrue}returnnewPromise(resolve=>setTimeout(()=>{this.props.setCounter(~~(Math.random()*100))resolve(true)},500),)}render(){return(<divclassName="container"><h2className={styles.heading}>Counter</h2><buttonclassName="btn btn-primary"onClick={this.props.decrease}> -</button>{this.props.counter}<buttonclassName="btn btn-primary"onClick={this.props.increase}> +</button></div>)}}
And inside the template passedtitle to bundle renderer:
<html><head><title>{{ title }}</title></head><body> ...</body></html>
Thenreact-server-renderer will automatically collect user styles and title on server and render them into output!
Notes:
- Use double-mustache (HTML-escaped interpolation) to avoid XSS attacks.
- You should provide a default title when creating the context object in case no component has set a title during render.
Using the same strategy, you can easily expand it into a generic head management utility.
So actually it's not so simple right? Yes and no, if you choose to start using SSR, it is certain that you need pay for it, and after digging exist react SSR solutions likereact-universally or any other, I find out Vue's solution is really great and simple.
Feel free tocreate an issue.
About
Yet another simple React SSR solution inspired by vue-server-render
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors4
Uh oh!
There was an error while loading.Please reload this page.