Skip to content

Commit b468355

Browse files
committed
improve document
1 parent 78ab4db commit b468355

File tree

2 files changed

+309
-5
lines changed

2 files changed

+309
-5
lines changed

README.md

Lines changed: 307 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,308 @@
1-
# react-server-render
1+
# react-server-renderer
22

3-
[![Greenkeeper badge](https://badges.greenkeeper.io/JounQin/react-server-render.svg)](https://greenkeeper.io/)
4-
simple React SSR solution inspired by vue-server-render
3+
[![Greenkeeper badge](https://badges.greenkeeper.io/JounQin/react-server-renderer.svg)](https://greenkeeper.io/)
4+
5+
Yet another simple React SSR solution inspired by vue-server-render with:
6+
7+
1. Server bundle with hot reload on development and source map support
8+
2. prefetch/preload client injection with ClientManifest, generated by [ssr-webpack-plugin](https://github.com/JounQin/ssr-webpack-plugin)
9+
3. server css support with [react-style-loader](https://github.com/JounQin/react-style-loader)
10+
4. Async component support with [react-async-component](https://github.com/ctrlplusb/react-async-component) and [react-async-bootstrapper](https://github.com/ctrlplusb/react-async-bootstrapper)
11+
12+
## Real World Demo
13+
14+
[react-study](https://github.com/JounQin/react-study)
15+
16+
## Usage
17+
18+
This module is heavily inspired by [vue-server-render](https://ssr.vuejs.org), it is recommended to read about [bundle-renderer](https://ssr.vuejs.org/en/bundle-renderer.html).
19+
20+
If you're using [react-router](https://github.com/ReactTraining/react-router), you should read about [Server Rendering](https://reacttraining.com/react-router/web/guides/server-rendering).
21+
22+
And also, data injection should be implement with [asyncBootstrap](https://github.com/ctrlplusb/react-async-bootstrapper).
23+
24+
### Build Configuration
25+
26+
#### Server Config
27+
28+
```js
29+
import webpack from 'webpack'
30+
import merge from 'webpack-merge'
31+
import nodeExternals from 'webpack-node-externals'
32+
import { SSRServerPlugin } from 'ssr-webpack-plugin'
33+
34+
import { resolve } from './config'
35+
36+
import base from './base'
37+
38+
export default merge(base, {
39+
// Point entry to your app's server entry file
40+
entry: resolve('src/entry-server.js'),
41+
42+
// This allows webpack to handle dynamic imports in a Node-appropriate
43+
// fashion, and also tells `react-style-loader` to emit server-oriented code when
44+
// compiling React components.
45+
target: 'node',
46+
47+
output: {
48+
path: resolve('dist'),
49+
filename: `[name].[chunkhash].js`,
50+
// This tells the server bundle to use Node-style exports
51+
libraryTarget: 'commonjs2',
52+
},
53+
54+
// https://webpack.js.org/configuration/externals/#function
55+
// https://github.com/liady/webpack-node-externals
56+
// Externalize app dependencies. This makes the server build much faster
57+
// and generates a smaller bundle file.
58+
externals: nodeExternals({
59+
// do not externalize dependencies that need to be processed by webpack.
60+
// you can add more file types here e.g. raw *.vue files
61+
// you should also whitelist deps that modifies `global` (e.g. polyfills)
62+
whitelist: /\.s?css$/,
63+
}),
64+
65+
plugins: [
66+
new webpack.DefinePlugin({
67+
__SERVER__: true,
68+
}),
69+
// This is the plugin that turns the entire output of the server build
70+
// into a single JSON file. The default file name will be
71+
// `ssr-server-bundle.json`
72+
new SSRServerPlugin(),
73+
],
74+
})
75+
```
76+
77+
#### Client Config
78+
79+
```js
80+
import webpack from 'webpack'
81+
import merge from 'webpack-merge'
82+
import HtmlWebpackPlugin from 'html-webpack-plugin'
83+
import { SSRClientPlugin } from 'ssr-webpack-plugin'
84+
85+
import { __DEV__, publicPath, resolve } from './config'
86+
87+
import base from './base'
88+
89+
export default merge.smart(base, {
90+
entry: {
91+
app: [resolve('src/entry-client.js')],
92+
vendors: [
93+
'react',
94+
'react-dom',
95+
'react-redux',
96+
'react-router',
97+
'react-router-redux',
98+
'react-router-config',
99+
'react-router-dom',
100+
],
101+
},
102+
output: {
103+
publicPath,
104+
path: resolve('dist/static'),
105+
filename: `[name].[${__DEV__ ? 'hash' : 'chunkhash'}].js`,
106+
},
107+
plugins: [
108+
new webpack.DefinePlugin({
109+
__SERVER__: false,
110+
}),
111+
new HtmlWebpackPlugin({
112+
template: resolve('src/index.pug'),
113+
}),
114+
new webpack.optimize.CommonsChunkPlugin({
115+
names: ['vendors', 'manifest'],
116+
}),
117+
// This plugins generates `ssr-client-manifest.json` in the
118+
// output directory.
119+
new SSRClientPlugin({
120+
// path relative to your output path, default to be `ssr-client-manifest.json`
121+
filename: '../ssr-client-manifest.json',
122+
}),
123+
],
124+
})
125+
```
126+
127+
You can then use the generated client manifest, together with a page template:
128+
129+
```js
130+
const { createBundleRenderer } = require('react-server-rendererer')
131+
132+
const template = require('fs').readFileSync('/path/to/template.html', 'utf-8')
133+
const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
134+
const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
135+
136+
const renderer = createBundleRenderer(serverBundle, {
137+
template,
138+
clientManifest,
139+
})
140+
```
141+
142+
With this setup, your server-rendered HTML for a build with code-splitting will look something like this (everything auto-injected):
143+
144+
```html
145+
<html>
146+
<head>
147+
<!-- chunks used for this render will be preloaded -->
148+
<link rel="preload" href="/manifest.js" as="script">
149+
<link rel="preload" href="/main.js" as="script">
150+
<link rel="preload" href="/0.js" as="script">
151+
<!-- unused async chunks will be prefetched (lower priority) -->
152+
<link rel="prefetch" href="/1.js" as="script">
153+
</head>
154+
<body>
155+
<!-- app content -->
156+
<div data-server-rendered="true"><div>async</div></div>
157+
<!-- manifest chunk should be first -->
158+
<script src="/manifest.js"></script>
159+
<!-- async chunks injected before main chunk -->
160+
<script src="/0.js"></script>
161+
<script src="/main.js"></script>
162+
</body>
163+
</html>`
164+
```
165+
166+
### Server bundle
167+
168+
All you need to do is for hot reload on development:
169+
170+
1. compile server webpack config via node.js API like: `const const serverCompiler = webpack(serverConfig)`
171+
2. watch serverCompiler and replace server bundle on change
172+
173+
Example: https://github.com/JounQin/react-study/blob/master/server/dev.js
174+
175+
Your server bundle entry should export a function with a `userContext` param which return a promise, and it should resolve a react component instance.
176+
177+
Example: https://github.com/JounQin/react-study/blob/master/src/entry-server.js
178+
179+
When you need to redirect on server or an error occurs, you should reject inside promise so that we can handle it.
180+
181+
### renderToString and renderToStream(use `ReactDomServer.renderToNodeStream` inside)
182+
183+
Since you generate server bundle renderer as above, you can easily call `renderer.renderToString(context)` or `renderer.renderToStream(context)`, where `context` should be a singloton of every request.
184+
185+
`renderToString` is very simple, just `try/catch` error to handle it.
186+
187+
`renderToStream` is a tiny complicated to handle, you can rediect or reject request by listening `error` event and handle error param. If you want to render application but change response status, you can listen `afterRender` event and handle with your own `userContext`, for example maybe you want to render 404 Not Found page via React Component but respond with 404 status.
188+
189+
### State management
190+
191+
If you set `context.state` on server, it will auto inject a script contains `window.__INITIAL_STATE__` in output, so that you can resue server state on client.
192+
193+
### Style injection on server
194+
195+
Without SSR, we can easily use `style-loader`, however we need to collect rendered components with there styles together on runtime, so we choose to use [react-style-loader](https://github.com/JounQin/react-style-loader) which forked [vue-style-loader](https://github.com/vuejs/vue-style-loader) indeed.
196+
197+
Let's create a simple HOC for server style and http injection.
198+
199+
```js
200+
import axios from 'axios'
201+
import hoistStatics from 'hoist-non-react-statics'
202+
import PropTypes from 'prop-types'
203+
import React from 'react'
204+
import { withRouter } from 'react-router'
205+
206+
export const withSsr = (styles, router = true) => Component => {
207+
class SsrConmponent extends React.PureComponent {
208+
static displayName = `Ssr${Component.displayName ||
209+
Component.name ||
210+
'Component'}`
211+
212+
static propTypes = {
213+
staticContext: PropTypes.object,
214+
}
215+
216+
componentWillMount() {
217+
// `styles.__inject__` will only be exist on server, and inject into `userContext`
218+
if (styles && styles.__inject__) {
219+
styles.__inject__(this.props.staticContext)
220+
}
221+
}
222+
223+
render() {
224+
return (
225+
<Component
226+
{...this.props}
227+
// use different axios instance on server
228+
http={__SERVER__ ? this.props.staticContext.axios : axios}
229+
/>
230+
)
231+
}
232+
}
233+
234+
return hoistStatics(
235+
router ? withRouter(SsrConmponent) : SsrConmponent,
236+
Component,
237+
)
238+
}
239+
```
240+
241+
Then use it:
242+
243+
```js
244+
import PropTypes from 'prop-types'
245+
import React from 'react'
246+
import { connect } from 'react-redux'
247+
248+
import { setCounter, increase, decrease } from 'store'
249+
import { withSsr } from 'utils'
250+
251+
import styles from './styles'
252+
253+
@withSsr(styles, false)
254+
@connect(
255+
({ counter }) => ({ counter }),
256+
dispatch => ({
257+
setCounter: counter => dispatch(setCounter(counter)),
258+
increase: () => dispatch(increase),
259+
decrease: () => dispatch(decrease),
260+
}),
261+
)
262+
export default class Home extends React.PureComponent {
263+
static propTypes = {
264+
counter: PropTypes.number.isRequired,
265+
setCounter: PropTypes.func.isRequired,
266+
increase: PropTypes.func.isRequired,
267+
decrease: PropTypes.func.isRequired,
268+
}
269+
270+
asyncBootstrap() {
271+
if (this.props.counter) {
272+
return true
273+
}
274+
275+
return new Promise(resolve =>
276+
setTimeout(() => {
277+
this.props.setCounter(~~(Math.random() * 100))
278+
resolve(true)
279+
}, 500),
280+
)
281+
}
282+
283+
render() {
284+
return (
285+
<div className="container">
286+
<h2 className={styles.heading}>Counter</h2>
287+
<button className="btn btn-primary" onClick={this.props.decrease}>
288+
-
289+
</button>
290+
{this.props.counter}
291+
<button className="btn btn-primary" onClick={this.props.increase}>
292+
+
293+
</button>
294+
</div>
295+
)
296+
}
297+
}
298+
```
299+
300+
Then `react-server-renderer` will automatically collect user styles on server and render them into output!
301+
302+
---
303+
304+
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 like [react-universally](https://github.com/ctrlplusb/react-universally) or any other, I find out Vue's solution is really great and simple.
305+
306+
## Feature Request or Troubleshooting
307+
308+
Feel free to [create an issue](https://github.com/JounQin/react-server-renderer/issues/new).

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "react-server-renderer",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "simple React SSR solution inspired by vue-server-render",
5-
"repository": "git@github.com:JounQin/react-server-render.git",
5+
"repository": "git@github.com:JounQin/react-server-renderer.git",
66
"main": "lib/index.js",
77
"types": "lib/index.d.ts",
88
"author": "JounQin <admin@1stg.me>",

0 commit comments

Comments
 (0)