教你如何在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的朋友,歡迎評論區分享哈~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM