前段時間一直在研究react ssr
技術,而後寫了一個完整的ssr
開發骨架。今天寫文,主要是把個人研究成果的精華內容整理落地,另外經過再次梳理但願發現更多優化的地方,也但願可讓更多的人少踩一些坑,讓跟多的人理解和掌握這個技術。javascript
相信看過本文(前提是能對你的胃口,也能較好的消化吸取)你必定會對 react ssr
服務端渲染技術有一個深刻的理解,能夠打造本身的腳手架,更能夠用來改造本身的實際項目,固然這不只限於 react
,其餘框架都同樣,畢竟原理都是類似的。php
至於爲何要服務端渲染,我相信你們都有所聞,並且每一個人都能說出幾點來。css
在 SPA 模式下,全部的數據請求和 Dom 渲染都在瀏覽器端完成,因此當咱們第一次訪問頁面的時候極可能會存在「白屏」等待,而服務端渲染全部數據請求和 html內容已在服務端處理完成,瀏覽器收到的是完整的 html 內容,能夠更快的看到渲染內容,在服務端完成數據請求確定是要比在瀏覽器端效率要高的多。html
有些網站的流量來源主要仍是靠搜索引擎,因此網站的 SEO 仍是很重要的,而 SPA 模式對搜索引擎不夠友好,要想完全解決這個問題只能採用服務端直出。改變不了別人(搜索yinqing),只能改變本身。前端
只實現 SSR
其實沒啥意義,技術上沒有任何發展和進步,不然 SPA
技術就不會出現。vue
可是單純的 SPA
又不夠完美,因此最好的方案就是這兩種體驗和技術的結合,第一次訪問頁面是服務端渲染,基於第一次訪問後續的交互就是 SPA
的效果和體驗,還不影響SEO
效果,這就有點完美了。java
單純實現 ssr
很簡單,畢竟這是傳統技術,也不分語言,隨便用 php 、jsp、asp、node 等均可以實現。node
可是要實現兩種技術的結合,同時能夠最大限度的重用代碼(同構),減小開發維護成本,那就須要採用 react
或者 vue
等前端框架相結合 node (ssr)
來實現。react
本文主要說 React SSR 技術
,固然 vue
也同樣,只是技術棧不一樣而已。webpack
總體來講 react
服務端渲染原理不復雜,其中最核心的內容就是同構。
node server
接收客戶端請求,獲得當前的req url path
,而後在已有的路由表內查找到對應的組件,拿到須要請求的數據,將數據做爲 props
、context
或者store
形式傳入組件,而後基於 react
內置的服務端渲染api renderToString() or renderToNodeStream()
把組件渲染爲 html字符串
或者 stream 流
, 在把最終的 html
進行輸出前須要將數據注入到瀏覽器端(注水),server 輸出(response)後瀏覽器端能夠獲得數據(脫水),瀏覽器開始進行渲染和節點對比,而後執行組件的componentDidMount
完成組件內事件綁定和一些交互,瀏覽器重用了服務端輸出的 html 節點
,整個流程結束。
技術點確實很多,但更多的是架構和工程層面的,須要把各個知識點進行連接和整合。
這裏放一個架構圖
實現 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);
複製代碼
上面咱們結合 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 --presets=@babel/preset-react
複製代碼
在上面很是簡單的就是實現了 react ssr
,把jsx
做爲模板引擎,不要小看上面的一小段代碼,他能夠幫咱們引出一系列的問題,這也是完整實現 react ssr
的基石。
首先咱們會發現我在 server
端定義了路由 '/',可是在 react SPA
模式下咱們須要使用react-router
來定義路由。那是否是就須要維護兩套路由呢?
發現數據獲取的fetch
寫的獨立的方法,和組件沒有任何關聯,咱們更但願的是每一個路由都有本身的 fetch 方法。
雖然組件在服務端獲得了數據,也能渲染到瀏覽器內,可是當瀏覽器端進行組件渲染的時候直出的內容會一閃而過消失。
好了,問題有了,接下來咱們就一步一步的來解決這些問題。
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編碼 ,用以前須要轉碼,看我的須要。
上一步數據已經注入到了瀏覽器端,這一步要在客戶端組件渲染前先拿到數據,而且傳入組件就能夠了。
客戶端能夠直接使用id=krs-server-render-data-BOX
進行數據獲取。
第一個方法簡單粗暴,可直接在組件內的constructor 構造函數
內進行獲取,若是怕代碼重複,能夠寫一個高階組件。
第二個方法能夠經過 context 傳遞,只須要在入口處傳入,在組件中聲明 static contextType
便可。
我是採用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 文件。
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" //引入
]
複製代碼
如今要說一個更加核心的內容,也是本文的一個壓軸亮點,能夠說是全網惟一,我以前也看過不少文章和資料都沒有細說這一起的實現。
不知道你有沒有發現,上面咱們已經一步一步的實現了 React SSR 同構
的完整流程,可是總感受少點什麼東西。
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
無效了,這是什麼緣由呢?
上面咱們在作路由同構的時候,雙端使用的是同一個 route配置文件routes-config.js
,如今組件改爲了按需加載,因此在路由查找後獲得的組件發生改變了 - AyncDetail,AyncIndex
,根本沒法轉換出組件內容。
其實很簡單,也是參考客戶端的處理方式,對路由配置進行二次處理。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>
,瞬間又變成直出的內容。
這個是爲何呢?
是否是看的有點累了,再堅持一下就成功了。
其實有問題纔是最好的學習方式,問題解決了,路就通了。
首先咱們知道瀏覽器端會對已有的節點進行雙端對比,若是對比失敗就會從新渲染,這很明顯就是個問題。
咱分析一下,首先服務端直出了 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
效果取決於頁面的主體內容和頁面的 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){%>
複製代碼
可使用isomorphic-fetch
、axios
或者whatwg-fetch + node-fetch
等庫來實現支持雙端的 fetch 數據請求
,這裏推薦使用axios
主要是比較方便。
沒有介紹結合 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 的總體實現原理上進行說明,而後逐步的拋出問題,按部就班的逐步解決,最終完成了整個React SSR
所須要處理的技術點,同時對每一個技術點和問題作了詳細的說明。
但實現方式並不惟一,還有不少其餘的方式, 好比 next.js
, umi.js
,可是原理類似,具體差別我會接下來進行對比後輸出。
因爲上面文中的代碼較爲零散,恐怕不能直接運行。爲了方便你們的參考和學習,我把涉及到代碼進行整理、完善和修改,增長了一些基礎配置和工程化處理,目前已造成一個完整的開發骨架,能夠直接運行看效果,全部的代碼都在這個骨架裏,歡迎star 歡迎 下載,交流學習。
項目代碼地址: github.com/Bigerfe/koa…
不少東西均可以基於你現有的知識創造出來。
只要明白了其中的原理,而後梳理出實現的思路,剩下的就是擼代碼了,期間會大量的自動或被動的從你現有的知識庫裏進行調取,一步一步的,只要不怕麻煩,都能搞得定。
這也是我爲何上來先要說下reac ssr 原理
的緣由,由於它指導了個人實踐。
全文都是本身親手一個一個碼出,也所有都是出自本人的理解,但我的文采有限,因此致使不少表達說的都是大白話,表達不夠清楚的地方還請指出和斧正,可是真正的核心已所有涵蓋。
但願本文的內容對你有所幫助,也能夠對得住我這個自信的標題。
github.com/ReactTraini… reacttraining.com/react-route… blog.seosiwei.com/detail/10 www.jianshu.com/p/47c8e364d…
更多精彩好玩有用的前端內容,請關注公衆號《前端張大胖》