服務器渲染(Server-Side Rendering)並非一個複雜的技術,而 服務器渲染
與 服務器同構渲染
則是2個不一樣的概念,重點在於:同構,要作到一套代碼完美的運行在瀏覽器與服務器之上不是一件簡單的事情,目前業界也沒有特別滿意的方案,都須要或多或少的對不一樣的環境作差別化處理。javascript
一般同構渲染主要是爲了:css
單頁(SPA)
和多頁路由
的用戶體驗一般同構渲染須要作到:html
單頁(SPA)
的用戶體驗。多頁路由
的用戶體驗。同構渲染的主要難點在於 Client 端渲染時組件生命週期鉤子承載了太多的職能與反作用,好比:獲取數據、路由、按需加載、模塊化等等,這些邏輯被分散在各個組件中隨着組件的渲染動態執行,而它們的執行又再次引發組件的從新渲染。簡單來講就是:java
Render -> Hooks -> Effects -> ReRender -> Hooks -> Effects...node
這樣的渲染流程在 Server 端是不行的,由於一般 Sever 端不會 ReRender,所以必須把全部反作用都提早執行,然後在一次性 Render,簡單來講就是:react
Effects -> State -> Renderwebpack
那麼解決方案就是將這些反作用盡可能的與組件的生命週期鉤子脫離,並引入獨立的狀態管理機制來管理它們,讓 UI 渲染變成簡單純粹的 PrueRender,而這正是@medux 所倡導的狀態驅動
理念。nginx
在 Client 端渲染時,爲了提高加載速度咱們一般對代碼進行 chunk 分包、並使用異步按需加載
來優化用戶體驗。而在 Server 端渲染時這變得徹底不必,反而會拖慢加載速度。如何在 server 端中替換異步代碼爲同步代碼呢?正好@medux將模塊加載視爲一種配置策略,它能夠很輕鬆的讓將模塊加載在同步和異步之間切換。git
本項目 fork 自medux-react-admin,這是一個使用 Medux+React+Antd4+Hooks+Typescript 開發的 WEB 單頁應用,你能夠從本項目中看到如何將一個 SinglePage(單頁應用
) 快速轉換爲支持 SEO 的多頁應用。github
項目地址:medux-react-ssr
打開如下頁面,使用鼠標右鍵點擊「查看網頁源碼」,看是否輸出了 Html
// 注意一下,由於本項目風格檢查要求以 LF 爲換行符
// 因此請先關閉 Git 配置中 autocrlf
git config --global core.autocrlf false
git clone https://github.com/wooline/medux-react-ssr.git
cd medux-react-ssr
yarn install
複製代碼
yarn start
,會自動啓動一個開發服務器。這是一個典型的後臺管理系統,頁面主要分爲 2 類:
咱們之因此要使用 SSR 改造它主要是爲了讓第二類頁面能被搜索引發收錄(SEO),而對於第一類頁面,由於須要用戶登陸,因此對於搜索引擎也沒什麼意義,咱們依然沿用純瀏覽器渲染就好。
既然是同構,咱們固然不但願爲 2 端平臺作太多的差別化處理,可是仍是會有少量的定製代碼。好比啓動入口,原來是./src/index.ts
,如今咱們須要將其區分爲:
利用這 2 個不一樣的入口,咱們集中構建一些 shim,抹平一些平臺的差別化。
運行在 Sever 端的代碼無需異步按需加載、無需處理 CSS、無需處理圖片等等,因此咱們使用 2 套 webpack 配置來進行編譯打包並分別輸出在 dist/client/
和 dist/server/
目錄下。
對於 Sever 端的輸出其實就只有一個 main.js 文件。
怎麼部署和運行編譯後輸出的代碼?本項目編寫了一個 express 的簡單樣例可供參考,目錄結構大體爲這樣:
dist
├── package.json // 運行須要的依賴
├── start.js // nodejs啓動入口
├── pm2.json // pm2部署配置
├── nginx.conf // nginx配置樣例
├── env.json // 運行環境變量配置
├── 404.html // 404錯誤頁面
├── 50x.html // 500錯誤頁面
├── index.html // SSR模版
├── mock // mock假數據目錄
├── html // 生成的純靜態化頁面目錄
│ ├── login.html
│ ├── register.html
│ └── article
├── server // Server端輸出目錄
│ └── main.js // SSR主程代碼
├── client // Client端輸出目錄
│ ├── css // 生成的CSS文件目錄
│ ├── imgs // 未經工程化處理的圖片目錄
│ ├── media // 經webpack處理過的圖片目錄
│ └── js // 瀏覽器運行JS目錄
複製代碼
對於 dist/client 就是一個靜態目錄,你可使用 Nginx 部署
對於 dist/server 其實就是一個 JS Module 文件 main.js
,它只有一個default export
的方法:
export default function render( location: string ): Promise<{
html: string | ReadableStream<any>;
data: any;
ssrInitStoreKey: string;
}>;
複製代碼
你可使用任意 node 服務器(如 express)來執行它,並獲得渲染後的 data、html、已及脫水數據的key
。至於你要如何讓服務器輸出這些結果,以及如何處理執行過程出現的異常和錯誤,你能夠自由發揮,例如:
前面咱們說過應用在 Server 端運行的流程是:Effects -> State -> Render,也就是說:先獲取數據,再渲染組件
。
在 medux 框架中數據處理是封裝在 model 中的,而初始化數據一般是在 model 中經過監聽 module.Init
這個 Action 來執行 Effect,從而處理數據並轉化爲 moduleState。當一個 module 被加載時,不論 Client 端仍是 Server 端都會觸發這個 Action,因此在這個 ActionHandler 中咱們要注意的是:若是 Server 端已經作過的工做,Client 端不必再重複作了。能夠經過moduleState.isHydrate
來判斷當前的 moduleState 是否已是服務器處理過,例如:
// src/modules/app/model.ts
@effect(null)
protected async ['this.Init']() {
if (this.state.isHydrate) {
//若是已經通過SSR服務器渲染,那麼getProjectConfig()無需執行了
const curUser = await api.getCurUser();
this.dispatch(this.actions.putCurUser(curUser));
if (curUser.hasLogin) {
this.getNoticeTimer();
this.checkLoginRedirect();
}
} else {
//若是是初次渲染,可能運行在client端也可能運行在server端
const projectConfig = await api.getProjectConfig();
this.updateState({projectConfig});
//服務端都是遊客,無需獲取用戶信息
if (!isServer()) {
const curUser = await api.getCurUser();
this.dispatch(this.actions.putCurUser(curUser));
if (curUser.hasLogin) {
this.getNoticeTimer();
this.checkLoginRedirect();
}
}
}
}
複製代碼
咱們只對無需用戶登陸的頁面進行 SSR,因此在 Server 端中用戶假定都是遊客。在全局的錯誤處理 Handler 中,遇到須要登陸的錯誤時:
// src/modules/app/model.ts
@effect(null)
protected async [ActionTypes.Error](error: CustomError) {
if (isServer()) {
//服務器中間件會catch 301錯誤,跳轉URL
if (error.code === CommonErrorCode.redirect) {
throw {code: '301', detail: error.detail};
} else {
//服務器直接終止渲染,改成client端渲染
//服務器中間件會catch 303錯誤,直接發送統一的 index.html
throw {code: '303'};
}
}
...
}
複製代碼
前面說過咱們必須在 Server 端代碼中將模塊異步按需加載端代碼替換成同步。medux 中控制模塊同步或異步加載是在src/modules/index.ts
中:
// 異步加載
export const moduleGetter = {
app: () => {
return import(/* webpackChunkName: "app" */ 'modules/app');
},
adminLayout: () => {
return import(/* webpackChunkName: "adminLayout" */ 'modules/admin/adminLayout');
},
...
};
// 替換爲同步加載
export const moduleGetter = {
app: () => {
return require('modules/app');
},
adminLayout: () => {
return require('modules/admin/adminLayout');
},
...
};
複製代碼
只須要將import
替換爲require
便可,固然這個簡單的替換工做你可使用本項目提供的一個簡單的 webpack-loader 來完成:
@medux/dev-utils/dist/webpack-loader/server-replace-async
它還支持用參數指定部分 module 替換,以減小 server 端 js 文件的大小,如:
// build/webpack.config.js
{
test: /\.(tsx|ts)?$/,
use: [
{
loader: require.resolve('@medux/dev-utils/dist/webpack-loader/server-replace-async'),
options: {modules: ['app', 'adminLayout', 'articleLayout', 'articleHome', 'articleAbout', 'articleService']},
},
{loader: 'babel-loader', options: {cacheDirectory: true, caller: {runtime: 'server'}}},
],
},
複製代碼
使用不少細節你們直接看源碼吧,有問題能夠問我,歡迎共同探討。