關(guān)于服務(wù)端渲染也就是我們說的SSR大多數(shù)人都聽過這個概念,很多同學或許在公司中已經(jīng)做過服務(wù)端渲染的項目了,主流的單頁面應(yīng)用比如說Vue或者React開發(fā)的項目采用的一般都是客戶端渲染的模式也就是我們說的CSR。
但是這種模式會帶來明顯的兩個問題,第一個就是TTFP時間比較長,TTFP指的就是首屏展示時間,同時不具備SEO排名的條件,搜索引擎上排名不是很好。所以我們可以借助一些工具來進行改良我們的項目,將單頁面應(yīng)用編程服務(wù)器端渲染項目,這樣就可以解決掉這些問題了。
目前主流的服務(wù)器端渲染框架也就是SSR框架有針對于Vue的Nuxt.js和針對React的Next.js這兩個。這里我們并不使用這些SSR框架,而是從零開始完整搭建一套SSR框架,來熟悉他的底層原理。
服務(wù)器端編寫 React 組件
如果是客戶端渲染,瀏覽器首先會向瀏覽器發(fā)送請求,服務(wù)器返回頁面的html文件,然后html中再向服務(wù)器發(fā)送請求,服務(wù)器返回js文件,js文件在瀏覽器中執(zhí)行繪制出頁面結(jié)構(gòu)渲染到瀏覽器完成頁面渲染。
如果是服務(wù)器端渲染這個流程就不同了,瀏覽器發(fā)送請求,服務(wù)器端運行React代碼生成頁面,然后服務(wù)器將生成好的頁面返回給瀏覽器,瀏覽器進行渲染。這種情況下React代碼就是服務(wù)器的一部分而不是前端部分了。
這里我們進行代碼的演示,首選需要npm init初始化項目,然后安裝react,express,webpack,webpack-cli,webpack-node-externals。
我們首先編寫一個React的組件。 .src/components/Home/index.js, 因為我們這個js是在node環(huán)境執(zhí)行的所以我們要遵循CommonJS規(guī)范,使用require和module.exports進行導(dǎo)入導(dǎo)出。
1
2
3
4
5
6
7
8
9
|
const React = require( 'react' ); const Home = () => { return <div>home</div> } module.exports = { default : Home }; |
我們這里開發(fā)的Home組件是不能直接在node中運行的,需要借助webpack工具將jsx語法打包編譯成js語法,讓nodejs可以爭取的識別,我們需要創(chuàng)建一個webpack.server.js文件。
在服務(wù)器端使用webpack需要添加一個target為node的鍵值對。我們知道在服務(wù)器端如果使用path路徑是不需要打包到j(luò)s中的,如果在瀏覽器端使用了path是需要打包到j(luò)s中的,所以在服務(wù)器端和在瀏覽器端需要編譯出來的js是完全不同的。所以我們在打包的時候要告訴webpack打包的是服務(wù)器端的代碼還是瀏覽器端的代碼。
entry入口文件就是我們node的啟動文件,這里我們寫成./src/index.js,輸出的output文件名稱為bundle,目錄在跟目錄的build文件夾中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
const Path = require( 'path' ); const NodeExternals = require( 'webpack-node-externals' ); // 服務(wù)端運行webpack需要運行NodeExternals, 他的作用是將express這類node模塊不被打包到j(luò)s里。 module.exports = { target: 'node' , mode: 'development' , entry: './src/server/index.js' , output: { filename: 'bundle.js' , path: Path.resolve(__dirname, 'build' ) }, externals: [NodeExternals()], module: { rules: [ { test: /.js?$/, loader: 'babel-loader' , exclude: /node_modules/, options: { presets: [ 'react' , 'stage-0' , [ 'env' , { targets: { browsers: [ 'last 2 versions' ] } }]] } } ] } } |
安裝依賴模塊
1
|
npm install babel-loader babel-core babel-preset-react babel-preset-stage-0 babel-preset- env --save |
接著我們這里基于express模塊來編寫一個簡單的服務(wù)。./src/server/index.js
1
2
3
4
5
6
7
8
|
var express = require( 'express' ); var app = express(); const Home = require( '../Components/Home' ); app.get( '*' , function (req, res) { res.send(`<h1>hello</h1>`); }) var server = app.listen(3000); |
運行webpack使用webpack.server.js配置文件來執(zhí)行。
1
|
webpack --config webpack.server.js |
打包之后在我們的目錄下會出現(xiàn)一個bundle.js,這個js就是我們打包生成的最終可以運行的代碼。我們可以使用node運行這個文件, 就啟動了一個3000端口的服務(wù)器。我們訪問127.0.0.1:3000可以訪問這個服務(wù),看到瀏覽器輸出Hello。
1
|
node ./build/bundile.js |
上面的代碼我們運行前會使用webpack進行編譯,所以也就支持了ES Modules規(guī)范,不再強制使用CommonJS了。
src/components/Home/index.js
1
2
3
4
5
6
7
|
import React from 'react' ; const Home = () => { return <div>home</div> } export default Home; |
/src/server/index.js中我們可以使用Home組件,這里我們首先需要安裝react-dom,借助renderToString將Home組件轉(zhuǎn)換為標簽字符串,當然這里需要依賴React所以我們需要引入React。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import express from 'express' ; import Home from '../Components/Home' ; import React from 'react' ; import { renderToString } from 'react-dom/server' ; const app = express(); const content = renderToString(<Home />); app.get( '*' , function (req, res) { res.send(` <html> <body>${content}</body> </html> `); }) var server = app.listen(3000); |
1
2
3
4
|
# 重新打包 webpack --config webpack.server.js # 運行服務(wù) node ./build/bundile.js |
這時候頁面就顯示出了我們React組件的代碼。
React的服務(wù)端渲染是建立在虛擬DOM上的服務(wù)器端渲染,而且服務(wù)端渲染會讓頁面的首屏渲染速度大大加快。不過服務(wù)端渲染也有弊端,客戶端渲染React代碼在瀏覽器端執(zhí)行,他消耗的是用戶瀏覽器端的性能,但是服務(wù)器端渲染消耗的是服務(wù)器端的性能,因為React代碼在服務(wù)器上運行。極大的消耗了服務(wù)器的性能,因為React代碼是很消耗計算性能的。
如果你的項目完全沒有必要使用SEO優(yōu)化并且你的項目訪問速度已經(jīng)很快了的情況下,建議還是不要使用SSR的技術(shù)了,因為他的成本開銷還是比較大的。
上面我們的代碼每次修改之后都需要重新執(zhí)行webpack打包和啟動服務(wù)器,這樣調(diào)試起來太過麻煩,為了解決這個問題我們需要做一下webpack的自動打包和node的重啟。我們在package.json中加入build命令,并且通過--watch監(jiān)聽文件變化進行自動打包。
1
2
3
4
5
6
7
|
{ ... "scripts" : { "build" : "webpack --config webpack.server.js --watch" } ... } |
只是重新打包還不夠,我們還需要重啟node服務(wù)器,這里我們需要借助nodemon模塊,這里我們使用全局安裝nodemon, 在package.json文件中添加一個start命令來啟動我們的node服務(wù)器。使用nodemon監(jiān)聽build文件并且發(fā)生改變之后重新exec運行"node ./build/bundile.js", 這里需要保留雙引號,轉(zhuǎn)譯一下就好了。
1
2
3
4
5
6
7
8
|
{ ... "scripts" : { "start" : "nodemon --watch build --exec node \"./build/bundile.js\"" , "build" : "webpack --config webpack.server.js --watch" } ... } |
這時我們啟動服務(wù)器,這里需要在兩個窗口運行下面的命令,因為build后不允許再輸入其他命令了。
1
2
|
npm run build npm run start |
這個時候我們修改代碼之后頁面就會自動更新了。
但是上面的流程還是有些麻煩,我們需要兩個窗口來執(zhí)行命令,我們想要一個窗口將兩個命令執(zhí)行完畢,我們需要借助一個第三方模塊npm-run-all,可以全局安裝這個模塊。然后再package.json中來修改一下。
我們在打包和調(diào)試應(yīng)該是在開發(fā)環(huán)境,我們創(chuàng)建一個dev命令, 里面執(zhí)行npm-run-all, --parallel表示并行執(zhí)行, 執(zhí)行dev:開頭的所有命令。我們將start和build前面追加一個dev:,這個時候我想啟動服務(wù)器同時監(jiān)聽文件改變運行npm run dev就可以了。
1
2
3
4
5
6
7
8
9
|
{ ... "scripts" : { "dev" : "npm-run-all --parallel dev:**" , "dev:start" : "nodemon --watch build --exec node \"./build/bundile.js\"" , "dev:build" : "webpack --config webpack.server.js --watch" } ... } |
什么叫做同構(gòu)
比如下面的代碼,我們給div綁定一個click事件,希望點擊的時候可以彈出click提示。但是運行之后我們會發(fā)現(xiàn)這個事件并沒有被綁定上,因為服務(wù)器端沒辦法綁定事件。
src/components/Home/index.js
1
2
3
4
5
6
7
|
import React from 'react' ; const Home = () => { return <div onClick={() => { alert( 'click' ); }}>home</div> } export default Home; |
一般我們的做法是先將頁面渲染出來,然后將相同的代碼在瀏覽器端像傳統(tǒng)的React項目一樣再去運行一遍,這樣的話這個點擊事件就有了。
這就衍生出一個同構(gòu)的概念,我的理解是一套React代碼在服務(wù)器端執(zhí)行一次,在客戶端再執(zhí)行一次。
同構(gòu)就可以解決點擊事件無效的問題,首先服務(wù)器端執(zhí)行一次能夠正常的展示頁面,客戶端再執(zhí)行一次就可以綁定上事件。
我們可以在頁面渲染的時候加載一個index.js, 使用app.use創(chuàng)建靜態(tài)文件的訪問路徑, 這樣訪問的index.js就會請求到/public/index.js文件中。
1
2
3
4
5
6
7
8
9
10
11
12
|
app.use(express.static( 'public' )); app.get( '/' , function (req, res) { res.send(` <html> <body> <div id= "root" >${content}</div> <script src= "/index.js" ></script> </body> </html> `); }) |
public/index.js
1
|
console.log( 'public' ); |
基于這種情況我們就可以將React代碼在瀏覽器中執(zhí)行一次,我們這里新建一個/src/client/index.js。將客戶端執(zhí)行的代碼帖進去。這里我們同構(gòu)代碼使用hydrate代替render。
1
2
3
4
5
6
|
import React from 'react' ; import ReactDOM from 'react-dom' ; import Home from '../Components/Home' ; ReactDOM.hydrate(<Home />, document.getElementById( 'root' )); |
然后我們還需要在根目錄創(chuàng)建一個webpack.client.js文件。入口文件為./src/client/index.js,出口文件到public/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
const Path = require( 'path' ); module.exports = { mode: 'development' , entry: './src/client/index.js' , output: { filename: 'index.js' , path: Path.resolve(__dirname, 'public' ) }, module: { rules: [ { test: /.js?$/, loader: 'babel-loader' , exclude: /node_modules/, options: { presets: [ 'react' , 'stage-0' , [ 'env' , { targets: { browsers: [ 'last 2 versions' ] } }]] } } ] } } |
package.json文件中添加一條打包client目錄的命令
1
2
3
4
5
6
7
8
9
10
|
{ ... "scripts" : { "dev" : "npm-run-all --parallel dev:**" , "dev:start" : "nodemon --watch build --exec node \"./build/bundile.js\"" , "dev:build" : "webpack --config webpack.server.js --watch" , "dev:build" : "webpack --config webpack.client.js --watch" , } ... } |
這樣我們啟動的時候會編譯client運行的文件。再去訪問頁面的時候就可以綁定好事件了。
下面我們對上面工程的代碼進行整理,上面webpack.server.js和webpack.client.js文件有很多重復(fù)的地方,我們可以使用webpack-merge插件對內(nèi)容進行合并。
webpack.base.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
module.exports = { module: { rules: [ { test: /.js?$/, loader: 'babel-loader' , exclude: /node_modules/, options: { presets: [ 'react' , 'stage-0' , [ 'env' , { targets: { browsers: [ 'last 2 versions' ] } }]] } } ] } } |
webpack.server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
const Path = require( 'path' ); const NodeExternals = require( 'webpack-node-externals' ); // 服務(wù)端運行webpack需要運行NodeExternals, 他的作用是將express這類node模塊不被打包到j(luò)s里。 const merge = require( 'webpack-merge' ); const config = require( './webpack.base.js' ); const serverConfig = { target: 'node' , mode: 'development' , entry: './src/server/index.js' , output: { filename: 'bundle.js' , path: Path.resolve(__dirname, 'build' ) }, externals: [NodeExternals()], } module.exports = merge(config, serverConfig); |
webpack.client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const Path = require( 'path' ); const merge = require( 'webpack-merge' ); const config = require( './webpack.base.js' ); const clientConfig = { mode: 'development' , entry: './src/client/index.js' , output: { filename: 'index.js' , path: Path.resolve(__dirname, 'public' ) } }; module.exports = merge(config, clientConfig); |
src/server中放置的是服務(wù)端運行的代碼,src/client放置的是瀏覽器端運行的js。
到此這篇關(guān)于React服務(wù)端渲染原理解析與實踐的文章就介紹到這了,更多相關(guān)React服務(wù)端渲染內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://juejin.cn/post/6935260102148489223