使用 React 在伺服器和瀏覽器上渲染您的應用程式有一些固有的注意事項。此外,在我們建構 Remix 時,我們一直專注於生產結果和可擴展性。存在一些我們尚未解決的開發人員體驗和生態系統相容性問題。
本文件應能幫助您克服這些障礙。
typeof window
檢查由於相同的 JavaScript 程式碼可以在瀏覽器以及伺服器中執行,因此有時您需要程式碼的一部分僅在其中一個環境中執行。
if (typeof window === "undefined") {
// running in a server environment
} else {
// running in a browser environment
}
這在 Node.js 環境中運作良好,但是,Deno 實際上支援 window
!因此,如果您真的想檢查是否在瀏覽器中執行,最好檢查 document
。
if (typeof document === "undefined") {
// running in a server environment
} else {
// running in a browser environment
}
這將適用於所有 JS 環境(Node.js、Deno、Workers 等)。
您可能會在瀏覽器中遇到此警告
Warning: Did not expect server HTML to contain a <script> in <html>.
這是 React 的 hydration 警告,很可能是因為您的其中一個瀏覽器擴充功能將腳本注入到伺服器渲染的 HTML 中,導致與產生的 HTML 產生差異。
在無痕模式下檢查頁面,警告應該會消失。
loader
中寫入 Session通常,您應該只在 actions 中寫入 sessions,但在 loaders 中寫入 sessions 也有其道理(匿名使用者、導航追蹤等)。
雖然多個 loaders 可以從同一個 session 中讀取,但在 loaders 中寫入 session 可能會導致問題。
Remix loaders 並行執行,有時在單獨的請求中執行(客戶端轉換會為每個 loader 呼叫 fetch
)。如果一個 loader 正在寫入 session,而另一個 loader 正在嘗試從中讀取,您將會遇到錯誤和/或不確定的行為。
此外,session 是建立在來自瀏覽器請求的 cookies 之上。在提交 session 後,它會以 Set-Cookie
標頭傳送到瀏覽器,然後在下一個請求中以 Cookie
標頭傳回伺服器。無論 loaders 是否並行,您都無法使用 Set-Cookie
寫入 cookie,然後嘗試從原始請求 Cookie
中讀取它並期望得到更新的值。它需要先往返瀏覽器,然後從下一個請求中取得。
如果您需要在 loader 中寫入 session,請確保該 loader 不會與任何其他 loader 共用該 session。
您可能會在瀏覽器中遇到這個奇怪的錯誤。它幾乎總是表示伺服器程式碼進入了瀏覽器套件。
TypeError: Cannot read properties of undefined (reading 'root')
例如,您不能直接將 fs-extra
匯入到路由模組中。
import { json } from "@remix-run/node"; // or cloudflare/deno
import fs from "fs-extra";
export async function loader() {
return json(await fs.pathExists("../some/path"));
}
export default function SomeRoute() {
// ...
}
若要修正此問題,請將匯入移至名為 *.server.ts
或 *.server.js
的不同模組中,然後從那裡匯入。在我們的範例中,我們會在 utils/fs-extra.server.ts
中建立一個新檔案。
export { default } from "fs-extra";
然後,將路由中的匯入變更為新的「包裝器」模組。
import { json } from "@remix-run/node"; // or cloudflare/deno
import fs from "~/utils/fs-extra.server";
export async function loader() {
return json(await fs.pathExists("../some/path"));
}
export default function SomeRoute() {
// ...
}
更好的是,向專案傳送 PR,將 "sideEffects": false
新增至其 package.json
,以便 tree shaking 的套件程式能夠知道可以安全地從瀏覽器套件中移除程式碼。
同樣地,如果您在路由模組的最上層範圍呼叫依賴僅限伺服器程式碼的函式,則可能會遇到相同的錯誤。
例如,Remix 上傳處理程式,例如 unstable_createFileUploadHandler
和 unstable_createMemoryUploadHandler
,在底層使用 Node 全域變數,而且應該只在伺服器上呼叫。您可以在 *.server.ts
或 *.server.js
檔案中呼叫這些函式,或者您可以將它們移至路由的 action
或 loader
函式中。
所以,不要執行
import { unstable_createFileUploadHandler } from "@remix-run/node"; // or cloudflare/deno
const uploadHandler = unstable_createFileUploadHandler({
maxPartSize: 5_000_000,
file: ({ filename }) => filename,
});
export async function action() {
// use `uploadHandler` here ...
}
您應該執行
import { unstable_createFileUploadHandler } from "@remix-run/node"; // or cloudflare/deno
export async function action() {
const uploadHandler = unstable_createFileUploadHandler({
maxPartSize: 5_000_000,
file: ({ filename }) => filename,
});
// use `uploadHandler` here ...
}
為什麼會發生這種情況?
Remix 使用「tree shaking」從瀏覽器套件中移除伺服器程式碼。路由模組 action
、headers
和 loader
匯出中的所有內容都將被移除。這是一個很棒的方法,但會受到生態系統相容性的影響。
當您匯入第三方模組時,Remix 會檢查該套件的 package.json
中是否含有 "sideEffects": false
。如果已設定,Remix 知道可以安全地從客戶端套件中移除程式碼。如果沒有設定,匯入將保留,因為程式碼可能會依賴模組的 side effects(例如設定全域 polyfills 等)。
當伺服器渲染時,您可能會嘗試將僅限 ESM 的套件匯入到您的應用程式,然後看到類似這樣的錯誤
Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/dot-prop/index.js from /app/project/build/index.js not supported.
Instead change the require of /app/project/node_modules/dot-prop/index.js in /app/project/build/index.js to a dynamic import() which is available in all CommonJS modules.
若要修正此問題,請將 ESM 套件新增至 serverDependenciesToBundle
選項 (在您的 remix.config.js
檔案中)。
在我們的範例中,我們使用的是 dot-prop
套件,因此我們會這樣做
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverDependenciesToBundle: ["dot-prop"],
// ...
};
為什麼會發生這種情況?
Remix 會將您的伺服器組建編譯為 CJS,而且不會將您的 node 模組打包在一起。CJS 模組無法匯入 ESM 模組。
將套件加入 serverDependenciesToBundle
會告訴 Remix 將 ESM 模組直接打包到伺服器建置中,而不是在執行時才載入。
ESM 不是未來趨勢嗎?
是的!我們的計畫是讓您能夠將應用程式編譯為伺服器端的 ESM。然而,這會帶來反向的問題,也就是無法導入一些與 ESM 導入不相容的 CommonJS 模組!因此,即使我們實現了這個目標,可能仍然需要這個設定。
您可能會問為什麼我們不直接打包伺服器端的所有內容。我們可以這麼做,但這會減慢建置速度,並讓生產環境的堆疊追蹤都指向整個應用程式的單一檔案。我們不希望這樣做。我們知道我們最終可以平滑地解決這個問題,而無需做出這種取捨。
隨著主要的部署平台現在都支援伺服器端的 ESM,我們相信未來會比過去更加光明。我們仍在努力為 ESM 伺服器建置提供穩定的開發體驗,我們目前的方法依賴於一些您無法在 ESM 中執行的操作。我們會實現這個目標的。
當使用 CSS 打包功能與 export *
結合使用時(例如,當使用像 components/index.ts
這樣的索引檔案,該檔案會從所有子目錄重新匯出時),您可能會發現重新匯出的模組的樣式在建置輸出中遺失。
這是由於 esbuild
的 CSS tree shaking 問題。作為一種變通方法,您應該改用具名的重新匯出。
- export * from "./Button";
+ export { Button } from "./Button";
請注意,即使沒有這個問題,我們仍然建議使用具名的重新匯出!雖然它可能會引入更多的樣板程式碼,但您可以明確控制模組的公共介面,而不是在無意中暴露所有內容。