爲何會有服務端渲染?html
webapp
開發模式不少框架都由瀏覽器渲染HTML內容,而seo
抓取url
內容時並不會執行js
代碼,抓取webapp時獲得的是一個HTML
[空內容]+js
[內容寫在js中],seo
難以進行推廣webapp HTML
內容必須等到js文件加載完成,首次請求時等待時間會比較長,體驗差nodejs
,使得用react
構建的app
可以在nodejs環境
中進行渲染,以獲得內容,再返回給瀏覽器,此時的HTML就可以被seo以及爬蟲抓取,用戶等待時間也會變短react-dom
下的server
模塊reat-dom是React專門爲web端開發的渲染工具。 在客戶端,咱們可使用react-dom的render方法渲染組件 在服務端,react-dom/server提供咱們將react組件渲染成HTML的方法
首先打開app.js
,發現以前是將<App />
這個JSX
標籤render
(中文:傳遞)到document.body
中的,而問題是在服務端中並無瀏覽器環境
,天然也就沒有document
這個dom
對象node
server-entry.js
,將須要服務端渲染的內容export
出去與webpack.config.js
同級創建webpack.config.server.js
內容拷貝自webpack.config.js,與entry選項同級添加react
target:"node",webpack
//指定webpack打包出來的文件是使用在哪一個執行環境中的,選項有且不只有web/node/...
entry中的app修改成server-entry.jsgit
output中的filename修改成'server-entry.js'而非hash化文件名github
//此項可用於輸出版本號用於緩存更新,而服務端無瀏覽器緩存概念,而且這會加大import難度 而且添加一項libraryTarget:"commonjs2" //打包出js時使用的模塊方案,有多種global/cmd/amd/umd/commjs/...,commonjs2適用於node端
刪除plugins:[new HTMLPlugin()],web
同時刪除其引用const HTMLPlugin=require('html-webpack-plugin') 由於服務端負責渲染而非輸出HTML文件
配置完上步以後就能夠把上部分的js打包出來,爲方便後續打包,修改下package.json
配置express
* 刪除`scripts`下的`test`,當前暫時用不到 "test": "echo \"Error: no test specified\" && exit 1", * 爲分別build`客戶端`和`服務端`以及區分不一樣端文件,在`scripts`添加兩個`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時覆蓋dist目錄,該包專門用於刪除文件夾,須要安裝[該段第五步] "build":"npm run clear && npm run build:client && npm run build:server" //使用npm 按順序 run clear,build:client 和build:server 腳本 $npm i rimraf -D //安裝完會自動更新package.json文件依賴關係
npm run
這個命令實際上是是運行package.json
中的scripts
中相應的script
片斷的key-value中的value,好比npm run clear
實際是運行npm run rimraf dist
npm run build
npm
編譯後能夠在output目錄dist中,分別看到客戶端js文件`[name].[hash].js`以及服務端js文件`server-entry.js` 對比能夠發現server-entry.js在頂部使用了module.exports這一node語法,所以能夠發現是能夠被node執行各類操做包括渲染的
在root
下newserver
文件夾用於添加node server
,用到express
,須要安裝,$npm i express -S
,由於該服務是正常服務
中須要用到的,因此要使用-S安裝到依賴模塊
當中json
安裝好後在server文件夾內新建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呢,由於若是不添加default時,下面的打印說明了問題 const app=express(); console.log(serverEntry); /*會發現打印出來的是這麼個東西,其中default中的東西纔是nodeServer須要渲染的東西 { __esModule: true, default: { '$$typeof': Symbol(react.element), type: [Function: App], key: null, ref: null, props: {}, _owner: null, _store: {} } } */ 由於當前使用的是commonjs2模塊方案,而commonjs2默認使用export.default來export模塊, 能夠回到server-entry.js看下代碼 export default <APP /> 對應的引入方法 import app from './App.jsx'】 export const app=App 對應的引入方法 import {app} from '..' 】->ES6中解構的寫法 node中使用的是require引入,require默認不會讀取default內容 app.get('*',function (req,res) { const appString=ReactSSR.renderToString(serverEntry); res.send(appString) }); app.listen(3000,function () { //監聽端口,成功函數 console.log('server is listening') })
$npm start
進入http://localhost:3000端口,打開network查看Headers以及Response 至此最簡單的服務端渲染已經完成,此時的返回內容html少以及未引用客戶端的業務代碼js 爲解決該問題,須要把服務端渲染出來的內容(當前爲server-entry.js)插入到index.html (html-webpack-plugin插件經過編譯成的HTML)的body當中並總體返回body,這纔算打通一個整的 服務端渲染過程
添加模板文件template.html
以及修改webpack.config.client.js
配置
在client目錄下新建`template.html`,在body中添加如下代碼 <div id="root"><app></app></div> //這個名爲root的div就能夠替代以前app.js中App組件掛載的document.body了,而內部的app標籤則是用於覆蓋,下面講怎麼覆蓋 在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')) //把渲染出來的內容render到root節點中
配置好template後,在server.js
中讀取template.html而且替換掉<app>標籤
const fs=require('fs') //node文件模塊 const path=require('path') //node路徑模塊 const template=fs.readFileSync(path.join(__dirname,'../client/template.html'),'utf-8') //同步讀取絕對路徑文件,而且以utf-8格式輸出,不指定的話回默認輸出buffer格式 //修改app的get方法 app.get('*',function (req,res) { const appString=ReactSSR.renderToString(serverEntry); res.send(template.replace('<app></app>',appString));//修改用於替換掉<app>標籤 });
$npm run build
->$npm start
進入http://localhost:3000,成功,可是network時發現請求localhost和請求js文件時返回結果同樣, 也就是說有資源被重複返回的狀況出現 由於咱們的服務接受到的全部請求都會返回服務端渲染的內容 針對此問題,須要添加配置以肯定哪些是不須要屢次返回的靜態資源文件 app.use('/public',express.static(path.join(__dirname,'../dist'))); //express中爲咱們提供的專門處理此問題的模塊,用於肯定哪些文件夾下的資源是靜態文件 由於咱們的client下的全部內容都在webpack.config.client.js中配置編譯到dist(output->path)下, 因此此處的靜態文件路徑就join dist,其中的publicPath在上次在跟走代碼時置爲空了,如今從新加回去, webpack.config.server.js中也須要加回去 #咱們能夠在編譯好的index.html中查看資源路徑是否正確
$npm run build
->$npm start
瀏覽器查看,無異常,可是有個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方法去渲染,新加了一個方法,若是咱們使用了服務端渲染,那麼須要使用hydrate()在客戶端的js中去渲染客戶端中的內容,由於react會比對server和client生成的代碼。若是有差異,則會使用客戶端的代碼 替換app.js中的render爲hydrate() ReactDOM.render(<App />,document.getElementById('root')); -> ReactDOM.hydrate(<App />,document.getElementById('root'));
有時間再寫吧 問題: * 每次修改client後,都須要 npm run build * 每次修改server後,都須要npm start * 下篇文章再接觸此內容