|
@@ -0,0 +1,226 @@
|
|
|
+# Server Rendering
|
|
|
+
|
|
|
+Alright, first things first. Server rendering, at its core is a simple
|
|
|
+concept in React.
|
|
|
+
|
|
|
+```js
|
|
|
+render(<App/>, domNode)
|
|
|
+// can be rendered on the server as
|
|
|
+const markup = renderToString(<App/>)
|
|
|
+```
|
|
|
+
|
|
|
+It's not rocket science, but it also isn't trivial. First I'm going to
|
|
|
+just throw a bunch of webpack shenanigans at you with little
|
|
|
+explanation, then we'll talk about the Router.
|
|
|
+
|
|
|
+Since node doesn't (and shouldn't) understand JSX, we need to compile
|
|
|
+the code somehow. Using something like `babel/register` is not fit for
|
|
|
+production use, so we'll use webpack to build a server bundle, just like
|
|
|
+we use it to build a client bundle.
|
|
|
+
|
|
|
+Make a new file called `webpack.server.config.js` and put this stuff in
|
|
|
+there:
|
|
|
+
|
|
|
+```js
|
|
|
+var fs = require('fs')
|
|
|
+var path = require('path')
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+
|
|
|
+ entry: path.resolve(__dirname, 'server.js'),
|
|
|
+
|
|
|
+ output: {
|
|
|
+ filename: 'server.bundle.js'
|
|
|
+ },
|
|
|
+
|
|
|
+ target: 'node',
|
|
|
+
|
|
|
+ // keep node_module paths out of the bundle
|
|
|
+ externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([
|
|
|
+ 'react-dom/server'
|
|
|
+ ]).reduce(function (ext, mod) {
|
|
|
+ ext[mod] = 'commonjs ' + mod
|
|
|
+ return ext
|
|
|
+ }, {}),
|
|
|
+
|
|
|
+ node: {
|
|
|
+ __filename: true,
|
|
|
+ __dirname: true
|
|
|
+ },
|
|
|
+
|
|
|
+ module: {
|
|
|
+ loaders: [
|
|
|
+ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Hopefully some of that makes sense, we aren't going to cover what all of
|
|
|
+that stuff does, it's sufficient to say that now we can run our
|
|
|
+`server.js` file through webpack and then run it.
|
|
|
+
|
|
|
+Now we need to make some scripts to build server bundle before we try to
|
|
|
+run our app. Update your `package.json` script config to look like
|
|
|
+this:
|
|
|
+
|
|
|
+```
|
|
|
+"scripts": {
|
|
|
+ "start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
|
|
|
+ "start:dev": "webpack-dev-server --inline --content-base public/ --history-api-fallback",
|
|
|
+ "start:prod": "npm run build && node server.bundle.js",
|
|
|
+ "build:client": "webpack",
|
|
|
+ "build:server": "webpack --config webpack.server.config.js",
|
|
|
+ "build": "npm run build:client && npm run build:server"
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+Now when we run `NODE_ENV=production npm start` both the client and
|
|
|
+server bundles get created by Webpack.
|
|
|
+
|
|
|
+Okay, let's talk about the Router. We're going to need our routes split
|
|
|
+out into a module so that both the client and server entries can require
|
|
|
+it. Make a file at `modules/routes` and move your routes and components
|
|
|
+into it.
|
|
|
+
|
|
|
+```js
|
|
|
+// modules/routes.js
|
|
|
+import React from 'react'
|
|
|
+import { Router, Route, browserHistory, IndexRoute } from 'react-router'
|
|
|
+import App from './App'
|
|
|
+import About from './About'
|
|
|
+import Repos from './Repos'
|
|
|
+import Repo from './Repo'
|
|
|
+import Home from './Home'
|
|
|
+
|
|
|
+module.exports = (
|
|
|
+ <Route path="/" component={App}>
|
|
|
+ <IndexRoute component={Home}/>
|
|
|
+ <Route path="/repos" component={Repos}>
|
|
|
+ <Route path="/repos/:userName/:repoName" component={Repo}/>
|
|
|
+ </Route>
|
|
|
+ <Route path="/about" component={About}/>
|
|
|
+ </Route>
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+```js
|
|
|
+// index.js
|
|
|
+import React from 'react'
|
|
|
+import { render } from 'react-dom'
|
|
|
+import { Router, browserHistory } from 'react-router'
|
|
|
+// import routes and pass them into <Router/>
|
|
|
+import routes from './modules/routes'
|
|
|
+
|
|
|
+render(
|
|
|
+ <Router routes={routes} history={browserHistory}/>,
|
|
|
+ document.getElementById('app')
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+Now open up `server.js`. We're going to bring in two modules from React
|
|
|
+Router to help us render on the server.
|
|
|
+
|
|
|
+If we tried to render a `<Router/>` on the server like we do in the
|
|
|
+client, we'd get an empty screen since server rendering is synchronous
|
|
|
+and route matching is asynchronous.
|
|
|
+
|
|
|
+Also, most apps will want to use the router to help them load data, so
|
|
|
+asynchronous routes or not, you'll want to know what screens are going
|
|
|
+to render before you actually render so you can use that information to
|
|
|
+load asynchronous data before rendering. We don't have any data loading
|
|
|
+in this app, but you'll see where it could happen.
|
|
|
+
|
|
|
+First we import `match` and `RouterContext` from react router, then
|
|
|
+we'll match the routes to the url, and finally render.
|
|
|
+
|
|
|
+```js
|
|
|
+// ...
|
|
|
+// import some new stuff
|
|
|
+import React from 'react'
|
|
|
+// we'll use this to render our app to an html string
|
|
|
+import { renderToString } from 'react-dom/server'
|
|
|
+// and these to match the url to routes and then render
|
|
|
+import { match, RouterContext } from 'react-router'
|
|
|
+import routes from './modules/routes'
|
|
|
+
|
|
|
+// ...
|
|
|
+
|
|
|
+// send all requests to index.html so browserHistory works
|
|
|
+
|
|
|
+app.get('*', (req, res) => {
|
|
|
+ // match the routes to the url
|
|
|
+ match({ routes: routes, location: req.url }, (err, redirect, props) => {
|
|
|
+ // `RouterContext` is the what `Router` renders. `Router` keeps these
|
|
|
+ // `props` in its state as it listens to `browserHistory`. But on the
|
|
|
+ // server our app is stateless, so we need to use `match` to
|
|
|
+ // get these props before rendering.
|
|
|
+ const appHtml = renderToString(<RouterContext {...props}/>)
|
|
|
+
|
|
|
+ // dump the HTML into a template, lots of ways to do this, but none are
|
|
|
+ // really influenced by React Router, so we're just using a little
|
|
|
+ // function, `renderPage`
|
|
|
+ res.send(renderPage(appHtml))
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+function renderPage(appHtml) {
|
|
|
+ return `
|
|
|
+ <!doctype html public="storage">
|
|
|
+ <html>
|
|
|
+ <meta charset=utf-8/>
|
|
|
+ <title>My First React Router App</title>
|
|
|
+ <link rel=stylesheet href=/index.css>
|
|
|
+ <div id=app>${appHtml}</div>
|
|
|
+ <script src="/bundle.js"></script>
|
|
|
+ `
|
|
|
+}
|
|
|
+
|
|
|
+var PORT = process.env.PORT || 8080
|
|
|
+app.listen(PORT, function() {
|
|
|
+ console.log('Production Express server running at localhost:' + PORT)
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+And that's it. Now if you run `NODE_ENV=production npm start` and visit
|
|
|
+the app, you can view source and see that the server is sending down our
|
|
|
+app to the browser. As you click around, you'll notice the client app
|
|
|
+has taken over and doesn't make requests to the server for UI. Pretty
|
|
|
+cool yeah?!
|
|
|
+
|
|
|
+
|
|
|
+Our callback to match is a little naive, here's what a production
|
|
|
+version would look like:
|
|
|
+
|
|
|
+```js
|
|
|
+app.get('*', (req, res) => {
|
|
|
+ match({ routes: routes, location: req.url }, (err, redirect, props) => {
|
|
|
+ // in here we can make some decisions all at once
|
|
|
+ if (err) {
|
|
|
+ // there was an error somewhere during route matching
|
|
|
+ res.status(500).send(err.message)
|
|
|
+ } else if (redirect) {
|
|
|
+ // we haven't talked about `onEnter` hooks on routes, but before a
|
|
|
+ // route is entered, it can redirect. Here we handle on the server.
|
|
|
+ res.redirect(redirect.pathname + redirect.search)
|
|
|
+ } else if (props) {
|
|
|
+ // if we got props then we matched a route and can render
|
|
|
+ const appHtml = renderToString(<RouterContext {...props}/>)
|
|
|
+ res.send(renderPage(appHtml))
|
|
|
+ } else {
|
|
|
+ // no errors, no redirect, we just didn't match anything
|
|
|
+ res.status(404).send('Not Found')
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+Server rendering is really new. There aren't really "best practices"
|
|
|
+yet, especially when it comes to data loading, so this tutorial is done,
|
|
|
+dropping you off at the bleeding edge.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+[Next: What's Next?](14-whats-next.md)
|
|
|
+
|