• 教你如何在React及Redux项目中进行服务端渲染


    服务端渲染(SSR: Server Side Rendering)在React项目中有着广泛的应用场景

    基于React虚拟DOM的特性,在浏览器端和服务端我们可以实现同构(可以使用同一份代码来实现多端的功能)

    服务端渲染的优点主要由三点

    1. 利于SEO

    2. 提高首屏渲染速度

    3. 同构直出,使用同一份(JS)代码实现,便于开发和维护

    一起看看如何在实际的项目中实现服务端渲染

    和以往一样,本次项目也放到了 Github 中,欢迎围观 star ~

    有纯粹的 React,也有 Redux 作为状态管理

    使用 webpack 监听编译文件,nodemon 监听服务器文件变动

    使用 redux-saga 处理异步action,使用 express 处理页面渲染

    本项目包含四个页面,四种组合,满满的干货,文字可能说不清楚,就去看代码吧!

    1. React
    2. React + SSR
    3. React + Redux
    4. React + Redux + SSR

    一、React

    实现一个最基本的React组件,就能搞掂第一个页面了

    /**
     * 消息列表
     */
    class Message extends Component {
        constructor(props) {
            super(props);
    
            this.state = {
                msgs: []
            };
        }
    
        componentDidMount() {
            setTimeout(() => {
                this.setState({
                    msgs: [{
                        id: '1',
                        content: '我是消息我是消息我是消息',
                        time: '2018-11-23 12:33:44',
                        userName: '王羲之'
                    }, {
                        id: '2',
                        content: '我是消息我是消息我是消息2',
                        time: '2018-11-23 12:33:45',
                        userName: '王博之'
                    }, {
                        id: '3',
                        content: '我是消息我是消息我是消息3',
                        time: '2018-11-23 12:33:44',
                        userName: '王安石'
                    }, {
                        id: '4',
                        content: '我是消息我是消息我是消息45',
                        time: '2018-11-23 12:33:45',
                        userName: '王明'
                    }]
                });
            }, 1000);
        }
    
        // 消息已阅
        msgRead(id, e) {
            let msgs = this.state.msgs;
            let itemIndex = msgs.findIndex(item => item.id === id);
    
            if (itemIndex !== -1) {
                msgs.splice(itemIndex, 1);
    
                this.setState({
                    msgs
                });
            }
        }
    
        render() {
            return (
                <div>
                    <h4>消息列表</h4>
                    <div className="msg-items">
                    {
                        this.state.msgs.map(item => {
                            return (
                                <div key={item.id} className="msg-item">
                                    <p className="msg-item__header">{item.userName} - {item.time}</p>
                                    <p className="msg-item__content">{item.content}</p>
                                    <a href="javascript:;" className="msg-item__read" onClick={this.msgRead.bind(this, item.id)}>&times;</a>
                                </div>
                            )
                        })
                    }
                    </div>
                </div>
            )
        }
    }
    
    render(<Message />, document.getElementById('content'));

    是不是很简单,代码比较简单就不说了

    来看看页面效果

    可以看到页面白屏时间比较长

    这里有两个白屏

    1. 加载完JS后才初始化标题

    2. 进行异步请求数据,再将消息列表渲染

    看起来是停顿地比较久的,那么使用服务端渲染有什么效果呢?

    二. React + SSR

    在讲如何实现之前,先看看最终效果

    可以看到页面是直出的,没有停顿

    在React 15中,实现服务端渲染主要靠的是 ReactDOMServer 的 renderToString 和 renderToStaticMarkup方法。

    let ReactDOMServer = require('react-dom/server');
    
    ReactDOMServer.renderToString(<Message preloadState={preloadState} />)
    
    ReactDOMServer.renderToStaticMarkup(<Message preloadState={preloadState} />)

    将组件直接在服务端处理为字符串,我们根据传入的初始状态值,在服务端进行组件的初始化

    然后在Node环境中返回,比如在Express框架中,返回渲染一个模板文件

          res.render('messageClient/message.html', {
                appHtml: appHtml,
                preloadState: JSON.stringify(preloadState).replace(/</g, '\u003c')
            });

    这里设置了两个变量传递给模板文件

    appHtml 即为处理之后的组件字符串

    preloadState 为服务器中的初始状态,浏览器的后续工作要基于这个初始状态,所以需要将此变量传递给浏览器初始化

            <div id="content">
                <|- appHtml |>
            </div>
            <script id="preload-state">
                var PRELOAD_STATE = <|- preloadState |>
            </script>

    express框架返回之后即为在浏览器中看到的初始页面

    需要注意的是这里的ejs模板进行了自定义分隔符,因为webpack在进行编译时,HtmlWebpackPlugin 插件中自带的ejs处理器可能会和这个模板中的ejs变量冲突

    在express中自定义即可

    // 自定义ejs模板
    app.engine('html', ejs.__express);
    app.set('view engine', 'html');
    ejs.delimiter = '|';

    接下来,在浏览器环境的组件中(以下这个文件为公共文件,浏览器端和服务器端共用),我们要按照 PRELOAD_STATE 这个初始状态来初始化组件

    class Message extends Component {
        constructor(props) {
            super(props);
    
            this.state = {
                msg: []
            };
    
            // 根据服务器返回的初始状态来初始化
            if (typeof PRELOAD_STATE !== 'undefined') {
                this.state.msgs = PRELOAD_STATE;
                // 清除
                PRELOAD_STATE = null;
                document.getElementById('preload-state').remove();
            }
            // 此文件为公共文件,服务端调用此组件时会传入初始的状态preloadState
            else {
                this.state.msgs = this.props.preloadState;
            }
    
            console.log(this.state);
        }
    
        componentDidMount() {
            // 此处无需再发请求,由服务器处理
        }
    ...

    核心就是这些了,这就完了么?

    哪有那么快,还得知道如何编译文件(JSX并不是原生支持的),服务端如何处理,浏览器端如何处理

    接下来看看项目的文件结构

       

    把注意力集中到红框中

    直接由webpack.config.js同时编译浏览器端和服务端的JS模块

    module.exports = [
        clientConfig,
        serverConfig
    ];

    浏览器端的配置使用 src 下的 client目录,编译到 dist 目录中

    服务端的配置使用 src 下的 server 目录,编译到 distSSR 目录中。在服务端的配置中就不需要进行css文件提取等无关的处理的,关注编译代码初始化组件状态即可

    另外,服务端的配置的ibraryTarget记得使用 'commonjs2',才能为Node环境所识别

    // 文件输出配置
        output: {
            // 输出所在目录
            path: path.resolve(__dirname, '../public/static/distSSR/js/'),
            filename: '[name].js',
            library: 'node',
            libraryTarget: 'commonjs2'
        },

    client和server只是入口,它们的公共部分在 common 目录中

    在client中,直接渲染导入的组件  

    import React, {Component} from 'react';
    import {render, hydrate, findDOMNode} from 'react-dom';
    import Message from '../common/message';
    
    hydrate(<Message />, document.getElementById('content'));

    这里有个 render和hydrate的区别

    在进行了服务端渲染之后,浏览器端使用render的话会按照状态重新初始化一遍组件,可能会有抖动的情况;使用 hydrate则只进行组件事件的初始化,组件不会从头初始化状态

    建议使用hydrate方法,在React17中 使用了服务端渲染之后,render将不再支持

    在 server中,导出这个组件给 express框架调用

    import Message from '../common/message';
    
    let ReactDOMServer = require('react-dom/server');
    
    /**
     * 提供给Node环境调用,传入初始状态
     * @param  {[type]} preloadState [description]
     * @return {[type]}              [description]
     */
    export function init(preloadState) {
        return ReactDOMServer.renderToString(<Message preloadState={preloadState} />);
    };

    需要注意的是,这里不能直接使用 module.exports = ... 因为webpack不支持ES6的 import 和这个混用

    在 common中,处理一些浏览器端和服务器端的差异,再导出

    这里的差异主要是变量的使用问题,在Node中没有window document navigator 等对象,直接使用会报错。且Node中的严格模式直接访问未定义的变量也会报错

    所以需要用typeof 进行变量检测,项目中引用的第三方插件组件有使用到了这些浏览器环境对象的,要注意做好兼容,最简便的方法是在 componentDidMount 中再引入这些插件组件

    另外,webpack的style-loader也依赖了这些对象,在服务器配置文件中需要将其移除

     {
                test: /.css$/,
                loaders: [
                    // 'style-loader',
                    'happypack/loader?id=css'
                ]
            }

    在Express的服务器框架中,messageSSR 路由 渲染页面之前做一些异步操作获取数据

    // 编译后的文件路径
    let distPath = '../../public/static/distSSR/js';
    
    module.exports = function(req, res, next) {
        // 如果需要id
        let id = 'req.params.id';
    
        console.log(id);
    
        getDefaultData(id);
    
        async function getDefaultData(id) {
            let appHtml = '';
            let preloadState = await getData(id);
    
            console.log('preloadState', preloadState);
    
            try {
                // 获取组件的值(字符串)
                appHtml = require(`${distPath}/message`).init(preloadState);
            } catch(e) {
                console.log(e);
                console.trace();
            }
    
            res.render('messageClient/message.html', {
                appHtml: appHtml,
                preloadState: JSON.stringify(preloadState).replace(/</g, '\u003c')
            });
        }
    };

    使用到Node来开启服务,每次改了服务器文件之后就得重启比较麻烦

    使用 nodemon工具来监听文件修改自动更新服务器,添加配置文件 nodemon.json

    {
        "restartable": "rs",
        "ignore": [
            ".git",
            "node_modules/**/node_modules"
        ],
        "verbose": true,
        "execMap": {
            "js": "node --harmony"
        },
        "watch": [
            "server/",
            "public/static/distSSR"
        ],
        "env": {
            "NODE_ENV": "development"
        },
        "ext": "js,json"
    }

    当然,对于Node环境不支持JSX这个问题,除了使用webpack进行编译之外,

    还可以在Node中执行 babel-node 来即时地编译文件,不过这种方式会导致每次编译非常久(至少比webpack久)

    在React16 中,ReactDOMServer 除了拥有 renderToString 和 renderToStaticMarkup这两个方法之外,

    还有 renderToNodeStream  和 renderToStaticNodeStream 两个流的方法

    它们不是返回一个字符串,而是返回一个可读流,一个用于发送字节流的对象的Node Stream类

    渲染到流可以减少你的内容的第一个字节(TTFB)的时间,在文档的下一部分生成之前,将文档的开头至结尾发送到浏览器。 当内容从服务器流式传输时,浏览器将开始解析HTML文档

    以下是使用实例,本文不展开

    // using Express
    import { renderToNodeStream } from "react-dom/server"
    import MyPage from "./MyPage"
    app.get("/", (req, res) => {
      res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
      res.write("<div id='content'>"); 
      const stream = renderToNodeStream(<MyPage/>);
      stream.pipe(res, { end: false });
      stream.on('end', () => {
        res.write("</div></body></html>");
        res.end();
      });
    });

    这便是在React中进行服务端渲染的流程了,说得有点泛泛,还是自己去看 项目代码 吧

    三、React + Redux

    React的中的数据是单向流动的,即父组件状态改变之后,可以通过props将属性传递给子组件,但子组件并不能直接修改父级的组件。

    一般需要通过调用父组件传来的回调函数来间接地修改父级状态,或者使用 Context ,使用 事件发布订阅机制等。

    引入了Redux进行状态管理之后,就方便一些了。不过会增加代码复杂度,另外要注意的是,React 16的新的Context特性貌似给Redux带来了不少冲击

    在React项目中使用Redux,当某个处理有比较多逻辑时,遵循胖action瘦reducer,比较通用的建议时将主要逻辑放在action中,在reducer中只进行更新state的等简单的操作

    一般还需要中间件来处理异步的动作(action),比较常见的有四种 redux-thunk  redux-saga  redux-promise  redux-observable ,它们的对比

    这里选用了 redux-saga,它比较优雅,管理异步也很有优势

    来看看项目结构

    我们将 home组件拆分出几个子组件便于维护,也便于和Redux进行关联

    home.js 为入口文件

    使用 Provider 包装组件,传入store状态渲染组件

    import React, {Component} from 'react';
    import {render, findDOMNode} from 'react-dom';
    import {Provider} from 'react-redux';
    
    // 组件入口
    import Home from './homeComponent/Home.jsx';
    import store from './store';
    
    /**
     * 组装Redux应用
     */
    class App extends Component {
        render() {
            return (
                <Provider store={store}>
                    <Home />
                </Provider>
            )
        }
    }
    
    render(<App />, document.getElementById('content'));

    store/index.js 中为状态创建的过程

    这里为了方便,就把服务端渲染的部分也放在一起了,实际上它们的区别不是很大,仅仅是 defaultState初始状态的不同而已

    import {createStore, applyMiddleware, compose} from 'redux';
    import createSagaMiddleware from 'redux-saga';
    // import {thunk} from 'redux-thunk';
    
    import reducers from './reducers';
    import wordListSaga from './workListSaga';
    import state from './state';
    
    const sagaMiddleware = createSagaMiddleware();
    
    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
    
    let defaultState = state;
    
    // 用于SSR
    // 根据服务器返回的初始状态来初始化
    if (typeof PRELOAD_STATE !== 'undefined') {
        defaultState = Object.assign({}, defaultState, PRELOAD_STATE);
        // 清除
        PRELOAD_STATE = null;
        document.getElementById('preload-state').remove();
    }
    
    let store = createStore(
        reducers,
        defaultState,
        composeEnhancers(
            applyMiddleware(sagaMiddleware)
        ));
    
    sagaMiddleware.run(wordListSaga);
    
    export default store;

    我们将一部分action(基本是异步的)交给saga处理

    在workListSaga.js中,

     1 import {delay} from 'redux-saga';
     2 import {put, fork, takeEvery, takeLatest, call, all, select} from 'redux-saga/effects';
     3 
     4 import * as actionTypes from './types';
     5 
     6 /**
     7  * 获取用户信息
     8  * @yield {[type]} [description]
     9  */
    10 function* getUserInfoHandle() {
    11     let state = yield select();
    12 
    13     return yield new Promise((resolve, reject) => {
    14         setTimeout(() => {
    15             resolve({
    16                 sex: 'male',
    17                 age: 18,
    18                 name: '王羲之',
    19                 avatar: '/public/static/imgs/avatar.png'
    20             });
    21         }, 500);
    22     });
    23 }
    24 
    25 /**
    26  * 获取工作列表
    27  * @yield {[type]} [description]
    28  */
    29 function* getWorkListHandle() {
    30     let state = yield select();
    31 
    32     return yield new Promise((resolve, reject) => {
    33         setTimeout(() => {
    34             resolve({
    35                 todo: [{
    36                     id: '1',
    37                     content: '跑步'
    38                 }, {
    39                     id: '2',
    40                     content: '游泳'
    41                 }],
    42 
    43                 done: [{
    44                     id: '13',
    45                     content: '看书'
    46                 }, {
    47                     id: '24',
    48                     content: '写代码'
    49                 }]
    50             });
    51         }, 1000);
    52     });
    53 }
    54 
    55 /**
    56  * 获取页面数据,action.payload中如果为回调,可以处理一些异步数据初始化之后的操作
    57  * @param {[type]} action        [description]
    58  * @yield {[type]} [description]
    59  */
    60 function* getPageInfoAsync(action) {
    61     console.log(action);
    62 
    63     let userInfo = yield call(getUserInfoHandle);
    64 
    65     yield put({
    66         type: actionTypes.INIT_USER_INFO,
    67         payload: userInfo
    68     });
    69 
    70     let workList = yield call(getWorkListHandle);
    71 
    72     yield put({
    73         type: actionTypes.INIT_WORK_LIST,
    74         payload: workList
    75     });
    76 
    77     console.log('saga done');
    78 
    79     typeof action.payload === 'function' && action.payload();
    80 }
    81 
    82 /**
    83  * 获取页面数据
    84  * @yield {[type]} [description]
    85  */
    86 export default function* getPageInfo() {
    87     yield takeLatest(actionTypes.INIT_PAGE, getPageInfoAsync);
    88 }
    View Code

    监听页面的初始化action actionTypes.INIT_PAGE ,获取数据之后再触发一个action ,转交给reducer即可

    let userInfo = yield call(getUserInfoHandle);
    
        yield put({
            type: actionTypes.INIT_USER_INFO,
            payload: userInfo
        });

    reducer中做的事主要是更新状态,

    import * as actionTypes from './types';
    import defaultState from './state';
    
    /**
     * 工作列表处理
     * @param  {[type]} state  [description]
     * @param  {[type]} action [description]
     * @return {[type]}        [description]
     */
    function workListReducer(state = defaultState, action) {
        switch (action.type) {
            // 初始化用户信息
            case actionTypes.INIT_USER_INFO:
                // 返回新的状态
                return Object.assign({}, state, {
                    userInfo: action.payload
                });
    
            // 初始化工作列表
            case actionTypes.INIT_WORK_LIST:
                return Object.assign({}, state, {
                    todo: action.payload.todo,
                    done: action.payload.done
                });
    
            // 添加任务
            case actionTypes.ADD_WORK_TODO:
                return Object.assign({}, state, {
                    todo: action.payload
                });
    
            // 设置任务完成
            case actionTypes.SET_WORK_DONE:
                return Object.assign({}, state, {
                    todo: action.payload.todo,
                    done: action.payload.done
                });
    
            default:
                return state
        }
    }

    在 action.js中可以定义一些常规的action,比如

    export function addWorkTodo(todoList, content) {
        let id = Math.random();
    
        let todo = [...todoList, {
            id,
            content
        }];
    
        return {
            type: actionTypes.ADD_WORK_TODO,
            payload: todo
        }
    }
    
    /**
     * 初始化页面信息
     * 此action为redux-saga所监听,将传入saga中执行
     */
    export function initPage(cb) {
        console.log(122)
        return {
            type: actionTypes.INIT_PAGE,
            payload: cb
        };
    }

    回到刚才的 home.js入口文件,在其引入的主模块 home.jsx中,我们需要将redux的东西和这个 home.jsx绑定起来

    import {connect} from 'react-redux';
    
    // 子组件
    import User from './user';
    import WorkList from './workList';
    
    import  {getUrlParam} from '../util/util'
    import '../../scss/home.scss';
    
    import {
        initPage
    } from '../store/actions.js';
    
    /**
     * 将redux中的state通过props传给react组件
     * @param  {[type]} state [description]
     * @return {[type]}       [description]
     */
    function mapStateToProps(state) {
        return {
            userInfo: state.userInfo,
            // 假如父组件Home也需要知悉子组件WorkList的这两个状态,则可以传入这两个属性
            todo: state.todo,
            done: state.done
        };
    }
    
    /**
     * 将redux中的dispatch方法通过props传给react组件
     * @param  {[type]} state [description]
     * @return {[type]}       [description]
     */
    function mapDispatchToProps(dispatch, ownProps) {
        return {
            // 通过props传入initPage这个dispatch方法
            initPage: (cb) => {
                dispatch(initPage(cb));
            }
        };
    }

    ...

    class Home extends Component {
    ...

    export default connect(mapStateToProps, mapDispatchToProps)(Home);

    当然,并不是只能给store绑定一个组件

    如果某个组件的状态可以被其他组件共享,或者这个组件需要访问store,按根组件一层一层通过props传入很麻烦的话,也可以直接给这个组件绑定store

    比如这里的 workList.jsx 也进行了绑定,user.jsx这种只需要展示数据的组件,或者其他一些自治(状态在内部管理,和外部无关)的组件,则不需要引入redux的store,也挺麻烦的

    绑定之后,我们需要在 Home组件中调用action,开始获取数据

       /**
         * 初始获取数据之后的某些操作
         * @return {[type]} [description]
         */
        afterInit() {
            console.log('afterInit');
        }
    
        componentDidMount() {
            console.log('componentDidMount');
    
            // 初始化发出 INIT_PAGE 操作
            this.props.initPage(() => {
                this.afterInit();
            });
        }

    这里有个小技巧,如果在获取异步数据之后要接着进行其他操作,可以传入 callback ,我们在action的payload中置入了这个 callback,方便调用

    然后Home组件中的已经没有多少state了,已经交由store管理,通过mapStateToProps传入

    所以可以根据props拿到这些属性

    <User {...this.props.userInfo} />

    或者调用传入的 reducer ,间接地派发一些action

        // 执行 ADD_WORK_TODO
            this.props.addWorkTodo(this.props.todo, content.trim());

    页面呈现

    四、React + Redux + SSR

    可以看到上图是有一些闪动的,因为数据不是一开始就存在

    考虑加入SSR,先来看看最终页面效果,功能差不多,但直接出来了,看起来很美好呀~

    在Redux中加入SSR, 其实跟纯粹的React组件是类似的。

    官方给了一个简单的例子

    都是在服务器端获取初始状态后处理组件为字符串,区别主要是React直接使用state, Redux直接使用store

    浏览器中我们可以为多个页面使用同一个store,但在服务器端不行,我们需要为每一个请求创建一个store

    再来看项目结构,Redux的SSR使用到了红框中的文件

    服务端路由homeSSR与messageSSR类似,都是返回数据

    服务端入口文件 server中的home.js 则是创建一个新的 store, 然后传入ReactDOMServer进行处理返回

    import {createStore} from 'redux';
    import reducers from '../store/reducers';
    import App from '../common/home';
    import defaultState from '../store/state';
    
    let ReactDOMServer = require('react-dom/server');
    
    export function init(preloadState) {
        // console.log(preloadState);
    
        let defaultState = Object.assign({}, defaultState, preloadState);
    
        // 服务器需要为每个请求创建一份store,并将状态初始化为preloadState
        let store = createStore(
            reducers,
            defaultState
        );
    
        return ReactDOMServer.renderToString(<App store={store} />);
    };

    同样的,我们需要在common文件中处理 Node环境与浏览器环境的一些差异

    比如在 home.jsx 中,加入

    // 公共部分,在Node环境中无window document navigator 等对象
    if (typeof window === 'undefined') {
        // 设置win变量方便在其他地方判断环境
        global.win = false;
        global.window = {};
        global.document = {};
    }

    另外组件加载之后也不需要发请求获取数据了

    /**
         * 初始获取数据之后的某些操作
         * @return {[type]} [description]
         */
        afterInit() {
            console.log('afterInit');
        }
    
        componentDidMount() {
            console.log('componentDidMount');
    
            // 初始化发出 INIT_PAGE 操作;
            // 已交由服务器渲染
            // this.props.initPage(() => {
                this.afterInit();
            // });
        }

    common中的home.js入口文件用于给组件管理store, 与未用SSR的文件不同(js目录下面的home.js入口)

    它需要同时为浏览器端和服务器端服务,所以增加一些判断,然后导出

    if (module.hot) {
        module.hot.accept();
    }
    
    import React, {Component} from 'react';
    import {render, findDOMNode} from 'react-dom';
    import Home from './homeComponent/home.jsx';
    import {Provider} from 'react-redux';
    import store from '../store';
    
    class App extends Component {
        render() {
            // 如果为Node环境,则取由服务器返回的store值,否则使用 ../store中返回的值
            let st = global.win === false ? this.props.store : store;
    
            return (
                <Provider store={st}>
                    <Home />
                </Provider>
            )
        }
    }
    
    export default App;

    浏览器端的入口文件 home.js 直接引用渲染即可

    import React, {Component} from 'react';
    import {render, hydrate, findDOMNode} from 'react-dom';
    import App from '../common/home';
    
    // render(<App />, document.getElementById('content'));
    hydrate(<App />, document.getElementById('content'));

    这便是Redux 加上 SSR之后的流程了

    其实还漏了一个Express的server.js服务文件,也就一点点代码

     1 const express = require('express');
     2 const path = require('path');
     3 const app = express();
     4 const ejs = require('ejs');
     5 
     6 // 常规路由页面
     7 let home = require('./routes/home');
     8 let message = require('./routes/message');
     9 
    10 // 用于SSR服务端渲染的页面
    11 let homeSSR = require('./routes/homeSSR');
    12 let messageSSR = require('./routes/messageSSR');
    13 
    14 app.use(express.static(path.join(__dirname, '../')));
    15 
    16 // 自定义ejs模板
    17 app.engine('html', ejs.__express);
    18 app.set('view engine', 'html');
    19 ejs.delimiter = '|';
    20 
    21 app.set('views', path.join(__dirname, '../views/'));
    22 
    23 app.get('/home', home);
    24 app.get('/message', message);
    25 
    26 app.get('/ssr/home', homeSSR);
    27 app.get('/ssr/message', messageSSR);
    28 
    29 let port = 12345;
    30 
    31 app.listen(port, function() {
    32     console.log(`Server listening on ${port}`);
    33 });
    View Code

    文章说得错错乱乱的,可能没那么好理解,还是去看 项目文件 自己琢磨吧,自己弄下来编译运行看看

    五、其他

    如果项目使用了其他服务器语言的,比如PHP Yii框架 Smarty ,把服务端渲染整起来可能没那么容易

    其一是 smarty的模板语法和ejs的不太搞得来

    其二是Yii框架的路由和Express的长得不太一样

    在Nginx中配置Node的反向代理,配置一个 upstream ,然后在server中匹配 location ,进行代理配置

    upstream connect_node {
        server localhost:54321;
        keepalive 64;
    }
    
    ...
    
    server
    {
        listen 80;
            ...
    
        location / {
            index index.php index.html index.htm;
        }
    
            location ~ (home|message)/d+$ {
                proxy_pass http://connect_node;
            }
    
        ...

    更多配置

    想得头大,干脆就不想了,有用过Node进行中转代理实现SSR的朋友,欢迎评论区分享哈~

  • 相关阅读:
    使用XHR2或Jsonp实现跨域以及实现原理
    正则表达式环视
    hibernate>一对一关联映射 (onetoone) 小强斋
    上传下载 小强斋
    Struts2><s:token />标签防止重复提交 小强斋
    hibernate>一对一关联映射 (onetoone) 小强斋
    Struts2>interceptor 小强斋
    hibernate>一对多关联映射 小强斋
    Struts2><s:token />标签防止重复提交 小强斋
    hibernate>多对多关联映射 小强斋
  • 原文地址:https://www.cnblogs.com/imwtr/p/9576546.html
Copyright © 2020-2023  润新知