天天看點

React 原生實作服務端渲染 React 原生實作服務端渲染

React 原生實作服務端渲染

文章出處: 拉 勾 大前端 高薪訓練營

練習代碼位址

一、伺服器端渲染快速開始

1. 實作 React SSR

  1. 引入要渲染的 React 元件
  2. 通過 renderToString 方法 将 React 元件轉換為 HTML字元串
  3. 将結果 HTML 字元串響應到用戶端

renderToString

方法用于将 React 元件轉換為 HTML 字元串,通過

react-dom/server

導入.

2. webpack 打包配置

問題: Node 環境不支援 ESModule 子產品系統,不支援 JSX 文法

3. 項目啟動指令配置

  1. 配置伺服器端打包指令:

    "dev:server-build": "webpack --config webpack.server.js --watch"

  2. 配置服務端啟動指令:

    "dev:server-run": "nodemon --watch build --exec\"node build/bundler.js\""

二、用戶端 React 附加事件

1. 實作思路分析

在用戶端對元件進行二次“渲染”,為元件元素附加事件

2. 用戶端二次“渲染” hydrate

使用 hydrate 方法對元件進行渲染,為元件元素附加事件。

hydrate 方法在實作渲染的時候,會複用原本已經存在的 DOM 節點,減少重新生成節點以及删除原本 DOM 節點的開銷。

通過 react-dom 導入 hydrate

ReactDOM.hydrate(<Home/>, document.getElementById('#root'))
           

3. 用戶端 React 打包配置

  1. webpack 配置

    打包目的:轉換 JSX 文法,轉換浏覽器不識别的進階 JavaScript 文法

    打包目标位置:public檔案夾

  2. 打包啟動指令配置

4. 添加用戶端封包件請求連結

在響應給用戶端的 HTML 代碼中添加 script 标簽,請求用戶端 JavaScript 打封包件。

<html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script src="bundle.js"></script>
    </body>
  </html>
           

5. 伺服器端實作靜态資源通路

伺服器端程式實作靜态資源通路功能,用戶端 JavaScript 打封包件會被作為靜态資源使用。

三、優化

1. 合并 webpack 配置

伺服器端 webpack 配置和用戶端 webpack 配置存在重複,将重複配置抽象到 webpack.base.js 配置檔案中

2. 合并項目啟動指令

目的:使用一個指令啟動項目,解決多個指令啟動的繁瑣問題,通過 npm-run-all 工具實作。

3. 伺服器端打封包件體積優化

問題:在伺服器端打封包件中,包含了 Node 系統子產品,導緻打封包件本身體積龐大。

解決:通過 webpack 配置剔除打封包件中的 Node 子產品。

const nodeExternals = require('webpack-node-externals')
const config = {
  externals: [nodeExternals()]
}
module.exports = merge(baseConfig, config)
           

4. 将啟動伺服器代碼和渲染代碼進行子產品化拆分

優化代碼組織方式,渲染 React 元件代碼是獨立功能,是以把它從伺服器端入口檔案中進行抽離。

四、路由支援

1. 實作思路分析

在 React SSR 項目中需要實作兩端路由。

用戶端路由是用于支援使用者通過點選連結的形式跳轉頁面。

伺服器端路由是用于支援使用者直接從浏覽器位址欄中通路頁面。

用戶端和伺服器端共用一套路由規則。

2. 編寫路由規則

share/routes.js

import Home from './pages/Home'
import List from './pages/List'

export default [
  {
    path: '/',
    component: Home,
    exact: true
  }, {
    path: '/list',
    component: List,
  }
]
           

3. 實作伺服器端路由

  1. Express 路由接受任何請求

    Express 路由接受任何請求

Express 路由接受所有 Get 請求,伺服器端 React 路由通過請求路徑比對要進行渲染的元件

  1. 伺服器端路由配置
import React from 'react'
import {renderToString} from 'react-dom/server'
import { StaticRouter } from "react-router-dom";
import routes from '../share/routes'
import { renderRoutes } from "react-router-config";

export default (req) => {
  const content = renderToString(
    <StaticRouter location={req.path}>
      {renderRoutes(routes)}
    </StaticRouter>
  )
  return `
  <html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script src="bundle.js"></script>
    </body>
  </html>
  `
}
           

4. 實作用戶端路由

添加用戶端路由配置

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from "react-router-dom"
import { renderRoutes } from "react-router-config";
import routes from '../share/routes'

ReactDOM.hydrate(
  <BrowserRouter>
    {renderRoutes(routes)}
  </BrowserRouter>
  , document.getElementById('root'))
           

五、Redux 支援

1. 實作思路分析

在實作了React SSR 的項目中需要實作兩端 Redux.

用戶端 Redux 就是通過用戶端 JavaScript 管理 Store 中的資料.

伺服器端 Redux 就是在伺服器端搭建一套 Redux 代碼,用于管理元件中的資料.

用戶端和伺服器端共用一套 Reducer 代碼.

建立 Store 的代碼由于參數傳遞不同是以不可以共用.

建立異步 dispatch 時報錯,因為浏覽器預設不支援異步函數

Uncaught ReferenceError: regeneratorRuntime is not defined

at eval (user.action.js:17)

at fetchUser (user.action.js:44)

at eval (List.js:16)

at invokePassiveEffectCreate (react-dom.development.js:23482)

at HTMLUnknownElement.callCallback (react-dom.development.js:3945)

at Object.invokeGuardedCallbackDev (react-dom.development.js:3994)

at invokeGuardedCallback (react-dom.development.js:4056)

at flushPassiveEffectsImpl (react-dom.development.js:23569)

at unstable_runWithPriority (scheduler.development.js:646)

at runWithPriority$1 (react-dom.development.js:11276)

babel 開啟 polyfill 支援:

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: [
        [
          '@babel/preset-env',
          {
            useBuiltIns: 'usage'
          }
        ],
        '@babel/preset-react'
      ]
    }
  }
}
           

2. 實作伺服器端 Redux

  1. 建立 Store

server/createStore.js

import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk'
import reducer from '../share/store/reducers'

export default () => createStore(reducer, {}, applyMiddleware(thunk))
           
  1. 配置 Store

server/index.js

import app from './http'
import renderer from './renderer'
import createStore from './createStore'

app.get('*', (req, res) => {
  const store = createStore()
  res.send(renderer(req, store))
})
           

server/renderer.js

import React from 'react'
import {renderToString} from 'react-dom/server'
import { StaticRouter } from "react-router-dom";
import routes from '../share/routes'
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";

export default (req, store) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>
        {renderRoutes(routes)}
      </StaticRouter>
    </Provider>
  )
  return `
  <html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script src="bundle.js"></script>
    </body>
  </html>
  `
}
           

3. 伺服器端 store 資料填充

問題:伺服器端建立的store 是空的,元件并不能從 Store 中擷取到任何資料。

解決:伺服器端在渲染元件之前擷取到元件所需要的資料。

  1. 在元件中添加loadData方法,此方法用于擷取元件所需資料,方法被伺服器端調用
  2. 将loadData方法儲存在目前元件的路由資訊對象中.
  3. 伺服器端在接收到請求後,根據請求位址比對出要渲染的元件的路由資訊
  4. 從路由資訊中擷取元件中的loadData方法并調用方法擷取元件所需資料
  5. 當資料擷取完成以後再渲染元件并将結果響應到用戶端

4. React 警告消除

react-dom.development.js:67 Warning: Did not expect server HTML to contain a
  • in
    • .

      at ul

      at div

      at List (webpack://react-ssr/./src/share/pages/List.js?:19:19)

      at Connect(List) (webpack://react-ssr/./node_modules/react-redux/es/components/connectAdvanced.js?:231:68)

      at Route (webpack://react-ssr/./node_modules/react-router/esm/react-router.js?:464:29)

      at Switch (webpack://react-ssr/./node_modules/react-router/esm/react-router.js?:670:29)

      at Router (webpack://react-ssr/./node_modules/react-router/esm/react-router.js?:93:30)

      at BrowserRouter (webpack://react-ssr/./node_modules/react-router-dom/esm/react-router-dom.js?:59:35)

      at Provider (webpack://react-ssr/./node_modules/react-redux/es/components/Provider.js?:16:20)

警告原因:用戶端 Store 在初始狀态下是沒有資料的,在渲染元件的時候生成的是空 ul ,但是伺服器端是先擷取資料再進行的元件渲染,

是以生成的是有子元素的 ul , hydrate 方法在對比的時候發現兩者不-緻, 是以報了個警告.

解決思路:将伺服器端擷取到的資料回填給用戶端,讓用戶端擁有初始資料.

  1. 伺服器響應 Store 初始狀态

server/renderer.js

import React from 'react'
import {renderToString} from 'react-dom/server'
import { StaticRouter } from "react-router-dom";
import routes from '../share/routes'
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";
import serialize from 'serialize-javascript'

export default (req, store) => {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>
        {renderRoutes(routes)}
      </StaticRouter>
    </Provider>
  )
  const initialState = JSON.stringify(JSON.parse(serialize(store.getState())))
  return `
  <html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script>window.INITIAL_STATE = ${initialState} </script>
      <script src="bundle.js"></script>
    </body>
  </html>
  `
}
           
  1. 用戶端設定 Store 初始狀态

client/createStore.js

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducer from '../share/store/reducers'

const store = createStore(reducer, window.INITIAL_STATE, applyMiddleware(thunk))

export default store
           

4. 防止 XSS 攻擊

轉移狀态中的惡意代碼

let response = {
  data: [{id: 1, name: '<script>alert(1)</script>'}]
}
import serialize from 'serialize-javascript'

const initialState = serialize(store.getState())