React服务端渲染-SSR

1,628 阅读9分钟

什么是服务端渲染

  • renderToString 将react组件转换为字符串随浏览器返回
  • 事件(onclick)处理,需要客户端实现同样的代码 单独打包 然后服务端直接引入 同时也要主要静态资源的跟目录
  • 路由控制 服务端使用react-router-dom/StaticRouter 来实现路由跳转
  • 请求数据 需要给组件添加一个静态方法 配置路由的时候添加一个属性 标志是当前组件需要请求数据
  • 使用react-router-dom/matchPath来进行路由匹配
  • 多级路由匹配使用react-router-config/renderRoutes
  • matchRoutes来匹配多级路由下当前匹配的路由
  • 匹配的路由遍历 然后将存在loadData标识的函数执行,即route.loadData(store),并将这个结果存在一个promises数组里面。loadData函数执行返回store.dispatch()(返回一个promise) promise.all 全部执行成功代表数据返回 开始渲染页面。
  • 404的实现可以在 static-router 上传递的context值添加一个404的标识,然后服务端判断这个标识来显示状态码404
  • 301 会根据context.action == 'REPLACE' 来进行页面跳转 res.redirect(301,context.url);
  • 渲染CSS 服务端使用isomorphic-style-loader的loader
    • 引入样式的页面this.props.staticContext.csses.push(styles._getCss());
    • 服务端判断if (context.csses.length>0) { cssStr=context.csses.join('\r\n');}

服务端渲染

浏览器直接返回HTML字符串到页面中

    let express=require('express');
    let app=express();
    app.get('/',(req,res) => {
        res.send(`
            <html>
              <body>
                <div id="root">hello</div>
              </body>
            </html>
        `);
    });
    app.listen(8080);

客户端渲染

页面上的内容由于浏览器运行JS脚本而渲染到页面上的

    
let express=require('express');
let app=express();
app.get('/',(req,res) => {
    res.send(`
        <html>
          <body>
            <div id="root"></div>
            <script>
                document.getElementById('root').innerHTML = 'hello';
            </script>
          </body>
        </html>
    `);
});
app.listen(9090);

服务器端打包React组件

  • webpack.config.js 配置
    //服务器端
let path = require('path');
<!--排除node本身自带的模块打包,因为服务端环境就是node-->
let nodeExternals=require('webpack-node-externals');
module.exports = {
    target: 'node',//打包的是服务器端node文件
    mode:'development',//开发模式
    output:{
        path:path.resolve(__dirname,'build'),
        filename:'bundle.js'
    },
    externals:[nodeExternals()],
    module:{
        rules:[
            {
                test:/\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                options: {
                    presets: [
                        [
                            "@babel/preset-env",{
                                targets: {
                                    browsers:['last 2 versions']
                                }
                            }
                        ],"@babel/preset-react"
                    ]
                }
            }
        ]
    }
}
  • Home/index.js
    import React,{Component} from 'react';
    export default class Home extends Component{
        render() {
            return <div>Home</div>
        }
    }
  • server/src/index.js
    import React from 'react';
    import Home from './containers/Home';
    <!--react 服务端渲染 将组建转换为字符串-->
    import {renderToString} from 'react-dom/server';
    import express from 'express';
    let app=express();
    const html=renderToString(<Home />);
    app.get('/',(req,res) => {
        res.send(`
        <html>
            <body>
            <div id="root">
              ${html}
            </div>
            </body>
        </html>
        `);
    });
    app.listen(9090);
  • package.json
    "scripts": {
        "start": "node ./server/build/bundle.js",
        "build":"webpack --config server/webpack.config.js"
    }
    
    // 优化后的package.json
    // npm-run-all 执行多条命令  nodemon 监控文件变化重启
    "scripts": {
        "dev":"npm-run-all --parallel dev:**",
        "dev:start":"nodemon './server/build/bundle.js'",
        "dev:build":"webpack --config server/webpack.config.js --watch"
    }

计数器组件(服务端处理事件)

  • src/client/index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import Counter from '../containers/Counter';
    
    ReactDOM.hydrate(<Counter/>,document.querySelector('#root'));
  • src/containers/Counter/index.js
    import React,{Component} from 'react';
    export default class Counter extends Component{
        state={number:0}
        render() {
            return (
                <div>
                    <p>{this.state.number}</p>
                    <button onClick={()=>this.setState({number:this.state.number+1})}>+</button>
                </div>
            )
        }
    }
  • webpack.client.js
    //浏览器端
let path = require('path');
module.exports = {
    mode: 'development',//开发模式
    entry:path.resolve(__dirname,'./src/client/index.js'),
    output:{
        path:path.resolve(__dirname,'public'),
        filename:'index.js'
    },
    module:{
        rules:[
            {
                test:/\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                options: {
                    presets: [
                        "@babel/preset-env",
                        "@babel/preset-react"
                    ],
                    plugins: [
                        "@babel/plugin-proposal-class-properties"
                    ]
                }
            }
        ]
    }
}
  • server/index.js
    import React from 'react';
    import {renderToString} from 'react-dom/server';
    import Home from '../containers/Home';
    import express from 'express';
    let app=express();
    <!--静态资源目录以public为准-->
    app.use(express.static('public'));
    const content=renderToString(<Home />);
    app.get('/',(req,res) => {
        res.send(`
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <meta http-equiv="X-UA-Compatible" content="ie=edge">
                <title></title>
            </head>
            <body>
                <div id="root">${content}</div>
                <!--引入客户端打包的index.js文件  来处理浏览器端事件-->
                <script src="/index.js"></script>
            </body>
        </html>
        `);
    });
    app.listen(9090);

使用路由

  • src/client/index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import {BrowserRouter} from 'react-router-dom';
    import routes from '../routes';
    <!--注意ReactDOM,render 和ReactDOM.hydrate的区别-->
    ReactDOM.hydrate(
        <BrowserRouter>
            {routes}
        </BrowserRouter>
        ,document.querySelector('#root'));
  • src/server/index.js
    import React from 'react';
    import {renderToString} from 'react-dom/server';
    import Home from '../containers/Home';
    import express from 'express';
    <!--服务端路由StaticRouter-->
    import {StaticRouter} from 'react-router-dom';
    import routes from '../routes';
    let app=express();
    app.use(express.static('public'));
    //context数据的传递 StaticRouter需要知道当前路径
    
    app.get('*',(req,res) => {
        const content=renderToString(
            <StaticRouter context={{}} location={req.path}>
                {routes}
            </StaticRouter>
        );
        res.send(`
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <meta http-equiv="X-UA-Compatible" content="ie=edge">
                <title></title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
            </body>
        </html>
        `);
    });
    app.listen(9090);
  • src/routes.js
    import React,{Fragment} from 'react';
    import {Route} from 'react-router-dom';
    import Home from './containers/Home';
    import Counter from './containers/Counter';
    export default (
        <Fragment>
            <Route path="/" exact component={Home}></Route>
            <Route path="/counter" exact component={Counter}></Route>
        </Fragment>
    )

跳转路由

  • src/client/index.js
    import React,{Fragment} from 'react';
    import ReactDOM from 'react-dom';
    import {BrowserRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    ReactDOM.hydrate(
        <BrowserRouter>
            <Fragment>
                <Header/>
                <div className="container" style={{marginTop:50}}>
                    {routes}
                </div>
            </Fragment>
        </BrowserRouter>
        ,document.querySelector('#root'));
  • /Header/index.js
    import React,{Component} from 'react';
    import {Link} from 'react-router-dom';
    export default class Home extends Component{
        render() {
            return (
                <nav className="navbar navbar-inverse navbar-fixed-top">
                        <div className="container">
                            <div className="navbar-header">
                                <a className="navbar-brand" href="#">珠峰SSR</a>
                            </div>
                            <div id="navbar" className="collapse navbar-collapse">
                                <ul className="nav navbar-nav">
                                    <li><Link to="/">Home</Link></li>
                                    <li><Link to="/counter">Counter</Link></li>
                                </ul>
                            </div>
                        </div>
                    </nav>
            )
        }
    }
  • src/server/index.js
    import express from 'express';
    import render from './render';
    let app=express();
    app.use(express.static('public'));
    //context数据的传递 StaticRouter需要知道当前路径
    app.get('*',(req,res) => {
        render(req,res);
    });
    app.listen(9090);
  • src/server/render.js
    import React,{Component,Fragment} from 'react';
    import {StaticRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {renderToString} from 'react-dom/server';
    export default function (req,res) {
        const content=renderToString(
            <StaticRouter context={{}} location={req.path}>
                    <Fragment>
                        <Header/>
                        <div className="container" style={{marginTop:50}}>
                            {routes}
                        </div>
                    </Fragment>
                </StaticRouter>
        );
        res.send(`
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <meta http-equiv="X-UA-Compatible" content="ie=edge">
                <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
                <title>珠峰SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
            </body>
        </html>
        `);
    }

Redux(引入状态管理)

  • src/client/index.js
    import React,{Fragment} from 'react';
    import ReactDOM from 'react-dom';
    import {BrowserRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {Provider} from 'react-redux';
    import getStore from '../store';
    ReactDOM.hydrate(
        <Provider store={getStore()}>
            <BrowserRouter>
                <Fragment>
                    <Header/>
                    <div className="container" style={{marginTop:50}}>
                        {routes}
                    </div>
                </Fragment>
            </BrowserRouter>
        </Provider>
        ,document.querySelector('#root'));
  • src/containers/Counter/index.js
    import React,{Component} from 'react';
    import {connect} from 'react-redux';
    import actions from '../../store/actions';
    class Counter extends Component{
        render() {
            return (
                <div>
                    <p>{this.props.number}</p>
                    <button className="btn btn-primary" onClick={this.props.increment}>+</button>
                </div>
            )
        }
    }
    export default connect(
        state => state,
        actions
    )(Counter);
  • src/server/render.js
    import React,{Component,Fragment} from 'react';
    import {StaticRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {renderToString} from 'react-dom/server';
    import getStore from '../store';
    import {Provider} from 'react-redux';
    export default function (req,res) {
        const content=renderToString(
            <Provider store={getStore()}>
                <StaticRouter context={{}} location={req.path}>
                        <Fragment>
                            <Header/>
                            <div className="container" style={{marginTop:50}}>
                                {routes}
                            </div>
                        </Fragment>
                    </StaticRouter>
            </Provider>
        );
        res.send(`
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <meta http-equiv="X-UA-Compatible" content="ie=edge">
                <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
                <title>珠峰SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
            </body>
        </html>
        `);
    }
  • src/store/index.js
    import reducer from './reducer';
    import {createStore,applyMiddleware} from 'redux';
    import thunk from 'redux-thunk';
    import logger from 'redux-logger';
    function getStore() {
        return createStore(reducer,applyMiddleware(thunk,logger));
    }
    export default getStore;

客户端加载数据

  • src/containers/Home/index.js
    import React,{Component} from 'react';
    import {connect} from 'react-redux';
    import actions from '../../store/actions/home';
    class Home extends Component{
        componentDidMount() {
            this.props.getHomeList();
        }
        render() {
            return (
                <div className="row">
                    <div className="col-md-12">
                        <ul className="list-group">
                            {
                                this.props.list.map(item => (
                                    <li className="list-group-item" key={item.id}>{item.name}</li>
                                ))
                            }
                        </ul>
                    </div>
                </div>
            )
        }
    }
    export default connect(
        state => state.home,
        actions
    )(Home);
  • src/store/reducers/home.js
    import * as types from '../action-types';
    import axios from 'axios';
    export default {
        getHomeList() {
            return function (dispatch,getState) {
                axios.get('http://localhost:4000/api/users').then(result => {
                    let list=result.data;
                    dispatch({
                        type: types.SET_HOME_LIST,
                        payload:list
                    });
                });
            }
        }
    }
  • api/server.js
    let express=require('express');
    let cors=require('cors');
    let app=express();
    var corsOptions = {
        origin: 'http://localhost:9090',
        optionsSuccessStatus: 200 
    }
    app.use(cors(corsOptions));
    let users=[{id:1,name:'zfpx1'},{id:2,name:'zfpx2'}];
    app.get('/api/users',function (req,res) {
        res.json(users);
    });
    app.listen(4000);

服务器端路由

  • src/client/index.js
    import React,{Fragment} from 'react';
    import ReactDOM from 'react-dom';
    import {BrowserRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {Provider} from 'react-redux';
    import {Route} from 'react-router-dom';
    import getStore from '../store';
    ReactDOM.hydrate(
        <Provider store={getStore()}>
            <BrowserRouter>
                <Fragment>
                    <Header/>
                    <div className="container" style={{marginTop: 70}}>
                        <Fragment>
                          {routes.map(route => (
                                <Route {...route}/>
                          ))}
                        </Fragment>
    
                    </div>
                </Fragment>
            </BrowserRouter>
        </Provider>
        ,document.querySelector('#root'));
  • src/containers/Home/index.js

Home组件添加一个静态方法loadData,标志这个组件需要动态获取数据。

    import React,{Component} from 'react';
    import {connect} from 'react-redux';
    import actions from '../../store/actions/home';
    class Home extends Component{
        static loadData=() => {
            console.log('加载数据');
        }
        //componentDidMount在服务器端是不执行的
        componentDidMount() {
            this.props.getHomeList();
        }
        render() {
            return (
                <div className="row">
                    <div className="col-md-12">
                        <ul className="list-group">
                            {
                                this.props.list.map(item => (
                                    <li className="list-group-item" key={item.id}>{item.name}</li>
                                ))
                            }
                        </ul>
                    </div>
                </div>
            )
        }
    }
    export default connect(
        state => state.home,
        actions
    )(Home);
  • src/routes.js
    import React,{Fragment} from 'react';
    import {Route} from 'react-router-dom';
    import Home from './containers/Home';
    import Counter from './containers/Counter';
    export default [
        {
            path: '/',
            component: Home,
            exact: true,
            key:'home',
            loadData:Home.loadData
        },
        {
            path: '/counter',
            component: Counter,
            key:'login',
            exact: true
        }
    ]
  • src/server/render.js

服务端使用matchPath来找出当前匹配的路由

    import React,{Component,Fragment} from 'react';
    import {StaticRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {renderToString} from 'react-dom/server';
    import {Route,matchPath} from 'react-router-dom';
    import getStore from '../store';
    import {Provider} from 'react-redux';
    export default function (req,res) {
        let store=getStore();
        <!--找出当前匹配的路由-->
        let matchedRoutes=routes.filter(route => {
            return matchPath(req.path,route);
        });
        console.log(matchedRoutes);
        const content=renderToString(
            <Provider store={store}>
                <StaticRouter context={{}} location={req.path}>
                        <Fragment>
                            <Header/>
                            <div className="container" style={{marginTop:70}}>
                            {routes.map(route => (
                                <Route {...route}/>
                                ))}
                            </div>
                        </Fragment>
                    </StaticRouter>
            </Provider>
        );
        res.send(`
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <meta http-equiv="X-UA-Compatible" content="ie=edge">
                <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
                <title>珠峰SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
            </body>
        </html>
        `);
    }

多级路由

  • src/client/index.js

使用react-router-config的renderRoutes来匹配多级路由

    import React,{Fragment} from 'react';
    import ReactDOM from 'react-dom';
    import {BrowserRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {Provider} from 'react-redux';
    import {Route,matchPath} from 'react-router-dom';
    import {matchRoutes,renderRoutes} from 'react-router-config';
    import getStore from '../store';
    ReactDOM.hydrate(
        <Provider store={getStore()}>
            <BrowserRouter>
                <Fragment>
                    <Header/>
                    <div className="container" style={{marginTop: 70}}>
                        <Fragment>
                          {renderRoutes(routes)}
                        </Fragment>
    
                    </div>
                </Fragment>
            </BrowserRouter>
        </Provider>
        ,document.querySelector('#root'));
  • src/components/Header/index.js
    import React,{Component} from 'react';
    import {Link} from 'react-router-dom';
    export default class Home extends Component{
        render() {
            return (
                <nav className="navbar navbar-inverse navbar-fixed-top">
                        <div className="container">
                            <div className="navbar-header">
                                <a className="navbar-brand" href="#">珠峰SSR</a>
                            </div>
                            <div id="navbar" className="collapse navbar-collapse">
                                <ul className="nav navbar-nav">
                                  <li><Link to="/">Home</Link></li>
                                  <li><Link to="/user/list">用户列表</Link></li>
                                  <li><Link to="/counter">Counter</Link></li>
                                </ul>
                            </div>
                        </div>
                    </nav>
            )
        }
    }
  • src/routes.js
    import React,{Fragment} from 'react';
    import Home from './containers/Home';
    import User from './containers/User';
    import UserList from './containers/User/components/UserList';
    import Counter from './containers/Counter';
    export default [
        {
            path: '/',
            component: Home,
            exact: true,
            key:'/home',
            loadData:Home.loadData
        },
        {
            path: '/user',
            component: User,
            key: '/user',
            routes: [
                {
                    path: '/user/list',
                    component: UserList,
                    key:'/user/list'
                }
            ]
        },
        {
            path: '/counter',
            component: Counter,
            key:'login',
            exact: true
        }
    ]
  • src/server/render.js

使用matchRoutes来匹配多级路由的情况下的当前匹配路由

    import React,{Component,Fragment} from 'react';
    import {StaticRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {renderToString} from 'react-dom/server';
    import {Route,matchPath} from 'react-router-dom';
    import {matchRoutes,renderRoutes} from 'react-router-config';
    import getStore from '../store';
    import {Provider} from 'react-redux';
    export default function (req,res) {
        let store=getStore();
        /**
        let matchedRoutes=routes.filter(route => {
            return matchPath(req.path,route);
        });
        */
        let matchedRoutes= matchRoutes(routes,req.path);
        console.log(matchedRoutes);
        const content=renderToString(
            <Provider store={store}>
                <StaticRouter context={{}} location={req.path}>
                        <Fragment>
                            <Header/>
                            <div className="container" style={{marginTop:70}}>
                              {renderRoutes(routes)}
                            </div>
                        </Fragment>
                    </StaticRouter>
            </Provider>
        );
        res.send(`
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <meta http-equiv="X-UA-Compatible" content="ie=edge">
                <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
                <title>珠峰SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
            </body>
        </html>
        `);
    }

后台获取数据

  • src/client/index.js
    import React,{Fragment} from 'react';
    import ReactDOM from 'react-dom';
    import {BrowserRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {Provider} from 'react-redux';
    import {renderRoutes} from 'react-router-config';
    import {getClientStore} from '../store';
    ReactDOM.hydrate(
        <Provider store={getClientStore()}>
            <BrowserRouter>
                <Fragment>
                    <Header/>
                    <div className="container" style={{marginTop: 70}}>
                        <Fragment>
                          {renderRoutes(routes)}
                        </Fragment>
    
                    </div>
                </Fragment>
            </BrowserRouter>
        </Provider>
        ,document.querySelector('#root'));
  • src/containers/Home/index.js
    import React,{Component} from 'react';
    import {connect} from 'react-redux';
    import actions from '../../store/actions/home';
    class Home extends Component{
        static loadData=(store) => {
            //dispatch方法的返回值是action,也就是返回一个promise
            //https://github.com/reduxjs/redux/blob/master/src/createStore.js
            return store.dispatch(actions.getHomeList());
        }
        //componentDidMount在服务器端是不执行的
        componentDidMount() {
            if(this.props.list.length==0)
                this.props.getHomeList();
        }
        render() {
            return (
                <div className="row">
                    <div className="col-md-12">
                        <ul className="list-group">
                            {
                                this.props.list.map(item => (
                                    <li className="list-group-item" key={item.id}>{item.name}</li>
                                ))
                            }
                        </ul>
                    </div>
                </div>
            )
        }
    }
    export default connect(
        state => state.home,
        actions
    )(Home);
  • src/server/render.js
    import React,{Component,Fragment} from 'react';
    import {StaticRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {renderToString} from 'react-dom/server';
    import {Route,matchPath} from 'react-router-dom';
    import {matchRoutes,renderRoutes} from 'react-router-config';
    import {getStore} from '../store';
    import {Provider} from 'react-redux';
    export default function (req,res) {
        let store=getStore();
        /**
        let matchedRoutes=routes.filter(route => {
            return matchPath(req.path,route);
        });
        */
        let matchedRoutes=matchRoutes(routes,req.path);
        let promises=[];
        matchedRoutes.forEach(item => {
            if (item.route.loadData)
                // 说明加载此组件需要请求数据,promises数组里面放入一个promise
                promises.push(item.route.loadData(store));
        });
        <!--promise执行成功,数据正确返回,开始渲染页面-->
        Promise.all(promises).then(result => {
            const content=renderToString(
                <Provider store={store}>
                    <StaticRouter context={{}} location={req.path}>
                            <Fragment>
                                <Header/>
                                <div className="container" style={{marginTop:70}}>
                                  {renderRoutes(routes)}
                                </div>
                            </Fragment>
                        </StaticRouter>
                </Provider>
            );
            res.send(`
            <!DOCTYPE html>
            <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <meta http-equiv="X-UA-Compatible" content="ie=edge">
                    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
                    <title>珠峰SSR</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script>
                    //  在window上挂在状态值
                      window.context = {
                          state:${JSON.stringify(store.getState())}
                      }
                    </script>
                    <script src="/index.js"></script>
                </body>
            </html>
            `);
        });
    
    }
  • src/store/index.js

将客户端store和服务端store拆离开,并且通过window.context.state进行关联

    import reducers from './reducers';
    import {createStore,applyMiddleware} from 'redux';
    import thunk from 'redux-thunk';
    import logger from 'redux-logger';
    export function getStore() {
        return createStore(reducers,applyMiddleware(thunk,logger));
    }
    export function getClientStore() {
        let initState=window.context.state;
        return createStore(reducers,initState,applyMiddleware(thunk,logger));
    }

Node代理服务器

  • src/server/index.js
    import express from 'express';
    import proxy from 'express-http-proxy';
    import render from './render';
    let app=express();
    app.use(express.static('public'));
    app.use('/api',proxy('http://127.0.0.1:4000',{
        //修改请求路径
        proxyReqPathResolver: function (req) {
            return `/api/${req.url}`;
        }
    }));
    //context数据的传递 StaticRouter需要知道当前路径
    app.get('*',(req,res) => {
        render(req,res);
    });
    app.listen(9090);
  • src/store/actions/home.js
    import * as types from '../action-types';
    import axios from 'axios';
    export default {
        getHomeList() {
            //https://github.com/reduxjs/redux-thunk/blob/master/src/index.js
            return function (dispatch,getState,request) {
                //http://localhost:4000/api/users
                return request.get('/api/users').then(result => {
                    let list=result.data;
                    dispatch({
                        type: types.SET_HOME_LIST,
                        payload:list
                    });
                });
            }
        }
    }
  • src/store/index.js

客户端和服务端分离请求

    import reducers from './reducers';
    import {createStore,applyMiddleware} from 'redux';
    import clientRequest from '../client/request';
    import serverRequest from '../server/request';
    import thunk from 'redux-thunk';
    import logger from 'redux-logger';
    export function getStore() {
        return createStore(reducers,applyMiddleware(thunk.withExtraArgument(serverRequest),logger));
    }
    export function getClientStore() {
        let initState=window.context.state;
        return createStore(reducers,initState,applyMiddleware(thunk.withExtraArgument(clientRequest),logger));
    }
  • src/client/request.js
    import axios from 'axios';
    export default axios.create({
        baseURL:'/'
    });
  • src/server/request.js
    import axios from 'axios';
    export default axios.create({
        baseURL:'http://localhost:4000/'
    });

404

  • src/routes.js
    import Home from './containers/Home';
    import User from './containers/User';
    import UserList from './containers/User/components/UserList';
    import Counter from './containers/Counter';
    import Login from './containers/Login';
    import Logout from './containers/Logout';
    import Profile from './containers/Profile';
    import NotFound from './containers/NotFound';
    import App from './containers/App';
    export default [
        {
            path: '/',
            component: App,
            loadData:App.loadData,
            routes: [
                {
                    path: '/',
                    component: Home,
                    exact: true,
                    key:'/home',
                    loadData:Home.loadData
                },
                {
                    path: '/user',
                    component: User,
                    key: '/user',
                    routes: [
                        {
                            path: '/user/list',
                            component: UserList,
                            key:'/user/list'
                        }
                    ]
                },
                {
                    path: '/counter',
                    component: Counter,
                    key:'counter',
                    exact: true
                },
                {
                    path: '/login',
                    component: Login,
                    key:'/login',
                    exact: true
                },
                {
                    path: '/logout',
                    component: Logout,
                    key:'/logout',
                    exact: true
                },
                {
                    path: '/profile',
                    component: Profile,
                    key:'/profile',
                    exact: true
                },
                {
                    component: NotFound
                }
            ]
        }
    
    ]
  • src/server/render.js
    import React,{Component,Fragment} from 'react';
    import {StaticRouter} from 'react-router-dom';
    import Header from '../components/Header';
    import routes from '../routes';
    import {renderToString} from 'react-dom/server';
    import {matchRoutes,renderRoutes} from 'react-router-config';
    import {getStore} from '../store';
    import {Provider} from 'react-redux';
    import App from '../containers/App';
    export default function (req,res) {
        let store=getStore(req);
        /**
        let matchedRoutes=routes.filter(route => {
            return matchPath(req.path,route);
        });
        */
        let matchedRoutes=matchRoutes(routes,req.path);
        let promises=[];
        matchedRoutes.forEach(item => {
            if (item.route.loadData)
                promises.push(item.route.loadData(store));
        });
        Promise.all(promises).then(result => {
            let context={};
            const content=renderToString(
                <Provider store={store}>
                    <StaticRouter context={context} location={req.path}>
                        {renderRoutes(routes)}
                    </StaticRouter>
                </Provider>
            );
            if (context.notFound) {
                res.status(404);
            }
            res.send(`
            <!DOCTYPE html>
            <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <meta http-equiv="X-UA-Compatible" content="ie=edge">
                    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
                    <title>珠峰SSR</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script>
                      window.context = {
                          state:${JSON.stringify(store.getState())}
                      }
                    </script>
                    <script src="/index.js"></script>
                </body>
            </html>
            `);
        });
    }
  • src/containers/NotFound/index.js
    import React,{Component} from 'react';
    export default class NotFound extends Component{
        componentWillMount() {
            if (this.props.staticContext) {
                this.props.staticContext.notFound=true;
            }
        }
        render() {
            return (
                <div>404</div>
            )
        }
    }

301

  • server/render.js
    const content=renderToString(
            <Provider store={store}>
                <StaticRouter context={context} location={req.path}>
                    {renderRoutes(routes)}
                </StaticRouter>
            </Provider>
        );
        <!--这个是返回的标识 证明是重定向-->
        if (context.action == 'REPLACE') {
            return res.redirect(301,context.url);
        } else if (context.notFound) {
            res.status(404);
        }

Promise.all

这个处理是为了如果多个请求,其中一个请求失败了,页面将不会渲染。所以需要处理一下

    let promises=[];
    matchedRoutes.forEach(item => {
        if (item.route.loadData) {
            let promise=new Promise(function (resolve,reject) {
                return item.route.loadData(store).then(resolve,resolve);
            });
            promises.push(promise);
        }    
    });

使用CSS

  • src/containers/App.js
    import React,{Component,Fragment} from 'react';
    import {renderRoutes} from 'react-router-config';
    import Header from '../components/Header';
    import actions from '../store/actions/session';
    import styles from './App.css';
    export default class App extends Component{
        static loadData=(store) => {
            return store.dispatch(actions.getUser());
        }
        render() {
            return (
                <Fragment>
                    <Header/>
                    <div className="container" className={styles.app}>
                        <Fragment>
                          {renderRoutes(this.props.route.routes)}
                        </Fragment>
                    </div>
                </Fragment>
            )
        }
    }
  • webpack.client.js
    module:{
        rules:[
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true,
                            localIdentName:'[name]_[local]_[hash:base64:5]'
                        }
                    }
                ]
            }
        ]
    }
  • webpack.server.js

服务端需要用isomorphic-style-loader插件

    module:{
        rules:[
            {
                test: /\.css$/,
                use: [
                    'isomorphic-style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true,
                            localIdentName:'[name]_[local]_[hash:base64:5]'
                        }
                    }
                ]
            }
        ]
    }
  • src/containers/App.css
    .app{
        margin-top:70px;
    }

CSS服务器端渲染

  • src/containers/App.js
    componentWillMount() {
       if (this.props.staticContext) {
           this.props.staticContext.csses.push(styles._getCss());
       }
   }
  • src/server/render.js
    let context={csses:[]};
        const content=renderToString(
            <Provider store={store}>
                <StaticRouter context={context} location={req.path}>
                    {renderRoutes(routes)}
                </StaticRouter>
            </Provider>
        );
+        let cssStr='';
+        if (context.csses.length>0) {
+            cssStr=context.csses.join('\r\n');
+        }
        if (context.action == 'REPLACE') {
            return res.redirect(301,context.url);
        } else if (context.notFound) {
            res.status(404);
        }
        res.send(`
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <meta http-equiv="X-UA-Compatible" content="ie=edge">
                <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" >
+                <style>${cssStr}</style>
                <title>珠峰SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script>
                  window.context = {
                      state:${JSON.stringify(store.getState())}
                  }
                </script>
                <script src="/index.js"></script>
            </body>
        </html>
        `);