栏目:01 · 前端工程化 | 1 月 · 前端基础与 Vue3 入门 | 选题 6
上个月组里并行起了两个新项目,都是中后台系统。我的做法很"传统":把老项目整个复制过来,改名,删页面,开工。结果第二个项目上线前联调,接口请求怎么都不对,排查了一下午才发现------老项目里三年前留下的 mock 拦截器还挂在 main.ts 里,把 /api 前缀的请求全劫持到本地假数据了。
当天我就决定花两天时间沉淀一个干净的项目模板,以后新项目从模板拉,不从老项目复制。这篇记录搭骨架的过程,重点是踩坑。
先说错误的做法
第一次搭,我直接 npm create vue@latest 把所有选项全勾上:TypeScript、Router、Pinia、ESLint、Prettier、Vitest 全要。生成之后光删演示代码就删了二十分钟,components 里一堆 TheWelcome.vue、WelcomeItem.vue,router 挂着两个示例路由,store 里躺着 counter。
更麻烦的是这些脚手架默认配置并不都是我想在公司项目里要的------比如 Prettier 和 ESLint 的冲突规则,默认勾选的组合跑起来会有格式打架的告警。所以后来我改成最小化起步:模板里只放"每个项目 100% 会用到的东西",Router 都不放(反正有些纯展示页用不上,加上也就一行命令的事)。
最终定稿的版本(node 22.12.0,npm 10.9.0):
bash
npm create vue@latest enterprise-template
# 交互选项:TypeScript -> Yes,其余全部 No
cd enterprise-template
npm install
npm i -D unocss@66.4.0 @unocss/eslint-plugin@66.4.0
环境核对:
bash
npx vite --version # vite/7.1.4
node -e "console.log(require('vue/package.json').version)" # 3.5.13
node -e "console.log(require('unocss/package.json').version)" # 66.4.0
最终目录结构,刻意保持扁平:
bash
src/
├── api/ # 按后端模块拆,一个文件对应一组接口
├── assets/
├── components/
├── composables/ # 组合式函数,和组件严格分开
├── router/
├── styles/ # 全局样式,UnoCSS 之外的少量补充
├── types/ # 公共类型,api 类型放 api/ 里就近维护
├── utils/
├── views/
├── App.vue
└── main.ts
composables/ 单独拎出来说一句:见过太多项目把 useXxx 散落在 utils/ 里,新人根本不知道哪些是响应式的哪些不是。约定先行,这比任何 lint 规则都管用。
踩坑一:路径别名,配了一半等于没配
@ 别名是每个项目的标配。我在 vite.config.ts 里配好:
ts
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
npm run dev 一切正常,页面渲染、路由跳转都对。我顺手在 src/api/user.ts 里写了 import type { UserInfo } from '@/types/user',浏览器里跑得好好的,就提交了。
第二天 CI 挂了。vue-tsc --noEmit 输出:
bash
src/api/user.ts(3,21): error TS2307: Cannot find module '@/types/user' or its corresponding type declarations.
我一开始以为是 vue-tsc 的 bug------dev 环境明明是好的。查了半天 Stack Overflow 才想明白:Vite 和 TypeScript 是两套独立的路径解析系统 。dev 阶段 Vite 的 esbuild 转译根本不做类型检查,别名解析走的是 Vite 自己的 resolve;而 vue-tsc 走的是 tsconfig 的 paths 配置。我只配了前者。
修法是在 tsconfig.app.json 里补上(vue-ts 模板把 tsconfig 拆成了三个,paths 要写在 tsconfig.app.json,不是根上那个只做 references 的壳):
json
// tsconfig.app.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
这次教训我记到现在:别信"本地能跑就是对的",类型检查是独立的一道闸,必须让它过。
踩坑二:UnoCSS 装了但"没生效",而且不报错
UnoCSS 接入就三步,我照文档抄的:
ts
// vite.config.ts 补充
import UnoCSS from 'unocss/vite'
export default defineConfig({
plugins: [vue(), UnoCSS()],
})
ts
// uno.config.ts
import { defineConfig, presetUno, presetAttributify } from 'unocss'
export default defineConfig({
presets: [presetUno(), presetAttributify()],
})
然后我在页面里写 <div class="flex items-center gap-2">,刷新------样式完全没生效。诡异的是控制台没有任何报错,Vite 启动日志也正常。
我当时的第一反应是版本兼容问题,差点 downgrade。后来在 DevTools 的 Elements 面板里发现 class 名明明挂在 DOM 上,但 Computed Styles 里对应规则一条都没有------说明 CSS 文件压根没被加载。回头逐行对照文档,才发现 main.ts 里少了这行:
ts
// main.ts,这个 import 不能少
import 'virtual:uno.css'
// 如果想统一 reset,再加(按需):
import '@unocss/reset/tailwind.css'
virtual:uno.css 是 UnoCSS 注入的虚拟模块,插件负责"生成"这些原子类,但不会自动"注入"到你的应用里。不 import 就是不加载,且全程静默。这个坑的隐蔽之处在于它违反直觉------一般依赖装完插件配完就该工作,这里偏偏缺一步手动引入。后来我把这三步固化成模板,新人再没踩过。
环境变量与类型声明
企业项目离不开多环境。约定:.env.development 本地、.env.production 生产,变量必须 VITE_ 开头。很多模板会漏掉类型声明,导致 import.meta.env.VITE_API_BASE 在 TS 里报隐式 any。补一个声明文件:
ts
// src/types/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE: string
readonly VITE_UPLOAD_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
这样写错变量名(比如手滑打成 VITE_API_BAE)在编码阶段就会标红,而不是跑到运行时才发现接口 404。这是我认为 TS 在中后台项目里性价比最高的一处应用,成本五行代码。
效果与代价
模板落地后这两个月的效果:新项目从拉模板到能写业务代码,从原来的半天缩短到 20 分钟以内(大部分时间花在跟后端对环境变量上)。代价是模板本身成了要维护的东西------上周 UnoCSS 升 minor 版本动了 preset 的默认行为,模板里的 uno.config.ts 跟着改了一行。模板不是一次性的,这点要有预期。
还有几块没做:husky + commitlint 没接(公司目前 git hook 管理不统一,接了反而跟部分老仓库的流程打架)、CI 流水线还是每个项目手配的。这些留到下一篇写"规范层"的时候一起整理。
代码我传了内部 GitLab,就不贴仓库地址了。如果你在搭类似模板时有更顺手的组织方式,评论区聊。