Browse Source

L13: Server-side rendering.

Frederic G. MARAND 8 years ago
parent
commit
88affd8a1c
12 changed files with 130 additions and 1654 deletions
  1. 7 17
      index.js
  2. 3 3
      modules/About.js
  3. 3 4
      modules/App.js
  4. 1 0
      modules/NavLink.js
  5. 2 1
      modules/Repo.js
  6. 1 2
      modules/Repos.js
  7. 18 0
      modules/routes.js
  8. 5 2
      package.json
  9. 0 1616
      public/index.css
  10. 53 3
      server.js
  11. 6 6
      webpack.config.js
  12. 31 0
      webpack.server.config.js

+ 7 - 17
index.js

@@ -1,20 +1,10 @@
-import About from "./modules/About";
-import App from "./modules/App";
-import Home from "./modules/Home";
 import React from "react";
-import Repos from "./modules/Repos";
-import Repo from "./modules/Repo";
 import { render } from "react-dom";
-import { IndexRoute, Router, Route, browserHistory as history} from "react-router";
+import { Router, browserHistory as history} from "react-router";
 
-render((
-  <Router history={history}>
-    <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>
-  </Router>
-  ), document.getElementById("app"));
+import routes from './modules/routes';
+
+render(
+  <Router routes={routes} history={history} />,
+  document.getElementById("app")
+);

+ 3 - 3
modules/About.js

@@ -1,7 +1,7 @@
-import React from "react";
+import React from 'react'
 
 export default React.createClass({
   render() {
-    return <div>About</div>;
+    return <div>About</div>
   }
-});
+})

+ 3 - 4
modules/App.js

@@ -1,4 +1,3 @@
-import { IndexLink, Link } from "react-router";
 import NavLink from './NavLink';
 import React from "react";
 
@@ -6,13 +5,13 @@ export default React.createClass({
   render() {
     return (
       <div>
-        <h1>React router tutorial</h1>
+        <h1>React Router Tutorial</h1>
         <ul role="nav">
-          <li><NavLink onlyActiveOnIndex={true} to="/">Home</NavLink></li>
+          <li><NavLink to="/" onlyActiveOnIndex>Home</NavLink></li>
           <li><NavLink to="/about">About</NavLink></li>
           <li><NavLink to="/repos">Repos</NavLink></li>
         </ul>
-        { this.props.children }
+        {this.props.children}
       </div>
     );
   }

+ 1 - 0
modules/NavLink.js

@@ -1,3 +1,4 @@
+// modules/NavLink.js
 import React from 'react';
 import { Link } from 'react-router';
 

+ 2 - 1
modules/Repo.js

@@ -2,9 +2,10 @@ import React from 'react';
 
 export default React.createClass({
   render() {
+    const { userName, repoName } = this.props.params
     return (
       <div>
-        {this.props.params.repoName}
+        <h2>{userName} / {repoName}</h2>
       </div>
     )
   }

+ 1 - 2
modules/Repos.js

@@ -11,7 +11,6 @@ export default React.createClass({
     const userName = event.target.elements[0].value;
     const repo = event.target.elements[1].value;
     const path = `/repos/${userName}/${repo}`;
-    console.log("Path:", path);
     this.context.router.push(path);
   },
 
@@ -20,7 +19,7 @@ export default React.createClass({
       <div>
         <h2>Repos</h2>
         <ul>
-          <li><NavLink to="/repos/reactjs/react-router">React router</NavLink></li>
+          <li><NavLink to="/repos/reactjs/react-router">React Router</NavLink></li>
           <li><NavLink to="/repos/facebook/react">React</NavLink></li>
           <li>
             <form onSubmit={this.handleSubmit}>

+ 18 - 0
modules/routes.js

@@ -0,0 +1,18 @@
+import React from "react";
+import { IndexRoute, Route } from "react-router";
+
+import About from "./About";
+import App from "./App";
+import Home from "./Home";
+import Repos from "./Repos";
+import Repo from "./Repo";
+
+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>
+);

+ 5 - 2
package.json

@@ -10,8 +10,11 @@
   },
   "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": "webpack && node server.js"
+    "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"
   },
   "version": "1.0.0",
 

File diff suppressed because it is too large
+ 0 - 1616
public/index.css


+ 53 - 3
server.js

@@ -2,17 +2,67 @@ var express = require('express');
 var path = require('path');
 var compression = require('compression');
 
+import React from 'react';
+import { renderToString } from 'react-dom/server';
+import { match, RouterContext } from 'react-router';
+import routes from './modules/routes';
+
 var app = express();
+
 app.use(compression());
 
-// Server our static stuff like index.css
+// serve our static stuff like index.css
 app.use(express.static(path.join(__dirname, 'public')));
 
 // Send all requests to index.html so browserHistory in React Router works
-app.get('*', function (req, res) {
-  res.sendFile(path.join(__dirname, 'public', 'index.html'));
+app.get('*', (req, res) => {
+  match({ routes, location: req.url }, (err, redirect, props) => {
+    console.log(`SSR for ${req.url}`);
+    // 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
+
+      // `RouterContext is what the `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));
+    } else {
+      // no errors, no redirect, we just didn't match anything
+      res.status(404).send("Not found");
+    }
+  });
 });
 
+function renderPage(appHtml) {
+  return `
+    <!DOCTYPE HTML>
+    <html>
+      <head>
+        <meta charset="utf-8" />
+        <title>My first React Router App</title>
+        <link rel="stylesheet" href="/index.css" />
+      </head>
+      <body>
+        <div id="app">${appHtml}</div>
+        <script src="/bundle.js"></script>
+      </body>
+    </html>
+  `;
+}
+
 var PORT = process.env.PORT || 8080;
 app.listen(PORT, function () {
   console.log('"Production" Express server running at localhost:' + PORT);

+ 6 - 6
webpack.config.js

@@ -3,18 +3,18 @@ var webpack = require('webpack');
 module.exports = {
   entry: './index.js',
 
-  output: {
-    filename: 'bundle.js',
-    path: 'public',
-    publicPath: 'public'
-  },
-
   module: {
     loaders: [
       { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' }
     ]
   },
 
+  output: {
+    filename: 'bundle.js',
+    path: 'public',
+    publicPath: ''
+  },
+
   plugins: process.env.NODE_ENV === 'production' ? [
     new webpack.optimize.DedupePlugin(),
     new webpack.optimize.OccurenceOrderPlugin(),

+ 31 - 0
webpack.server.config.js

@@ -0,0 +1,31 @@
+var fs = require('fs');
+var path = require('path');
+
+module.exports = {
+  entry: path.resolve(__dirname, 'server.js'),
+
+  module: {
+    loaders: [
+      { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' }
+    ]
+  },
+
+  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
+  }
+};

Some files were not shown because too many files changed in this diff