欲語還休检访,欲語還休症革,卻道天涼好個秋 ---- 《丑奴兒·書博山道中壁》辛棄疾
什么是 SSR
ShadowsocksR阔逼?陰陽師?FGO地沮?
Server-side rendering (SSR)是應(yīng)用程序通過在服務(wù)器上顯示網(wǎng)頁而不是在瀏覽器中渲染的能力嗜浮。服務(wù)器端向客戶端發(fā)送一個完全渲染的頁面(準確來說是僅僅是 HTML 頁面)。同時摩疑,結(jié)合客戶端的 JavaScript bundle
使得頁面可以運行起來危融。與 SSR 相對的,還有一種 Client-side rendering(CSR)雷袋。CSR 和 SSR 的最大區(qū)別只是提供 rendering 的是客戶端還是服務(wù)端吉殃,其本質(zhì)還有一種東西。故以下如果沒有著重提出 CSR 和 SSR 不一樣的地方楷怒,則默認是一致的蛋勺。
為什么要 SSR
得益于 React
等前端框架的發(fā)展,前后端分離鸠删,webpack
等編譯工具的流行抱完,以及 ajax
實現(xiàn)頁面的局部刷新,使得我們現(xiàn)在的應(yīng)用程序不再像曾經(jīng)的應(yīng)用程序一般需要從服務(wù)端獲取頁面刃泡,可以動態(tài)的修改局部的頁面數(shù)據(jù)巧娱,避免頁面頻繁跳轉(zhuǎn)影響用戶體驗等問題。也就是 SPA 越來越成為主流應(yīng)用程序模型烘贴。
但是 SPA 的使用禁添,除了以上提到的優(yōu)勢以外,必然會帶來劣勢桨踪。譬如:
- 由于需要在頁面加載之前就加載所有頁面需要的 JavaScript 庫老翘,這使得首次打開頁面所需要的時間比較久;
- 需要研發(fā)專門針對于 SPA 的 Web 框架(各種具備 SSR 能力的框架锻离,包括
Next.js
等) - 搜索引擎爬蟲
- 瀏覽器歷史記錄的問題(基于
pushState
的各種router
)
為了解決上述提到的 1. 和 3. 的問題铺峭,SSR 開始登上歷史的舞臺。
SSR 怎么做
基于上述的理論纳账,我們可以設(shè)計一個具有 SSR
功能的 React
框架逛薇。
首先,我們通過 create-react-app
命令初始化一個 React
項目疏虫,可以把初始化完成后的項目理解為具有最簡單功能的項目永罚。我們將基于該項目去實現(xiàn)一個 SSR
的功能。
# Yarn
$ yarn create react-app ssr-demo
?? 同學們實踐的時候需要注意卧秘,當前版本的
cra
命令新建項目的時候呢袱,啟動會報類似于Mini.... is not a function
的問題。這是因為mini-css-extract-plugin
該插件版本更新導致的翅敌,只需要在package.json
里面通過resolutions
限制mini-css-extract-plugin
的版本為2.4.5
即可
生成項目的目錄如下:
./
├── README.md
├── build
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock
已經(jīng)自動安裝完依賴羞福,啟動項目我們可以在「本地環(huán)境」看到一個最簡單的頁面。
接下來蚯涮,我們?nèi)崿F(xiàn)一個 SSR 功能治专。首先卖陵,我們需要安裝 express
(如果是 CSR
的話就不需要這一步)
yarn add express
安裝完成后,我們需要在 server/index.js
文件中編寫如下代碼
import express from "express";
import serverRenderer from "./serverRenderer.js";
const PORT = 3000;
const path = require("path");
const app = express();
const router = express.Router();
// 當爬蟲的請求進來的時候张峰,把所有請求導向 serverRenderer 路由
router.use("*", serverRenderer);
app.use(router);
app.listen(PORT, () => console.log(`listening on: ${PORT}`));
其中serverRenderer
該文件內(nèi)容如下:
import React from "react";
import ReactDOMServer from "react-dom/server";
import App from "../src/App";
const path = require("path");
const fs = require("fs");
export default (req, res, next) => {
// 獲取當前項目的 HTML 模板文件路徑
const filePath = path.resolve(__dirname, "..", "build", "index.html");
// 讀取該文件
fs.readFile(filePath, "utf8", (err, htmlData) => {
if (err) {
console.error("err", err);
return res.status(404).end();
}
// 借助 react-dom 依賴下的方法將 JSX 渲染成 HTML string
const html = ReactDOMServer.renderToString(<App />);
// 將 HTML string 替換到 root 中
return res.send(
htmlData.replace('<div id="root"></div>', `<div id="root">${html}</div>`)
);
});
};
如上泪蔫,我們完成了一個非常簡單的具有 SSR 功能的服務(wù)端。
但是僅僅如此是不夠的喘批,我們還需要在根目錄下撩荣,新建parser.js
將ESM
轉(zhuǎn)成 CommonJS
運行起來,代碼如下:
require("ignore-styles");
require("@babel/register")({
ignore: [/(node_modules)/],
presets: ["@babel/preset-env", "@babel/preset-react"],
});
require("./server");
解釋一下上面引入的包的作用:
-
@babel/register
:該依賴會將 node 后續(xù)運行時所需要 require 進來的擴展名為.es6
饶深、.es
餐曹、.jsx
、.mjs
和.js
的文件將由 Babel 自動轉(zhuǎn)換敌厘。 -
ignore-styles
:該依賴也是一個 Babel 的鉤子台猴,主要用于在 Babel 編譯的過程中忽略樣式文件的導入碾阁。
在經(jīng)過上述的操作之后腋颠,我們先 yarn build
出我們的產(chǎn)物嘶窄,然后通過node parser.js
來啟動 SSR 服務(wù)缅帘。
經(jīng)過上述的操作之后汽畴,我們設(shè)計出了一個非常簡單的但合理的 SSR 服務(wù)端翩剪。作為對比腕柜,我們在這里簡單的和 Next.js
做對比膀篮。
在 Next.js
項目的根目錄中的 package.json
中毯焕,我們可以看到同樣選擇了 express
作為服務(wù)器.
...
"eslint-plugin-react-hooks": "4.2.0",
"execa": "2.0.3",
"express": "4.17.0",
"faker": "5.5.3",
...
我們可以在 ~/packages/next/server/next.ts
文件夾中衍腥,發(fā)現(xiàn) Next.js
會通過 createServer
方法,啟動一個 NextServer
對象纳猫,該對象負責啟動服務(wù)器以及渲染模板模板婆咸。
命令調(diào)用如下:
在 [Next.js](https://nextjs.org/docs/basic-features/pages#server-side-rendering)
的官網(wǎng)中,我們可以看到其支持在頁面通過 getServerSideProps
函數(shù)芜辕,來實現(xiàn)動態(tài)獲取接口數(shù)據(jù)尚骄。其實,在大多數(shù)支持 SSR 的框架庫中侵续,都有類似的設(shè)計倔丈。因為 SPA 的應(yīng)用,難免需要通過服務(wù)端獲取動態(tài)數(shù)據(jù)状蜗,并渲染頁面需五,而實現(xiàn)渲染動態(tài)數(shù)據(jù)的 SSR 的設(shè)計思路都較為一致。即在該頁面的組件同一文件中導出一固定方法轧坎,并且 return 某一固定格式宏邮。框架會將該數(shù)據(jù)用作初始數(shù)據(jù)對頁面進行 SSR 渲染。
我們以Next.js
為例蜜氨,了解了 SSR 的大致設(shè)計思路械筛,那么接下來我們了解一下 CSR 的大致思路.。
CSR 可以理解為閹割版的 SSR飒炎,只實現(xiàn)了 SSR 的預(yù)渲染功能变姨。一般用于靜態(tài)網(wǎng)站,不具備動態(tài)獲取數(shù)據(jù)的功能厌丑。
CSR 的渲染思路同 SSR 一致,不同點在于 SSR 是需要安裝 express
而 CSR 不需要安裝 express
渔呵。這也就導致了 CSR 和 SSR 在部署流程上的不同怒竿。SSR 項目如 Next.js
應(yīng)用在執(zhí)行完 build
命令后,可以通過 start
命令執(zhí)行啟動服務(wù)器扩氢,不再需要配合 nginx
的反向代理耕驰。而 CSR 項目如 Umi
仍然需要 nginx
的代理。
CSR 最大的不同點在于編譯后產(chǎn)物的不同录豺。通常一個前端項目在編譯后的產(chǎn)物包括一下:
-
bundle.js
或者chunk.js
index.html
index.css
public/*
- 其他相關(guān)文件朦肘,如
rss.xml
等
而具備 CSR 的項目通過編譯后,會有更多的 HTML
文件双饥,這些文件的架構(gòu)會按照路由生成媒抠。譬如:我們目前路由如下:
/a
/b
分別對應(yīng) ComponentA
和 ComponentB
,那么在我們編譯后產(chǎn)物中會生成a.html
和b.html
咏花。在我們將產(chǎn)物部署到 nginx
服務(wù)上后趴生,就可以實現(xiàn)預(yù)渲染功能。
要實現(xiàn)以上功能昏翰,最重要的步驟如下:
- 獲取到當前項目的路由
- 獲取到路由對應(yīng)的組件苍匆,如果組件未編譯過,需要編譯
- 借助
react-dom
的能力將JSX
渲染成HTML
棚菊,并插入到模板HTML
中 - 在編譯后產(chǎn)物中根據(jù)路由創(chuàng)建文件夾浸踩,并將結(jié)果 HTML 生成到對應(yīng)路徑中
到這里,我們了解了整個 SSR 的流程统求,相信大家對 SSR 都有了一定程度的了解检碗。目前社區(qū)的絕大部分框架都不需要我們自行去做 SSR。我們了解渲染過程有助于我們在應(yīng)對各種層出不窮的框架時球订,能夠以不變應(yīng)萬變后裸。