React SSR 同構入門與原理

2020-12-12 前端學習棧

前言

所謂同構,簡而言之就是,第一次訪問後臺服務時,後臺直接把前端要顯示的界面全部返回,而不是像 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等)
  • 設計素描的表達方法——異質同構
    異質同構異質同構是在同一個畫面中將兩個或兩個以上帶有獨立意義的元素按一定的規律加以構成、排列與融合,從而產生新的或更具意義的視覺形態,是設計中特別吸引人的一種表達方式。它的重點在於用更廣闊的視角全方位地觀察元素之間的同構特點,巧妙地把同構原理運用於設計。
  • 如何用純css打造類materialUI的按鈕點擊動畫並封裝成react組件
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言作為一個前端框架的重度使用者,在技術選型上也會非常注意其生態和完整性.筆者先後開發過基於vue,react,angular等框架的項目,碧如vue生態的elementUI
  • 同構空間「薔薇戰爭」
    並肩戰鬥的號角已經吹響~~勇敢的少年啊請不要再猶豫——超期待的同構時空又又又又來啦!
  • 《精通react/vue組件設計》之快速實現一個可定製的進度條組件
    props屬性)可以很快的實現多個不同的表現和重用.我將會使用react帶大家實現這個進度條組件, 大家不用擔心技術棧不一樣,因為react實現的組件可以很快套用於vue項目中, 所以說底層原理非常重要.
  • 這5個React應用程式庫不要錯過……
    涉及到測試React應用程式時,有一個黃金標準:testing-library/react,也稱為react-testing-library。它已經內置在create-react-app中,所以入門就變得很簡單啦。使用testing-library,可以從用戶的角度測試組件。什麼意思?下面用一個實際的例子來說明。
  • 同構時空——學園孤島開啟!
    2.等級40級以上的米娜桑可以進入同構時空關卡~    注意事項:    玩家剩餘持有的【同構晶片】將在11月12日24:00按照五個【同構晶片】轉化成一個【遺忘之塵】的比例轉入兌換商店若想要兌換同構時空兌換商店中的物品,請在11月12日24:00前進行。由於學園孤島開放時間承接薔薇戰爭,所以米娜桑在薔薇戰爭中獲得的同構晶片將會繼承至學園孤島中喲~~~    米娜桑,崩壞娘最近注意有部分同學使用科技手段獲取不當利益。    這樣做不僅破壞了學園的規則,也對米娜桑的學園生活帶來了不好的體驗。
  • 同構圖形最簡單、最有效
    雖然置換同構使物體間的結構關係不變,但經過異常組合後的置換圖形在新的形態結構中,卻因這種組合方法導致了邏輯上的"張冠李戴"。 異形同構 異形同構是將兩個完全不同的物體
  • 基於jsoneditor二次封裝一個可實時預覽的json編輯器組件react版
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言做為一名前端開發人員,掌握vue/react/angular等框架已經是必不可少的技能了,我們都知道,vue或react等MVVM框架提倡組件化開發
  • AJ React Elevation開箱測評
    還記得這雙耐克的全新團隊鞋款Jordan React Elevation麼,剛爆出的時候很多小夥伴還以為是鑽石2代鞋款,其實小編覺得鞋款和威少2代才是最像的哈,前掌Zoom+後掌react的配置加持還是得到了很多人的關注的,下面一起來看看吧。
  • 陰陽師高概率抽ssr技巧,非酋不知火超高ssr概率現世召喚陣分享
    在陰陽師遊戲中一共有三種道具可以抽到ssr式神,分別是勾玉、神秘符咒(藍票)、現世召喚符咒,而這幾種中最稀有的就是現世召喚符咒,下面小編就給大家帶來超高概率出ssr的現世召喚陣。陰陽師非酋福音不知火超高ssr概率現世召喚陣分享在陰陽師遊戲中,玩家對於ssr的全收錄是永遠的追求,但是非酋的黑臉已經是出了名的,那麼該怎麼逆轉非酋的命運呢?下面小編就給廣大非酋玩家帶來了史上最強現世召喚陣。
  • 漫畫改編手遊《10Count for App react》正式推出
    漫畫改編手遊《10Count for App react》正式推出 來源:www.18183.com作者:泡沫時間:2019-01-13 分享到: 以寶井理人原作,發行量超過150萬部人氣BL 漫畫《10COUNT(テンカウント
  • 拆解Nike React 瑞亞起跑,能否勝過boost
    最近幾年,nike一直側重於氣墊以及zoom X的研究,但是在鞋底上沒有做出重大的突破在2018年年初,react泡棉科技出現了這張海報就可以說明一切採用海綿加枕頭加彈簧的設計足以證明它的舒適跟軟彈一起來看公司級的拆解環節拆解後的鞋子可以分為六部分鞋面、鞋墊、大底布、nike react、水晶外底、後跟TPU
  • React-Router v6新特性解讀及遷移指南
    新鉤子useRoutes代替react-router-config。  大小減少:從20kb到8kb1. <Switch>重命名為<Routes>該頂級組件將被重命名。但是,其功能大部分保持不變(嗨,瞎折騰)。
  • 大數據入門:Hadoop HDFS存儲原理
    今天的大數據入門分享,我們就來講講Hadoop HDFS存儲原理。1、什麼是HDFS?HDFS即Hadoop distributed file system(hadoop分布式文件系統),在Hadoop當中負責分布式存儲階段的任務,它是一個分布式的文件系統,也可以用來存放單個機器的數據,只是大部分用來存儲分布式數據。
  • 意境、移情說和異質同構理論:究竟如何理解美學中的意境?
    如果我們以西方美學的角度來看待這一問題的話,那麼我們就會發現,與中國意境這一美學概念直接相對應的兩個非常重要的問題,一個是移情說,一個就是阿恩海姆的格式塔心理學派,即所謂的異質同構理論。在西方理論中,移情理論是最接近王國維口中所說的意境。
  • react中關於hook介紹及其使用
    前言最近由於公司的項目開發,就學習了在react關於hook的使用,對其有個基本的認識以及如何在項目中去應用hook。簡單來說就是可以使用函數組件去使用react中的一些特性所要解決的問題:解決組件之間復用狀態邏輯很難得問題,hook能解決的就是在你無需修改之前組件結構的情況下復用狀態邏輯,在不使用hook的情況下,需要使用到一些高級的用法如高級組件、provider、customer等,這種方式對於新手來說不太友好
  • 輕鬆使用純css3打造有點意思的故障藝術(附React加強組件版)
    實現原理該效果的實現依賴於Css3的新特性mix-blend-mode和background-blend-mode.接下來筆者簡單介紹一下:1. mix-blend-mode該屬性描述了元素的內容應該與元素的直系父元素的內容和元素的背景如何混合.
  • 精通react/vue組件設計教你實現一個極具創意的加載(Loading)組件
    正文在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先看看實現後的組件效果:因為動圖體積太大,就不給大家傳gif了,接下來我們具體分析一下該組件的特點.1.