Remix 模板项目实战指南:最小化全栈应用的目录结构、核心模块与开发命令
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
本指南围绕本仓库template/目录下的官方 Starter 模板展开,系统讲解一个最小 Remix 应用从目录骨架、路由契约、资产管线到开发命令的完整形态。读者学完后,将掌握如何基于该模板快速起步一个全栈应用、如何按官方约定组织app/actions/与app/router.ts、如何配置服务端资产管线,以及npm run dev、npm run hmr、npm run start等命令背后的真实行为。
模板定位:一个可立即运行的最小 Remix 应用
template/README.md开篇即点明模板本质——"A minimal Remix application starter with a home page",即一个自带首页的最小 Remix 应用 Starter。它不是为了展示全部功能,而是给出官方认可的最小目录骨架与文件职责划分,让新项目以此为基础"生长"(Growing The App)。
模板根目录结构如下:
template/ ├── app/ │ ├── actions/ │ │ ├── public/ # 浏览器运行时入口与交互组件 │ │ │ ├── entry.ts │ │ │ └── prompt-button.tsx │ │ ├── controller.tsx # 顶层路由 actions 的归属地 │ │ ├── document.tsx # HTML 文档壳(head、字体、入口脚本) │ │ └── home-page.tsx # 路由自带的首页 UI │ ├── assets.ts # 服务端资产管线(编译、HMR、预加载) │ ├── router.ts # 路由→处理器绑定 + 中间件装配 │ └── routes.ts # 共享路由契约(类型安全 href) ├── public/ │ └── favicon.svg # 静态文件,原样从应用根路径提供 ├── hmr.ts # 独立 HMR 代理进程入口 ├── server.ts # Node HTTP 服务入口 ├── package.json └── tsconfig.json模板的全部文件均可在仓库的 template/ 目录中逐一查看,其中 template/README.md 是本文所述约定的原始出处。
Starter Shape:每个文件各司其职
README 用 7 条清单概括了模板的"标准形态",逐条对照源码展开如下。
1.app/actions/controller.tsx:顶层路由 actions 的归属地
README 指明该文件 "owns the top-level route actions"。其真实实现是:
// template/app/actions/controller.tsx import { createController } from 'remix/router' import { assets } from '../assets.ts' import { routes } from '../routes.ts' import { HomePage } from './home-page.tsx' export default createController(routes, { actions: { async assets(context) { return (await assets.fetch(context.request)) ?? new Response('Not Found', { status: 404 }) }, home(context) { return context.render(<HomePage />) }, }, })createController的职责在 packages/fetch-router/src/lib/controller.ts 中有明确注释:它把路由树的叶子节点映射到 action,同时保留每个 action 的 params 与 request-context 类型契约。也就是说,routes里定义了哪些路由,controller 的actions就必须恰好覆盖哪些键——类型系统会在编译期兜底。这里home直接调用context.render(<HomePage />)完成服务端渲染。
2.app/actions/home-page.tsx与app/actions/document.tsx:路由自带的 UI
home-page.tsx是首页 UI,文件头注释写着 "Delete this file and put your own home page in app/actions/controller.tsx",即官方明确允许、甚至鼓励你删除它,把首页替换成自己的实现。document.tsx导出Document组件,负责 HTML 壳:<html>、meta、favicon、<title>(默认值来自模板占位符%%RMX_APP_DISPLAY_NAME_URI_COMPONENT%%,未被替换时回退为 "Remix App")、字体 preconnect,以及关键的入口脚本注入:
{entryPreloads.map((href) => ( <link key={href} rel="modulepreload" href={href} /> ))} <script type="module" src={entryHref}></script>entryHref与entryPreloads均来自app/assets.ts,即浏览器入口的 URL 与预加载清单由服务端资产管线统一产出。
3.app/actions/public/:浏览器运行时入口与交互组件
该目录是浏览器端专属代码的约定位置,包含两个文件:
entry.ts:调用remix/ui的run()启动浏览器运行时,并在import.meta.hot存在时订阅server:update事件,收到服务端更新后重新加载顶层 frame。prompt-button.tsx:一个**独立水合(clientEntry)**的"复制提示词到剪贴板"按钮,组件头注释点明 "This component hydrates independently; the rest of the page stays static HTML"——页面的其余部分保持静态 HTML,只有这个交互按钮被单独激活,这也是 Remix 逐组件水合思路的落地示例。
4.app/routes.ts:共享路由契约与类型安全 href
// template/app/routes.ts import { get, route } from 'remix/routes' export const routes = route({ assets: get('/assets/*path'), home: '/', })README 强调它 "defines the shared route contract used by server and browser modules for type-safe hrefs"。因为服务端与浏览器模块都从同一份routes.ts导入,路由名与路径模式是单一事实来源,拼写错误会在编译期暴露。
5.app/router.ts:路由→处理器绑定与标准渲染中间件
// template/app/router.ts import { createRouter, type MiddlewareContext } from 'remix/router' import { render } from 'remix/middleware/render' import { staticFiles } from 'remix/middleware/static' import controller from './actions/controller.tsx' import { assets } from './assets.ts' import { routes } from './routes.ts' const renderMiddleware = render({ assets }) type AppContext = MiddlewareContext<[typeof renderMiddleware]> declare module 'remix/router' { interface RouterTypes { context: AppContext } } export const router = createRouter<AppContext>({ middleware: [staticFiles('./public', { index: false }), renderMiddleware], }) router.map(routes, controller)这里演示了两个官方推荐的标准中间件:
staticFiles('./public', { index: false }):将public/目录下的静态文件原样服务(index: false表示不做目录索引),对应 README 第 7 条 "Rootpublic/contains static files served unchanged from the app root"。render({ assets }):标准 Remix UI 渲染器,把context.render()的结果渲染成完整 HTML;MiddlewareContext<[typeof renderMiddleware]>把渲染中间件追加的上下文类型化,再通过declare module注入全局RouterTypes.context,让所有 action 的context.render拥有类型。
6.app/assets.ts:服务端资产管线
// template/app/assets.ts import { createAssetServer } from 'remix/assets' import { uiHmr } from 'remix/ui-hmr/assets' const rootDir = process.cwd() const nodeEnv = process.env.NODE_ENV ?? 'development' const isDevelopment = nodeEnv === 'development' const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR) export const assets = createAssetServer({ basePath: '/assets', rootDir, allowFiles: ['app/routes.ts', 'app/**/public/**'], allowPackages: ['remix'], denyFiles: ['app/**/*.test.*'], sourceMaps: isDevelopment ? 'external' : undefined, minify: !isDevelopment, watch: isDevelopment, hmr: isHmr ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel() : undefined, scripts: { loaders: isHmr ? [uiHmr()] : undefined }, }) const entry = 'app/actions/public/entry.ts' export const entryHref = await assets.getHref(entry) export const entryPreloads = await assets.getPreloads(entry)关键配置项一览(createAssetServer定义于 packages/assets/src/lib/asset-server.ts):
| 配置项 | 模板取值 | 含义 |
|---|---|---|
basePath | /assets | 资产统一挂载的 URL 前缀 |
rootDir | process.cwd() | 资产编译的工作根目录 |
allowFiles | ['app/routes.ts', 'app/**/public/**'] | 允许被服务/编译的文件白名单(浏览器端代码约定放public/子目录) |
allowPackages | ['remix'] | 允许被引用的包白名单 |
denyFiles | ['app/**/*.test.*'] | 测试文件禁止进入浏览器产物 |
sourceMaps | 开发环境'external' | 仅开发时产出外部 source map |
minify | 生产环境为true | 仅生产时压缩 |
watch | 开发环境为true | 开发时监听文件变更 |
hmr/scripts.loaders | 仅 HMR 模式启用 | 注入浏览器 HMR 通道与uiHmr()loader |
7.public/:静态文件原样服务
public/favicon.svg是模板唯一静态资源,document.tsx中以/favicon.svg引用,由staticFiles中间件原样提供。
Growing The App:官方推荐的生长路径
README 的 "Growing The App" 一节给出了四条可操作的扩展约定,这是模板最有实战价值的部分:
- 顶层路由 actions 放
app/actions/controller.tsx——新路由的处理器继续追加到现有 controller 的actions中。 - 嵌套路由地图需要自己的 actions 或中间件时,新建
app/actions/<route-key>/controller.tsx——即按路由键划分子目录,每个子 controller 可声明自己的middleware与actions。 - 确实需要时才新增目录,如
app/data/、test/——模板刻意不预置,避免空目录噪音;仓库中其他 demo 目录(如 demos/bookstore/、demos/timeboxer/)正是按此约定生长后的完整样例。 - 共享 UI 超过一个路由需要时才提升到
app/ui/——保持就近维护,避免过早抽象。
入口与服务:server.ts 与 hmr.ts
标准服务进程server.ts
template/server.ts 使用node:http+createRequestListener把 Remix router 暴露为 HTTP 服务:
- 端口默认
44100,可用环境变量PORT覆盖;HMR 场景下还支持HMR_PROXY_PORT。 - 请求处理直接
router.fetch(request),异常时返回 500;SIGINT/SIGTERM触发优雅关闭(server.close+closeAllConnections)。
开发期 HMR 代理hmr.ts
template/hmr.ts 是一个独立 HMR 代理进程,端口分配如下:
| 端口来源 | 默认值 | 用途 |
|---|---|---|
PORT(代理) | 44100 | HMR 代理对外端口 |
HMR_PORT | 代理端口 + 1 | 浏览器 HMR 事件通道 |
APP_PORT | 事件端口 + 1 | 实际应用进程端口 |
它用run('server.ts', ...)以额外参数(--import remix/node-tsx、--import remix/ui-hmr/node)拉起应用进程,再通过createFetchProxy转发请求并注入xForwardedHeaders,配合createHmrReadyFetch实现"服务就绪前排队、就绪后转发",从而获得完整的服务端 + 浏览器端热更新体验。
命令速查:package.json 的六个脚本
README 的 Commands 一节给出的命令在 template/package.json 中有完整定义,逐条解析其真实行为:
npm i # 安装依赖(本仓库为 pnpm workspace,安装时执行 workspace 协议解析) npm run dev # NODE_ENV=development node --watch --import remix/node-tsx server.ts # → node --watch 监听改动自动重启;node-tsx 提供 TS 直接执行 npm run hmr # NODE_ENV=development node hmr.ts # → 启动 HMR 代理进程,获得浏览器/服务端热更新 npm run start # NODE_ENV=production node --import remix/node-tsx server.ts # → 生产模式启动(无 --watch,资产管线自动开启 minify) npm test # NODE_ENV=test node --import remix/node-tsx --test # → 使用 Node 内置 test runner 执行测试 npm run typecheck # tsc --noEmit → 全量类型检查(strict 模式)几点实操要点:
- Node 版本要求:
package.json的engines声明node >= 24.3.0,这是模板的硬性前提(依赖node --watch、内置 test runner 与较新的--import链)。 - 唯一运行时依赖:
dependencies仅remix一个包(workspace 协议workspace:^);TypeScript 与@types/node走 devDependencies 的catalog:版本目录。 - dev 与 hmr 的取舍:
dev简单可靠(进程级热重启),hmr体验更好但多一个代理进程;二选一即可。 - 类型安全基础:template/tsconfig.json 采用
strict: true、module/moduleResolution: NodeNext、jsx: react-jsx+jsxImportSource: remix/ui、allowImportingTsExtensions+rewriteRelativeImportExtensions,这解释了模板源码里随处可见的import ... from '../assets.ts'(带.ts后缀的导入写法)能被正常编译。
从模板到真实项目:可复制的三条路径
- 本地快速起步:把
template/复制为你的应用根目录,按npm i && npm run dev启动,浏览器访问http://localhost:44100即可看到首页;之后按 "Growing The App" 约定替换home-page.tsx、扩展routes.ts与 controller。 - 对照成熟样例学习增长路径:仓库 demos/ 下多个 demo 展示了模板约定的放大形态——demos/bookstore/ 有完整的数据层与中间件、demos/timeboxer/ 有数据库迁移与安全测试、demos/social-auth/ 有认证流程,可作为"模板长大后长什么样"的参照。
- 理解底层机制:controller 的类型约束见 packages/fetch-router/src/lib/controller.ts,资产管线的配置语义见 packages/assets/src/lib/asset-server.ts,渲染中间件见 packages/render-middleware/src。
小结
template/README.md虽短,却浓缩了 Remix 项目官方约定的最小骨架:app/actions/承载路由处理器、app/routes.ts提供类型安全路由契约、app/router.ts装配中间件、app/assets.ts统一管理浏览器资产,外加一套覆盖开发、HMR、生产、测试与类型检查的命令体系。以它为起点,配合 "Growing The App" 的生长约定,即可稳健地把最小应用扩展成完整全栈项目。
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考