教你如何在React及Redux項目中進行服務端渲染

服務端渲染(SSR: Server Side Rendering)在React項目中有着普遍的應用場景javascript

基於React虛擬DOM的特性,在瀏覽器端和服務端咱們能夠實現同構(可使用同一份代碼來實現多端的功能)php

服務端渲染的優勢主要由三點css

1. 利於SEOhtml

2. 提升首屏渲染速度java

3. 同構直出,使用同一份(JS)代碼實現,便於開發和維護node

 

一塊兒看看如何在實際的項目中實現服務端渲染react

和以往同樣,本次項目也放到了 Github 中,歡迎圍觀 star ~webpack

有純粹的 React,也有 Redux 做爲狀態管理git

使用 webpack 監聽編譯文件,nodemon 監聽服務器文件變更github

使用 redux-saga 處理異步action,使用 express 處理頁面渲染

本項目包含四個頁面,四種組合,滿滿的乾貨,文字可能說不清楚,就去看代碼吧!

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

 

1、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中進行服務端渲染的流程了,說得有點泛泛,仍是本身去看 項目代碼 吧

 

3、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());

 

頁面呈現

 

4、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

 

文章說得錯錯亂亂的,可能沒那麼好理解,仍是去看 項目文件 本身琢磨吧,本身弄下來編譯運行看看

 

5、其餘

若是項目使用了其餘服務器語言的,好比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的朋友,歡迎評論區分享哈~

相關文章
相關標籤/搜索