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

来源:https://www.cnblogs.com/imwtr/archive/2018/09/03/9576546.html
-Advertisement-
Play Games

服務端渲染(SSR: Server Side Rendering)在React項目中有著廣泛的應用場景 基於React虛擬DOM的特性,在瀏覽器端和服務端我們可以實現同構(可以使用同一份代碼來實現多端的功能) 服務端渲染的優點主要由三點 1. 利於SEO 2. 提高首屏渲染速度 3. 同構直出,使用 ...


服務端渲染(SSR: Server Side Rendering)在React項目中有著廣泛的應用場景

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

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

1. 利於SEO

2. 提高首屏渲染速度

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

 

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

項目地址 ,歡迎圍觀!

有純粹的 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的朋友,歡迎評論區分享哈~

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Hbase過濾器簡介 HBase的基本API,包括增、刪、改、查等,增、刪都是相對簡單的操作,與傳統的RDBMS相比,這裡的查詢操作略顯蒼白,只能根據特性的行鍵進行查詢(Get)或者根據行鍵的範圍來查詢(Scan)。 HBase不僅提供了這些簡單的查詢,而且提供了更加高級的過濾器(Filter)來查 ...
  • MySQL5.5.40破解版地址(永久有效):鏈接:https://pan.baidu.com/s/1n-sODjoCdeSGP8bDGxl23Q 密碼:qjjy 第2節 資料庫的介紹 MySQL:開源免費的資料庫,小型的資料庫,已經被 Oracle 收購了。 MySQL6.x 版本也開始收費。後來 ...
  • UNIQUE約束添加規則 1、唯一約束確保表中的一列數據沒有相同的值。 2、與主鍵約束類似,唯一約束也強制唯一性,但唯一約束用於非主鍵的一列或者多列的組合,且一個表可以定義多個唯一約束。 使用SSMS資料庫管理工具添加UNIQUE約束 1、連接資料庫,選擇資料庫,選擇數據表-》右鍵點擊-》選擇設計。 ...
  • 一. 遍歷目錄 在 linux系統上,可以使用cd切換目錄命令。 分二種路徑,一是絕對文件路徑,另一種是相對文件路徑。 1. 絕對文件路徑 在虛擬目錄中採用文件路徑,以虛擬目錄根目錄開始,相當於目錄的全名。例如指定usr目錄下的bin目錄(查看文件路徑,使用pwd命令,是一個很好的習慣。該命令可以返 ...
  • 參考http://redisdoc.com/ 參考http://redis.io/commands 連接操作相關的命令 預設直接連接 遠程連接-h 192.168.1.20 -p 6379 ping:測試連接是否存活如果正常會返回pong echo:列印 select:切換到指定的資料庫,資料庫索引 ...
  • 移動互聯網的迅速崛起讓數據變得更為多樣、豐富。它的移動性,它的碎片化,它的私密性和隨時性都剛好彌補了用戶離開桌面電腦之後的數據,從而與原有的互聯網數據一起很好滴勾勒出一個網民一天的生活,日常生活的數據化。現如今大數據已經上升到國家戰略層面,企業對於大數據的關註和重視程度也在不斷提升。今天小編就給大家 ...
  • --Web移動端商城移動端商城手機網站html整套模板,web移動商城仿app手機模板下載。原生的js和jquery-1.6.2.min.js,頁面才有html5自適應。包括首頁(輪播,導航)、兼職(視頻,圖文話題,申請)、優惠(商家,優惠捲)、美食(購物車,結算,地址)我的(登錄,訂單,收藏,地址 ...
  • 作為前端最火的構建工具,是前端自動化工具鏈 最重要的部分 ,使用門檻較高。本系列是筆者自己的學習記錄,比較基礎,希望通過 問題 + 解決方式 的模式,以前端構建中遇到的具體需求為出發點,學習 工具中相應的處理辦法。(本篇中的參數配置及使用方式均基於 ) 一. tapable概述 地址: "【tapa ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...