靜態網站生成 (SSG)
在架構中,我們提到主題是在 Webpack 中運行的。但請注意:這並不意味著它始終可以訪問瀏覽器全域變數!主題會被建置兩次
- 在伺服器端渲染期間,主題會在一個稱為 React DOM Server 的沙盒中編譯。您可以將其視為「無頭瀏覽器」,其中沒有
window
或document
,只有 React。SSR 會產生靜態 HTML 頁面。 - 在客戶端渲染期間,主題會被編譯成最終在瀏覽器中執行的 JavaScript,因此它可以訪問瀏覽器變數。
伺服器端渲染和靜態網站生成可能是不同的概念,但我們可以互換使用。
嚴格來說,Docusaurus 是一個靜態網站生成器,因為它沒有伺服器端運行時——我們靜態地渲染為部署在 CDN 上的 HTML 文件,而不是在每次請求時動態地預渲染。這與 Next.js 的工作模式不同。
因此,雖然您可能知道不要訪問 Node 全域變數(例如 process
(或者我們可以嗎?)或 'fs'
模組),但您也不能隨意訪問瀏覽器全域變數。
import React from 'react';
export default function WhereAmI() {
return <span>{window.location.href}</span>;
}
這看起來像是慣用的 React,但如果您運行 docusaurus build
,您將會收到錯誤訊息
ReferenceError: window is not defined
這是因為在伺服器端渲染期間,Docusaurus 應用程式實際上並沒有在瀏覽器中運行,並且它不知道 window
是什麼。
那麼 process.env.NODE_ENV
呢?
「無 Node 全域變數」規則的一個例外是 process.env.NODE_ENV
。事實上,您可以在 React 中使用它,因為 Webpack 將此變數作為全域變數注入
import React from 'react';
export default function expensiveComp() {
if (process.env.NODE_ENV === 'development') {
return <>This component is not shown in development</>;
}
const res = someExpensiveOperationThatLastsALongTime();
return <>{res}</>;
}
在 Webpack 建置期間,process.env.NODE_ENV
將被替換為值,可以是 'development'
或 'production'
。在消除無效程式碼後,您將會得到不同的建置結果
- 開發
- 生產
import React from 'react';
export default function expensiveComp() {
if ('development' === 'development') {
+ return <>This component is not shown in development</>;
}
- const res = someExpensiveOperationThatLastsALongTime();
- return <>{res}</>;
}
import React from 'react';
export default function expensiveComp() {
- if ('production' === 'development') {
- return <>This component is not shown in development</>;
- }
+ const res = someExpensiveOperationThatLastsALongTime();
+ return <>{res}</>;
}
瞭解 SSR
React 不僅僅是一個動態 UI 運行時——它還是一個模板引擎。因為 Docusaurus 網站主要包含靜態內容,所以它應該可以在沒有任何 JavaScript(React 在其中運行)的情況下工作,而只需要純 HTML/CSS。這就是伺服器端渲染提供的功能:將您的 React 程式碼靜態地渲染為 HTML,而不需要任何動態內容。HTML 文件沒有客戶端狀態的概念(它純粹是標記),因此它不應該依賴於瀏覽器 API。
這些 HTML 文件是在訪問 URL 時首先到達使用者瀏覽器螢幕的內容(請參閱路由)。之後,瀏覽器會提取並運行其他 JS 程式碼,以提供您網站的「動態」部分——任何使用 JavaScript 實現的內容。然而,在此之前,您頁面的主要內容已經可見,從而允許更快的載入速度。
在僅限 CSR 的應用程式中,所有 DOM 元素都是在客戶端使用 React 生成的,並且 HTML 文件始終只包含一個供 React 掛載 DOM 的根元素;在 SSR 中,React 已經面對一個完整構建的 HTML 頁面,它只需要將 DOM 元素与其模型中的虛擬 DOM 相關聯。此步驟稱為「水合」。在 React 對靜態標記進行水合後,應用程式就會像任何普通的 React 應用程式一樣開始工作。
請注意,Docusaurus 最終是一個單頁應用程式,因此靜態網站生成只是一種優化(稱為漸進式增強),但我們的功能並不完全依賴於這些 HTML 文件。這與 Jekyll 和 Docusaurus v1 等網站生成器相反,後者會將所有文件靜態地轉換為標記,並通過與 <script>
標籤連結的外部 JavaScript 添加互動性。如果您檢查建置輸出,您仍然會在 build/assets/js
下看到 JS 資產,這些資產實際上是 Docusaurus 的核心。
逃生出口
如果您想要在螢幕上渲染任何依賴於瀏覽器 API 才能運作的動態內容,例如
您可能需要從 SSR 中逃脫,因為靜態 HTML 在不知道客戶端狀態的情況下無法顯示任何有用的資訊。
第一次客戶端渲染產生與伺服器端渲染完全相同的 DOM 結構非常重要,否則 React 將會把虛擬 DOM 與錯誤的 DOM 元素相關聯。
因此,天真地嘗試使用 if (typeof window !== 'undefined) {/* 渲染某些內容 */}
作為瀏覽器與伺服器偵測的方法是行不通的,因為第一次客戶端渲染會立即渲染與伺服器生成的標記不同的標記。
您可以在水合的危險中閱讀更多關於這個陷阱的資訊。
我們提供了几種更可靠的方法來逃脫 SSR。
<BrowserOnly>
如果您需要僅在瀏覽器中渲染某些元件(例如,因為该元件依賴於瀏覽器特性才能運作),一種常見的方法是使用 <BrowserOnly>
包裹您的元件,以確保它在 SSR 期間不可見,並且僅在 CSR 中渲染。
import BrowserOnly from '@docusaurus/BrowserOnly';
function MyComponent(props) {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
const LibComponent =
require('some-lib-that-accesses-window').LibComponent;
return <LibComponent {...props} />;
}}
</BrowserOnly>
);
}
重要的是要意識到 <BrowserOnly>
的子元件不是 JSX 元素,而是一個返回元素的函數。這是一個設計決策。請考慮以下程式碼
import BrowserOnly from '@docusaurus/BrowserOnly';
function MyComponent() {
return (
<BrowserOnly>
{/* DON'T DO THIS - doesn't actually work */}
<span>page url = {window.location.href}</span>
</BrowserOnly>
);
}
雖然您可能希望 BrowserOnly
在伺服器端渲染期間隱藏子元件,但它實際上做不到。當 React 渲染器嘗試渲染此 JSX 樹時,它確實會將 {window.location.href}
變數視為此樹的一個節點,並嘗試渲染它,儘管它實際上沒有被使用!使用函數可以確保我們只在需要時才讓渲染器看到僅限瀏覽器的元件。
useIsBrowser
您也可以使用 useIsBrowser()
鉤子來測試元件當前是否在瀏覽器環境中。它在 SSR 中返回 false
,在第一次客戶端渲染後在 CSR 中返回 true
。如果您只需要在客戶端執行某些條件操作,而不需要渲染完全不同的 UI,請使用此鉤子。
import useIsBrowser from '@docusaurus/useIsBrowser';
function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
}
useEffect
最後,您可以將您的邏輯放在 useEffect()
中,以將其執行延遲到第一次 CSR 之後。如果您只是在執行副作用,而不是從客戶端狀態獲取數據,那麼這是最合適的。
function MyComponent() {
useEffect(() => {
// Only logged in the browser console; nothing is logged during server-side rendering
console.log("I'm now in the browser");
}, []);
return <span>Some content...</span>;
}
ExecutionEnvironment
ExecutionEnvironment
命名空間包含幾個值,canUseDOM
是一種偵測瀏覽器環境的有效方法。
請注意,它在本質上是在檢查 typeof window !== 'undefined'
,因此您不應該將其用於與渲染相關的邏輯,而只能用於指令式程式碼,例如通過發送 Web 請求來響應使用者輸入,或者動態地導入函式庫,其中 DOM 根本不會被更新。
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
document.title = "I'm loaded!";
}