项目:xiaoye-admin-react | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本文是工程配置的逐文件参考手册:五份 .env 的差异矩阵与类型转换机制、vite.config.ts 与 build/ 三件套的逐段解读、tsconfig 三件套、ESLint/Prettier 的规则取舍、nas-deploy/ 部署链路逐文件(tar 管道、原子切换、分级缓存、frp 隧道),以及 package.json scripts 全量。行为层的影响(各模式下的权限/Mock/登录差异)见《00-项目总览》6 与《06-部署运行》3,本文专注"每个配置项是什么、为什么这么写"。
| 文件 | 职责 |
|---|---|
.env / .env.development / .env.production / .env.test / .env.mock | 环境变量五件套(mode 叠加) |
vite.config.ts | Vite 主配置:define 注入、alias、dev server、构建产物结构、manualChunks |
build/getEnv.ts | wrapperEnv:环境变量类型转换 |
build/plugins.ts | 插件组装:checker / compression / PWA / visualizer |
build/proxy.ts | VITE_PROXY → dev server 代理表 |
tsconfig.json / tsconfig.app.json / tsconfig.node.json | project references 三件套 |
eslint.config.js / .prettierrc.json / .prettierignore | 代码规范 |
vitest.config.ts | 测试配置(独立于构建链,见《09-测试与质量保障》2.1) |
scripts/openapi.mjs | OpenAPI 生成入口 |
nas-deploy/* | NAS 演示站部署链路五件套 |
叠加机制:.env 为全模式基底,.env.<mode> 按 --mode 叠加覆盖(vite.config.ts 的 loadEnv(mode, root))。取值矩阵如下(省略号表示未声明):
| 变量 | .env | development | production | test | mock |
|---|---|---|---|---|---|
VITE_GLOB_APP_TITLE | Xiaoye Admin React | — | — | — | — |
VITE_PORT | 5173 | — | — | — | — |
VITE_OPEN | true | — | — | — | — |
VITE_REPORT | false | — | — | — | — |
VITE_USER_NODE_ENV | — | development | production | test | production |
VITE_PUBLIC_PATH | — | / | / | / | / |
VITE_ROUTER_MODE | — | history | history | history | history |
VITE_DROP_CONSOLE | — | true | true | true | true |
VITE_PWA | — | false | true | false(注释出入见《13-技术债务》D3) | true |
VITE_API_URL | — | /api | /api | /api | /api |
VITE_BUILD_COMPRESS | — | — | none | — | none |
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE | — | — | false | — | — |
VITE_DEV_AUTO_LOGIN | — | true | — | — | — |
VITE_ENABLE_MOCK | — | — | — | — | true |
三个结构性观察:其一,.env.mock 是 production 的"最小差分"——仅多一条 VITE_ENABLE_MOCK = true,其余逐字复制(VITE_USER_NODE_ENV 也是 production),这保证了演示构建与生产构建的产物形态完全一致、唯一差异是 MSW,是"演示站即生产彩排"的配置基础;其二,VITE_API_URL 在四个 mode 都显式重申 /api 而非省略,让任何模式的行为单文件可看全;其三,VITE_PROXY 当前在所有 env 文件中均未声明(README.md 明确"留空走 MSW"),接入真实后端时才在 .env.development 添加。
build/getEnv.ts)loadEnv 读到的一切都是字符串,wrapperEnv 只做三类转换:值字面量为 "true"/"false" → 布尔;VITE_PORT → 数字;VITE_PROXY → JSON.parse(解析失败仅 console.error 并保留原字符串)。其余变量保持字符串。这条边界产生一个最易踩的暗坑:VITE_ENABLE_MOCK 在代码中以字符串比较(import.meta.env.VITE_ENABLE_MOCK !== "true",src/main.tsx),写无引号的 true 反而不匹配;同理 VITE_BUILD_COMPRESS 以字符串 "none" 为默认。新增变量时先在 src/typings/global.d.ts 的 ViteEnv 接口补类型(唯一类型契约),再决定是否需要 wrapperEnv 转换。getEnv.ts 还导出 isDevFn/isProdFn/isTestFn 等 mode 判断辅助(当前主流程用 import.meta.env.PROD,见《08-安全与权限设计》2.2 关于 P1-3 修复的记录)。
新增一个环境变量的完整动线(以假设的 VITE_ANALYTICS_ID 为例):.env(或对应 mode 文件)声明键值 → ViteEnv 接口补 VITE_ANALYTICS_ID: string → 消费方以 import.meta.env.VITE_ANALYTICS_ID 读取(客户端可见性:Vite 只注入 VITE_ 前缀变量,其余不会进产物,敏感值严禁以此通道下发)→ 若需要类型转换(数字/布尔/JSON)在 wrapperEnv 补规则并经 viteEnv 传递。注意环境变量是构建期注入:改 .env 后 dev server 必须重启,已构建的产物不会变——这与 runtime 配置(如后端下发的开关)是两个时代的东西,选型时先想清楚变量属于"构建时"还是"运行时"。
define 注入:__APP_INFO__(package.json 的依赖清单 + dayjs 构建时间,供"关于"页与 Footer)与 __COMMIT_HASH__(git rev-parse --short HEAD,非 git 环境 try/catch 留空)。二者是"构建时才可知"的信息通道,不进 .env(见《06-部署运行》3 的三层细节)。
resolve.alias:@ → src,注释要求与 tsconfig 的 paths 保持一致(tsconfig.app.json 中同值定义,两处必须同步改)。
server:host: "0.0.0.0"(局域网设备可访问,便于真机调试)、port 取 VITE_PORT、open 取 VITE_OPEN、cors: true、proxy 来自 createProxy(viteEnv.VITE_PROXY)。
build:outDir: "dist"、sourcemap: false(放开策略见《10-性能设计与优化》7)、chunkSizeWarningLimit: 1500;rollupOptions.output 三条命名规则(JS assets/js/[name]-[hash].js、其余 assets/[ext]/[name]-[hash].[ext],内容哈希寻址配合 Nginx immutable 缓存);manualChunks 三分 vendor(react 系 / antd 系 / echarts 系),动机与评估见《10-性能设计与优化》2.2。文件末尾注释重申无 Less 原则:Vite 仅在安装 less 后才处理 .less,本项目不安装、不配置 preprocessorOptions,样式一律原生 .css。
build/plugins.ts:createVitePlugins(viteEnv) 按开关组装并 filter(Boolean)。四插件:checker({ typescript: true })(dev 期 TS 错误浮层到浏览器,无条件启用);createCompression(解析 VITE_BUILD_COMPRESS 的逗号组合,gzip → .gz、brotli → .br,DELETE_ORIGIN_FILE 控制是否保留原文件);VITE_PWA && createVitePwa(generateSW 模式:registerType: "autoUpdate"、maximumFileSizeToCacheInBytes: 4MiB(注释点名 vendor-antd 超默认 2MiB)、navigateFallback: "index.html" 且 navigateFallbackDenylist: [/^\/api\//](接口不回退走真实网络)、manifest 三图标 public/logo-*.png,name 取 VITE_GLOB_APP_TITLE);VITE_REPORT && visualizer(产出 stats.html,含 gzip/brotli 尺寸)。
插件组装有一个隐含的顺序与形态约定:返回值是 (PluginOption | PluginOption[])[],compression 可能返回数组(gzip 与 brotli 同时开启时是两个插件实例),框架用 filter(Boolean) 剔除关闭项而不是条件展开——新增"可开关插件"时照此模式(条件 && 工厂函数)即可保持清单式组装的可读性;插件的执行顺序按声明序(checker 最先报告错误、visualizer 最后产出报告),若新增插件对顺序敏感(如压缩必须在特定插件后),需要显式验证产物。
build/proxy.ts:createProxy 把 [["/api","http://target"]] 形态翻译为 { [prefix]: { target, changeOrigin: true, ws: true, rewrite, secure? } }——rewrite 用正则把前缀从请求路径剥掉(后端路由不带 /api),https 目标自动 secure: false(容忍自签证书),ws 支持使 WebSocket 代理开箱即用。
build/getEnv.ts:见第 2.2 节。
tsconfig.json 仅做引用聚合:files: [] + references 指向另两份,本身不含编译配置。tsconfig.app.json 管业务源码(include 仅 src):target ES2022、moduleResolution: "bundler" + allowImportingTsExtensions、verbatimModuleSyntax: true(类型导入编译期完全擦除,与 ESLint 的 consistent-type-imports 配套,业务代码大量 import type 的来源)、noEmit、jsx react-jsx;严格组四开关 strict + noUnusedLocals + noUnusedParameters + noFallthroughCasesInSwitch;paths 定义 @/*(注释:必须与 vite alias 一致)。tsconfig.node.json 管构建侧(include:vite.config.ts、vitest.config.ts、build/**、scripts/**、src/typings/global.d.ts):target ES2023、多了 resolveJsonModule 与 "types": ["node"](Node 脚本需要),lint 开关与 app 侧一致。两份 tsBuildInfoFile 都落在 node_modules/.tmp/——这就是 tsc -b 增量缓存的物理位置,"绿了但有问题"时删它或 --force(见《09-测试与质量保障》4.1)。版本约束:TypeScript 锁 5.9.x 不可升 7(typescript-eslint peer 约束,见《06-部署运行》1)。
三件套有几个连带效应值得知晓。moduleResolution: "bundler" 意味着导入路径解析与 Vite 对齐,既不支持 Node 传统的目录索引写法,也要求新增顶层目录时显式纳入某个 include(游离目录会静默逃过类型检查);verbatimModuleSyntax 下 import type 与值导入必须显式区分,混写会被直接拒绝——这解释了为何本项目的类型引用在 import 语句层面就一目了然;skipLibCheck: true 跳过第三方库的类型内部检查(提速编译,代价是库内部的类型错误不在此暴露,antd 大版本升级后若出问题,报错点通常在自己的调用处而非库内);src/typings/global.d.ts 被放进 node 侧 include 是因为 vite.config.ts 消费其声明的 ViteEnv。若将来新增独立的 e2e 或工具目录,正确做法是扩展现有 include 或新建第四个 project reference,而不是让它留在检查体系之外。
入口 HTML 与编辑器约定:index.html 除挂载点与脚本外还承载运行时行为——内联的 #boot-loading 首屏占位(样式与明暗跟随全部写在 HTML 内,先于任何 CSS/JS 生效,见《02-页面设计》7)与 favicon.svg;public/ 目录整体按原样拷贝进产物根路径,其中 mockServiceWorker.js 必须保持在根路径(MSW 注册路径指向它,见《07-模块详解》4)。编辑器对齐三件套:.editorconfig 提供跨编辑器的缩进与换行基线,.vscode/ 提供推荐扩展与保存即格式化设置,package.json 的 engines 字段(node >= 20.19.0)在版本不符时让 pnpm 拒绝安装——这是刻意的护栏而非麻烦。
eslint.config.js(Flat Config)的六段结构,顺序有语义:① ignores(dist/node_modules/coverage/*.min.js 与生成物 src/services/**);② js.configs.recommended;③ tseslint.configs.recommended;④ 项目自定义 TS/TSX 规则——no-undef: "off"(TS 已查未定义,重复检查误报)、react-hooks/rules-of-hooks: "error" 与 exhaustive-deps: "warn"(手动声明而非引用插件预设对象,源码注释:插件预设的导出结构在大版本间会变)、react-refresh/only-export-components(allowConstantExport)、no-unused-vars 三类 ^_ 豁免、consistent-type-imports、no-console 仅放行 warn/error;④.5 scripts/** 补 Node 全局变量;⑤ 纯类型文件(*.d.ts、src/api/interface/**、src/typings/**)豁免 no-explicit-any(Recordable<T = any> 这类通用工具类型必须用 any);⑥ eslint-config-prettier 必须置底(插队会导致格式类误报)。
.prettierrc.json 核心项:printWidth: 120、双引号、带分号、trailingComma: "none"、arrowParens: "avoid"、endOfLine: "lf"。.prettierignore 的关键项是 public/mockServiceWorker.js——注释写明 MSW SW 文件带完整性校验,被 Prettier 改写会导致校验失败,这是"ignore 不是可选项"的少数硬约束之一。ignore 清单同时排除 dist、coverage、锁文件与各类 min 文件,format 的作用域因此始终是手写源码——执行 pnpm format 前不需要(也不应该)用 git diff 人工圈范围,ignore 即边界。
ESLint 与 Prettier 的分工边界在配置里有明确表达:ESLint 管"正确性与模式"(Hooks 规则、未使用变量、console 纪律),Prettier 管"格式"(换行、引号、缩进),eslint-config-prettier 置底关闭一切格式类 lint 规则以避免双重标准。日常开发的最简心法:格式问题一律交给 pnpm format,lint 只修它报的"非格式"问题;若 lint 报了引号/换行类问题,说明 prettier 插件位置被动过,先查配置而不是逐条手修。
sync.sh(一键发布,set -euo pipefail):第 1 步演示构建 CI=true node node_modules/vite/bin/vite.js build --mode mock(绕过 pnpm 直调 vite,等价 build:mock);第 2 步 tar czf - ... | ssh ... tar xzf - 管道同步 compose 与 nginx conf(幂等覆盖,COPYFILE_DISABLE=1 防 macOS AppleDouble 污染文件);第 3 步同法把 dist/ 直传 NAS 的 dist-new(纪律:不走 NAS 挂载卷拷小文件——SMB/NFS 协议拷散文件极慢且 tar 流一次成型);第 4 步 SSH 到 NAS 执行 rm -rf dist.old && mv dist dist.old && mv dist-new dist && sudo docker compose up -d 后 curl 127.0.0.1:5960 验证。原子切换的原理:mv 是同文件系统内的改名操作,瞬时完成,公网用户不会遇到"半套新半套旧"的目录;dist.old 保留一代用于回滚(回滚即反向 mv)。必须重启容器的原因:bind-mount 指向目录 inode,mv 换掉了 inode 而容器内的挂载还指着旧的,不重启则 nginx 继续服务旧文件(nas-deploy/README.md "踩过的坑"第一条)。
docker-compose.yml:单服务 admin-react-web,nginx:1.27-alpine,restart: unless-stopped,端口 127.0.0.1:5960:80 只绑回环(注释:公网流量必须走 frp → VPS nginx,端口不对公网暴露),两份 ro 卷挂载(./dist 与 ./nginx-default.conf),无 docker build——纯静态站的镜像零定制。
nginx-default.conf 分级缓存三段:location /assets/(内容哈希资源)max-age=31536000, immutable + try_files $uri =404;location = /index.html no-cache(注释:发版后存活标签页必得新版,旧 hash chunk 失效由 ErrorBoundary 自愈兜底);location / 执行 try_files $uri $uri/ /index.html history 回退。另开 gzip(level 5,JS/CSS/JSON/SVG 等类型)。这套分级与前端自愈机制的联动分析见《10-性能设计与优化》5。
frpc-add.toml:frp 隧道追加片段([[proxies]] tcp,NAS 127.0.0.1:5960 → VPS remotePort 5960)。头注释三条纪律:改前备份 frpc.toml;多行配置本地生成文件后管道 cat >> 合并、勿用 printf 转义(双层 ssh 会吃掉换行);重启后 tail /var/log/frpc.log 应见 [admin-react-web] start proxy success。安全组不放行 5960——公网唯一入口是 VPS nginx。
vps-nginx-admin-react.conf:VPS 反代站点(80 端口,certbot 后补 443),proxy_pass http://127.0.0.1:5960 加四组转发头(Host/X-Real-IP/X-Forwarded-For/X-Forwarded-Proto)与缓冲区、超时参数。投递方式 base64 编码后 VPS 侧解码(防 $ 变量被 shell 转义);头注释警告不要预置 return 404 占位块(会压过 certbot 的 80→301 跳转)。
nas-deploy/README.md 是以上一切的运行手册(首次上线四件套、日常发布、验收 curl 三连、Umami 统计可选注入、资产清单更新),发布排错见《06-部署运行》5.5。
| 命令 | 实际执行 | 说明 |
|---|---|---|
pnpm dev | vite | 读 .env + .env.development,端口 5173 |
pnpm build | tsc -b && vite build | 先类型检查再构建(production mode) |
pnpm build:mock | tsc -b && vite build --mode mock | 演示构建(production + MSW) |
pnpm preview | vite preview | 本地预览 dist(自带 history 回退) |
pnpm typecheck | tsc -b | project references 增量检查(CI 建议 --force) |
pnpm lint / lint:fix | eslint . / --fix | Flat Config |
pnpm format / format:check | prettier --write . / --check . | 受 .prettierignore 约束 |
pnpm openapi | node scripts/openapi.mjs | 从 openapi/oneapi.json 生成 src/services/api |
pnpm test / test:watch / test:coverage | vitest run / vitest / run --coverage | 见《09-测试与质量保障》 |
scripts/openapi.mjs 的两个实现细节:@umijs/openapi 是 CJS 包而脚本是 ESM,用 createRequire(import.meta.url) 加载;generateService 五参数——schemaPath 指向本地契约、serversPath 输出 src/services、requestLibPath 固定注入适配器导入、projectName: "api"、namespace: "API"。失败时 process.exitCode = 1 保证 CI 可判失败。
关于 scripts 的两条工程纪律。第一,pnpm build 与 pnpm build:mock 的差异只有 --mode mock,但产物差异是结构性的(是否包含 MSW、VITE_ENABLE_MOCK 是否置位),部署时拿错命令的症状见《12-FAQ与故障排查》3.4;直接执行 vite build(跳过 tsc -b)只应在 nas-deploy/sync.sh 这类已外置类型检查的场景使用。第二,脚本没有 pre/post 钩子串联,build 里的 tsc -b && vite build 是显式写在命令里的——这意味着新增自定义脚本(如 deploy、analyze)时,类型检查是否前置由脚本作者决定,建议沿用"先 typecheck 后动作"的显式写法,不要依赖执行者记得先跑检查。
改配置后行为不符时的三个高频点:环境变量不生效——先查类型转换边界(字符串比较 vs 布尔,见 2.2)与是否需要重启 dev server(VITE_DEV_AUTO_LOGIN 等构建期变量改后必须重启,部分还要求删缓存);alias 新增路径不生效——vite 的 resolve.alias 与 tsconfig 的 paths 是两套独立配置,必须同步改;nas-deploy 同步后页面没变——容器未重启(inode 问题)或本地忘了重新 build。