面試官:說說React服務(wù)端渲染怎么做?原理是什么?
本文轉(zhuǎn)載自微信公眾號(hào)「JS每日一題」,作者灰灰。轉(zhuǎn)載本文請(qǐng)聯(lián)系JS每日一題公眾號(hào)。
一、是什么
在SSR中,我們了解到Server-Side Rendering ,簡稱SSR,意為服務(wù)端渲染
指由服務(wù)側(cè)完成頁面的 HTML 結(jié)構(gòu)拼接的頁面處理技術(shù),發(fā)送到瀏覽器,然后為其綁定狀態(tài)與事件,成為完全可交互頁面的過程
其解決的問題主要有兩個(gè):
- SEO,由于搜索引擎爬蟲抓取工具可以直接查看完全渲染的頁面
- 加速首屏加載,解決首屏白屏問題
二、如何做
在react中,實(shí)現(xiàn)SSR主要有兩種形式:
- 手動(dòng)搭建一個(gè) SSR 框架
- 使用成熟的SSR 框架,如 Next.JS
這里主要以手動(dòng)搭建一個(gè)SSR框架進(jìn)行實(shí)現(xiàn)
首先通過express啟動(dòng)一個(gè)app.js文件,用于監(jiān)聽3000端口的請(qǐng)求,當(dāng)請(qǐng)求根目錄時(shí),返回HTML,如下:
- const express = require('express')
- const app = express()
- app.get('/', (req,res) => res.send(`
- <html>
- <head>
- <title>SSR demo</title>
- </head>
- <body>
- Hello world
- </body>
- </html>
- `))
- app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))
然后再服務(wù)器中編寫react代碼,在app.js中進(jìn)行應(yīng)引用
- import React from 'react'
- const Home = () =>{
- return <div>home</div>
- }
- export default Home
為了讓服務(wù)器能夠識(shí)別JSX,這里需要使用webpakc對(duì)項(xiàng)目進(jìn)行打包轉(zhuǎn)換,創(chuàng)建一個(gè)配置文件webpack.server.js并進(jìn)行相關(guān)配置,如下:
- const path = require('path') //node的path模塊
- const nodeExternals = require('webpack-node-externals')
- module.exports = {
- target:'node',
- mode:'development', //開發(fā)模式
- entry:'./app.js', //入口
- output: { //打包出口
- filename:'bundle.js', //打包后的文件名
- path:path.resolve(__dirname,'build') //存放到根目錄的build文件夾
- },
- externals: [nodeExternals()], //保持node中require的引用方式
- module: {
- rules: [{ //打包規(guī)則
- test: /\.js?$/, //對(duì)所有js文件進(jìn)行打包
- loader:'babel-loader', //使用babel-loader進(jìn)行打包
- exclude: /node_modules/,//不打包node_modules中的js文件
- options: {
- presets: ['react','stage-0',['env', {
- //loader時(shí)額外的打包規(guī)則,對(duì)react,JSX,ES6進(jìn)行轉(zhuǎn)換
- targets: {
- browsers: ['last 2versions'] //對(duì)主流瀏覽器最近兩個(gè)版本進(jìn)行兼容
- }
- }]]
- }
- }]
- }
- }
接著借助react-dom提供了服務(wù)端渲染的 renderToString方法,負(fù)責(zé)把React組件解析成html
- import express from 'express'
- import React from 'react'//引入React以支持JSX的語法
- import { renderToString } from 'react-dom/server'//引入renderToString方法
- import Home from'./src/containers/Home'
- const app= express()
- const content = renderToString(<Home/>)
- app.get('/',(req,res) => res.send(`
- <html>
- <head>
- <title>SSR demo</title>
- </head>
- <body>
- ${content}
- </body>
- </html>
- `))
- app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
上面的過程中,已經(jīng)能夠成功將組件渲染到了頁面上
但是像一些事件處理的方法,是無法在服務(wù)端完成,因此需要將組件代碼在瀏覽器中再執(zhí)行一遍,這種服務(wù)器端和客戶端共用一套代碼的方式就稱之為「同構(gòu)」
重構(gòu)通俗講就是一套R(shí)eact代碼在服務(wù)器上運(yùn)行一遍,到達(dá)瀏覽器又運(yùn)行一遍:
- 服務(wù)端渲染完成頁面結(jié)構(gòu)
- 瀏覽器端渲染完成事件綁定
瀏覽器實(shí)現(xiàn)事件綁定的方式為讓瀏覽器去拉取JS文件執(zhí)行,讓JS代碼來控制,因此需要引入script標(biāo)簽
通過script標(biāo)簽為頁面引入客戶端執(zhí)行的react代碼,并通過express的static中間件為js文件配置路由,修改如下:
- import express from 'express'
- import React from 'react'//引入React以支持JSX的語法
- import { renderToString } from'react-dom/server'//引入renderToString方法
- import Home from './src/containers/Home'
- const app = express()
- app.use(express.static('public'));
- //使用express提供的static中間件,中間件會(huì)將所有靜態(tài)文件的路由指向public文件夾
- const content = renderToString(<Home/>)
- app.get('/',(req,res)=>res.send(`
- <html>
- <head>
- <title>SSR demo</title>
- </head>
- <body>
- ${content}
- <script src="/index.js"></script>
- </body>
- </html>
- `))
- app.listen(3001, () =>console.log('Example app listening on port 3001!'))
然后再客戶端執(zhí)行以下react代碼,新建webpack.client.js作為客戶端React代碼的webpack配置文件如下:
- const path = require('path') //node的path模塊
- module.exports = {
- mode:'development', //開發(fā)模式
- entry:'./src/client/index.js', //入口
- output: { //打包出口
- filename:'index.js', //打包后的文件名
- path:path.resolve(__dirname,'public') //存放到根目錄的build文件夾
- },
- module: {
- rules: [{ //打包規(guī)則
- test: /\.js?$/, //對(duì)所有js文件進(jìn)行打包
- loader:'babel-loader', //使用babel-loader進(jìn)行打包
- exclude: /node_modules/, //不打包node_modules中的js文件
- options: {
- presets: ['react','stage-0',['env', {
- //loader時(shí)額外的打包規(guī)則,這里對(duì)react,JSX進(jìn)行轉(zhuǎn)換
- targets: {
- browsers: ['last 2versions'] //對(duì)主流瀏覽器最近兩個(gè)版本進(jìn)行兼容
- }
- }]]
- }
- }]
- }
- }
這種方法就能夠簡單實(shí)現(xiàn)首頁的react服務(wù)端渲染,過程對(duì)應(yīng)如下圖:
在做完初始渲染的時(shí)候,一個(gè)應(yīng)用會(huì)存在路由的情況,配置信息如下:
- import React from 'react' //引入React以支持JSX
- import { Route } from 'react-router-dom' //引入路由
- import Home from './containers/Home' //引入Home組件
- export default (
- <div>
- <Route path="/" exact component={Home}></Route>
- </div>
- )
然后可以通過index.js引用路由信息,如下:
- import React from 'react'
- import ReactDom from 'react-dom'
- import { BrowserRouter } from'react-router-dom'
- import Router from'../Routers'
- const App= () => {
- return (
- <BrowserRouter>
- {Router}
- </BrowserRouter>
- )
- }
- ReactDom.hydrate(<App/>, document.getElementById('root'))
這時(shí)候控制臺(tái)會(huì)存在報(bào)錯(cuò)信息,原因在于每個(gè)Route組件外面包裹著一層div,但服務(wù)端返回的代碼中并沒有這個(gè)div
解決方法只需要將路由信息在服務(wù)端執(zhí)行一遍,使用使用StaticRouter來替代BrowserRouter,通過context進(jìn)行參數(shù)傳遞
- import express from 'express'
- import React from 'react'//引入React以支持JSX的語法
- import { renderToString } from 'react-dom/server'//引入renderToString方法
- import { StaticRouter } from 'react-router-dom'
- import Router from '../Routers'
- const app = express()
- app.use(express.static('public'));
- //使用express提供的static中間件,中間件會(huì)將所有靜態(tài)文件的路由指向public文件夾
- app.get('/',(req,res)=>{
- const content = renderToString((
- //傳入當(dāng)前path
- //context為必填參數(shù),用于服務(wù)端渲染參數(shù)傳遞
- <StaticRouter location={req.path} context={{}}>
- {Router}
- </StaticRouter>
- ))
- res.send(`
- <html>
- <head>
- <title>SSR demo</title>
- </head>
- <body>
- <div id="root">${content}</div>
- <script src="/index.js"></script>
- </body>
- </html>
- `)
- })
- app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
這樣也就完成了路由的服務(wù)端渲染
三、原理
整體react服務(wù)端渲染原理并不復(fù)雜,具體如下:
node server 接收客戶端請(qǐng)求,得到當(dāng)前的請(qǐng)求url 路徑,然后在已有的路由表內(nèi)查找到對(duì)應(yīng)的組件,拿到需要請(qǐng)求的數(shù)據(jù),將數(shù)據(jù)作為 props、context或者store 形式傳入組件
然后基于 react 內(nèi)置的服務(wù)端渲染方法 renderToString()把組件渲染為 html字符串在把最終的 html進(jìn)行輸出前需要將數(shù)據(jù)注入到瀏覽器端
瀏覽器開始進(jìn)行渲染和節(jié)點(diǎn)對(duì)比,然后執(zhí)行完成組件內(nèi)事件綁定和一些交互,瀏覽器重用了服務(wù)端輸出的 html 節(jié)點(diǎn),整個(gè)流程結(jié)束
參考文獻(xiàn)
- https://zhuanlan.zhihu.com/p/52693113
- https://segmentfault.com/a/1190000020417285
- https://juejin.cn/post/6844904000387563533#heading-14