天天看點

一文吃透React SSR服務端同構渲染寫在前面為什麼要服務端渲染(ssr)核心原理react ssr引出問題同構才是核心css 過濾動态路由的 SSR其他TODO 和 思考最後源碼參考說點感想參考資料

寫在前面

前段時間一直在研究

react ssr

技術,然後寫了一個完整的

ssr

開發骨架。今天寫文,主要是把我的研究成果的精華内容整理落地,另外通過再次梳理希望發現更多優化的地方,也希望可以讓更多的人少踩一些坑,讓更多的人了解和掌握這個技術。

相信看過本文(前提是能對你的胃口,也能較好的消化吸收)你一定會對

react ssr

服務端渲染技術有一個深入的了解,可以打造自己的腳手架,更可以用來改造自己的實際項目,當然這不僅限于

react

,其他架構都一樣,畢竟原理都是相似的。

為什麼要服務端渲染(ssr)

至于為什麼要服務端渲染,我相信大家都有所聞,而且每個人都能說出幾點來。

首屏等待

在 SPA 模式下,所有的資料請求和 Dom 渲染都在浏覽器端完成,是以當我們第一次通路頁面的時候很可能會存在“白屏”等待,而服務端渲染所有資料請求和 html内容已在服務端處理完成,浏覽器收到的是完整的 html 内容,可以更快的看到渲染内容,在服務端完成資料請求肯定是要比在浏覽器端效率要高的多。

沒考慮SEO的感受

有些網站的流量來源主要還是靠搜尋引擎,是以網站的 SEO 還是很重要的,而 SPA 模式對搜尋引擎不夠友好,要想徹底解決這個問題隻能采用服務端直出。改變不了别人(搜尋yinqing),隻能改變自己。

SSR + SPA 體驗更新

隻實作

SSR

其實沒啥意義,技術上沒有任何發展和進步,否則

SPA

技術就不會出現。

但是單純的

SPA

又不夠完美,是以最好的方案就是這兩種體驗和技術的結合,第一次通路頁面是服務端渲染,基于第一次通路後續的互動就是

SPA

的效果和體驗,還不影響

SEO

效果,這就有點完美了。

單純實作

ssr

很簡單,畢竟這是傳統技術,也不分語言,随便用 php 、jsp、asp、node 等都可以實作。

但是要實作兩種技術的結合,同時可以最大限度的重用代碼(同構),減少開發維護成本,那就需要采用

react

或者

vue

等前端架構相結合

node(ssr)

來實作。

本文主要說

ReactSSR技術

,當然

vue

也一樣,隻是技術棧不同而已。

核心原理

整體來說

react

服務端渲染原理不複雜,其中最核心的内容就是同構。

node server

接收用戶端請求,得到目前的

req url path

,然後在已有的路由表内查找到對應的元件,拿到需要請求的資料,将資料作為

props

context

或者

store

形式傳入元件,然後基于

react

内置的服務端渲染api

renderToString()orrenderToNodeStream()

把元件渲染為

html字元串

或者

stream流

, 在把最終的

html

進行輸出前需要将資料注入到浏覽器端(注水),server 輸出(response)後浏覽器端可以得到資料(脫水),浏覽器開始進行渲染和節點對比,然後執行元件的

componentDidMount

完成元件内事件綁定和一些互動,浏覽器重用了服務端輸出的

html節點

,整個流程結束。

技術點确實不少,但更多的是架構和工程層面的,需要把各個知識點進行連結和整合。

這裡放一個架構圖

一文吃透React SSR服務端同構渲染寫在前面為什麼要服務端渲染(ssr)核心原理react ssr引出問題同構才是核心css 過濾動态路由的 SSR其他TODO 和 思考最後源碼參考說點感想參考資料

react ssr

從 ejs 開始

實作 ssr 很簡單,先看一個

node ejs

的栗子。

// index.html

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<meta http-equiv="X-UA-Compatible" content="ie=edge">

<title>react ssr <%= title %></title>

</head>

<body>

<%= data %>

</body>

</html>           
//node ssr

const ejs = require('ejs');

const http = require('http');



http.createServer((req, res) => {

if (req.url === '/') {

res.writeHead(200, {

'Content-Type': 'text/html'

});

// 渲染檔案 index.ejs

ejs.renderFile('./views/index.ejs', {

title: 'react ssr',

data: '首頁'},

(err, data) => {

if (err ) {

console.log(err);

} else {

res.end(data);

}

})

}

}).listen(8080);           

jsx 到字元串

上面我們結合

ejs模闆引擎

,實作了一個服務端渲染的輸出,html 和 資料直接輸出到用戶端。

參考以上,我們結合

react元件

來實作服務端渲染直出,使用

jsx

來代替

ejs

,之前是在 html 裡使用

ejs

來綁定資料,現在改寫成使用

jsx

來綁定資料,使用 react 内置 api 來把元件渲染為 html 字元串,其他沒有差别。

為什麼react 元件可以被轉換為 html字元串呢?

簡單的說我們寫的 jsx 看上去就像在寫 html(其實寫的是對象) 标簽,其實經過編譯後都會轉換成

React.createElement

方法,最終會被轉換成一個對象(虛拟DOM),而且和平台無關,有了這個對象,想轉換成什麼那就看心情了。

const React = require('react');



const { renderToString} = require( 'react-dom/server');



const http = require('http');



//元件

class Index extends React.Component{

constructor(props){

super(props);

}



render(){

return <h1>{this.props.data.title}</h1>

}

}



//模拟資料的擷取

const fetch = function () {

return {

title:'react ssr',

data:[]

}

}



//服務

http.createServer((req, res) => {

if (req.url === '/') {

res.writeHead(200, {

'Content-Type': 'text/html'

});



const data = fetch();



const html = renderToString(<Index data={data}/>);

res.end(html);

}

}).listen(8080);           

ps:以上代碼不能直接運作,需要結合babel 使用 @babel/preset-react 進行轉換

npx babel script.js --out-file script-compiled.js [email protected]/preset-react           

引出問題

在上面非常簡單的就是實作了

react ssr

,把

jsx

作為模闆引擎,不要小看上面的一小段代碼,他可以幫我們引出一系列的問題,這也是完整實作

react ssr

的基石。

  • 雙端路由如何維護?

首先我們會發現我在

server

端定義了路由 '/',但是在

react SPA

模式下我們需要使用

react-router

來定義路由。那是不是就需要維護兩套路由呢?

  • 擷取資料的方法和邏輯寫在哪裡?

發現資料擷取的

fetch

寫的獨立的方法,群組件沒有任何關聯,我們更希望的是每個路由都有自己的 fetch 方法。

  • 服務端 html 節點無法重用

雖然元件在服務端得到了資料,也能渲染到浏覽器内,但是當浏覽器端進行元件渲染的時候直出的内容會一閃而過消失。

好了,問題有了,接下來我們就一步一步的來解決這些問題。

同構才是核心

react ssr

的核心就是同構,沒有同構的 ssr 是沒有意義的。

所謂同構就是采用一套代碼,建構雙端(server 和 client)邏輯,最大限度的重用代碼,不用維護兩套代碼。而傳統的服務端渲染是無法做到的,react 的出現打破了這個瓶頸,并且現在已經得到了比較廣泛的應用。

路由同構

雙端使用同一套路由規則,

node server

通過

req url path

進行元件的查找,得到需要渲染的元件。

//元件和路由配置 ,供雙端使用 routes-config.js

class Detail extends React.Component{



render(){

return <div>detail</div>

}

}



class Index extends React.Component {



render() {

return <div>index</div>

}

}





const routes = [



{

path: "/",

exact: true,

component: Home

},

{

path: '/detail', exact: true,

component:Detail,

},

{

path: '/detail/:a/:b', exact: true,

component: Detail

}



];



//導出路由表

export default routes;           

//用戶端 路由元件

import routes from './routes-config.js';



function App(){

return (

<Layout>

<Switch>



{

routes.map((item,index)=>{

return <Route path={item.path} key={index} exact={item.exact} render={item.component}></Route>

})

}

</Switch>

</Layout>

);

}



export default App;

           

node server 進行元件查找

路由比對其實就是對 元件

path

規則的比對,如果規則不複雜可以自己寫,如果情況很多種還是使用官方提供的庫來完成。

matchRoutes(routes,pathname)

//引入官方庫

import { matchRoutes } from "react-router-config";

import routes from './routes-config.js';



const path = req.path;



const branch = matchRoutes(routes, path);



//得到要渲染的元件

const Component = branch[0].route.component;





//node server

http.createServer((req, res) => {



const url = req.url;

//簡單容錯,排除圖檔等資源檔案的請求

if(url.indexOf('.')>-1) { res.end(''); return false;}



res.writeHead(200, {

'Content-Type': 'text/html'

});

const data = fetch();



//查找元件

const branch = matchRoutes(routes,url);



//得到元件

const Component = branch[0].route.component;



//将元件渲染為 html 字元串

const html = renderToString(<Component data={data}/>);



res.end(html);



}).listen(8080);           

可以看下

matchRoutes方法

的傳回值,其中

route.component

就是 要渲染的元件

[

{



route:

{ path: '/detail', exact: true, component: [Function: Detail] },

match:

{ path: '/detail', url: '/detail', isExact: true, params: {} }



}

]           

react-router-config

這個庫由react 官方維護,功能是實作嵌套路由的查找,代碼沒有多少,有興趣可以看看。

文章走到這裡,相信你已經知道了路由同構,是以上面的第一個問題 :【雙端路由如何維護?】 解決了。

資料同構(預取同構)

這裡開始解決我們最開始發現的第二個問題 - 【擷取資料的方法和邏輯寫在哪裡?】

資料預取同構,解決雙端如何使用同一套資料請求方法來進行資料請求。

先說下流程,在查找到要渲染的元件後,需要預先得到此元件所需要的資料,然後将資料傳遞給元件後,再進行元件的渲染。

我們可以通過給元件定義靜态方法來處理,元件内定義異步資料請求的方法也合情合理,同時聲明為靜态(static),在 server 端群組件内都也可以直接通過元件(function) 來進行通路。

比如

Index.getInitialProps

//元件

class Index extends React.Component{

constructor(props){

super(props);

}



//資料預取方法 靜态 異步 方法

static async getInitialProps(opt) {

const fetch1 =await fetch('/xxx.com/a');

const fetch2 = await fetch('/xxx.com/b');



return {

res:[fetch1,fetch2]

}

}



render(){

return <h1>{this.props.data.title}</h1>

}

}





//node server

http.createServer((req, res) => {



const url = req.url;

if(url.indexOf('.')>-1) { res.end(''); return false;}



res.writeHead(200, {

'Content-Type': 'text/html'

});



//元件查找

const branch = matchRoutes(routes,url);



//得到元件

const Component = branch[0].route.component;



//資料預取

const data = Component.getInitialProps(branch[0].match.params);



//傳入資料,渲染元件為 html 字元串

const html = renderToString(<Component data={data}/>);



res.end(html);



}).listen(8080);           

另外還有在聲明路由的時候把資料請求方法關聯到路由中,比如定一個 loadData 方法,然後在查找到路由後就可以判斷是否存在

loadData

這個方法。

看下參考代碼

const loadBranchData = (location) => {

const branch = matchRoutes(routes, location.pathname)



const promises = branch.map(({ route, match }) => {

return route.loadData

? route.loadData(match)

: Promise.resolve(null)

})



return Promise.all(promises)

}           

上面這種方式實作上沒什麼問題,但從職責劃分的角度來說有些不夠清晰,我還是比較喜歡直接通過元件來得到異步方法。

好了,到這裡我們的第二個問題 - 【擷取資料的方法和邏輯寫在哪裡?】 解決了。

渲染同構

假設我們現在基于上面已經實作的代碼,同時我們也使用 webpack 進行了配置,對代碼進行了轉換和打包,整個服務可以跑起來。

路由能夠正确比對,資料預取正常,服務端可以直出元件的 html ,浏覽器加載 js 代碼正常,檢視網頁源代碼能看到 html 内容,好像我們的整個流程已經走完。

但是當浏覽器端的 js 執行完成後,發現資料重新請求了,元件的重新渲染導緻頁面看上去有些閃爍。

這是因為在浏覽器端,雙端節點對比失敗,導緻元件重新渲染,也就是隻有當服務端和浏覽器端渲染的元件具有相同的

props

和 DOM 結構的時候,元件才能隻渲染一次。

剛剛我們實作了雙端的資料預取同構,但是資料也僅僅是服務端有,浏覽器端是沒有這個資料,當用戶端進行首次元件渲染的時候沒有初始化的資料,渲染出的節點肯定和服務端直出的節點不同,導緻元件重新渲染。

資料注水

在服務端将預取的資料注入到浏覽器,使浏覽器端可以通路到,用戶端進行渲染前将資料傳入對應的元件即可,這樣就保證了

props

的一緻。

//node server 參考代碼

http.createServer((req, res) => {



const url = req.url;

if(url.indexOf('.')>-1) { res.end(''); return false;}



res.writeHead(200, {

'Content-Type': 'text/html'

});



console.log(url);



//查找元件

const branch = matchRoutes(routes,url);

//得到元件

const Component = branch[0].route.component;



//資料預取

const data = Component.getInitialProps(branch[0].match.params);



//元件渲染為 html

const html = renderToString(<Component data={data}/>);



//資料注水

const propsData = `<textarea style="display:none" id="krs-server-render-data-BOX">${JSON.stringify(data)}</textarea>`;



// 通過 ejs 模闆引擎将資料注入到頁面

ejs.renderFile('./index.html', {

htmlContent: html,

propsData

}, // 渲染的資料key: 對應到了ejs中的index

(err, data) => {

if (err) {

console.log(err);

} else {

console.log(data);

res.end(data);

}

})



}).listen(8080);



//node ejs html



<!DOCTYPE html>

<html lang="en">



<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<meta http-equiv="X-UA-Compatible" content="ie=edge">

</head>



<body>

<div id="rootEle">

<%- htmlContent %> //元件 html内容

</div>



<%- propsData %> //元件 init state ,現在是個字元串

</body>



</html>

</body>           

需要借助 ejs 模闆,将資料綁定到頁面上,為了防止

XSS

攻擊,這裡我把資料寫到了

textarea

标簽裡。

下圖中,我看着明文資料難受,對資料做了base64編碼 ,用之前需要轉碼,看個人需要。

一文吃透React SSR服務端同構渲染寫在前面為什麼要服務端渲染(ssr)核心原理react ssr引出問題同構才是核心css 過濾動态路由的 SSR其他TODO 和 思考最後源碼參考說點感想參考資料

資料脫水

上一步資料已經注入到了浏覽器端,這一步要在用戶端元件渲染前先拿到資料,并且傳入元件就可以了。

用戶端可以直接使用

id=krs-server-render-data-BOX

進行資料擷取。

第一個方法簡單粗暴,可直接在元件内的

constructor構造函數

内進行擷取,如果怕代碼重複,可以寫一個高階元件。

第二個方法可以通過 context 傳遞,隻需要在入口處傳入,在元件中聲明

staticcontextType

即可。

我是采用context 傳遞,為了後面友善內建

redux

狀态管理 。

// 定義 context 生産者 元件



import React,{createContext} from 'react';

import RootContext from './route-context';



export default class Index extends React.Component {

constructor(props,context) {

super(props);

}



render() {

return <RootContext.Provider value={this.props.initialData||{}}>

{this.props.children}

</RootContext.Provider>

}

}



//入口 app.js

import React from 'react';

import ReactDOM from 'react-dom';

import { BrowserRouter } from 'react-router-dom';

import Routes from '../';

import Provider from './provider';





//渲染入口 接收脫水資料

function renderUI(initialData) {

ReactDOM.hydrate(<BrowserRouter><Provider initialData={initialData}>

<Routes />

</Provider>

</BrowserRouter>, document.getElementById('rootEle'), (e) => {

});

}



//函數執行入口

function entryIndex() {

let APP_INIT_DATA = {};

let state = true;



//取得資料

let stateText = document.getElementById('krs-server-render-data-BOX');



if (stateText) {

APP_INIT_DATA = JSON.parse(stateText.value || '{}');

}





if (APP_INIT_DATA) {//用戶端渲染



renderUI(APP_INIT_DATA);

}

}



//入口執行

entryIndex();           

行文至此,核心的内容已經基本說完,剩下的就是元件内如何使用脫水的資料。

下面通過

context

拿到資料 , 代碼僅供參考,可根據自己的需求來進行封裝和調整。

import React from 'react';

import './css/index.scss';



export default class Index extends React.Component {



constructor(props, context) {

super(props, context);



//将context 存儲到 state

this.state = {

... context

}



}



//設定此參數 才能拿到 context 資料

static contextType = RootContext;



//資料預取方法

static async getInitialProps(krsOpt) {



if (__SERVER__) {

//如果是服務端渲染的話 可以做的處理,node 端設定的全局變量

}



const fetch1 = fetch.postForm('/fe_api/filed-manager/get-detail-of-type', {

data: { ofTypeId: 4000 }

});



const fecth2 = fetch.postForm('/fe_api/filed-manager/get-detail-of-type', {

data: { ofTypeId: 2000 }

});



const resArr = await fetch.multipleFetch(fetch1, fecth2);

//傳回所有資料

return {

page: {},

fetchData: resArr

}

}



componentDidMount() {

if (!this.isSSR) { //非服務端渲染需要自身進行資料擷取

Index.getInitialProps(this.props.krsOpt).then(data => {

this.setState({

...data

}, () => {

//可有的一些操作

});

});

}

}



render() {



//得到 state 内的資料,進行邏輯判斷和容錯,然後渲染

const { page, fetchData } = this.state;

const [res] = fetchData || [];



return <div className="detailBox">

{

res && res.data.map(item => {

return <div key={item.id}>{item.keyId}:{item.keyName}---{item.setContent}</div>

})

}

</div>

}

}           

到此我們的第三個問題:【服務端 html 節點無法重用 】已經解決,但人不夠完美,請繼續看。

css 過濾

我們在寫元件的時候大部分都會導入相關的 css 檔案。

import './css/index.scss';//導入css



//元件

class Index extends React.Component{

constructor(props){

super(props);

}





static async getInitialProps() {

const fetch1 =await fetch('/xxx.com/a');

const fetch2 = await fetch('/xxx.com/b');



return {

res:[fetch1,fetch2]

}

}



render(){

return <h1>{this.props.data.title}</h1>

}

}           

但是這個

css

檔案在服務端無法執行,其實想想在服務端本來就不需要渲染 css 。為什麼不直接幹掉?是以為了友善,我這裡寫了一個

babel

插件,在編譯的時候幹掉 css 的導入代碼。

/**

* 删除 css 的引入

* 可能社群已經有現成的插件但是不想費勁兒找了,還是自己寫一個吧。

*/

module.exports = function ({ types: babelTypes }) {

return {

name: "no-require-css",

visitor: {

ImportDeclaration(path, state) {

let importFile = path.node.source.value;

if(importFile.indexOf('.scss')>-1){

// 幹掉css 導入

path.remove();

}

}

}

};

};



//.babelrc 中使用



"plugins": [

"./webpack/babel/plugin/no-require-css" //引入

]           

動态路由的 SSR

現在要說一個更加核心的内容,也是本文的一個壓軸亮點,可以說是全網唯一,我之前也看過很多文章和資料都沒有細說這一塊兒的實作。

不知道你有沒有發現,上面我們已經一步一步的實作了

ReactSSR同構

的完整流程,但是總感覺少點什麼東西。

SPA

模式下大部分都會實作元件分包和按需加載,防止所有代碼打包在一個檔案過大影響頁面的加載和渲染,影響使用者體驗。

那麼基于

SSR

的元件按需加載如何實作呢?

當然我們所限定按需的粒度是路由級别的,請求不同的路由動态加載對應的元件。

如何實作元件的按需加載?

webpack2

時期主要使用

require.ensure

方法來實作按需加載,他會單獨打包指定的檔案,在當下

webpack4

,有了更加規範的的方式實作按需加載,那就是動态導入

import('./xx.js')

,當然實作的效果和

require.ensure

是相同的。

咱們這裡隻說如何借助這個規範實作按需加載的路由,關于動态導入的實作原理先按下不表。

我們都知道

import

方法傳入一個js檔案位址,傳回值是一個

promise

對象,然後在

then

方法内回調得到按需的元件。他的原理其實就是通過 jsonp 的方式,動态請求腳本,然後在回調内得到元件。

import('../index').then(res=>{

//xxxx

});           

那現在我們已經得到了幾個比較有用的資訊。

  • 如何加載腳本 - 

    import結合webpack

     自動完成
  • 腳本是否加載完成 - 通過在 

    then

     方法回調進行處理
  • 擷取異步按元件 - 通過在 

    then

     方法回調内擷取

我們可以試着把上面的邏輯抽象成為一個元件,然後在路由配置的地方進行導入後,那麼是不是就完成了元件的按需加載呢?

先看下按需加載元件, 目的是在

import

完成的時候得到按需的元件,然後更改容器元件的

state

,将這個

異步元件

進行渲染。

/**

* 按需加載的容器元件

* @class Bundle

* @extends {Component}

*/

export default class Async extends React.Component {

constructor(props) {

super(props);

this.state = {

COMPT: null

};

}



UNSAFE_componentWillMount() {

//執行元件加載

if (!this.state.COMPT) {

this.load(this.props);

}

}





load(props) {

this.setState({

COMPT: null

});

//注意這裡,傳回Promise對象; C.default 指向按需元件

props.load().then((C) => {

this.setState({

COMPT: C.default ? C.default : COMPT

});

});

}



render() {

return this.state.COMPT ? this.props.children(this.state.COMPT) : <span>正在加載......</span>;

}

}           

Async

容器元件接收一個 props 傳過來的 load 方法,傳回值是

Promise

類型,用來動态導入元件。

在生命周期

UNSAFE_componentWillMount

得到按需的元件,并将元件存儲到

state.COMPT

内,同時在

render

方法中判斷這個狀态的可用性,然後調用

this.props.children

方法進行渲染。

//調用

const LazyPageCom = (props) => (

<Async load={() => import('../index')}>

{(C) => <C {...props} />}//傳回函數元件

</Async>

);           

當然這隻是其中一種方法,也有很多是通過

react-loadable庫

來進行實作,但是實作思路基本相同,有興趣的可以看下源碼。

//參考代碼

import React from 'react';

import Loadable from 'react-loadable';



//loading 元件

const Loading =()=>{

return (

<div>loading</div>

)

}



//導出元件

export default Loadable({

loader:import('../index'),

loading:Loading

});           

到這裡我們已經實作了元件的按需加載,剩下就是配置到路由。

看下僞代碼

//index.js



class Index extends React.Component {



render() {

return <div>detail</div>

}

}





//detail.js



class Detail extends React.Component {



render() {

return <div>detail</div>

}

}



//routes.js



//按需加載 index 元件

const AyncIndex = (props) => (

<Async load={() => import('../index')}>

{(C) => <C {...props} />}

</Async>

);



//按需加載 detai 元件

const AyncDetail = (props) => (

<Async load={() => import('../index')}>

{(C) => <C {...props} />}

</Async>

);



const routes = [



{

path: "/",

exact: true,

component: AyncIndex

},

{

path: '/detail', exact: true,

component: AyncDetail,

}

];           

結合路由的按需加載已經配置完成,先不管 server端 是否需要進行調整,此時的代碼是可以運作的,按需也是 ok 的。

但是ssr無效了,檢視網頁源代碼無内容。

動态路由 SSR 雙端配置

ssr

無效了,這是什麼原因呢?

上面我們在做路由同構的時候,雙端使用的是同一個 route配置檔案

routes-config.js

,現在元件改成了按需加載,是以在路由查找後得到的元件發生改變了 -

AyncDetail,AyncIndex

,根本無法轉換出元件内容。

ssr 模式下 server 端如何處理路由按需加載

其實很簡單,也是參考用戶端的處理方式,對路由配置進行二次處理。server 端在進行元件查找前,強制執行

import

方法,得到一個全新的靜态路由表,再去進行元件的查找。

//獲得靜态路由



import routes from 'routes-config.js';//得到動态路由的配置



export async function getStaticRoutes() {



const staticRoutes = [];//存放新路由



for (; i < len; i++) {

let item = routes[i];



//存放靜态路由

staticRoutes.push({

...item,

...{

component: (await item.component().props.load()).default

}

});



}

return staticRoutes; //傳回靜态路由

}           

如今我們離目标更近了一步,

server

端已相容了按需路由的查找。但是還沒完!

我們這個時候通路頁面的話,ssr 生效了,檢視網頁源代碼可以看到對應的 html 内容。

但是頁面上會顯示直出的内容,然後顯示

<span>正在加載......</span>

,瞬間又變成直出的内容。

ssr 模式下 client 端如何處理路由按需加載

這個是為什麼呢?

是不是看的有點累了,再堅持一下就成功了。

其實有問題才是最好的學習方式,問題解決了,路就通了。

首先我們知道浏覽器端會對已有的節點進行雙端對比,如果對比失敗就會重新渲染,這很明顯就是個問題。

咱分析一下,首先服務端直出了 html 内容,而此時浏覽器端js執行完後需要做按需加載,在按需加載前的元件預設的内容就是

<span>正在加載......</span>

這個預設内容和服務端直出的 html 内容完全不同,是以對比失敗,頁面會渲染成

<span>正在加載......</span>

,然後按需加載完成後元件再次渲染,此時渲染的就是真正的元件了。

如何解決呢?

其實也并不複雜,隻是不确定是否可行,試過就知道。

既然用戶端需要處理按需,那麼我們等這個按需元件加載完後再進行渲染是不是就可以了呢?

答案是:可以的!

*如何按需呢?*

向“服務端同學”學習,找到對應的元件并強制 執行

import

按需,隻是這裡不是轉換為靜态路由,隻找到按需的元件完成動态加載即可。

既然有了思路,那就撸起代碼。

import React,{createContext} from 'react';

import RootContext from './route-context';



export default class Index extends React.Component {

constructor(props,context) {

super(props);

}



render() {

return <RootContext.Provider value={this.props.initialData||{}}>

{this.props.children}

</RootContext.Provider>

}

}



//入口 app.js

import React from 'react';

import ReactDOM from 'react-dom';

import { BrowserRouter } from 'react-router-dom';

import Routes from '../';

import Provider from './provider';





//渲染入口

function renderUI(initialData) {

ReactDOM.hydrate(<BrowserRouter><Provider initialData={initialData}>

<Routes />

</Provider>

</BrowserRouter>, document.getElementById('rootEle'), (e) => {

});

}



function entryIndex() {

let APP_INIT_DATA = {};

let state = true;



//取得資料

let stateText = document.getElementById('krs-server-render-data-BOX');



//資料脫水

if (stateText) {

APP_INIT_DATA = JSON.parse(stateText.value || '{}');

}





if (APP_INIT_DATA) {//用戶端渲染



- renderUI(true, APP_INIT_DATA);

//查找元件

+ matchComponent(document.location.pathname, routesConfig()).then(res => {

renderUI(true, APP_INIT_DATA);

});

}

}



//執行入口

entryIndex();           

matchComponent

是我封裝的一個元件查找的方法,在文章開始已經介紹過類似的實作,代碼就不貼了。

核心亮點說完,整個流程基本結束,剩下的都是些有的沒的了,我打算要收工了。

其他

SEO 支援

頁面的

SEO

效果取決于頁面的主體内容和頁面的 TDK(标題 title,描述 description,關鍵詞 keyword)以及關鍵詞的分布和密度,現在我們實作了

ssr

是以頁面的主體内容有了,那如何設定頁面的标題并且讓每個頁面(路由)的标題都不同呢?

隻要我們每請求一個路由的時候傳回不同的

tdk

就可以了。

這裡我在所對應元件資料預取的方法内加了約定,傳回的資料為固定格式,必須包含

page對象

,page 對象内包含 tdk 的資訊。

看代碼瞬間就明白。

import './css/index.scss';



//元件

class Index extends React.Component{

constructor(props){

super(props);

}



static async getInitialProps() {

const fetch1 =await fetch('/xxx.com/a');

const fetch2 = await fetch('/xxx.com/b');



return {

page:{

tdk:{

title:'标題',

keyword:'關鍵詞',

description:'描述'

}

}

res:[fetch1,fetch2]

}

}



render(){

return <h1>{this.props.data.title}</h1>

}

}           

這樣你的

tdk

可以根據你的需要設定成靜态還是從接口拿到的。然後可以在

esj

模闆裡進行綁定,也可以在

componentDidMount

通過 js

document.title=this.state.page.tdk.title

設定頁面的标題。

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<meta http-equiv="X-UA-Compatible" content="ie=edge">

<meta name="keywords" content="<%=page.tdk.keyword%>" />

<meta name="description" content="content="<%=page.tdk.description%>" />

<title><%=page.tdk.title%></title>

</head>

<body>

<div id="rootEle">

<%- htmlContent %>

</div>

<%- propsData %>

</body>

</html>

</body>

<%page.staticSource.js.forEach(function(item){%>           

fetch 同構

可以使用

isomorphic-fetch

axios

或者

whatwg-fetch+node-fetch

等庫來實作支援雙端的

fetch資料請求

,這裡推薦使用

axios

主要是比較友善。

TODO 和 思考

沒有介紹結合

redux

狀态管理的

ssr

實作,其實也不複雜,關鍵還是看業務中是否需要使用redux,因為文中已經實作了使用

context

傳遞資料,直接改成按

store

傳遞也很容易,但是更多的還是對

react-redux

的應用。

//渲染入口 代碼僅供參考

function renderUI(initialData) {

ReactDOM.hydrate(<BrowserRouter><Provider store={initialData}>

<Routes />

</Provider>

</BrowserRouter>, document.getElementById('rootEle'), (e) => {

});

}           

服務端同構渲染雖然可以提升首屏的出現時間,利于 SEO,對低端使用者友好,但是開發複雜度有所提高,代碼需要相容雙端運作(runtime),還有一些庫隻能在浏覽器端運作,在服務端加載會直接報錯,這種情況就需要進行做一些特殊處理。

同時也會大大的增加服務端負載,當然這都容易解決,可以改用

renderToNodeStream()

方法通過流式輸出來提升服務端渲染性能,可以進行監控和擴容,是以是否需要 ssr 模式,還要看具體的産品線和使用者定位。

最後

本文最初從 react ssr 的整體實作原理上進行說明,然後逐漸的抛出問題,循序漸進的逐漸解決,最終完成了整個

ReactSSR

所需要處理的技術點,同時對每個技術點和問題做了詳細的說明。

但實作方式并不唯一,還有很多其他的方式, 比如

next.js

,

umi.js

,但是原理相似,具體差異我會接下來進行對比後輸出。

源碼參考

由于上面文中的代碼較為零散,恐怕不能直接運作。為了友善大家的參考和學習,我把涉及到代碼進行整理、完善和修改,增加了一些基礎配置和工程化處理,目前已形成一個完整的開發骨架,可以直接運作看效果,所有的代碼都在這個骨架裡,歡迎star 歡迎 下載下傳,交流學習。

項目代碼位址: https://github.com/Bigerfe/koa-react-ssr

說點感想

很多東西都可以基于你現有的知識創造出來。

隻要明白了其中的原理,然後梳理出實作的思路,剩下的就是撸代碼了,期間會大量的自動或被動的從你現有的知識庫裡進行調取,一步一步的,隻要不怕麻煩,都能搞得定。

這也是我為什麼上來先要說下

reac ssr原理

的原因,因為它指導了我的實踐。

全文都是自己親手一個一個碼出,也全部都是出自本人的了解,但個人文采有限,是以導緻很多表達說的都是大白話,表達不夠清楚的地方還請指出和斧正,但是真正的核心已全部涵蓋。

希望本文的内容對你有所幫助,也可以對得住我這個自信的标題。

參考資料

https://github.com/ReactTraining/react-router

https://reacttraining.com/react-router/core/api/Router/children-node

https://blog.seosiwei.com/detail/10

https://www.jianshu.com/p/47c8e364d0bc?

appinstall=1&mType=Group

感謝您的朗讀,關注首頁擷取更多前端相關資料!

繼續閱讀