React Redux Sever Rendering(Isomorphic JavaScript)
前言
由于可能有些讀者沒聽過 Isomorphic JavaScript 邓了。因此在進(jìn)到開發(fā) React Redux Sever Rendering 應(yīng)用程式的主題之前我們先來聊聊 Isomorphic JavaScript 這個議題设塔。
根據(jù) Isomorphic JavaScript 這個網(wǎng)站的說明:
Isomorphic JavaScript
Isomorphic JavaScript apps are JavaScript applications that can run both client-side and server-side.
The backend and frontend share the same code.
Isomorphic JavaScript 系指瀏覽器端和伺服器端共用 JavaScript 的程式碼。
另外溯捆,除了 Isomorphic JavaScript 外,讀者或許也有聽過 Universal JavaScript 這個用詞。那什麼是 Universal JavaScript 呢?它和 Isomorphic JavaScript 是指一樣的意思嗎吸祟?針對這個議題網(wǎng)路上有些開發(fā)者提出了自己的觀點: Universal JavaScript、Isomorphism vs Universal JavaScript桃移。其中 Isomorphism vs Universal JavaScript 這篇文章的作者 Gert Hengeveld 指出 Isomorphic JavaScript
主要是指前后端共用 JavaScript 的開發(fā)方式屋匕,而 Universal JavaScript
是指 JavaScript 程式碼可以在不同環(huán)境下運行,這當(dāng)然包含瀏覽器端和伺服器端借杰,甚至其他環(huán)境过吻。也就是說 Universal JavaScript
在意義上可以涵蓋的比 Isomorphic JavaScript
更廣泛一些,然而在 Github 或是許多技術(shù)討論上通常會把兩者視為同一件事情蔗衡,這部份也請讀者留意纤虽。
Isomorphic JavaScript 的好處
在開始真正撰寫 Isomorphic JavaScript 前我們在進(jìn)一步探討使用 Isomorphic JavaScript 有哪些好處?在談好處之前绞惦,我們先看看最早 Web 開發(fā)是如何處理頁面渲染和 state 管理逼纸,還有遇到哪些挑戰(zhàn)。
最早的時候我們談?wù)?Web 很單純济蝉,都是由 Server 端進(jìn)行模版的處理杰刽,你可以想成 template 是一個函數(shù),我們傳送資料進(jìn)去王滤,template 最后產(chǎn)生一張 HTML 給瀏覽器顯示贺嫂。例如:Node 使用的(EJS、Jade)雁乡、Python/Django 的 Template 或替代方案 Jinja第喳、PHP 的 Smarty、Laravel 使用的 Blade蔗怠,甚至是 Ruby on Rails 用的 ERB墩弯。都是由后端去 render 所有資料和頁面,前端處理相對單純寞射。
然而隨著前端工程的軟體工程化和使用者體驗的要求,開始出現(xiàn)各式前端框架的百花齊放锌钮,例如:Backbone.js桥温、Ember.js 和 Angular.js 等前端 MVC (Model-View-Controller) 或 MVVM (Model-View-ViewModel) 框架,將頁面于前端渲染的不刷頁單頁式應(yīng)用程式(Single Page App)也因此開始流行梁丘。
后端除了提供初始的 HTML 外侵浸,還提供 API Server 讓前端框架可以取得資料用于前端 template旺韭。複雜的邏輯由 ViewModel/Presenter 來處理,前端 template 只處理簡單的是否顯示或是元素迭代的狀況掏觉,如下圖所示:
然而前端渲染 template 雖然有它的好處但也遇到一些問題包括效能区端、SEO 等議題。此時我們就開始思考 Isomorphic JavaScript 的可能性:為什麼我們不能前后端都使用 JavaScript 甚至是 React澳腹?
事實上织盼,React 的優(yōu)勢就在于它可以很優(yōu)雅地實現(xiàn) Server Side Rendering 達(dá)到 Isomorphic JavaScript 的效果。在 react-dom/server
中有兩個方法 renderToString
和 renderToStaticMarkup
可以在 server 端渲染你的 components酱塔。其主要都是將 React Component 在 Server 端轉(zhuǎn)成 DOM String沥邻,也可以將 props 往下傳,然而事件處理會失效羊娃,要到 client-side 的 React 接收到后才會把它加上去(但要注意 server-side 和 client-side 的 checksum 要一致不然會出現(xiàn)錯誤)唐全,這樣一來可以提高渲染速度和 SEO 效果。renderToString
和 renderToStaticMarkup
最大的差異在于 renderToStaticMarkup
會少加一些 React 內(nèi)部使用的 DOM 屬性蕊玷,例如:data-react-id
邮利,因此可以節(jié)省一些資源。
使用 renderToString
進(jìn)行 Server 端渲染:
import ReactDOMServer from 'react-dom/server';
ReactDOMServer.renderToString(<HelloButton name="Mark" />);
渲染出來的效果:
<button data-reactid=".7" data-react-checksum="762752829">
Hello, Mark
</button>
總的來說使用 Isomorphic JavaScript 會有以下的好處:
- 有助于 SEO
- Rendering 速度較快垃帅,效能較佳
- 放棄蹩腳的 Template 語法擁抱 Component 元件化思考延届,便于維護(hù)
- 盡量前后端共用程式碼節(jié)省開發(fā)時間
不過要注意的是如果有使用 Redux 在 Server Side Rendering 中,其流程相對複雜挺智,不過大致流程如下:
由后端預(yù)先載入需要的 initialState祷愉,由于 Server 渲染必須全部都轉(zhuǎn)成 string,所以先將 state 先 dehydration(脫水)赦颇,等到 client 端再 rehydration(覆水)二鳄,重建 store 往下傳到前端的 React Component。
而要把資料從伺服器端傳遞到客戶端媒怯,我們需要:
- 把取得初始 state 當(dāng)做參數(shù)并對每個請求建立一個全新的 Redux store 實體
- 選擇性地 dispatch 一些 action
- 把 state 從 store 取出來
- 把 state 一起傳到客戶端
接下來我們就開始動手實作一個簡單的 React Server Side Rendering app
專案成果截圖
Server Rendering
獲取數(shù)據(jù)可以調(diào)用 action订讼,routes 在服務(wù)器端的處理參考 react-router server rendering,在服務(wù)器端用一個 match 方法將拿到的 request url 匹配到我們之前定義的 routes扇苞,解析成和客戶端一致的 props 對象傳遞給組件欺殿。
./devServer.js
var express = require('express');
var webpack = require('webpack');
var config = require('./webpack.config.dev');
import React from 'react';
import { renderToString } from 'react-dom/server';
import { RouterContext, match } from 'react-router';
import { Provider } from 'react-redux';
import createRouter from './client/routes';
import configureStore from './client/store';
var app = express();
var compiler = webpack(config);
import comments from './client/data/comments';
import posts from './client/data/posts';
// create an object for the default data
const defaultState = {
posts,
comments
};
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}));
app.use(require('webpack-hot-middleware')(compiler));
function renderFullPage(html, initialState) {
return `
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>isomorphic-redux-app</title>
<link rel="shortcut icon" type="image/png" />
</head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`;
}
app.use((req, res) => {
const store = configureStore(defaultState);
const routes = createRouter();
const state = store.getState();
match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
if (err) {
res.status(500).end(`Internal Server Error ${err}`);
} else if (redirectLocation) {
res.redirect(redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const html = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
res.end(renderFullPage(html, store.getState()));
} else {
res.status(404).end('Not found');
}
});
});
app.listen(7770, 'localhost', function(err) {
if (err) {
console.log(err);
return;
}
console.log('Listening at http://localhost:7770');
});
服務(wù)器端渲染部分可以直接通過共用客戶端 store.dispatch(action) 來統(tǒng)一獲取 Store 數(shù)據(jù)。另外注意 renderFullPage 生成的頁面 HTML 在 React 組件 mount 的部分(<div id="root">)鳖敷,前后端的 HTML 結(jié)構(gòu)應(yīng)該是一致的脖苏。然后要把 store 的狀態(tài)樹寫入一個全局變量(INITIAL_STATE),這樣客戶端初始化 render 的時候能夠校驗服務(wù)器生成的 HTML 結(jié)構(gòu)定踱,并且同步到初始化狀態(tài)棍潘,然后整個頁面被客戶端接管。