一文帶你搞懂 SSR

欲語還休,欲語還休,卻道天涼好個(gè)秋 ---- 《丑奴兒·書博山道中壁》辛棄疾

什么是 SSR

ShadowsocksR?陰陽師?FGO?

Server-side rendering (SSR)是應(yīng)用程序通過在服務(wù)器上顯示網(wǎng)頁而不是在瀏覽器中渲染的能力。服務(wù)器端向客戶端發(fā)送一個(gè)完全渲染的頁面(準(zhǔn)確來說是僅僅是 HTML 頁面)。同時(shí),結(jié)合客戶端的 JavaScript bundle 使得頁面可以運(yùn)行起來。與 SSR 相對(duì)的,還有一種 Client-side rendering(CSR)。CSR 和 SSR 的最大區(qū)別只是提供 rendering 的是客戶端還是服務(wù)端,其本質(zhì)還有一種東西。故以下如果沒有著重提出 CSR 和 SSR 不一樣的地方,則默認(rèn)是一致的。

為什么要 SSR

得益于 React 等前端框架的發(fā)展,前后端分離,webpack 等編譯工具的流行,以及 ajax 實(shí)現(xiàn)頁面的局部刷新,使得我們現(xiàn)在的應(yīng)用程序不再像曾經(jīng)的應(yīng)用程序一般需要從服務(wù)端獲取頁面,可以動(dòng)態(tài)的修改局部的頁面數(shù)據(jù),避免頁面頻繁跳轉(zhuǎn)影響用戶體驗(yàn)等問題。也就是 SPA 越來越成為主流應(yīng)用程序模型。
但是 SPA 的使用,除了以上提到的優(yōu)勢以外,必然會(huì)帶來劣勢。譬如:

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

為了解決上述提到的 1. 和 3. 的問題,SSR 開始登上歷史的舞臺(tái)。

SSR 怎么做

file

基于上述的理論,我們可以設(shè)計(jì)一個(gè)具有 SSR 功能的 React 框架。

首先,我們通過 create-react-app 命令初始化一個(gè) React 項(xiàng)目,可以把初始化完成后的項(xiàng)目理解為具有最簡單功能的項(xiàng)目。我們將基于該項(xiàng)目去實(shí)現(xiàn)一個(gè) SSR 的功能。

# Yarn
$ yarn create react-app ssr-demo

?? 同學(xué)們實(shí)踐的時(shí)候需要注意,當(dāng)前版本的 cra 命令新建項(xiàng)目的時(shí)候,啟動(dòng)會(huì)報(bào)類似于 Mini.... is not a function的問題。這是因?yàn)?mini-css-extract-plugin該插件版本更新導(dǎo)致的,只需要在 package.json里面通過 resolutions 限制mini-css-extract-plugin的版本為 2.4.5 即可

生成項(xiàng)目的目錄如下:

./
├── README.md
├── build
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

已經(jīng)自動(dòng)安裝完依賴,啟動(dòng)項(xiàng)目我們可以在「本地環(huán)境」看到一個(gè)最簡單的頁面。

接下來,我們?nèi)?shí)現(xiàn)一個(gè) 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();

// 當(dāng)爬蟲的請(qǐng)求進(jìn)來的時(shí)候,把所有請(qǐng)求導(dǎo)向 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) => {
  // 獲取當(dāng)前項(xiàng)目的 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>`)
    );
  });
};

如上,我們完成了一個(gè)非常簡單的具有 SSR 功能的服務(wù)端。
但是僅僅如此是不夠的,我們還需要在根目錄下,新建parser.jsESM 轉(zhuǎn)成 CommonJS 運(yùn)行起來,代碼如下:

require("ignore-styles");
require("@babel/register")({
  ignore: [/(node_modules)/],
  presets: ["@babel/preset-env", "@babel/preset-react"],
});

require("./server");

解釋一下上面引入的包的作用:

  • @babel/register:該依賴會(huì)將 node 后續(xù)運(yùn)行時(shí)所需要 require 進(jìn)來的擴(kuò)展名為 .es6、.es、.jsx、 .mjs.js 的文件將由 Babel 自動(dòng)轉(zhuǎn)換。
  • ignore-styles:該依賴也是一個(gè) Babel 的鉤子,主要用于在 Babel 編譯的過程中忽略樣式文件的導(dǎo)入。

在經(jīng)過上述的操作之后,我們先 yarn build出我們的產(chǎn)物,然后通過node parser.js來啟動(dòng) SSR 服務(wù)。


經(jīng)過上述的操作之后,我們?cè)O(shè)計(jì)出了一個(gè)非常簡單的但合理的 SSR 服務(wù)端。作為對(duì)比,我們?cè)谶@里簡單的和 Next.js 做對(duì)比。

Next.js 項(xiàng)目的根目錄中的 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會(huì)通過 createServer方法,啟動(dòng)一個(gè) NextServer 對(duì)象,該對(duì)象負(fù)責(zé)啟動(dòng)服務(wù)器以及渲染模板模板。
命令調(diào)用如下:

file

[Next.js](https://nextjs.org/docs/basic-features/pages#server-side-rendering)的官網(wǎng)中,我們可以看到其支持在頁面通過 getServerSideProps函數(shù),來實(shí)現(xiàn)動(dòng)態(tài)獲取接口數(shù)據(jù)。其實(shí),在大多數(shù)支持 SSR 的框架庫中,都有類似的設(shè)計(jì)。因?yàn)?SPA 的應(yīng)用,難免需要通過服務(wù)端獲取動(dòng)態(tài)數(shù)據(jù),并渲染頁面,而實(shí)現(xiàn)渲染動(dòng)態(tài)數(shù)據(jù)的 SSR 的設(shè)計(jì)思路都較為一致。即在該頁面的組件同一文件中導(dǎo)出一固定方法,并且 return 某一固定格式。框架會(huì)將該數(shù)據(jù)用作初始數(shù)據(jù)對(duì)頁面進(jìn)行 SSR 渲染。


我們以Next.js為例,了解了 SSR 的大致設(shè)計(jì)思路,那么接下來我們了解一下 CSR 的大致思路.。

CSR 可以理解為閹割版的 SSR,只實(shí)現(xiàn)了 SSR 的預(yù)渲染功能。一般用于靜態(tài)網(wǎng)站,不具備動(dòng)態(tài)獲取數(shù)據(jù)的功能。

CSR 的渲染思路同 SSR 一致,不同點(diǎn)在于 SSR 是需要安裝 express而 CSR 不需要安裝 express。這也就導(dǎo)致了 CSR 和 SSR 在部署流程上的不同。SSR 項(xiàng)目如 Next.js應(yīng)用在執(zhí)行完 build 命令后,可以通過 start 命令執(zhí)行啟動(dòng)服務(wù)器,不再需要配合 nginx 的反向代理。而 CSR 項(xiàng)目如 Umi仍然需要 nginx 的代理。

CSR 最大的不同點(diǎn)在于編譯后產(chǎn)物的不同。通常一個(gè)前端項(xiàng)目在編譯后的產(chǎn)物包括一下:

  • bundle.js或者 chunk.js
  • index.html
  • index.css
  • public/*
  • 其他相關(guān)文件,如 rss.xml

而具備 CSR 的項(xiàng)目通過編譯后,會(huì)有更多的 HTML文件,這些文件的架構(gòu)會(huì)按照路由生成。譬如:我們目前路由如下:

  • /a
  • /b

分別對(duì)應(yīng) ComponentAComponentB,那么在我們編譯后產(chǎn)物中會(huì)生成a.htmlb.html。在我們將產(chǎn)物部署到 nginx 服務(wù)上后,就可以實(shí)現(xiàn)預(yù)渲染功能。

要實(shí)現(xiàn)以上功能,最重要的步驟如下:

  • 獲取到當(dāng)前項(xiàng)目的路由
  • 獲取到路由對(duì)應(yīng)的組件,如果組件未編譯過,需要編譯
  • 借助 react-dom 的能力將 JSX 渲染成 HTML,并插入到模板 HTML
  • 在編譯后產(chǎn)物中根據(jù)路由創(chuàng)建文件夾,并將結(jié)果 HTML 生成到對(duì)應(yīng)路徑中

到這里,我們了解了整個(gè) SSR 的流程,相信大家對(duì) SSR 都有了一定程度的了解。目前社區(qū)的絕大部分框架都不需要我們自行去做 SSR。我們了解渲染過程有助于我們?cè)趹?yīng)對(duì)各種層出不窮的框架時(shí),能夠以不變應(yīng)萬變。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容