vite.config.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { fileURLToPath, URL } from 'node:url';
  2. import vue from '@vitejs/plugin-vue';
  3. import { defineConfig, loadEnv } from 'vite';
  4. /**
  5. * 框架运行时全走 UMD 全局(README §3.3-1「禁 import 框架」)的构建侧落地:
  6. *
  7. * ① define 注入:业务 .ts/.vue 代码里的**裸标识符**(ref / computed / defineStore / createRouter …)
  8. * 在编译期被替换为 Vue.* / Pinia.* / VueRouter.* 全局访问。
  9. * 注意:vite 的 define 按标识符整词替换(带点号前缀的 obj.ref 不替换),
  10. * 但**解构/对象键位置不做上下文分析**——因此禁止 `const { ref } = Vue` 这类写法
  11. * (README §3.3-1:要么裸写 ref(),要么显式 Vue.ref())。
  12. *
  13. * ② alias 指向 shims:SFC 编译器产物形如 `import { openBlock } from 'vue'`,
  14. * 经别名重定向到 shims/*.ts(运行时从 UMD 全局转发,类型来自安装的框架 dev 依赖)。
  15. * 该层是**唯一允许接触框架模块名**的地方,业务代码一律不写。
  16. */
  17. /** 可裸写的 Vue 组合式 API 标识符(编译期注入为 Vue.*) */
  18. const VUE_APIS: readonly string[] = [
  19. 'ref',
  20. 'shallowRef',
  21. 'reactive',
  22. 'shallowReactive',
  23. 'readonly',
  24. 'shallowReadonly',
  25. 'computed',
  26. 'watch',
  27. 'watchEffect',
  28. 'watchPostEffect',
  29. 'watchSyncEffect',
  30. 'effectScope',
  31. 'onMounted',
  32. 'onUnmounted',
  33. 'onBeforeMount',
  34. 'onBeforeUnmount',
  35. 'onUpdated',
  36. 'onBeforeUpdate',
  37. 'onActivated',
  38. 'onDeactivated',
  39. 'onErrorCaptured',
  40. 'onRenderTracked',
  41. 'onRenderTriggered',
  42. 'onScopeDispose',
  43. 'nextTick',
  44. 'h',
  45. 'inject',
  46. 'provide',
  47. 'toRef',
  48. 'toRefs',
  49. 'toRaw',
  50. 'toValue',
  51. 'markRaw',
  52. 'isRef',
  53. 'isReactive',
  54. 'isReadonly',
  55. 'isVNode',
  56. 'customRef',
  57. 'triggerRef',
  58. 'unref',
  59. 'createApp',
  60. ] as const;
  61. /** 可裸写的 Vue Router 标识符(注入为 VueRouter.*) */
  62. const VUE_ROUTER_APIS: readonly string[] = [
  63. 'createRouter',
  64. 'createWebHashHistory',
  65. 'createWebHistory',
  66. 'useRoute',
  67. 'useRouter',
  68. ] as const;
  69. /** 可裸写的 Pinia 标识符(注入为 Pinia.*) */
  70. const PINIA_APIS: readonly string[] = [
  71. 'defineStore',
  72. 'createPinia',
  73. 'getActivePinia',
  74. 'setActivePinia',
  75. 'storeToRefs',
  76. ] as const;
  77. const defineMap: Record<string, string> = {};
  78. for (const api of VUE_APIS) defineMap[api] = `Vue.${api}`;
  79. for (const api of VUE_ROUTER_APIS) defineMap[api] = `VueRouter.${api}`;
  80. for (const api of PINIA_APIS) defineMap[api] = `Pinia.${api}`;
  81. /**
  82. * Vue 编译期特性开关(SFC 编译器 / @vitejs/plugin-vue 产物会引用这些裸标识符)。
  83. *
  84. * 必须在此显式 define,原因:本项目框架层全走 UMD 全局(index.html <script> 自托管),
  85. * UMD 构建不在 window 上声明这些 flag。若某条编译产物(尤其复杂模板的 render 函数)
  86. * 输出 `if (__VUE_PROD_DEVTOOLS__) {…}` 分支,而 define 未内联 → 浏览器把裸标识符当
  87. * 全局变量求值 → `ReferenceError: __VUE_PROD_DEVTOOLS__ is not defined`(仅命中该分支
  88. * 的页面白屏/无响应,其余页面正常)。
  89. * 生产语义:prod 构建关闭 devtools 钩子与 dev 告警 → 全部置 false。
  90. */
  91. defineMap.__VUE_OPTIONS_API__ = 'true'; // 使用 Options API(EP 组件均为 Options API)
  92. defineMap.__VUE_PROD_DEVTOOLS__ = 'false'; // prod 不启用 devtools 钩子(与 prod UMD 一致)
  93. defineMap.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = 'false'; // 非 SSR 项目,关闭水合差异详情
  94. export default defineConfig(({ command, mode }) => {
  95. // 契约 §4 四层口径(v0.1.24 定稿):所有对外请求 = /api + 相对路径,无一例外;
  96. // 前端调用路径自带 /api,vite rewrite 无条件去前缀,后端路由一律不含 /api。
  97. const env = loadEnv(mode, process.cwd(), 'VITE_');
  98. return {
  99. plugins: [vue()],
  100. /**
  101. * dev(command === 'serve'):本地把 vendor/ 当 public 根,index.html 的相对路径
  102. * vendor/<库>@<版本>/<文件> 直接命中仓库文件,无需 OSS 即可联调;
  103. * build:不拷贝任何框架文件进 dist(框架由 nginx/OSS 在运行时提供),
  104. * dist/ 只含业务产物 —— scripts/check-dist.mjs 对此有门禁。
  105. */
  106. publicDir: command === 'serve' ? 'vendor' : false,
  107. resolve: {
  108. alias: {
  109. // 框架模块名 → 全局转发胶水层(唯一落点;业务代码禁写这些 import)
  110. vue: fileURLToPath(new URL('./shims/vue.ts', import.meta.url)),
  111. 'vue-router': fileURLToPath(new URL('./shims/vue-router.ts', import.meta.url)),
  112. pinia: fileURLToPath(new URL('./shims/pinia.ts', import.meta.url)),
  113. // 业务层路径别名
  114. '@': fileURLToPath(new URL('./src', import.meta.url)),
  115. },
  116. },
  117. define: defineMap,
  118. server: {
  119. port: 5173,
  120. proxy: {
  121. // dev 联调:/api 前缀请求代理到后端 dev 地址(.env / .env.development 的 VITE_API_PROXY_TARGET 可覆盖;
  122. // loadEnv 显式读 .env,不依赖 shell env——shell 里 export 对 vite 进程不可见)
  123. '/api': {
  124. target: env.VITE_API_PROXY_TARGET ?? 'http://127.0.0.1:8080',
  125. changeOrigin: true,
  126. // 契约 §4:去 /api 前缀(无条件、无例外——含 /api/pay/...,支付渠道回调地址由 nginx 侧配置)
  127. rewrite: (p) => p.replace(/^\/api/, ''),
  128. },
  129. },
  130. },
  131. build: {
  132. // 质量门禁:主 chunk 上限(README §3.6);框架误打包时体积会显著超限
  133. chunkSizeWarningLimit: 1024,
  134. },
  135. };
  136. });