React SSR 同構入門與原理

2020-12-08 前端學習棧

前言

所謂同構,簡而言之就是,第一次訪問後臺服務時,後臺直接把前端要顯示的界面全部返回,而不是像 SPA 項目只渲染一個 <div id="root"></div> 剩下的都是靠 JavaScript 腳本去加載。這樣一來可以大大減少首屏等待時間。

同構概念並不複雜,它也非項目必需品,但是探索它的原理卻是必須的。

閱讀本文需要你具備以下技術基礎: Node.js 、 React 、 React Router 、 Redux 、 webpack 。

本文將分以下兩部分去講述:

同構思路分析,讓你對同構有一個概念上的了解;手寫同構框架,深入理解同構原理。同構思路

CSR 客戶端渲染

CSR 客戶端渲染,這個就是很好理解了,使用 React , React Router 前端自己控制路由的 SPA 項目,就可以理解成客戶端渲染。它有一個非常大的優勢就是,只是首次訪問會請求後臺服務加載相應文件,之後的訪問都是前端自己判斷 URL 展示相關組件,因此除了首次訪問速度慢些之外,之後的訪問速度都很快。

執行命令: create-react-app react-csr 創建一個 React SPA 單頁面應用項目 。執行命令: npm run start 啟動項目。

查看網頁原始碼:

只有一個 <div id="root"></div> 和 一些 script 腳本。最終呈現出來的界面卻是這樣的:

原理很簡單,相信學習過 webpack 的同學都知道,那就是 webpack 把所有代碼都打包成相應腳本並插入到 HTML 界面中,瀏覽器會解析 script 腳本,通過動態插入 DOM 的方式展示出相應界面。

客戶端渲染的優劣勢

客戶端渲染流程如下:

優勢:

前端負責渲染頁面,後端負責實現接口,各自幹好各自的事情,對開發效率有極大的提升;前端在跳轉界面的時候不需要請求後臺,加速了界面跳轉的速度,提高用戶體驗。劣勢:

由於需要等待 JS 文件加載以及後臺接口數據請求因此首屏加載時間長,用戶體驗較差;由於大部分內容都是通過 JS 加載因此搜尋引擎無法爬取分析網頁內容導致網站無法 SEO 。SSR 服務端渲染

SSR 是服務端渲染技術,它本身是一項比較普通的技術, Node.js 使用 ejs 模板引擎輸出一個界面這就是服務端渲染。每次訪問一個路由都是請求後臺服務,重新加載文件渲染界面。

同樣我們也來創建一個簡單的 Node.js 服務:

mkdir express-ssrcd express-ssrnpm init -ytouch app.jsnpm i express --save複製代碼app.js

const express = require('express')const app = express()app.get('/',function (req,res) { res.send(`<html> <head> <title>express ssr</title> </head> <body> <h1>Hello SSR</h1> </body> </html>` )})app.listen(3000);複製代碼啟動服務: node app.js

這就是最簡單的服務端渲染一個界面了。服務端渲染的本質就是頁面顯示的內容是伺服器端生產出來的。

服務端渲染的優劣勢

服務端渲染流程:

優勢:

整個 HTML 都通過服務端直接輸出 SEO 友好;加載首頁不需要加載整個應用的 JS 文件,首頁加載速度快。劣勢:

訪問一個應用程式的每個界面都需要訪問伺服器,體驗對比 CSR 稍差。我們會發現一件很有意思的事,服務端渲染的優點就是客戶端渲染的缺點,服務端渲染的缺點就是客戶端渲染的優點,反之亦然。那為何不將傳統的純服務端直出的首屏優勢和客戶端渲染站內跳轉優勢結合,以取得最優解?這就引出了當前流行的服務端渲染( Server Side Rendering ),或者稱之為「同構渲染」更為準確。

同構渲染

所謂同構,通俗的講,就是一套 React 代碼在伺服器上運行一遍,到達瀏覽器又運行一遍。服務端渲染完成頁面結構,客戶端渲染綁定事件。它是在 SPA 的基礎上,利用服務端渲染直出首屏,解決了單頁面應用首屏渲染慢的問題。

同構渲染流程

簡單同構案例

要實現同構,簡單來說就是以下兩步:

服務端要能運行 React 代碼;瀏覽器同樣運行 React 代碼。1、創建項目

mkdir react-ssrcd react-ssrnpm init -y複製代碼2、項目目錄結構分析

├── src│ ├── client│ │ ├── index.js // 客戶端業務入口文件│ ├── server│ │ └── index.js // 服務端業務入口文件│ ├── container // React 組件│ │ └── Home│ │ └── Home.js│ │├── config // 配置文件夾│ ├── webpack.client.js // 客戶端配置文件│ ├── webpack.server.js // 服務端配置文件│ ├── webpack.common.js // 共有配置文件├── .babelrc // babel 配置文件├── package.json複製代碼首先我們編寫一個簡單的 React 組件, container/Home/Home.js

import React from"react";const Home = ()=>{return (<div> hello world<br/><button onClick={()=> alert("hello world")}>按鈕</button></div> )}exportdefault Home;複製代碼安裝客戶端渲染的慣例,我們寫一個客戶端渲染的入口文件, client/index.js

import React from"react";import ReactDom from"react-dom";import Home from"../containers/Home";ReactDom.hydrate(<Home/>,document.getElementById("root"));// ReactDom.render(<Home/>,document.getElementById("root"));複製代碼以前看到的都是調用 render 方法,這裡使用 hydrate 方法,它的作用是什麼?

ReactDOM.hydrate

與 render() 相同,但它用於在 ReactDOMServer 渲染的容器中對 HTML 的內容進行 hydrate 操作。 React 會嘗試在已有標記上綁定事件監聽器。

我們都知道純粹的 React 代碼放在瀏覽器上是無法執行的,因此需要打包工具進行處理,這裡我們使用 webpack ,下面我們來看看 webpack 客戶端的配置:

webpack.common.js

module.exports = { module:{ rules:[ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", } ] }}複製代碼.babelrc

{"presets":[ ["@babel/preset-env"], ["@babel/preset-react"] ]}複製代碼webpack.client.js

const path = require("path");const {merge} = require("webpack-merge");const commonConfig = require("./webpack.common");const clientConfig = { mode: "development", entry:"./src/client/index.js", output:{ filename:"index.js", path:path.resolve(__dirname,"../public") },}module.exports = merge(commonConfig,clientConfig);複製代碼代碼解析:通過 entry 配置的入口文件,對 React 代碼進行打包,最後輸出到 public 目錄下的 index.js 。

在以往,直接在 HTML 引入這個打包後的 JS 文件,界面就顯示出來了,我們稱之為純客戶端渲染。這裡我們就不這樣使用,因為我們還需要服務端渲染。

接下來,看看服務端渲染文件 server/index.js

import express from"express";import { renderToString } from"react-dom/server";import React from"react";import Home from"../containers/Home";const app = express(); // {1}app.use(express.static('public')) // {2}const content = renderToString(<Home />); //{3}app.get('/',function (req,res) {// {4} res.send(` <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>React SSR</title> </head> <body> <div id="root">${content}</div> <script src="/index.js"></script> </body> </html> `)})app.listen(3000);複製代碼代碼解析:

{1},創建一個 express 實例對象{2},開啟一個靜態資源服務,監聽 public 目錄,還記得客戶端的打包文件就放到了 public 目錄了把,這裡通過監聽,我們就可以這樣 localhost:3000/index.js 訪問該靜態資源{3},把 React 組件通過 renderToString 方法生成 HTML{4},當用戶訪問 localhost:3000 時便會返回 res.send 中的 HTML 內容,該 HTML 中把 React 生成的 HTML 片段也插入進去一同返回給用戶了,這樣就實現了服務端渲染。通過 <script src="/index.js"></script> 這段腳本加載了客戶端打包後的 React 代碼,這樣就實現了客戶端渲染,因此一個簡單同構項目就這樣實現了。你會發現一個奇怪的現象,為什麼寫 Node.js 代碼使用的卻是 ESModule 語法,是的沒錯,因為我們要在服務端解析 React 代碼,作為同構項目,因此統一語法也是非常必要的。所以 Node.js 也需要配置相應的 webpack 編譯文件:

webpack.server.js

const path = require("path");const nodeExternals = require("webpack-node-externals");const {merge} = require("webpack-merge");const commonConfig = require("./webpack.common");const serverConfig = { target:"node", //為了不把nodejs內置模塊打包進輸出文件中,例如:fs net模塊等; mode: "development", entry:"./src/server/index.js", output:{ filename:"bundle.js", path:path.resolve(__dirname,"../build") }, externals:[nodeExternals()], //為了不把node_modules目錄下的第三方模塊打包進輸出文件中,因為nodejs默認會去node_modules目錄下去尋找和使用第三方模塊。};module.exports = merge(serverConfig,commonConfig);複製代碼如果 webpack 不熟悉的可以查看作者的>>>深入淺出 webpack 之基礎配置篇

本小結完整代碼地址>>>點擊查看

到此我們就完成了一個簡單的同構項目,這裡您應該會有幾個疑問?

renderToString 有什麼作用?為什麼服務端加載了一次,客戶端還需要再次加載呢?服務端加載了 React 輸出的代碼片段,客戶端又執行了一次,這樣是不是會加載兩次導致資源浪費呢?ReactDOMServer.renderToString(element)

將 React 元素渲染為初始 HTML 。 React 將返回一個 HTML 字符串。你可以使用此方法在服務端生成 HTML ,並在首次請求時將標記下發,以加快頁面加載速度,並允許搜尋引擎爬取你的頁面以達到 SEO 優化的目的。

為什麼服務端加載了一次,客戶端還需要再次加載呢?

原因很簡單,服務端使用 renderToString 渲染頁面,而 react-dom/server 下的 renderToString 並沒有做事件相關的處理,因此返回給瀏覽器的內容不會有事件綁定,渲染出來的頁面只是一個靜態的 HTML 頁面。只有在客戶端渲染 React 組件並初始化 React 實例後,才能更新組件的 state 和 props ,初始化 React 的事件系統,讓 React 組件真正「 動」 起來。

是否加載兩次?

如果你在已有服務端渲染標記的節點上調用 ReactDOM.hydrate() 方法, React 將會保留該節點且只進行事件處理綁定,從而讓你有一個非常高性能的首次加載體驗。因此不必擔心加載多次的問題。

是否意猶未盡?那就讓我們更加深入的學習它,手寫一個同構框架,徹底理解同構渲染的原理。

手寫同構框架

實現一個同構框架,我們還有很多問題需要解決:

兼容路由;兼容 Redux ;兼容異步數據請求;兼容 CSS 樣式渲染。問題很多,我們逐個擊破。

兼容路由

同構項目中當在瀏覽器中輸入 URL 後,瀏覽器是如何找到對應的界面?

瀏覽器收到 URL 地址例如: http://localhost:3000/login ;後臺路由找到對應的 React 組件傳入到 renderToString 中,然後拼接 HTML 輸出頁面;瀏覽器加載打包後的 JS 文件,並解析執行前端路由,輸出相應的前端組件,發現是服務端渲染,因此只做事件綁定處理,不進行重複渲染,此時前端路由路由開始接管界面,之後跳轉界面與後臺無關。既然需要路由我們就先安裝下: npm install react-router-dom

之前我們只定義了一個 Home 組件,為了演示路由,我們再定義一個 Login 組件:

...import { Link } from"react-router-dom";const Login = ()=>{return (<div><h1>登錄頁</h1><br/><Link to="/">跳轉到首頁</Link></div> )}複製代碼改造 Home 組件

const Home = ()=>{return (<div><h1>首頁</h1><br/><Link to="/login">跳轉到登錄頁</Link><br/><button onClick={() => console.log("click me")}>點擊</button></div> )}複製代碼現在我們有兩個組件了,可以開始定義相關路由:

src/Routes.js

...import {Route} from"react-router-dom";exportdefault (<div><Route path="/"exactcomponent={Home} /> // 訪問根路徑時展示Home組件<Route path="/login"component={Login} /> // 訪問/login路徑時展示Login組件</div>)複製代碼改造客戶端路由:src/client/index.js

...import { BrowserRouter } from"react-router-dom";import Routes from"../Routes";const App = ()=>{return (<BrowserRouter> {Routes}</BrowserRouter> )}ReactDom.hydrate(<App />,document.getElementById("root"));複製代碼與普通 SPA 項目沒有任何區別。

改造服務端路由:src/server/index.js

...import { StaticRouter } from"react-router-dom";import Routes from"../Routes";const app = express();app.use(express.static('public'))const render = (req)=>{const content = renderToString((<StaticRouter location={req.path}> {Routes}</StaticRouter> ));return` <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>React SSR</title> </head> <body> <div id="root">${content}</div> <script src="/index.js"></script> </body> </html> `}app.get('*',function (req,res) { res.send(render(req))})複製代碼服務端跟之前的區別就是這段代碼:

<StaticRouter location={req.path}> {Routes}</StaticRouter>複製代碼為什麼不是 BrowserRouter 而是 StaticRouter 呢?

主要是因為 BrowserRouter 使用的是 History API 記錄位置,而 History API 是屬於瀏覽器的 API ,在 SSR 的環境下,服務端不能使用瀏覽器 API 。

StaticRouter

靜態路由,通過初始傳入的 location 地址找到相應組件。區別於客戶端的動態路由。官網解釋

查看最終效果:

如果對於 React Router 不熟悉的同學可以查看作者的>>>React Router 入門與原理

本小結完整代碼地址>>>點擊查看

兼容 Redux

Redux 一直以來都是 React 技術棧裡最難理解的部分,它的概念繁多,如果想要徹底理解本小節及以後的內容,需要您對 Redux 有一定的了解,如果對於 Redux 不熟悉的同學可以查看作者的>>>Redux 應用與原理。

安裝包:

npm i redux react-redux redux-thunk --save複製代碼redux 庫;react-redux 是 react 與 redux 的橋梁;redux-thunk 是 redux 中間件, redux 處理異步請求方案。src/store/index.js

import {createStore, applyMiddleware} from"redux";import thunk from"redux-thunk";const reducer = (state={name:"Lion"},action)=>{return state;}const getStore = ()=>{return createStore(reducer,applyMiddleware(thunk));}exportdefault getStore;複製代碼輸出一個方法 getStore 用於創建全局 store 對象。

改造 server 端, src/server/render.js

... 省略import { Provider } from"react-redux";import getStore from"../store";exportconst render = (req)=>{const content = renderToString((<Provider store={getStore()}><StaticRouter location={req.path}> {Routes}</StaticRouter></Provider> ));return` ... 省略 `}複製代碼通過 Provider 組件把 store 對象數據共享給所有子組件,它的本質還是通過 context 共享數據。

改造 client 端, src/client/index.js

...import { Provider } from"react-redux";import getStore from"../store";const App = ()=>{return (<Provider store={getStore()}><BrowserRouter> {Routes}</BrowserRouter></Provider> )}ReactDom.hydrate(<App />,document.getElementById("root"));複製代碼同 server 端改造非常類似。

redux 都添加完畢後,最後我們在組件中使用 redux 的方式獲取數據,改造 Home 組件:

import React from"react";import { Link } from"react-router-dom";import { connect } from"react-redux";const Home = (props)=>{return (<div><h1>首頁</h1><div>{props.name}</div><br/><Link to="/login">跳轉到登錄頁</Link><br/><button onClick={() => console.log("click me")}>點擊</button></div> )}const mapStateToProps = (state)=>({ name:state.name})exportdefault connect(mapStateToProps,null)(Home);複製代碼其實核心就是這幾行代碼:

const mapStateToProps = (state)=>({ name:state.name})exportdefault connect(mapStateToProps,null)(Home);複製代碼connect 接收 mapStateToProps 、 mapDispatchToProps 兩個方法,返回一個高階函數,這個高階函數接收一個組件,返回一個新組件,其實就是給傳入的組件增加一些屬性和功能。

這樣一來我們的 Home 組件就可以使用 name 屬性了。改造完畢,我們來看看效果:

可以正常使用,這樣我們就輕鬆的集成了 redux 。

本小結完整代碼地址>>>點擊查看

兼容異步數據請求

在構建企業級項目時, redux 使用就更為複雜,而且實戰中我們一般都需要請求後臺數據,讓我們來改造改造項目,使他成為企業級項目。

redux 改造

一般我們會把 redux 相關的代碼都放入 store 文件夾下,我們來看看它的新目錄:

├── src│ ├── store│ │ ├── actions.js│ │ ├── constans.js│ │ └── reducer.js└───────└── index.js複製代碼actions 負責生成 action ;constans 定義常量;reducer 定義 reducer ;index 輸出 store 。actions.js

import axios from'axios';import {CHANGE_USER_LIST} from"./constants";const changeUserList = (list)=>{return { type:CHANGE_USER_LIST, list }}exportconst getUserList = (dispatch)=>{return ()=>{ axios.get('https://reqres.in/api/users').then((res)=>{ dispatch(changeUserList(res.data.data)); }); }}複製代碼導出 getUserList 方法,它的主要職責是向後臺發送真實數據請求。

[注意] 這裡發送的請求是真實的,具體可以參考Reqres是真正的API。

constants.js

exportconst CHANGE_USER_LIST = 'HOME/CHANGE_USER_LIST';複製代碼輸出常量,定義常量可以保證您在調用時不容易出錯。

reducer.js

import { CHANGE_USER_LIST } from"./constants";// {1}const defaultState = { userList:[]};exportdefault (state = defaultState , action)=>{switch (action.type) {// {2}case CHANGE_USER_LIST:return { ...state, userList:action.list } default:return state; }}複製代碼代碼解析:

{1},定義默認 state , userList 為空數組;{2},當接收到 type 為 CHANGE_USER_LIST 的 dispatch 時,更新用戶列表,這也是我們在 actions 那裡接收到後臺請求數據之後發送的 dispatch , dispatch(changeUserList(res.data.data));redux 改造的差不多了,接下來改造 Home 組件:src/containers/Home/index.js

import React,{useEffect} from"react";import { Link } from"react-router-dom";import { connect } from"react-redux";import { getUserList } from"../../store/actions";const Home = ({getUserList,name,userList})=>{// {2} useEffect(()=>{ getUserList(); },[])return (<div><h1>首頁</h1><ul> { {/* 3 */} userList.map(user=>{ const { first_name, last_name, email, avatar, id } = user; return <li key={id}><img src={avatar}alt="用戶頭像"style={{width:"30px",height:"30px"}}/><div>姓名:{`${first_name}${last_name}`}</div><div>email:{email}</div></li> }) }</ul><br/><Link to="/login">跳轉到登錄頁</Link><br/><button onClick={() => console.log("click me")}>點擊</button></div> )}const mapStateToProps = (state)=>({ name:state.name, userList:state.userList});// {1}const mapDispatchToProps = (dispatch)=>({getUserList(){ dispatch(getUserList(dispatch)) }})exportdefault connect(mapStateToProps,mapDispatchToProps)(Home);複製代碼代碼解析:

{1}, mapDispatchToProps 同 mapStateToProps 作用一致都是 connect 的入參,把相關的 dispatch 與 state 傳入 Home 組件中。{2}, useEffect Hook 中調用 getUserList 方法,獲取後臺真實數據{3},根據真實返回的 userList 渲染組件我們來看看實際效果:

看起來很不錯, react-router 與 redux 都已經支持了,但是當你查看下網頁源碼時會發現一個問題:

用戶列表數據並不是服務端渲染的,而是通過客戶端渲染的。為什麼會這樣呢?我們一起分析下請求過程你就會明白:

接下來我們主要的目標就是服務端如何可獲取到數據?既然 useEffect 不會在服務端執行,那麼我們就自己創建一個 「Hook」 。

在 Next.js 中 getInitialProps 就是這個被創建的 「Hook」 ,它的主要職責就是使服務端渲染可以獲取初始化數據。

getInitialProps 實現

在 Home 組件中我們先添加這個靜態方法:

Home.getInitialData = (store)=>{return store.dispatch(getUserList());}複製代碼在 getInitialData 中做的事情同 useEffect 相同,都是去發送後臺請求獲取數據。

在 React Router 文檔中關於服務端渲染想要先獲取到數據需要把路由改為靜態路由配置。

src/Routes.js

import { Home, Login } from"./containers";exportdefault [ { key:"home", path: "/", exact: true, component: Home, }, { key:"login", path: "/login", exact: true, component: Login, }];複製代碼現在剩下最主要的工作就是服務端渲染網頁之前拿到後臺數據了。

react-router-config 這個包是 React Router 提供給我們用於分析靜態路由配置的包。我們先安裝它 npm install react-router-config --save

src/server/render.js

... 省略import {matchRoutes, renderRoutes} from"react-router-config";import Routes from"../Routes";exportconst render = (req,res)=>{const store = getStore();// {1}const promises = matchRoutes(Routes, req.path).map(({ route }) => {const component = route.component;return component.getInitialData ? component.getInitialData(store) : null; });// {2}Promise.all(promises).then(()=>{const content = renderToString((<Provider store={store}> // {3}<StaticRouter location={req.path}>{renderRoutes(Routes)}</StaticRouter></Provider> )); res.send( ` ... `) })}複製代碼代碼解析:

{1}, matchRoutes 獲取當前訪問路由所匹配到的組件,匹配到的組件如果有 getInitialData 方法就直接調用;{2}, component.getInitialData(store) 返回都是 Promise , 等待全部 Promise 執行完成後, store 中的 state 就有數據了,此時服務端就已經獲取到相應組件的後臺數據;{3},renderRoutes 它的作用是根據靜態路由配置渲染出 <Route /> 組件,類似下面代碼,不過 renderRoutes 邊界處理的更加完善。{routes.map(route => (<Route {...route} />))}複製代碼此時運行效果:

細心的你肯定會發現,明明伺服器已經拿到數據了為什麼刷新瀏覽器會一閃一閃呢,原因在於,客戶端渲染接管時,初始化的用戶列表依然是個空數組,通過發送後臺請求獲取到數據這個異步過程,導致的頁面一閃一閃的。它的解決方案有一個術語叫做數據的脫水與注水。

數據脫水與注水

其實非常簡單,在渲染服務端時,已經拿到了後臺請求數據,因此我們可以做:

res.send( ` <!doctype html> <html lang="en"> ... <body> <div id="root">${content}</div> <script> window.INITIAL_STATE = ${JSON.stringify(store.getState())} </script> <script src="/index.js"></script> </body> </html> `)複製代碼通過 INITIAL_STATE 全局變量把後臺請求到的數據存起來。客戶端創建 store 時,當做初始化的 state 使用即可:

src/store/index.js

exportconst getClientStore = ()=>{const defaultState = window.INITIAL_STATE;return createStore(reducer,defaultState,applyMiddleware(thunk));}複製代碼這樣創建出來的 store 初始化的 state 中就已經有了用戶列表。界面就不再會出現一閃一閃的效果了。

到這裡為止,一個簡易的同構框架已經有了。

本小結完整代碼地址>>>點擊查看

兼容 CSS 樣式渲染

在 Home 組件中添加一個樣式文件: styles.module.css ,隨便寫點樣式

.box{ background: red; margin-top: 100px;}複製代碼在 Home 組件中引入樣式:

import styles from"./styles.module.css";<div className={styles.box}>...</div>複製代碼直接編譯肯定報錯,我們需要在 webpack 中添加相應的 loader

webpack.client.js

module:{ rules:[ { test:/\.css$/i, // 正則匹配到.css樣式文件 use:['style-loader', // 把得到的CSS內容插入到HTML中 { loader: 'css-loader', options: { modules: true// 開啟 css modules } } ] } ] }複製代碼webpack.server.js

module:{ rules:[ { test:/\.css$/i, use:['isomorphic-style-loader', { loader: 'css-loader', options: { modules: true } }, ] } ] }複製代碼細心的你肯定會發現, server 端的配置使用了 isomorphic-style-loader 而 client 端使用了 style-loader ,它們的區別是什麼?

isomorphic-style-loader vs style-loader

style-loader 它的作用是把生成出來的 css 樣式動態插入到 HTML 中,然而在服務端渲染是沒有辦法使用 DOM 的,因此服務端渲染不能使用它。

isomorphic-style-loader 主要是導出了3個函數, _getCss 、 _insertCss 與_getContent ,供使用者調用,而不再是簡單粗暴的插入 DOM 中。

server 端支持樣式

src/server/render.js

exportconst render = (req,res)=>{const context = { css: [] };Promise.all(promises).then(()=>{const content = renderToString((<Provider store={store}><StaticRouter location={req.path}context={context}>{renderRoutes(Routes)}</StaticRouter></Provider> ));const css = context.css.length ? context.css.join('\n') : ''; res.send( ` <!doctype html> <html lang="en"> <head> ... <style>${css}</style> </head> ... </html> `)}複製代碼StaticRouter 支持傳入一個 context 屬性,這樣被訪問的組件則可以共享該屬性。在被訪問組件的生命周期中通過調用 _getCss() 方法向 staticContext 中推入樣式。最後在服務端拼接出所有樣式插入到 HTML 中。

Home 組件(改造成 class 組件)

componentWillMount() {if(this.props.staticContext){this.props.staticContext.css.push(styles._getCss()); } }複製代碼在 componentWillMount 生命周期(服務端渲染會調用該生命周期),向 staticContext 中推入組件使用的樣式。最後在服務端拼接成完整的樣式文件。

這裡使用 staticContext 可以實現,使用 redux 也一樣可以實現。

本小結完整代碼地址>>>點擊查看

總結

到此為止我們就實現了一個簡易的同構框架。下面做一個簡單的總結:

同構渲染其實就是將同一套 react 代碼在服務端執行一遍渲染靜態頁面,又在客戶端執行一遍完成事件綁定。它的優勢是,加快首頁訪問速度以及 SEO 友好,如果你的項目沒有這方面的需求,則不需要選擇同構。它的缺點是,不能在服務端渲染期間操作 DOM 、 BOM 等 api ,比如 document 、 window 對象等,並且它增加了代碼的複雜度,某些代碼操作需要區分運行環境。在實際項目中,建議使用 Next.js 框架去做,站在巨人的肩旁上,可以少踩很多坑。

相關焦點

  • 快速在你的vue/react應用中實現ssr(服務端渲染)
    摘要ssr(服務端渲染)技術實現方案接下來筆者將列舉幾個常用的基於vue/react的服務端渲染方案,如下:使用next.js/nuxt.js的服務端渲染方案>使用node+vue-server-renderer實現vue項目的服務端渲染使用node+React renderToStaticMarkup實現react項目的服務端渲染傳統網站通過模板引擎來實現ssr(比如ejs, jade, pug等)
  • React Native 從入門到原理
    作者:bestswifter連結:http://www.jianshu.com/p/978c4bd3a759React Native 是最近非常火的一個話題,介紹如何利用 React Native 進行開發的文章和書籍多如牛毛,但面向入門水平並介紹它工作原理的文章卻寥寥無幾。
  • 手寫ReactHook核心原理,再也不怕面試官問我ReactHook原理
    所以,useMemo的原理跟useCallback的差不多,仿寫即可。原理看原理你會發現十分簡單,簡單到不用我說什麼,不到十行代碼,不信你直接看代碼import React from 'react';import ReactDOM from 'react-dom';let lastState// useReducer原理
  • React Status 中文周刊 #19 - React Hook發布兩周年回顧
    原文連結:https://dev.to/ryansolid/the-react-hooks-announcement-in-retrospect-2-years-later-18lmRyan CarniatoBBC 是如何將 3100 萬 UV 的站點遷移為 React 同構應用,並提高頁面性能的?
  • React Hook 入門教程
    React Hook 入門教程Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。react hook 產生的原因在組件之間復用狀態邏輯很難組件之間復用狀態邏輯很困難,我們可能會把組件連結到我們的 store 裡面。
  • react腳手架create-react-app入門
    記得關注,每天都在分享不同知識不管是哪個前端框架都支持快速開發,通過腳手架可以讓咱們更便捷的構建項目,無需考慮搭建環境問題,今天來聊聊create-react-app腳手架安裝腳手架>npm install -g create-react-app創建項目create-react-app myapp # myapp是項目的名稱,這樣就會在當前目錄生成一個myapp的項目
  • React Native:從入門到原理
    ▲點擊上方「CocoaChina」關注即可免費學習 iOS 開發本文授權轉載,作者:bestswifter(簡書)React Native 是最近非常火的一個話題,介紹如何利用 React Native 進行開發的文章和書籍多如牛毛,但面向入門水平並介紹它工作原理的文章卻寥寥無幾
  • React 入門
    正所謂看不如寫,本篇文章的目的就是從原理層面探究 React 是如何工作的。React 通過 props 和 state 來簡化關鍵數據的存儲,對於一個 react 組件函數而言,在 1 秒內可能被執行很多次。
  • 「源碼解析 」這一次徹底弄懂react-router路由原理
    筆者個人感覺學習react-router,有助於我們學習單頁面應用(spa)路由跳轉原理,讓我們理解從history.push,到組件頁面切換的全套流程,使我們在面試的時候不再為路由相關的問題發怵,廢話不說,讓我們開啟深入react-router源碼之旅吧。一 正確理解react-router1 理解單頁面應用什麼是單頁面應用?
  • 設計素描的表達方法——異質同構
    異質同構異質同構是在同一個畫面中將兩個或兩個以上帶有獨立意義的元素按一定的規律加以構成、排列與融合,從而產生新的或更具意義的視覺形態,是設計中特別吸引人的一種表達方式。它的重點在於用更廣闊的視角全方位地觀察元素之間的同構特點,巧妙地把同構原理運用於設計。異質同構中對設計符號組合和轉換的方法,經常被運用於標誌設計、海報設計、招貼廣告、字體設計、包裝設計等領域。中國傳統文化中的神話人物人首蛇身的伏羲、女媧,埃及的獅身人面像,安徒生童話中的美人魚形象等,都是異質同構創作的代表。
  • React系列教程
    數字時鐘組件:顯示目前時間的一個React的數字時鐘組件加減數字框:通過加減按鈕來控制數字的React組件第四講:React獲取節點方法、雙向綁定和拖拽案例1.React的雙向綁定的實現方法2.React獲取DOM對象的方法3.Reaact實現拖拽的原理第五講:React生命周期、事件冒泡
  • ReactJS 服務端同構實踐【QQ音樂web團隊】
    作者:calvin 騰訊 QQ音樂 數位音樂部  工程師最近在項目中接入了 ReactJS 並在服務端做了同構直出
  • 同構的宇宙
    同構同構,相同的結構。同構能有這麼漂亮的一個漢語名字,一半的功勞要歸翻譯家,因為他翻譯得很巧妙。另一半的功勞要歸漢語本身,因為漢語的造詞能力特別強,像這樣簡潔精妙的詞語還有很多。聽到這裡,你可能會有點害怕。同構是不是一個很難理解的概念呢?不用怕,我來舉一個最最簡單的例子,保證你一聽就明白。射擊用的靶子大家都看到過吧。它是由10個圓圈組成的。每個圓圈的大小都不一樣。
  • Redux 入門教程(三):React-Redux 的用法
    import { connect } from 'react-redux'const VisibleTodoList = connect()(TodoList);上面代碼中,TodoList是 UI 組件,VisibleTodoList就是由 React-Redux 通過connect方法自動生成的容器組件。
  • 手寫React-Router源碼,深入理解其原理
    看起來我們要搞懂react-router-dom的源碼還必須得去看react-router和history的源碼,現在我們手上有好幾個需要搞懂的庫了,為了看懂他們的源碼,我們得先理清楚他們的結構關係。>:瀏覽器上使用的庫,會引用react-router核心庫react-router-native:支持React-Native的路由庫,也會引用react-router核心庫像這樣多個倉庫,發布多個包的情況,傳統模式是給每個庫都建一個git repo,這種方式被稱為multi-repo。
  • React 測試入門教程
    $ git clone https://github.com/ruanyf/react-testing-demo.git$ cd react-testing-demo然後,打開 http://127.0.0.1:8080/,你會看到一個 Todo 應用。
  • React + Redux + React-router 全家桶
    web端開發需要搭配React-dom使用import React from 'react';import ReactDOM from 'react-dom';const App = () => (  <div>      <
  • React Hooks 原理與最佳實踐
    看到實現後你就會理解 render props 的原理Custom Hooks對於 react 來說,在函數組件中使用 state 固然有一些價值,但最有價值的還是可以編寫通用 custom hooks 的能力。想像一下,一個單純不依賴 UI 的業務邏輯 hook,我們開箱即用。不僅可以在不同的項目中復用,甚至還可以跨平臺使用,react、react native、react vr 等等。
  • 助力 SSR,使用 concent 為 Next.js 應用加點料
    序言nextjs是一個非常流行的 React 服務端渲染應用框架,它很輕量,簡單易上手,社區活躍,所以當我們使用react寫一個需要ssr(server side render)的應用的話,基本都會首選nextjs,concent是一個新生代的react狀態管理方案,它內置依賴收集系統,同時兼具有0入侵、可預測、漸進式、高性能的特點,並提供了
  • 同構手法
    因此,圖形同構這種創造性行為,實質上是一種尋求視覺傳達的獨創性意念、在理性思維前提下的一種發散思維方式,屬創新思維。圖形同構的方法圖形同構創意的設計手法,主要是圖形同構。所謂圖形同構,是指兩個或兩個以上的圖形由於相似性的輪廓,而巧妙地組合在一起,構成新的圖形。同構很大的特點是創新性。