一文帶你搞懂 SSR

欲語還休检访,欲語還休症革,卻道天涼好個秋 ---- 《丑奴兒·書博山道中壁》辛棄疾

什么是 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)勢以外,必然會帶來劣勢桨踪。譬如:

  1. 由于需要在頁面加載之前就加載所有頁面需要的 JavaScript 庫老翘,這使得首次打開頁面所需要的時間比較久;
  2. 需要研發(fā)專門針對于 SPA 的 Web 框架(各種具備 SSR 能力的框架锻离,包括 Next.js 等)
  3. 搜索引擎爬蟲
  4. 瀏覽器歷史記錄的問題(基于 pushState 的各種 router

為了解決上述提到的 1. 和 3. 的問題铺峭,SSR 開始登上歷史的舞臺。

SSR 怎么做

file

基于上述的理論纳账,我們可以設(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.jsESM 轉(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)用如下:

file

[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) ComponentAComponentB,那么在我們編譯后產(chǎn)物中會生成a.htmlb.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)萬變后裸。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市冒滩,隨后出現(xiàn)的幾起案子微驶,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件因苹,死亡現(xiàn)場離奇詭異苟耻,居然都是意外死亡,警方通過查閱死者的電腦和手機扶檐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門凶杖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人款筑,你說我怎么就攤上這事智蝠。” “怎么了奈梳?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵杈湾,是天一觀的道長。 經(jīng)常有香客問我攘须,道長漆撞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任于宙,我火速辦了婚禮浮驳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捞魁。我一直安慰自己至会,他們只是感情好,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布署驻。 她就那樣靜靜地躺著奋献,像睡著了一般。 火紅的嫁衣襯著肌膚如雪旺上。 梳的紋絲不亂的頭發(fā)上瓶蚂,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機與錄音宣吱,去河邊找鬼窃这。 笑死,一個胖子當著我的面吹牛征候,可吹牛的內(nèi)容都是我干的杭攻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼疤坝,長吁一口氣:“原來是場噩夢啊……” “哼兆解!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起跑揉,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤锅睛,失蹤者是張志新(化名)和其女友劉穎埠巨,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體现拒,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡辣垒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了印蔬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勋桶。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖侥猬,靈堂內(nèi)的尸體忽然破棺而出例驹,到底是詐尸還是另有隱情,我是刑警寧澤退唠,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布眠饮,位于F島的核電站,受9級特大地震影響铜邮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寨蹋,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一松蒜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧已旧,春花似錦秸苗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至秸讹,卻和暖如春檀咙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背璃诀。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工弧可, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人劣欢。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓棕诵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親凿将。 傳聞我的和親對象是個殘疾皇子校套,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容