iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
Vue.js

在 Vue 過氣前要學的三十件事系列 第 28

在 Vue 過氣前要學的第二十八件事 - 我不想用 Nuxt 但又想要 SSR

  • 分享至 

  • xImage
  •  

前言

在此篇系列文中你可能會注意到說,
我是用 Vue 而不是現在討論度也相當高的 Nuxt;

這樣我是不是透過 CSR * 渲染網頁了,SSR 怎辦,SEO 怎辦;
https://ithelp.ithome.com.tw/upload/images/20250928/20172784OLWGQAcZSX.png
其實 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,請斟酌使用。

回家作業

  1. 試著開啟自己的 Vite SSR 專案吧

上一篇
在 Vue 過氣前要學的第二十七件事 - 是一輩子喔? 一輩子
下一篇
在 Vue 過氣前要學的第二十九件事 - 先用飛雷神做個標記
系列文
在 Vue 過氣前要學的三十件事29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言