ReactApp項(xiàng)目構(gòu)建流程【2】
React服務(wù)端渲染
- 為什么會(huì)有服務(wù)端渲染?
-
webapp
開(kāi)發(fā)模式很多框架都由瀏覽器渲染HTML內(nèi)容,而seo
抓取url
內(nèi)容時(shí)并不會(huì)執(zhí)行js
代碼蔫浆,抓取webapp時(shí)得到的是一個(gè)HTML
[空內(nèi)容]+js
[內(nèi)容寫(xiě)在js中],seo
難以進(jìn)行推廣 - 并且由瀏覽器渲染的
webapp HTML
內(nèi)容必須等到j(luò)s文件加載完成,首次請(qǐng)求時(shí)等待時(shí)間會(huì)比較長(zhǎng)紊浩,體驗(yàn)差 - React團(tuán)隊(duì)運(yùn)用
nodejs
,使得用react
構(gòu)建的app
能夠在nodejs環(huán)境
中進(jìn)行渲染句携,以得到內(nèi)容,再返回給瀏覽器愉豺,此時(shí)的HTML就能夠被seo以及爬蟲(chóng)抓取篓吁,用戶等待時(shí)間也會(huì)變短
-
服務(wù)端渲染準(zhǔn)備
工具 react-dom
下的server
模塊
reat-dom是React專門(mén)為web端開(kāi)發(fā)的渲染工具。
在客戶端蚪拦,我們可以使用react-dom的render方法渲染組件
在服務(wù)端杖剪,react-dom/server提供我們將react組件渲染成HTML的方法
基礎(chǔ)配置
打開(kāi)我們之前做好的項(xiàng)目,這里是之前的項(xiàng)目鏈接驰贷,未上傳至github
-
首先打開(kāi)
app.js
盛嘿,發(fā)現(xiàn)之前是將<App />
這個(gè)JSX
標(biāo)簽render
(中文:傳遞)到document.body
中的,而問(wèn)題是在服務(wù)端中并沒(méi)有瀏覽器環(huán)境
括袒,自然也就沒(méi)有document
這個(gè)dom
對(duì)象同級(jí)目錄下新建
server-entry.js
,將需要服務(wù)端渲染的內(nèi)容export
出去
import React from 'react' //用到了JSX就需要import 'react'
import App from './App.jsx' //需要添加 [./],因?yàn)槟J(rèn)的import目錄是node_modules
export default <App /> //返回的應(yīng)該是jsx語(yǔ)法再轉(zhuǎn)義
至此一個(gè)簡(jiǎn)單的server-entry.js文件就寫(xiě)好了
該文件的作用:在服務(wù)端渲染時(shí)使用該文件次兆,要把該文件單獨(dú)打包出來(lái),因?yàn)槭荍SX語(yǔ)法锹锰,需要用babel-
與
webpack.config.js
同級(jí)建立webpack.config.server.js
內(nèi)容拷貝自webpack.config.js芥炭,與entry選項(xiàng)同級(jí)添加target:"node", //指定webpack打包出來(lái)的文件是使用在哪個(gè)執(zhí)行環(huán)境中的,選項(xiàng)有且不僅有web/node/... entry中的app修改為server-entry.js output中的filename修改為'server-entry.js'而非hash化文件名 //此項(xiàng)可用于輸出版本號(hào)用于緩存更新恃慧,而服務(wù)端無(wú)瀏覽器緩存概念蚤认,并且這會(huì)加大import難度 并且添加一項(xiàng)libraryTarget:"commonjs2" //打包出js時(shí)使用的模塊方案,有多種global/cmd/amd/umd/commjs/...糕伐,commonjs2適用于node端 刪除plugins:[new HTMLPlugin()], 同時(shí)刪除其引用const HTMLPlugin=require('html-webpack-plugin') 因?yàn)榉?wù)端負(fù)責(zé)渲染而非輸出HTML文件
-
配置完上步之后就可以把上部分的js打包出來(lái)蘸嘶,為方便后續(xù)打包良瞧,修改下
package.json
配置* 刪除`scripts`下的`test`,當(dāng)前暫時(shí)用不到 "test": "echo \"Error: no test specified\" && exit 1", * 為分別build`客戶端`和`服務(wù)端`以及區(qū)分不同端文件训唱,在`scripts`添加兩個(gè)`script`并修改`build`用于褥蚯,將`webpack.config.js`修改為下端代碼中的文件名 "build:client":"webpack --config webpack.config.client.js", "build:server":"webpack --config webpack.config.server.js", "clear":"rimraf dist", //用于每次build時(shí)覆蓋dist目錄,該包專門(mén)用于刪除文件夾况增,需要安裝[該段第五步] "build":"npm run clear && npm run build:client && npm run build:server" //使用npm 按順序 run clear赞庶,build:client 和build:server 腳本 $npm i rimraf -D //安裝完會(huì)自動(dòng)更新package.json文件依賴關(guān)系
寫(xiě)到這里我們可以發(fā)現(xiàn)npm run
這個(gè)命令其實(shí)是是運(yùn)行package.json
中的scripts
中相應(yīng)的script
片段的key-value中的value,比如npm run clear
實(shí)際是運(yùn)行npm run rimraf dist
npm run build
編譯后可以在output目錄dist中,分別看到客戶端js文件[name].[hash].js
以及服務(wù)端js文件server-entry.js
對(duì)比可以發(fā)現(xiàn)server-entry.js在頂部使用了module.exports這一node語(yǔ)法,因此可以發(fā)現(xiàn)是可以被node執(zhí)行各種操作包括渲染的-
在
root
下newserver
文件夾用于添加node server
歧强,用到express
澜薄,需要安裝,$npm i express -S
摊册,因?yàn)樵摲?wù)是正常服務(wù)
中需要用到的肤京,所以要使用-S安裝到依賴模塊
當(dāng)中
```
安裝好后在server文件夾內(nèi)新建server.js并加入以下代碼const express=require('express'); //node方式引入expeess const ReactSSR=require('react-dom/server'); //node方式引入react-dom/serve const serverEntry=require('../dist/server-entry').default; //此處為何需要添加.default呢,因?yàn)槿绻惶砑觗efault時(shí)茅特,下面的打印說(shuō)明了問(wèn)題 const app=express(); console.log(serverEntry); /*會(huì)發(fā)現(xiàn)打印出來(lái)的是這么個(gè)東西忘分,其中default中的東西才是nodeServer需要渲染的東西 { __esModule: true, default: { '$$typeof': Symbol(react.element), type: [Function: App], key: null, ref: null, props: {}, _owner: null, _store: {} } } */ 因?yàn)楫?dāng)前使用的是commonjs2模塊方案,而commonjs2默認(rèn)使用export.default來(lái)export模塊白修, 可以回到server-entry.js看下代碼 export default <APP /> 對(duì)應(yīng)的引入方法 import app from './App.jsx'】 export const app=App 對(duì)應(yīng)的引入方法 import {app} from '..' 】->ES6中解構(gòu)的寫(xiě)法 node中使用的是require引入妒峦,require默認(rèn)不會(huì)讀取default內(nèi)容 app.get('*',function (req,res) { const appString=ReactSSR.renderToString(serverEntry); res.send(appString) }); app.listen(3000,function () { //監(jiān)聽(tīng)端口,成功函數(shù) console.log('server is listening') })
-
$npm start
```
進(jìn)入http://localhost:3000端口兵睛,打開(kāi)network查看Headers以及Response至此最簡(jiǎn)單的服務(wù)端渲染已經(jīng)完成肯骇,此時(shí)的返回內(nèi)容html少以及未引用客戶端的業(yè)務(wù)代碼js 為解決該問(wèn)題,需要把服務(wù)端渲染出來(lái)的內(nèi)容(當(dāng)前為server-entry.js)插入到index.html (html-webpack-plugin插件通過(guò)編譯成的HTML)的body當(dāng)中并整體返回body卤恳,這才算打通一個(gè)整的 服務(wù)端渲染過(guò)程
-
添加模板文件
template.html
以及修改webpack.config.client.js
配置在client目錄下新建`template.html`,在body中添加以下代碼 <div id="root"><app></app></div> //這個(gè)名為root的div就可以替代之前app.js中App組件掛載的document.body了累盗,而內(nèi)部的app標(biāo)簽則是用于覆蓋,下面講怎么覆蓋 在webpack.config.client.js的HTMLPlugin中添加剛才新建的template模板 { template:path.join(__dirname,'../client/template.html') } app.js中之前掛載的document.body修改為document.getElementById('root') ReactDOM.render(<App />,document.body) -> ReactDOM.render(<App />,document.getElementById('root')) //把渲染出來(lái)的內(nèi)容render到root節(jié)點(diǎn)中
-
配置好template后突琳,在
server.js
中讀取template.html并且替換掉<app>標(biāo)簽const fs=require('fs') //node文件模塊 const path=require('path') //node路徑模塊 const template=fs.readFileSync(path.join(__dirname,'../client/template.html'),'utf-8') //同步讀取絕對(duì)路徑文件若债,并且以u(píng)tf-8格式輸出,不指定的話回默認(rèn)輸出buffer格式 //修改app的get方法 app.get('*',function (req,res) { const appString=ReactSSR.renderToString(serverEntry); res.send(template.replace('<app></app>',appString));//修改用于替換掉<app>標(biāo)簽 });
-
$npm run build
->$npm start
進(jìn)入http://localhost:3000拆融,成功蠢琳,但是network時(shí)發(fā)現(xiàn)請(qǐng)求localhost和請(qǐng)求js文件時(shí)返回結(jié)果一樣, 也就是說(shuō)有資源被重復(fù)返回的情況出現(xiàn) 因?yàn)槲覀兊姆?wù)接受到的所有請(qǐng)求都會(huì)返回服務(wù)端渲染的內(nèi)容 針對(duì)此問(wèn)題镜豹,需要添加配置以確定哪些是不需要多次返回的靜態(tài)資源文件 app.use('/public',express.static(path.join(__dirname,'../dist'))); //express中為我們提供的專門(mén)處理此問(wèn)題的模塊傲须,用于確定哪些文件夾下的資源是靜態(tài)文件 因?yàn)槲覀兊腸lient下的所有內(nèi)容都在webpack.config.client.js中配置編譯到dist(output->path)下, 所以此處的靜態(tài)文件路徑就join dist趟脂,其中的publicPath在上次在跟走代碼時(shí)置為空了泰讽,現(xiàn)在重新加回去, webpack.config.server.js中也需要加回去 #我們可以在編譯好的index.html中查看資源路徑是否正確
-
$npm run build
->$npm start
瀏覽器查看昔期,無(wú)異常已卸,但是有個(gè)warning app.cad52c1053474fca53c1.js:14238 Warning: render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v17. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML 這是在react16中,原本是使用ReactDom.render方法去渲染硼一,新加了一個(gè)方法累澡,如果我們使用了服務(wù)端渲染,那么需要使用hydrate()在客戶端的js中去渲染客戶端中的內(nèi)容般贼,因?yàn)閞eact會(huì)比對(duì)server和client生成的代碼愧哟。如果有差別奥吩,則會(huì)使用客戶端的代碼 替換app.js中的render為hydrate() ReactDOM.render(<App />,document.getElementById('root')); -> ReactDOM.hydrate(<App />,document.getElementById('root'));
小結(jié)
有時(shí)間再寫(xiě)吧
問(wèn)題:
* 每次修改client后,都需要 npm run build
* 每次修改server后蕊梧,都需要npm start
* 下篇文章再接觸此內(nèi)容