Manual Dev Server
このコンテンツはまだ日本語訳がありません。
手动模式
本指南仅在使用 Classic Remix 编译器 时相关。
默认情况下,remix dev
就像自动驱动一样。
每当检测到应用代码中的文件更改时,它都会自动重新启动应用服务器,从而使您的应用服务器与最新的代码更改保持同步。
这是一种简单的方法,不会妨碍您,我们认为它适用于大多数应用。
但是如果应用服务器重启减慢了你的速度,你可以像手册一样掌控并驾驶remix dev
:
remix dev --manual -c "node ./server.js"
这意味着要学习如何使用离合器换档。 这也意味着你可能会在找到方向时停转。 这需要更多的时间来学习,并且需要你维护更多的代码。
能力越大,责任越大。
我们认为,除非你对默认自动模式感到不舒服,否则它不值得。 但如果是这样,Remix 可以满足你的需求。
remix dev
的心理模型
在开始飙车之前,了解 Remix 的底层工作原理会有所帮助。
尤其重要的是要了解 remix dev
启动的不是一个进程,而是两个进程:Remix 编译器和应用服务器。
观看我们的视频 新开发流程的心理模型🧠
了解更多详细信息。
<文档信息>
之前,我们将 Remix 编译器称为新开发服务器
或v2 开发服务器
。
从技术上讲,remix dev
是 Remix 编译器周围的一个薄层,它确实包含一个带有单个端点(/ping
)的微型服务器,用于协调热更新。
但将remix dev
视为开发服务器
是无益的,错误地暗示它正在替换开发中的应用服务器。
remix dev
不是替换您的应用服务器,而是将您的应用服务器与 Remix 编译器一起运行,因此您可以同时获得两全其美的效果:
- Remix 编译器管理的热更新
- 在应用服务器中以开发方式运行的实际生产代码路径
remix-serve
Remix 应用服务器 (remix-serve
) 支持开箱即用的手动模式:
remix dev --manual
<文档信息>
如果您在运行remix dev
时没有使用-c
标志,则您隐式使用remix-serve
作为您的应用服务器。
无需学习驾驶手动挡汽车,因为 remix-serve
具有内置运动模式,可在更高转速下自动更积极地为您换挡。
好吧,我想我们正在延伸这个汽车比喻。😅
换句话说,remix-serve
知道如何重新导入服务器代码更改,而无需重新启动自身。
但如果您使用 -c
来运行自己的应用服务器,请继续阅读。
学习驾驶手动挡汽车
当您使用 --manual
开启手动模式时,您将承担一些新的职责:
- 检测服务器代码更改何时可用
- 在保持应用服务器运行的同时重新导入代码更改
- 在获取这些更改后向 Remix 编译器发送
就绪
消息
由于 JS 导入已被缓存,因此重新导入代码更改变得很棘手。
import fs from "node:fs";
const original = await import("./build/index.js");fs.writeFileSync("./build/index.js", someCode);const changed = await import("./build/index.js");// ^^^^^^^ this will return the original module from the import cache without the code changes
当您想要重新导入代码发生变化的模块时,您需要某种方法来破坏导入缓存。
此外,CommonJS(require
)和ESM(import
)之间的模块导入方式也不同,这让事情变得更加复杂。
<文档警告>
如果您使用tsx
或ts-node
来运行server.ts
,这些工具可能会将您的 ESM Typescript 代码转换为 CJS Javascript 代码。
在这种情况下,您需要在server.ts
中使用 CJS 缓存清除,即使其余服务器代码使用import
。
这里重要的是您的服务器代码如何_执行_而不是如何_编写_。
1.a CJS: require
缓存清除
CommonJS 使用 require
进行导入,让您直接访问 require
缓存。
这样,当重建发生时,您就可以清除缓存中的服务器代码。
例如,下面是如何破坏 Remix 服务器构建的 require
缓存的方法:
const path = require("node:path");
/** @typedef {import('@remix-run/node').ServerBuild} ServerBuild */
const BUILD_PATH = path.resolve("./build/index.js");const VERSION_PATH = path.resolve("./build/version.txt");const initialBuild = reimportServer();
/** * @returns {ServerBuild} */function reimportServer() { // 1. manually remove the server build from the require cache Object.keys(require.cache).forEach((key) => { if (key.startsWith(BUILD_PATH)) { delete require.cache[key]; } });
// 2. re-import the server build return require(BUILD_PATH);}
<文档信息>
require
缓存键是_绝对路径_,因此请确保将服务器构建路径解析为绝对路径!
1.b ESM: import
缓存清除
与 CJS 不同,ESM 不允许您直接访问导入缓存。 为了解决这个问题,您可以使用时间戳查询参数来强制 ESM 将导入视为新模块。
import * as fs from "node:fs";import * as path from "node:path";import * as url from "node:url";
/** @typedef {import('@remix-run/node').ServerBuild} ServerBuild */
const BUILD_PATH = path.resolve("./build/index.js");const VERSION_PATH = path.resolve("./build/version.txt");const initialBuild = await reimportServer();
/** * @returns {Promise<ServerBuild>} */async function reimportServer() { const stat = fs.statSync(BUILD_PATH);
// convert build path to URL for Windows compatibility with dynamic `import` const BUILD_URL = url.pathToFileURL(BUILD_PATH).href;
// use a timestamp query parameter to bust the import cache return import(BUILD_URL + "?t=" + stat.mtimeMs);}
<文档警告>
在 ESM 中,无法从导入
缓存中删除条目。
虽然我们的时间戳解决方法有效,但这意味着导入
缓存将随着时间的推移而增长,最终可能导致内存不足错误。
如果发生这种情况,您可以重新启动remix dev
以使用新的导入缓存重新启动。
将来,Remix 可能会预先捆绑您的依赖项以保持导入缓存较小。
2. 检测服务器代码变更
现在您已经找到了一种打破 CJS 或 ESM 导入缓存的方法,是时候通过动态更新应用服务器中的服务器构建来使用它了。 要检测服务器代码何时更改,您可以使用文件观察器,例如 chokidar:
import chokidar from "chokidar";
async function handleServerUpdate() { build = await reimportServer();}
chokidar .watch(VERSION_PATH, { ignoreInitial: true }) .on("add", handleServerUpdate) .on("change", handleServerUpdate);
3. 发送就绪
消息
现在是时候仔细检查您的应用服务器在 Remix 编译器首次启动时是否向其发送了就绪
消息:
const port = 3000;app.listen(port, async () => { console.log(`Express server listening on port ${port}`);
if (process.env.NODE_ENV === "development") { broadcastDevReady(initialBuild); }});
在手动模式下,您还需要在重新导入服务器构建时发送就绪
消息:
async function handleServerUpdate() { // 1. re-import the server build build = await reimportServer(); // 2. tell Remix that this app server is now up-to-date and ready broadcastDevReady(build);}
4. 开发感知请求处理程序
最后一步是将所有这些包装在开发模式请求处理程序中:
/** * @param {ServerBuild} initialBuild */function createDevRequestHandler(initialBuild) { let build = initialBuild; async function handleServerUpdate() { // 1. re-import the server build build = await reimportServer(); // 2. tell Remix that this app server is now up-to-date and ready broadcastDevReady(build); }
chokidar .watch(VERSION_PATH, { ignoreInitial: true }) .on("add", handleServerUpdate) .on("change", handleServerUpdate);
// wrap request handler to make sure its recreated with the latest build for every request return async (req, res, next) => { try { return createRequestHandler({ build, mode: "development", })(req, res, next); } catch (error) { next(error); } };}
太棒了! 现在让我们在开发模式下运行时插入新的手动变速器:
app.all( "*", process.env.NODE_ENV === "development" ? createDevRequestHandler(initialBuild) : createRequestHandler({ build: initialBuild }));
有关完整的应用服务器代码示例,请查看我们的[模板][模板]或社区示例。
在重建过程中保持内存服务器状态
重新导入服务器代码时,任何服务器端内存状态都会丢失。 其中包括数据库连接、缓存、内存数据结构等。
这是一个实用程序,它可以记住您想要在重建过程中保留的任何内存值:
// Borrowed & modified from https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts// Thanks @jenseng!
export const singleton = <Value>( name: string, valueFactory: () => Value): Value => { const g = global as any; g.__singletons ??= {}; g.__singletons[name] ??= valueFactory(); return g.__singletons[name];};
例如,要在重建过程中重用 Prisma 客户端:
import { PrismaClient } from "@prisma/client";
import { singleton } from "~/utils/singleton.server";
// hard-code a unique key so we can look up the client when this module gets re-importedexport const db = singleton( "prisma", () => new PrismaClient());
如果您愿意使用的话,还有一个方便的[记住
实用程序][记住]可以在这里提供帮助。