詳解基于React.js和Node.js的SSR實現(xiàn)方案
基礎(chǔ)概念
SSR:即服務(wù)端渲染(Server Side Render) 傳統(tǒng)的服務(wù)端渲染可以使用Java,php 等開發(fā)語言來實現(xiàn),隨著 Node.js 和相關(guān)前端領(lǐng)域技術(shù)的不斷進步,前端同學也可以基于此完成獨立的服務(wù)端渲染。
過程:瀏覽器發(fā)送請求 -> 服務(wù)器運行 react代碼生成頁面 -> 服務(wù)器返回頁面 -> 瀏覽器下載HTML文檔 -> 頁面準備就緒 即:當前頁面的內(nèi)容是服務(wù)器生成好給到瀏覽器的。
對應(yīng)CSR:即客戶端渲染(Client Side Render) 過程:瀏覽器發(fā)送請求 -> 服務(wù)器返回空白 HTML(HTML里包含一個root節(jié)點和js文件) -> 瀏覽器下載js文件 -> 瀏覽器運行react代碼 -> 頁面準備就緒 即:當前頁面的內(nèi)容是js渲染出來

如何區(qū)分頁面是否服務(wù)端渲染: 右鍵點擊 -> 顯示網(wǎng)頁源代碼,如果頁面上的內(nèi)容在HTML文檔里,是服務(wù)端渲染,否則就是客戶端渲染。
對比
- CSR:首屏渲染時間長,react代碼運行在瀏覽器,消耗的是瀏覽器的性能
- SSR:首屏渲染時間短,react代碼運行在服務(wù)器,消耗的是服務(wù)器的性能
為什么要用服務(wù)端渲染
首屏加載時間優(yōu)化,由于SSR是直接返回生成好內(nèi)容的HTML,而普通的CSR是先返回空白的HTML,再由瀏覽器動態(tài)加載JavaScript腳本并渲染好后頁面才有內(nèi)容;所以SSR首屏加載更快、減少白屏的時間、用戶體驗更好。
SEO (搜索引擎優(yōu)化),搜索關(guān)鍵詞的時候排名,對大多數(shù)搜索引擎,不識別JavaScript 內(nèi)容,只識別 HTML 內(nèi)容。 (注:原則上可以不用服務(wù)端渲染時最好不用,所以如果只有 SEO 要求,可以用預(yù)渲染等技術(shù)去替代)
構(gòu)建一個服務(wù)端渲染的項目
(1) 使用 Node.js 作為服務(wù)端和客戶端的中間層,承擔 proxy代理,處理cookie等操作。
(2) hydrate 的使用:在有服務(wù)端渲染情況下,使用hydrate代替render,它的作用主要是將相關(guān)的事件注水進HTML頁面中(即:讓React組件的數(shù)據(jù)隨著HTML文檔一起傳遞給瀏覽器網(wǎng)頁),這樣可以保持服務(wù)端數(shù)據(jù)和瀏覽器端一致,避免閃屏,使第一次加載體驗更高效流暢。
ReactDom.hydrate(<App />, document.getElementById('root'));
(3) 服務(wù)端代碼webpack編譯:通常會建一個webpack.server.js文件,除了常規(guī)的參數(shù)配置外,還需要設(shè)置target參數(shù)為'node'。
const serverConfig = {
target: 'node',
entry: './src/server/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '../dist')
},
externals: [nodeExternals()],
module: {
rules: [{
test: /\.js?$/,
loader: 'babel-loader',
exclude: [
path.join(__dirname, './node_modules')
]
}
...
]
}
(此處省略樣式打包,代碼壓縮,運行壞境配置等等...)
...
};
(4) 使用react-dom/server下的 renderToString方法在服務(wù)器上把各種復(fù)雜的組件和代碼轉(zhuǎn)化成 HTML 字符串返回到瀏覽器,并在初始請求時發(fā)送標記以加快頁面加載速度,并允許搜索引擎抓取頁面以實現(xiàn)SEO目的。
const render = (store, routes, req, context) => {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={context}>
<div>
{renderRoutes(routes)}
</div>
</StaticRouter>
</Provider>
));
return `
<html>
<head>
<title>ssr</title>
</head>
<body>
<div id='root'>${content}</div>
<script src='/index.js'></script>
</body>
</html>
`;
}
app.get('*', function (req, res) {
...
const html = render(store, routes, req, context);
res.send(html);
});
與renderToString類似功能的還有: i. renderToStaticMarkup:區(qū)別在于renderToStaticMarkup 渲染出的是不帶data-reactid的純HTML,在JavaScript加載完成后因為不認識之前服務(wù)端渲染的內(nèi)容導(dǎo)致重新渲染(可能頁面會閃一下)。
ii. renderToNodeStream:將React元素渲染為其初始HTML,返回一個輸出HTML字符串的可讀流。
iii. renderToStaticNodeStream:與renderToNodeStream此類似,除了這不會創(chuàng)建React在內(nèi)部使用的額外DOM屬性,例如data-reactroot。
(5) 使用redux 承擔數(shù)據(jù)準備,狀態(tài)維護的職責,通常搭配react-redux, redux-thunk(中間件:發(fā)異步請求用到action)使用。(本猿目前使用比較多是就是Redux和Mobx,這里以Redux為例)。 A. 創(chuàng)建store(服務(wù)器每次請求都要創(chuàng)建一次,客戶端只創(chuàng)建一次):
const reducer = combineReducers({
home: homeReducer,
page1: page1Reducer,
page2: page2Reducer
});
export const getStore = (req) => {
return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
}
export const getClientStore = () => {
return createStore(reducer, window.STATE_FROM_SERVER, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}
B. action: 負責把數(shù)據(jù)從應(yīng)用傳到store,是store數(shù)據(jù)的唯一來源
export const getData = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('interfaceUrl/xxx')
.then((res) => {
dispatch({
type: 'HOME_LIST',
list: res.list
})
});
}
}
C. reducer:接收舊的state和action,返回新的state,響應(yīng)actions并發(fā)送到store。
export default (state = { list: [] }, action) => {
switch(action.type) {
case 'HOME_LIST':
return {
...state,
list: action.list
}
default:
return state;
}
}
export default (state = { list: [] }, action) => {
switch(action.type) {
case 'HOME_LIST':
return {
...state,
list: action.list
}
default:
return state;
}
}
D. 使用react-redux的connect,Provider把組件和store連接起來
Provider 將之前創(chuàng)建的store作為prop傳給Provider
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={context}>
<div>
{renderRoutes(routes)}
</div>
</StaticRouter>
</Provider>
));
connect([mapStateToProps],[mapDispatchToProps],[mergeProps], [options])接收四個參數(shù) 常用的是前兩個屬性 mapStateToProps函數(shù)允許我們將store中的數(shù)據(jù)作為props綁定到組件上mapDispatchToProps將action作為props綁定到組件上
connect(mapStateToProps(),mapDispatchToProps())(MyComponent)
(6) 使用react-router承擔路由職責 服務(wù)端路由不同于客戶端,它是無狀態(tài)的。React 提供了一個無狀態(tài)的組件StaticRouter,向StaticRouter傳遞當前URL,調(diào)用ReactDOMServer.renderToString() 就能匹配到路由視圖。
服務(wù)端
import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'
<StaticRouter location={req.path} context={{context}}>
{renderRoutes(routes)}
</StaticRouter>
瀏覽器端
import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
當瀏覽器的地址欄發(fā)生變化的時候,前端會去匹配路由視圖,同時由于req.path發(fā)生變化,服務(wù)端匹配到路由視圖,這樣保持了前后端路由視圖的一致,在頁面刷新時,仍然可以正常顯示當前視圖。如果只有瀏覽器端路由,而且是采用BrowserRouter,當頁面地址發(fā)生變化后去刷新頁面時,由于沒有對應(yīng)的HTML,會導(dǎo)致頁面找不到,但是加了服務(wù)端路由后,刷新發(fā)生時服務(wù)端會返回一個完整的html給客戶端,頁面仍然正常顯示。 推薦使用 react-router-config插件,然后如上代碼在StaticRouter和BrowserRouter標簽的子元素里加renderRoutes(routes):建一個router.js文件
const routes = [{ component: Root,
routes: [
{ path: '/',
exact: true,
component: Home,
loadData: Home.loadData
},
{ path: '/child/:id',
component: Child,
loadData: Child.loadData
routes: [
path: '/child/:id/grand-child',
component: GrandChild,
loadData: GrandChild.loadData
]
}
]
}];
在瀏覽器端請求一個地址的時候,server.js 里在實際渲染前可以通過matchRouters 這種方式確定要渲染的內(nèi)容,調(diào)用loaderData函數(shù)進行action派發(fā),返回promise->promiseAll->renderToString,最終生成HTML文檔返回。
import { matchRoutes } from 'react-router-config'
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)
}
(7) 寫組件注意代碼同構(gòu)(即:一套React代碼在服務(wù)端執(zhí)行一次,在客戶端再執(zhí)行一次) 由于服務(wù)器端綁定事件是無效的,所以服務(wù)器返回的只有頁面樣式(&注水的數(shù)據(jù)),同時返回JavaScript文件,在瀏覽器上下載并執(zhí)行JavaScript時才能把事件綁上,而我們希望這個過程只需編寫一次代碼,這個時候就會用到同構(gòu),服務(wù)端渲染出樣式,在客戶端執(zhí)行時綁上事件。
優(yōu)點: 共用前端代碼,節(jié)省開發(fā)時間 弊端: 由于服務(wù)器端和瀏覽器環(huán)境差異,會帶來一些問題,如document等對象找不到,DOM計算報錯,前端渲染和服務(wù)端渲染內(nèi)容不一致等;前端可以做非常復(fù)雜的請求合并和延遲處理,但為了同構(gòu),所有這些請求都在預(yù)先拿到結(jié)果才會渲染。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
forwardRef?中React父組件控制子組件的實現(xiàn)代碼
forwardRef 用于拿到父組件傳入的 ref 屬性,這樣在父組件便能通過 ref 控制子組件,這篇文章主要介紹了forwardRef?-?React父組件控制子組件的實現(xiàn)代碼,需要的朋友可以參考下2024-01-01
React中的權(quán)限組件設(shè)計問題小結(jié)
這篇文章主要介紹了React中的權(quán)限組件設(shè)計,整個過程也是遇到了很多問題,本文主要來做一下此次改造工作的總結(jié),對React權(quán)限組件相關(guān)知識感興趣的朋友一起看看吧2022-07-07
React如何使用sortablejs實現(xiàn)拖拽排序
這篇文章主要介紹了React如何使用sortablejs實現(xiàn)拖拽排序問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
react hook使用useState更新數(shù)組,無法更新問題及解決
這篇文章主要介紹了react hook使用useState更新數(shù)組,無法更新問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
解讀react的onClick自動觸發(fā)等相關(guān)問題
這篇文章主要介紹了解讀react的onClick自動觸發(fā)等相關(guān)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02

