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 頁面源碼
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 頁面事件源代碼
2.7 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 模板中
- 這樣,就實現了最簡單的服務端渲染