在此篇系列文中你可能會注意到說,
我是用 Vue 而不是現在討論度也相當高的 Nuxt;
這樣我是不是透過 CSR * 渲染網頁了,SSR 怎辦,SEO 怎辦;
其實 Vite 有提供 SSR 擴充來幫你處理掉許多細節,
讓你可以配置屬於自己的架構;
CSR
一種渲染方式:所有頁面內容都在,瀏覽器端 (client) 生成。
那使用上我們還是從頭開始建構專案;
如果你還沒用 Vite 創建過專案的話,
可以看在 Vue 過氣前要學的第二件事 - Vue 到底是什麼;
還有一個套件要先安裝 :
$ npm install express // 5.1.0
├─ index.html // 提前設定好佔位符, 讓 entry-server.js 有地方可以塞入 HTML
├─ server.js // SSR 伺服器啟動檔
│
├─ src
│ ├─ main.js // 應用入口
│ ├─ entry-client.js // 負責 Hydration, 把應用掛載到已經產生的 DOM 上
│ └─ entry-server.js // 負責產生 HTML 字串, 讓瀏覽器能讀到
│
└─ package.json // 修改 script 指令來處理 server 跟打包
這邊強調一下,此篇章的重點在於一步一步搭建出 Vue + Vite SSR 專案,
而不是 SSR 程式碼講解,你不需要也沒有必要理解每行程式碼;
絕對不是我偷懶
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue + Vite + SSR</title>
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/entry-client.js"></script>
</body>
</html>
▲index.html
import { createSSRApp } from "vue";
import App from "./App.vue";
// 匯出一個函式,每次呼叫都會回傳新的 Vue SSR 應用 Instance
export function createApp() {
const app = createSSRApp(App);
return { app };
}
▲main.js
import "./style.css";
import { createApp } from "./main";
const { app } = createApp();
app.mount("#app");
▲entry-client.js
import { renderToString } from "vue/server-renderer";
import { createApp } from "./main";
export async function render(_url) {
const { app } = createApp();
// 建立一個新的 Vue SSR 應用,把它渲染成 HTML 字串,同時記錄此次渲染過程中用到的元件資訊。
const ctx = {};
const html = await renderToString(app, ctx);
return { html };
}
▲entry-server.js
{
"name": "vue_vite_ssr",
// 其餘程式碼
"scripts": {
"dev": "node server", // 開啟 server
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.js"
},
"dependencies": {
// 其餘程式碼...
}
▲package.json
import fs from "node:fs/promises";
import express from "express";
// 常數
const isProduction = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5173;
const base = process.env.BASE || "/";
// 快取打包後的靜態檔案
const templateHtml = isProduction
? await fs.readFile("./dist/client/index.html", "utf-8")
: "";
// 建立 http server
const app = express();
// Add Vite or respective production middlewares
let vite;
if (!isProduction) {
const { createServer } = await import("vite");
vite = await createServer({
server: { middlewareMode: true },
appType: "custom",
base,
});
app.use(vite.middlewares);
} else {
const compression = (await import("compression")).default;
const sirv = (await import("sirv")).default;
app.use(compression());
app.use(base, sirv("./dist/client", { extensions: [] }));
}
// 收到請求後,用 SSR 把對應頁面渲染成 HTML,塞進模板,然後回傳給瀏覽器。
app.use("*all", async (req, res) => {
try {
const url = req.originalUrl.replace(base, "");
let template;
let render;
if (!isProduction) {
// Always read fresh template in development
template = await fs.readFile("./index.html", "utf-8");
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule("/src/entry-server.js")).render;
} else {
template = templateHtml;
render = (await import("./dist/server/entry-server.js")).render;
}
const rendered = await render(url);
const html = template.replace(`<!--app-html-->`, rendered.html ?? "");
res.status(200).set({ "Content-Type": "text/html" }).send(html);
} catch (e) {
console.log(e.stack);
vite?.ssrFixStacktrace(e);
res.status(500).end(e.stack);
}
});
// 啟動 http server
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`);
});
▲server.js
對著你的頁面按 ctrl + u
打開程式原始碼,
其實最主要的差異就是在 <body>
這一塊;
▲沒有 SSR
▲有 SSR
原本沒有 SSR 的情況下,基本上是空的,
因為 SPA 的情況,頁面的 DOM 是動態渲染出來的,
並不會在一開始原始碼就出來;
而這邊 SSR 專案可以看到 <div id="app">
後面是有渲染出頁面內容的,
這樣瀏覽器在做爬蟲的時候就有內容可以爬,
也能提升首屏渲染速度(FCP;
這邊順便解答昨天的問題
這邊想問大家覺得靜態網頁跟動態的差異是什麼?
其實這個問題背後是想問怎麼樣算動態網頁?
就是網頁上呈現的內容有沒有經過 server
處理過才選染。
這篇文章中我們講解了如何一步一步的建立 Vue + Vite SSR 應用,
即便受某些限制還是需要有 SSR 的情況也能解決問題;
例如你原本專案是用 Vue 但你不想用 Nuxt 重寫,之類的情況;
不過一但加上 SSR 就會容易出現很多問題: 水合,refresh token,異步請求,etc.
如果是後台類的產品,不見得要用 SSR,請斟酌使用。