在現代網頁應用開發中,前端框架如 React 提供了豐富的互動體驗,但純客戶端渲染(CSR)模式常伴隨著首頁載入緩慢與搜尋引擎優化(SEO)的挑戰。為了解決這些問題,伺服器端渲染(SSR)成為主流解決方案之一。將 React 前端組件與 Express 後端框架結合,不僅是技術堆疊的整合,更代表一種架構思維的轉變。此模式允許開發者在伺服器上預先渲染頁面,提供極速的首次內容呈現(FCP),再由客戶端接管後續互動。本文將深入探討此整合的具體實踐,從基本的組件渲染到實現跨平台 JavaScript 的統一路由與狀態管理,解析其如何建構一個高效能、高維護性的同構應用程式,並奠定現代化網站開發的堅實基礎。
React 與 Express 框架的整合:從組件到伺服器端 HTML 渲染
將 React 組件在 Express 伺服器上渲染成 HTML 頁面,是實現伺服器端渲染的基礎。這不僅涉及技術層面的整合,更關乎如何高效地將前端組件的表現力延伸至後端環境。
渲染簡單文本內容
最基本的伺服器端渲染是將 React 組件渲染為純文本。這通常用於測試或簡單的內容輸出,例如生成一個包含特定訊息的段落。
// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
// 假設這是一個簡單的 React 組件
const SimpleTextComponent = ({ message }) => (
  React.createElement('div', null, `伺服器端渲染的訊息:${message}`)
);
const app = express();
app.get('/', (req, res) => {
  const html = ReactDOMServer.renderToString(
    React.createElement(SimpleTextComponent, { message: "你好,世界!" })
  );
  res.send(html);
});
app.listen(3000, () => {
  console.log('伺服器已啟動於 http://localhost:3000');
});
在上述範例中,SimpleTextComponent 是一個簡單的 React 函數組件,它接收一個 message 屬性並顯示出來。Express 路由處理器捕獲根路徑的請求,然後使用 ReactDOMServer.renderToString() 將 SimpleTextComponent 渲染成 HTML 字串,並直接作為響應發送。
渲染完整的 HTML 頁面
實際應用中,我們需要渲染的不僅僅是組件的 HTML,而是一個完整的 HTML 頁面,包含 <head>、<body>、CSS 連結、JavaScript 腳本等。這需要一個 HTML 模板來包裹 React 組件渲染出的內容。
// server.js (續)
// ... (之前的 require 和 SimpleTextComponent 定義)
// 假設有一個更複雜的 React 組件
const App = ({ data }) => (
  React.createElement('div', null,
    React.createElement('h1', null, `歡迎來到 ${data.title}`),
    React.createElement('p', null, data.description),
    React.createElement('button', null, '點擊我')
  )
);
app.get('/app', (req, res) => {
  const initialData = {
    title: "玄貓科技",
    description: "探索高科技理論與商業養成系統。"
  };
  const reactAppHtml = ReactDOMServer.renderToString(
    React.createElement(App, { data: initialData })
  );
  const fullHtml = `
    <!DOCTYPE html>
    <html lang="zh-TW">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>${initialData.title}</title>
        <link rel="stylesheet" href="/styles.css">
    </head>
    <body>
        <div id="root">${reactAppHtml}</div>
        <script>
            // 將初始數據傳遞給客戶端
            window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
        </script>
        <script src="/bundle.js"></script> <!-- 客戶端 React 應用程式 -->
    </body>
    </html>
  `;
  res.send(fullHtml);
});
// 提供靜態文件
app.use(express.static('public')); // 假設 bundle.js 和 styles.css 在 public 資料夾
app.listen(3000, () => {
  console.log('伺服器已啟動於 http://localhost:3000');
});
在這個範例中:
- App組件接收- data屬性來顯示內容。
- 在 /app路由中,我們定義了initialData,這是伺服器端獲取到的數據。
- ReactDOMServer.renderToString()將- App組件與- initialData一起渲染成- reactAppHtml。
- 這個 reactAppHtml被嵌入到一個完整的 HTML 模板中,該模板還包含了<head>資訊、CSS 連結 (/styles.css) 和客戶端 JavaScript 腳本 (/bundle.js)。
- 特別重要的是,window.__INITIAL_DATA__將伺服器端使用的初始數據傳遞給客戶端。這樣,當客戶端 React 應用程式啟動時,它可以使用這些數據進行水合,而無需再次發送請求獲取數據。
- app.use(express.static('public'))確保客戶端可以載入靜態資源,如編譯後的客戶端 JavaScript 檔案 (- bundle.js) 和樣式表 (- styles.css)。
玄貓認為,這種將 React 組件渲染為完整 HTML 的模式,是實現高效伺服器端渲染的標準做法。它不僅提供了快速的首頁載入體驗,也為客戶端應用程式的漸進式增強奠定了基礎。
  graph TD
    A[客戶端請求 /app] --> B(Express 伺服器)
    B --> C{處理路由 /app}
    C --> D[獲取初始數據]
    D --> E[React.createElement(App, { data })]
    E --> F[ReactDOMServer.renderToString()]
    F --> G[生成 React 組件 HTML]
    G --> H[HTML 模板]
    H -- 嵌入 --> I[完整 HTML 頁面]
    I --> J[發送 HTML 響應]
    J --> K[瀏覽器接收並顯示]
    K --> L[載入客戶端 JS (bundle.js)]
    L --> M[客戶端 React 應用程式啟動]
    M --> N[水合 (Hydration) 初始數據]
    N --> O[提供互動性]
看圖說話:
此圖示描繪了 React 與 Express 整合實現伺服器端渲染的詳細流程。當客戶端請求特定路徑(如 /app)時,Express 伺服器會處理該請求,獲取所需的初始數據。接著,伺服器利用 React 的 createElement 和 ReactDOMServer.renderToString() 將帶有數據的 React 組件渲染成 HTML 字串。這個 HTML 字串隨後被嵌入到一個完整的 HTML 模板中,形成一個完整的 HTML 頁面作為響應發送給瀏覽器。瀏覽器接收並顯示該頁面後,會載入客戶端 JavaScript,並通過「水合」過程將靜態 HTML 轉化為具備互動功能的動態 React 應用程式。
跨平台 JavaScript 與 Express 及 React 的深度整合
跨平台 JavaScript(Universal JavaScript)的核心價值在於讓同一套 JavaScript 程式碼能夠在伺服器端和客戶端同時運行,從而實現程式碼共用、提升開發效率並優化使用者體驗。當 Express 作為伺服器框架,React 作為 UI 庫時,這種整合的深度與效益尤為顯著。
統一的路由管理與數據獲取邏輯
在傳統的前後端分離架構中,路由通常在伺服器端和客戶端分別定義。伺服器負責處理 URL 路徑並返回 HTML 或 API 響應,而客戶端則有自己的路由來處理應用程式內部的導航。跨平台 JavaScript 允許我們在一個地方定義路由,並在伺服器端用於匹配請求和渲染初始頁面,在客戶端則用於處理後續的頁面導航。
同樣地,數據獲取邏輯也可以共用。一個 React 組件可能需要在渲染前獲取數據。在 SSR 環境下,伺服器可以在渲染組件之前執行這些數據獲取邏輯;而在客戶端,當使用者導航到新頁面時,相同的數據獲取邏輯可以在瀏覽器中執行。
實務案例: 考慮一個新聞網站,其文章頁面需要根據文章 ID 獲取內容。
- 伺服器端:當使用者直接訪問 myapp.com/article/123時,Express 路由會捕獲article/:id,然後在伺服器端執行數據獲取函數fetchArticle(123),將獲取到的文章數據傳遞給 React 的ArticlePage組件進行渲染。
- 客戶端:當使用者在網站內部點擊另一個文章連結時,客戶端路由會攔截導航,然後在瀏覽器中執行相同的 fetchArticle(456)函數,更新ArticlePage組件的內容,而無需重新載入整個頁面。
這種模式確保了數據獲取邏輯的一致性,減少了重複編寫和維護的成本。
狀態管理與水合(Hydration)
在跨平台應用中,狀態管理是另一個關鍵環節。伺服器端渲染時,應用程式的初始狀態會在伺服器上生成。為了確保客戶端應用程式能夠「接管」伺服器生成的 HTML,並在不重新渲染的情況下繼續運行,這個初始狀態必須從伺服器傳遞到客戶端。
通常的做法是將初始狀態序列化為 JSON 字串,並嵌入到伺服器響應的 HTML 頁面中,例如通過 window.__INITIAL_STATE__ 全局變數。客戶端應用程式啟動時,會從這個全局變數中讀取初始狀態,並將其注入到客戶端的狀態管理系統(如 Redux 或 Context API)中。這個過程就是水合。
// server.js (簡化範例)
app.get('/article/:id', async (req, res) => {
  const articleId = req.params.id;
  const initialData = await fetchArticle(articleId); // 伺服器端獲取數據
  const store = createStore(initialData); // 創建 Redux store 並預載入數據
  const reactAppHtml = ReactDOMServer.renderToString(
    React.createElement(Provider, { store }, React.createElement(ArticlePage))
  );
  const fullHtml = `
    <!DOCTYPE html>
    <html>
    <head>...</head>
    <body>
        <div id="root">${reactAppHtml}</div>
        <script>
            window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())};
        </script>
        <script src="/bundle.js"></script>
    </body>
    </html>
  `;
  res.send(fullHtml);
});
// client.js (簡化範例)
const initialData = window.__INITIAL_STATE__;
const store = createStore(initialData); // 客戶端使用伺服器傳遞的初始狀態
ReactDOM.hydrate(
  React.createElement(Provider, { store }, React.createElement(ArticlePage)),
  document.getElementById('root')
);
挑戰與考量
儘管跨平台 JavaScript 帶來諸多優勢,但也存在挑戰:
- 環境差異:伺服器端和客戶端環境存在差異(例如 window和document對象在伺服器端不存在)。開發者需要編寫環境感知的程式碼,或者使用抽象層來處理這些差異。
- 構建複雜性:需要一個更複雜的構建流程來處理伺服器端和客戶端的程式碼,通常涉及 Webpack 等工具進行打包。
- 效能考量:雖然 SSR 提升了首頁載入速度,但伺服器端渲染本身也會消耗伺服器資源。需要仔細權衡,避免將所有渲染負擔都轉移到伺服器。
玄貓認為,成功的跨平台 JavaScript 實踐,需要開發團隊對前端與後端技術有深入的理解,並能夠靈活運用工具和模式來解決這些挑戰。其最終目標是實現一個既能提供優異使用者體驗,又能保持高開發效率的現代網頁應用程式。
  graph TD
    A[統一路由定義] --> B{伺服器端執行}
    B --> C[匹配 URL]
    C --> D[執行數據獲取]
    D --> E[React 組件渲染 (SSR)]
    E --> F[嵌入初始狀態]
    F --> G[發送完整 HTML]
    A --> H{客戶端執行}
    H --> I[客戶端路由攔截]
    I --> J[執行數據獲取]
    J --> K[React 組件更新 (CSR)]
    G --> L[瀏覽器接收 HTML]
    L --> M[客戶端水合初始狀態]
    M --> N[應用程式互動]
    K --> N
看圖說話:
此圖示展示了跨平台 JavaScript 在伺服器端和客戶端如何協同工作。統一的路由定義在伺服器端用於匹配 URL、執行數據獲取並進行 React 組件的伺服器端渲染(SSR),同時將初始狀態嵌入到生成的 HTML 中,然後將完整的 HTML 發送給瀏覽器。在客戶端,相同的路由定義用於攔截導航、執行數據獲取並進行客戶端渲染(CSR)。瀏覽器接收到伺服器發送的 HTML 後,客戶端會進行水合,利用嵌入的初始狀態,使應用程式具備互動性。這張圖清晰地描繪了程式碼共用在不同環境下的執行路徑。
結論
檢視此整合架構在高負載情境下的實踐效益,可以發現它不僅是傳統伺服器渲染(MPA)與客戶端渲染(SPA)的簡單疊加,而是一種全新的應用程式模型。其核心挑戰已從單純的技術實現,轉向對開發環境差異的精準控制與構建流程的深度最佳化。真正的價值在於,透過路由與數據邏輯的共用,實現了開發體驗與使用者體驗的同步提升,將前端的表現力與後端的穩定性無縫融合,創造出顯著的協同效應。
展望未來,隨著相關工具鏈的持續成熟,前後端開發的職能界線將更趨模糊,這種跨平台 JavaScript 的整合思維,預計將成為打造高效能網頁應用的標準範式。
玄貓認為,此整合架構已展現其不可忽視的戰略價值。對於追求極致效能與開發效率的團隊而言,掌握伺服器與客戶端之間的狀態「水合」(Hydration)機制,將是成功駕馭此模式並釋放其完整潛力的關鍵所在。
 
            