React服務(wù)端渲染原理解析與實(shí)踐
關(guān)于服務(wù)端渲染也就是我們說的SSR大多數(shù)人都聽過這個概念,很多同學(xué)或許在公司中已經(jīng)做過服務(wù)端渲染的項(xiàng)目了,主流的單頁面應(yīng)用比如說Vue或者React開發(fā)的項(xiàng)目采用的一般都是客戶端渲染的模式也就是我們說的CSR。
但是這種模式會帶來明顯的兩個問題,第一個就是TTFP時間比較長,TTFP指的就是首屏展示時間,同時不具備SEO排名的條件,搜索引擎上排名不是很好。所以我們可以借助一些工具來進(jìn)行改良我們的項(xiàng)目,將單頁面應(yīng)用編程服務(wù)器端渲染項(xiàng)目,這樣就可以解決掉這些問題了。
目前主流的服務(wù)器端渲染框架也就是SSR框架有針對于Vue的Nuxt.js和針對React的Next.js這兩個。這里我們并不使用這些SSR框架,而是從零開始完整搭建一套SSR框架,來熟悉他的底層原理。
服務(wù)器端編寫 React 組件
如果是客戶端渲染,瀏覽器首先會向?yàn)g覽器發(fā)送請求,服務(wù)器返回頁面的html文件,然后html中再向服務(wù)器發(fā)送請求,服務(wù)器返回js文件,js文件在瀏覽器中執(zhí)行繪制出頁面結(jié)構(gòu)渲染到瀏覽器完成頁面渲染。
如果是服務(wù)器端渲染這個流程就不同了,瀏覽器發(fā)送請求,服務(wù)器端運(yùn)行React代碼生成頁面,然后服務(wù)器將生成好的頁面返回給瀏覽器,瀏覽器進(jìn)行渲染。這種情況下React代碼就是服務(wù)器的一部分而不是前端部分了。
這里我們進(jìn)行代碼的演示,首選需要npm init初始化項(xiàng)目,然后安裝react,express,webpack,webpack-cli,webpack-node-externals。
我們首先編寫一個React的組件。 .src/components/Home/index.js, 因?yàn)槲覀冞@個js是在node環(huán)境執(zhí)行的所以我們要遵循CommonJS規(guī)范,使用require和module.exports進(jìn)行導(dǎo)入導(dǎo)出。
const React = require('react');
const Home = () => {
return <div>home</div>
}
module.exports = {
default: Home
};
我們這里開發(fā)的Home組件是不能直接在node中運(yùn)行的,需要借助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文件夾中。
const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // 服務(wù)端運(yùn)行webpack需要運(yùn)行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']
}
}]]
}
}
]
}
}
安裝依賴模塊
npm install babel-loader babel-core babel-preset-react babel-preset-stage-0 babel-preset-env --save
接著我們這里基于express模塊來編寫一個簡單的服務(wù)。./src/server/index.js
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);
運(yùn)行webpack使用webpack.server.js配置文件來執(zhí)行。
webpack --config webpack.server.js
打包之后在我們的目錄下會出現(xiàn)一個bundle.js,這個js就是我們打包生成的最終可以運(yùn)行的代碼。我們可以使用node運(yùn)行這個文件, 就啟動了一個3000端口的服務(wù)器。我們訪問127.0.0.1:3000可以訪問這個服務(wù),看到瀏覽器輸出Hello。
node ./build/bundile.js
上面的代碼我們運(yùn)行前會使用webpack進(jìn)行編譯,所以也就支持了ES Modules規(guī)范,不再強(qiáng)制使用CommonJS了。
src/components/Home/index.js
import React from 'react';
const Home = () => {
return <div>home</div>
}
export default Home;
/src/server/index.js中我們可以使用Home組件,這里我們首先需要安裝react-dom,借助renderToString將Home組件轉(zhuǎn)換為標(biāo)簽字符串,當(dāng)然這里需要依賴React所以我們需要引入React。
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);
# 重新打包 webpack --config webpack.server.js # 運(yùn)行服務(wù) node ./build/bundile.js
這時候頁面就顯示出了我們React組件的代碼。
React的服務(wù)端渲染是建立在虛擬DOM上的服務(wù)器端渲染,而且服務(wù)端渲染會讓頁面的首屏渲染速度大大加快。不過服務(wù)端渲染也有弊端,客戶端渲染React代碼在瀏覽器端執(zhí)行,他消耗的是用戶瀏覽器端的性能,但是服務(wù)器端渲染消耗的是服務(wù)器端的性能,因?yàn)镽eact代碼在服務(wù)器上運(yùn)行。極大的消耗了服務(wù)器的性能,因?yàn)镽eact代碼是很消耗計(jì)算性能的。
如果你的項(xiàng)目完全沒有必要使用SEO優(yōu)化并且你的項(xiàng)目訪問速度已經(jīng)很快了的情況下,建議還是不要使用SSR的技術(shù)了,因?yàn)樗某杀鹃_銷還是比較大的。
上面我們的代碼每次修改之后都需要重新執(zhí)行webpack打包和啟動服務(wù)器,這樣調(diào)試起來太過麻煩,為了解決這個問題我們需要做一下webpack的自動打包和node的重啟。我們在package.json中加入build命令,并且通過--watch監(jiān)聽文件變化進(jìn)行自動打包。
{
...
"scripts": {
"build": "webpack --config webpack.server.js --watch"
}
...
}
只是重新打包還不夠,我們還需要重啟node服務(wù)器,這里我們需要借助nodemon模塊,這里我們使用全局安裝nodemon, 在package.json文件中添加一個start命令來啟動我們的node服務(wù)器。使用nodemon監(jiān)聽build文件并且發(fā)生改變之后重新exec運(yùn)行"node ./build/bundile.js", 這里需要保留雙引號,轉(zhuǎn)譯一下就好了。
{
...
"scripts": {
"start": "nodemon --watch build --exec node \"./build/bundile.js\"",
"build": "webpack --config webpack.server.js --watch"
}
...
}
這時我們啟動服務(wù)器,這里需要在兩個窗口運(yùn)行下面的命令,因?yàn)閎uild后不允許再輸入其他命令了。
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)聽文件改變運(yùn)行npm run dev就可以了。
{
...
"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事件,希望點(diǎn)擊的時候可以彈出click提示。但是運(yùn)行之后我們會發(fā)現(xiàn)這個事件并沒有被綁定上,因?yàn)榉?wù)器端沒辦法綁定事件。
src/components/Home/index.js
import React from 'react';
const Home = () => {
return <div onClick={() => { alert('click'); }}>home</div>
}
export default Home;
一般我們的做法是先將頁面渲染出來,然后將相同的代碼在瀏覽器端像傳統(tǒng)的React項(xiàng)目一樣再去運(yùn)行一遍,這樣的話這個點(diǎn)擊事件就有了。
這就衍生出一個同構(gòu)的概念,我的理解是一套React代碼在服務(wù)器端執(zhí)行一次,在客戶端再執(zhí)行一次。
同構(gòu)就可以解決點(diǎn)擊事件無效的問題,首先服務(wù)器端執(zhí)行一次能夠正常的展示頁面,客戶端再執(zhí)行一次就可以綁定上事件。
我們可以在頁面渲染的時候加載一個index.js, 使用app.use創(chuàng)建靜態(tài)文件的訪問路徑, 這樣訪問的index.js就會請求到/public/index.js文件中。
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
console.log('public');
基于這種情況我們就可以將React代碼在瀏覽器中執(zhí)行一次,我們這里新建一個/src/client/index.js。將客戶端執(zhí)行的代碼帖進(jìn)去。這里我們同構(gòu)代碼使用hydrate代替render。
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
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文件中添加一條打包c(diǎn)lient目錄的命令
{
...
"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運(yùn)行的文件。再去訪問頁面的時候就可以綁定好事件了。
下面我們對上面工程的代碼進(jìn)行整理,上面webpack.server.js和webpack.client.js文件有很多重復(fù)的地方,我們可以使用webpack-merge插件對內(nèi)容進(jìn)行合并。
webpack.base.js
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
const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // 服務(wù)端運(yùn)行webpack需要運(yùn)行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
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ù)端運(yùn)行的代碼,src/client放置的是瀏覽器端運(yùn)行的js。
到此這篇關(guān)于React服務(wù)端渲染原理解析與實(shí)踐的文章就介紹到這了,更多相關(guān)React服務(wù)端渲染內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談redux以及react-redux簡單實(shí)現(xiàn)
這篇文章主要介紹了淺談redux以及react-redux簡單實(shí)現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
React Native項(xiàng)目中使用Lottie動畫的方法
這篇文章主要介紹了React Native 實(shí)現(xiàn)Lottie動畫的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-10-10
React引入css的幾種方式及應(yīng)用小結(jié)
這篇文章主要介紹了React引入css的幾種方式及應(yīng)用小結(jié),操作styled組件的樣式屬性,可在組件標(biāo)簽上定義屬性、也可以通過attrs定義屬性,本文通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03
react實(shí)現(xiàn)一個優(yōu)雅的圖片占位模塊組件詳解
這篇文章主要給大家介紹了關(guān)于react如何實(shí)現(xiàn)一個還算優(yōu)雅的占位模塊圖片組件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10
react native仿微信PopupWindow效果的實(shí)例代碼
本篇文章主要介紹了react native仿微信PopupWindow效果的實(shí)例代碼,具有一定的參考價值,有興趣的可以了解一下2017-08-08
詳解使用webpack+electron+reactJs開發(fā)windows桌面應(yīng)用
這篇文章主要介紹了詳解使用webpack+electron+reactJs開發(fā)windows桌面應(yīng)用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02
React中Ant?Design組件日期編輯回顯的實(shí)現(xiàn)
本文主要介紹了React中Ant?Design組件日期編輯回顯的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04
利用React實(shí)現(xiàn)一個有點(diǎn)意思的電梯小程序
這篇文章主要為大家詳解介紹了如何利用React實(shí)現(xiàn)一個有點(diǎn)意思的電梯小程序,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2022-08-08

