资讯动态

epic-stack 采用 Vite 构建:从 Remix 编译器到 Vite 插件的迁移决策与工程实践

发布时间:2026/9/17 22:05:41 来源:尧图企业网站定制
epic-stack 采用 Vite 构建从 Remix 编译器到 Vite 插件的迁移决策与工程实践【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack导读本文基于 epic-stack 仓库的架构决策记录 docs/decisions/036-vite.md完整还原了这个 Full Stack 启动模板从 Remix 官方编译器迁移到 Vite 构建体系的背景、决策过程与落地细节。你将了解到为什么不采用 Vite 就会被困在 Remix v2、Vite 插件对服务端/客户端代码分离提出的新规则、handle导出中服务端代码的vite-env-only例外处理以及 epic-stack 当前的 Vite 配置全景与配套的 React Router 构建配置、测试与脚本体系。读完本文你能理解这套构建方案的核心约束并能在自己的 React Router / Remix 项目中复刻同样的代码组织规范与配置思路。一、决策背景为什么必须迁移到 Vitedocs/decisions/036-vite.md记录于 2024 年 2 月彼时 Remix 团队正式发布了稳定版的 Remix Vite 插件它可以替代原有的 Remix 编译器。这份决策文档给出了三个核心理由这是唯一的前进方向在 Remix v3 中Vite 插件将成为构建 Remix 应用的唯一受支持方式。如果项目不采用 Vite就只能永远停留在 Remix v2 上原文用 stuck on Remix v2 forever 来描述这种窘境。更好的开发体验采用 Vite 意味着获得更好的热模块替换Hot Module ReplacementHMR这是开发效率的关键提升点。生态红利Vite 拥有一个庞大且活跃的工具生态采用它意味着与其他使用 Vite 的项目共享建设成果工具链的投入可以复用。从当前仓库的 package.json 可以看到这一决策已经彻底落地并持续演进项目依赖中vite已升级到^7.3.1react-router/dev为^7.16.0Remix 已演进为 React Router v7插件也随之更名为react-router/dev/vite同时配套了vite-env-only、vite-plugin-icons-spritesheet、vitest/coverage-v8等一整条 Vite 生态工具链。二、决策本身Adopt Vite决策结论只有一句话采用 Vite。这一决策被标注为 Status: accepted意味着它已经正式生效。结合 docs/decisions/README.md 对决策记录目录的说明记录我们为这个启动模板所做的所有决策方便后人理解当初为什么这么做可以看到这类文档的价值不在于下结论而在于保留决策上下文——尤其是那些回头看很容易被误解的工程取舍。在后续演进中项目还基于 Vite 生态做了更多配套决策例如 docs/decisions/045-rr-auto-routes.md 使用react-router-auto-routes替换remix-flat-routes生成路由清单使路由文件系统约定更贴近 React Router 上游原生约定进一步降低自研工具链的维护面。三、迁移带来的硬约束服务端/客户端代码分离规则采用 Vite 并非没有代价。决策文档明确指出在 Vite 中路由模块不能导出任何使用了服务端专用代码的函数。迁移前epic-stack 的少数路由模块把服务端工具与服务端/客户端混合代码放在了一起迁移时这些工具必须被移出路由文件。文档给出了三条简洁的规则规则一RemixReact Router导出可以留在路由里像loader、action这类框架约定的导出是路由模块的正常组成部分可以原样保留在路由文件中。规则二自有工具导出必须放进.server文件如果是项目自己写的、被路由导出的工具函数文档举例为/verify路由中曾经的requireRecentVerification则必须移动到独立的.server文件或.server.tsx中。这条规则在当前仓库的代码结构中可以清晰印证app/routes/_auth/verify.server.ts 就是一个典型的.server文件它导出了requireRecentVerification、getRedirectToUrl、VerifyFunctionArgs等验证相关工具而同目录的 app/routes/_auth/verify.tsx 只保留 UI 与纯前端相关导出两者通过导入关系协作而非在单个文件中混合导出。类似地app/routes/_auth/login.server.ts 承载登录逻辑而login.tsx只负责表单界面与提交。整个app/routes/_auth/目录下凡是带.server后缀的文件都严格遵循这一约定。规则三不导出的服务端工具函数没有问题如果服务端专用工具函数只是被路由文件内部使用、不对外导出那么它留在路由文件中也是安全的。问题只出在被导出这一点上。为什么能构建就等于能运行决策文档还提到一个重要的工程保障Vite 插件在构建时如果发现任何违反上述规则的问题会直接让构建失败。这意味着如果它能构建它就一定能工作——迁移过程中的代码分离问题会被构建器强制暴露而不是留到运行时才炸出来。这同时带来一个附带收益迫使项目形成更干净的服务端代码与服务端/客户端混合代码分离长期来看是件好事。四、一个有趣的例外handle导出中的服务端代码与vite-env-only规则看似简单但存在一个特殊场景handle导出。handle是路由模块中的一个导出对象它会同时进入客户端与服务端构建产物但某些handle的字段例如 SEO 场景下的getSitemapEntries只应运行在服务端。以 docs/seo.md 中记录的 epic-stack 用法为例站点地图生成依赖nasa-gcn/remix-seo提供的SEOHandle类型配合vite-env-only/macros的serverOnly$宏可以保证该函数在客户端构建中被彻底移除// routes/blog/_layout.tsx示例 import { type SEOHandle } from nasa-gcn/remix-seo import { serverOnly$ } from vite-env-only/macros export const handle: SEOHandle { getSitemapEntries: serverOnly$(async (request) { const blogs await db.blog.findMany() return blogs.map((blog) { return { route: /blog/${blog.slug}, priority: 0.7 } }) }), }与之对应如果某个页面不需要进入站点地图只需让getSitemapEntries返回null// 在不需要进 sitemap 的路由中 import { type SEOHandle } from nasa-gcn/remix-seo export const handle: SEOHandle { getSitemapEntries: () null, }serverOnly$之所以必要是因为handle本身是路由导出对象会同时参与客户端与服务端构建而getSitemapEntries内部调用了数据库等仅服务端可用的能力必须保证客户端构建产物中不含该函数。vite-env-only的这套支持在 vite.config.ts 中已经预配置完毕见下文envOnlyMacros()插件。在仓库的实际实现中站点地图资源路由 app/routes/_seo/sitemap[.]xml.ts 通过nasa-gcn/remix-seo的generateSitemap生成最终 XML并设置了public, max-age300的缓存策略配合robots.txt资源路由app/routes/_seo/robots[.]txt.ts构成完整的 SEO 基础设施。五、仓库中的 Vite 配置全景决策文档只给出了方向真正的工程细节体现在 vite.config.ts 中。这份配置完整呈现了以 React Router 为核心、多插件协同的构建体系1. 核心构建插件reactRouter()import { reactRouter } from react-router/dev/vite这是 Remix Vite 插件在 React Router v7 时代的形态替代了原 Remix 编译器负责把app/routes目录编译为 React Router 应用。注意一个细节在测试模式下该插件会被跳过isTest ? null : reactRouter()因为单元测试不需要完整构建应用。2.envOnlyMacros()—— 构建期代码裁剪import { envOnlyMacros } from vite-env-only envOnlyMacros(),这就是第四节serverOnly$/clientOnly$宏得以生效的底层支撑它作为 Vite 插件在构建期识别宏调用并按目标环境服务端/客户端剔除对应代码。3.tailwindcss()—— CSS 编译import tailwindcss from tailwindcss/viteTailwind CSS v4 的官方 Vite 插件替代了传统的 PostCSS 链式配置样式入口为 app/styles/tailwind.css。4.reactRouterDevTools()—— 开发期调试集成react-router-devtools为开发环境提供路由调试面板。5.iconsSpritesheet()—— SVG 图标雪碧图iconsSpritesheet({ inputDir: ./other/svg-icons, outputDir: ./app/components/ui/icons, fileName: sprite.svg, withTypes: true, iconNameTransformer: (name) name, })把 other/svg-icons 目录下的 SVG 图标编译为雪碧图并自动生成类型声明供 app/components/ui/icon.tsx 这类图标组件使用。6. Sentry 构建集成条件启用mode production process.env.SENTRY_AUTH_TOKEN ? sentryReactRouter(sentryConfig, config) : null,只有生产构建且配置了SENTRY_AUTH_TOKEN时才启用 Sentry 插件避免本地开发时无谓的构建开销。sentryConfig通过SENTRY_AUTH_TOKEN、SENTRY_ORG、SENTRY_PROJECT、COMMIT_SHA等环境变量驱动并将 sourcemap 设为hidden——即生成映射文件用于 Sentry 源码映射上传但不向浏览器暴露//# sourceMappingURL防止公开资源泄露映射地址。7. 测试环境的缓存桩插件const cacheServerStubPlugin { name: vitest-cache-server-stub, enforce: pre as const, resolveId(source: string) { if (!process.env.VITEST) return null if (source.endsWith(cache.server.ts)) { return path.resolve(tests/mocks/cache-server.ts) } return null }, }这是 epic-stack 为单元测试定制的轻量插件在 Vitest 运行时把对cache.server.ts的导入解析到 tests/mocks/cache-server.ts 的 mock 实现避免真实缓存依赖对应决策 docs/decisions/047-mock-cache-server-in-tests.md。8. 内联的 Vitest 配置vite.config.ts中的test字段直接承载了 Vitest 配置测试范围app/**/*.test.{ts,tsx}、setup 文件 tests/setup/setup-test-env.ts、全局 setup tests/setup/global-setup.ts以及覆盖app/**/*.{ts,tsx}的覆盖率统计。此外build.rollupOptions.external将node:*模块与fsevents标记为外部依赖SSR 构建入口指向 server/app.ts。六、配套的 React Router 构建配置与脚本构建体系不止一个配置文件react-router.config.ts 承担 React Router 层面的构建编排ssr: true保持服务端渲染模式注释明确提示设为 false 将启用全路由 SPA 模式routeDiscovery: { mode: initial }控制路由发现策略future.unstable_optimizeDeps: true启用依赖预优化buildEnd钩子生产构建且配置了SENTRY_AUTH_TOKEN时调用sentryOnBuildEnd在构建收尾阶段上传 sourcemap 与 release 信息依据COMMIT_SHA关联提交。package.json 中的脚本与这套构建体系一一对应脚本作用npm run build执行react-router build即通过 Vite 完成应用构建npm run dev以开发模式启动MOCKStrue启用 mocknpm run typecheckreact-router typegen tsc先生成路由类型再静态检查npm run test运行 Vitest 单元测试npm run test:e2e:run先构建再运行 Playwright 端到端测试npm run validate串行执行单元测试、lint、typecheck、e2e 的完整校验依赖清单也印证了 Vite 生态的深度整合vite-env-only构建期环境裁剪、vite-plugin-icons-spritesheet图标雪碧图、vitest与vitest/coverage-v8测试与覆盖率、tailwindcss/vite样式、react-router-devtools调试工具等。七、后果评估更复杂的规则换来更少的意外决策文档的 Consequences 部分对迁移结果做了坦诚的总结几乎一切都变好了Almost everything is better更好的 HMR、更丰富的生态、持续跟进的框架升级路径服务端/客户端代码分离的规则变复杂了一些需要严格遵守框架导出留在路由、自有工具导出移入.server文件、handle中的服务端代码用vite-env-only裁剪这三条规则但总体上规则是更好的清晰的边界带来更少的运行时惊喜构建期强制校验把问题前置暴露。对于正在迁移或新上手 React Router / Remix Vite 项目的开发者可以从这套实践中直接借鉴为.server文件建立明确约定凡是被路由导出的服务端工具统一放入.server.ts/.server.tsx让能否进客户端 bundle从隐式约束变成文件系统层面的显式信号信任构建器的失败Vite 插件会在构建期发现违规导出并报错因此构建通过本身就是一个高价值的安全网把构建纳入 CI 必检项如npm run validate善用vite-env-only处理混合导出对象凡是对客户端、服务端同时可见的导出如handle其中的环境专属字段一律用serverOnly$/clientOnly$包裹按环境条件挂载重型插件如 Sentry 仅在生产且配置 token 时启用避免拖慢本地开发。本文核心依据决策记录 docs/decisions/036-vite.md配置实现 vite.config.ts、react-router.config.ts、package.json代码组织实例 app/routes/_auth/verify.server.ts 与 app/routes/_auth/verify.tsxSEO 例外处理实例 docs/seo.md 与 app/routes/_seo/sitemap[.]xml.ts。【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价