React服務端渲染之路02——最簡單的服務端渲染

1. 最簡單的服務端渲染

1.1 Home 組件

  • 在 src/containers 下新建 Home 組件,每一個組件都是一個文件夾,組件名都採用 index.js
  • 以 Home 組件爲例,Home 組件就是 src/container/Home/index.js
// 一個非常簡單的組件
import React, { Component } from 'react';

class Home extends Component {
  render() {
    return (
      <div>
        <h1>HELLO, HOME PAGE</h1>
      </div>
    );
  }
}

export default Home;

1.2 創建 server

  • 在 src/server 文件夾下新建 index.js 文件
  • 由於我們使用了 Babel,所以我們可以直接採用 ES6/7/8 語法來寫服務端代碼
  • 這裏渲染的原理就是通過 react-dom/server 的一個方法 renderToString,把 React 組件轉爲普通的 HTML 字符串,直接把這個 HTML 字符串返回給瀏覽器,瀏覽器接收到這串 HTML,會自動解析渲染到瀏覽器上。
  • 這樣我們就創建了一個端口爲 3000 的服務,在瀏覽器直接打開 http://localhost:3000,就可以看到 Home 組件裏的內容已經展示在頁面上,查看網頁源代碼,就發現網頁的源代碼就是 renderToString(<Home />) 生成的 HTML 字符串
  • 這樣,我們就實現了一個最簡單的服務端渲染
  • 注意,renderToString 要與 renderToMarkUp 區分
在 react-dom/server 中,還有一個方法 renderToStaticMarkup,這個方法與 renderToString 的主要作用都是將 React Component 轉化成 HTML 字符串。區別在於 renderToString 生成的 HTML 中的 DOM 會帶有額外的屬性,比如 data-reactroot="",在 renderToStaticMarkup 生成的 HTML 中的 DOM 沒有額外的屬性,可以節省 HTML 字符串的大小。

renderToString 生成的 HTML 裏邊的 DOM 屬性,在客戶端渲染 React 組件的時候,會根據 DOM 的屬性,判斷屬性值是否相等,如果相等,那麼不需要渲染組件,如果不相等,那麼就要重新渲染組件,可以提高頁面性能。而 renderToStaticMarkup 生成的 HTML 裏的 DOM 沒有屬性,所以頁面數據變更的時候,會重新渲染組件,覆蓋掉服務器端的組件。所以,如果頁面是一個純粹的靜態頁面,最好使用 renderToStaticMarkup,否則,最好使用 renderToString。

import express from 'express';
import React from 'react';
// react-dom 提供的一個方法,用來把 React 組件轉爲普通的 html 字符串
// 使用方法就是直接把組件放入這個方法裏即可
import { renderToString } from 'react-dom/server';
import Home from '../containers/Home';

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  let html = renderToString(<Home />);
  console.log(html);
  // 在控制檯輸入 html,得到的就是一個非常簡單的 HTML 字符串
  // <div data-reactroot=""><h1>HELLO, HOME PAGE</h1></div>
  res.send(html);
});

app.listen(PORT, err => {
  if (err) {
    console.log(err);
  } else {
    console.log(`Server is running at http://localhost:${PORT}`);
  }
});
  • home 頁面效果

Home 頁面效果

  • home 頁面源碼

Home 頁面源代碼

2. 同構

  • 在剛纔的頁面上,我們可以看到服務端渲染的頁面,以及頁面展示到頁面上的效果,但是這並不能滿足我們的需要,我們還需要做一些其他的操作

2.1 註冊事件

  • 我們可以在 Home 組件裏添加一個按鈕,給按鈕註冊一個 click 事件,每次點擊,都會加 1。
  • 修改 Home/index.js 裏的代碼,這樣,我們就給 Home 組件註冊了一個事件,在頁面上可以看到效果
  • 但是我們發現,點擊按鈕的時候,沒有任何改變,state 裏的 number 的值沒有發生改變,console.log 也沒有輸出任何值。
  • 因爲我們是服務端渲染,我們的 HTML 代碼是從服務端獲取的,而我們的事件是綁定在 DOM 元素上的,服務端沒有類似於客戶端的 click,mouseover 等事件。所以,點擊這個按鈕沒有任何的效果
import React, { Component } from 'react';

class Home extends Component {

  state = {
    number: 0
  };

  handleClick = () => {
    this.setState({
      number: this.state.number + 1
    });
    console.log(this.state.number);
  };

  render() {
    return (
      <div>
        <h1>HELLO, HOME PAGE</h1>
        <h2>number: {this.state.number}</h2>
        <button onClick={this.handleClick}>click</button>
      </div>
    );
  }
}

export default Home;
  • 此時我們查看頁面的源代碼,我們會發現,頁面上只有 HTML 代碼,沒有任何的 js 代碼
  • 原因就在於服務端使用 react-dom/server 的 renderToString 方法的時候,只能夠處理 HTML,而不能處理事件
  • 因爲服務端是沒有客戶端的 click,mouseout 等事件的,以前我們能夠在頁面點擊發送請求之類的事件,都是客戶端自己創建的,而不是服務端給的,所以我們需要一種方法把事件也註冊到 DOM 節點上,所以我們需要 同構

2.2 同構

  • 什麼是同構?

    • 同構就是前後端採用同一套 js 代碼,採用不同的構建方式,就比如說同一段 js 代碼,既可以運行在瀏覽器端,也可以運行在 Node 端。
  • 爲什麼要同構?

    • 優點是提高代碼的複用,減少代碼的開發,體驗 SSR 帶來的好處。
    • 缺點

      • 需要在不同的平臺上進行不同的構建,有一定的構建成本和開發成本
      • 最主要的是性能損失,客戶端和服務端都要渲染頁面,雖然我們可以通過 DOM DIFF 來優化,但是這個問題,依然不可避免
  • 有一點需要注意到的是,服務端預渲染幫助客戶端獲取到的數據資源,客戶端也要能夠去獲取,因爲如果服務端獲取失敗,客戶端依然可以獲取
  • 在上邊的例子中,我們僅僅是在服務端構建了 React 組件,客戶端沒有構建,所以我們需要在客戶端構建同樣的 React 組件代碼

2.3 配置客戶端的 webpack.client.js

  • 在 package.json 中添加 dev:build:client 的啓動命令,命令內容是 webpack --config webpack.client.js --watch
const path = require('path');

module.exports = {
  mode: 'development',
  target: 'web',
  entry: './src/client/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'client.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};
  • 我們可以發現,在 webpack.server.js 和 webpack.client.js 裏,都有相同的 module 和 mode 屬性,在後邊我們還會添加其他的屬性,所以我們可以把他們相同的內容提取出來,減少代碼的重複。

2.4 公共的 webpack 代碼

  • 我們使用 webpack-merge 這個庫,可以把 webpack 的配置組裝起來,類似於 Object.assign 方法,可以添加很多個 webpack 配置對象,後邊的會把前邊的相同的屬性覆蓋掉。
  • 把公共的代碼添加到 webpack.base.js 中
// webpack.base.js
module.exports = {
  mode: 'development',
  target: 'web',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};
  • 修改 webpack.server.js
// webpack.server.js
const path = require('path');
const merge = require('webpack-merge');
const WebpackNodeExternals = require('webpack-node-externals');
const baseConfig = require('./webpack.base');

module.exports = merge(baseConfig, {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'server.js'
  },
  externals: [WebpackNodeExternals()],
});
  • 修改 webpack.client.js
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');

module.exports = merge(baseConfig, {
  entry: './src/client/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'client.js'
  }
});
  • 這樣,我們就配置好了客戶端和服務端的 webpack,包括 webpack 的基礎配置,接下來就可以構建客戶端的代碼

2.5 構建客戶端代碼

  • 在 client 下創建 index.js 文件
  • 這也是一個 React 文件,所以我們要引入 react,react-dom
  • 由於同構時需要把前後端都用到的代碼進行構建,所以我們要把 Home 組件構建到客戶端代碼中
// client/index.js
import React from 'react'
import { render } from 'react-dom';
import Home from '../containers/Home';

render(<Home/>, window.root);

2.6 服務端添加 HTML 模板

  • 給服務端渲染的內容添加一個模板,在模板中添加一個容器位置,供客戶端使用
  • 同時,要把客戶端構建的 js 代碼,加載到 HTML 頁面中
  • 加載的時候就需要有靜態資源路徑,所以我們用 express 開啓靜態資源服務 app.use(express.static('public'));,這個目錄就是我們在 webpack.client.js 裏配置生成的目錄,裏邊的 client.js 文件就是客戶端 webpack 打包後生成的代碼
  • 這時,剛纔的按鈕的點擊就有效果了,可以看到 number 的改變
app.use(express.static('public'));

app.get('/', (req, res) => {
  let domContent = renderToString(<Home />);
  let html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <title>react-ssr</title>
</head>
<body>
<div id="root">${domContent}</div>
<script src="/client.js"></script>
</body>
</html>
`;
  res.send(html);
});
  • home 頁面事件

home 頁面事件

  • home 頁面事件源代碼

home 頁面事件源代碼

2.7 hydrate

  • 但是現在我們在控制檯看到了一個警告信息

hydrate

  • 這個警告信息是說,如果我們客戶端渲染的和服務端渲染的內容一樣的話,就要使用 hydrate 替換掉 render,所以,我們把客戶端裏的 render 渲染方法替換成 hydrate 渲染方法就可以了
  • 這個警告信息如果不處理也可以,不影響操作,但是在 react 後邊的版本里,如果需要使用 hydrate 但是卻使用了 render,那麼是會報錯的,所以建議還是處理掉
  • 這樣,就沒有了警告信息,同時按鈕也可以正常點擊
// client/index.js
import React from 'react'
import { hydrate } from 'react-dom';
import Home from '../containers/Home';

hydrate(<Home/>, window.root);

3. 總結

  • 到這裏,我們已經實現了一個最簡單的 react 服務端渲染,並且可以觸發瀏覽器的事件
  • 原理

    • 服務端建立一個 HTML 的模板,通過 react-dom/server 下的 renderToString 方法,把 react 組件轉換成純粹的 HTML 字符串,代碼裏叫做 domContent
    • 服務端把 react 組件轉換後的 domContent 字符串,作爲 HTML 模板的內容,填充到模板中,對應的是 id = "root" 的容器
    • 但是現在僅僅是服務端渲染了 HTML 字符串,沒有事件,我們通過同構的方式,把用到的組件,在客戶端也生成同樣的一份 js 代碼,作爲 js 腳本加載到 html 模板中
    • 這樣,就實現了最簡單的服務端渲染
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章