前言
自从在项目中把 Webpack
迁移到 Vite
后,开发体验迅速提升,一个字总结:就是快!!!新的项目也都用了 Vite 搭建项目。 刚好最近项目刚发完版得一两天清闲,索性也浅浅记录一下~ 手把手教你从 0-1 搭建一套规范的 Vue3 + Vite + Typescript + Unocss 前端项目工程化项目。
入门最全面最系统的方法就是查看并实践官方文档。
如果跟着一步一步搭建,建议顺序:
项目初始化 => 代码规范 => 提交规范 => Vite 基础配置 => 集成 Element Plus => 集成 Unocss => 集成 Vue Router、Pinia
本项目完整代码托管地址 GitHub 仓库
技术栈
- Vue 3.3 - 渐进式 JavaScript 框架
- Vite 3 - 构建工具
- Vue Router 4.x - 路由管理工具
- Pinia - 全局状态管理工具
- Element Plus - UI组件库
- Unocss - 原子化 CSS
- ESlint - 代码检查工具
- Prettier - 代码格式化工具
- Husky + lint-stage - git hooks工具
- Commitlint - 提交规范工具
环境
- nodejs - 20.x
- pnpm - 7.x
sh
# 查看 node 版本
node -v
# 更新 node 版本到最新
nvm install stable
架构搭建
项目初始化
1、借助脚手架的力量,快速初始化一个项目。
sh
# pnpm
pnpm create vue@latest
# npm
npm create vue@latest
# 安装指定版本
npm create vue@3.3
按照指令安装相关支持工具:create-vue - Vue 官方的项目脚手架工具,已经帮我们做了很多事情,它提供了基本的项目结构、构建流程和开发工具链等。
js
Vue.js - The Progressive JavaScript Framework
✔ Project name: ... vue3-template // 自定义项目名称
✔ Add TypeScript? ... No / Yes // 是否支持 TypeScript
✔ Add JSX Support? ... No / Yes // 是否支持 JSX
✔ Add Vue Router for Single Page Application development? ... No / Yes // 是否支持 Vue Router
✔ Add Pinia for state management? ... No / Yes // 是否使用 Pinia 状态管理器
✔ Add Vitest for Unit Testing? ... No / Yes // 是否使用 Vitest 进行单元测试
✔ Add an End-to-End Testing Solution? › No // 是否使用端到端的测试方案
✔ Add ESLint for code quality? ... No / Yes // 是否使用 ESLint
✔ Add Prettier for code formatting? ... No / Yes // 是否使用 Prettier
项目脚手架的功能是很强大的,它为项目集成了多种相关生态工具:TypeScript、JSX、Vue Router、Pinia、Vitest、ESLint、Prettier。节约了开发者的时间,让我们更加关注业务代码的开发,接下来对这些工具进行改造,让它更规范、功能更强大,发挥其本质作用。
项目默认目录如下
2、 进入项目,安装依赖,并启动项目:
sh
cd vue3-tamplate # 进入项目
pnpm i # 安装依赖
npm run dev # 启动项目
项目骨架搭建完毕,接下来为项目集成相关生态工具,以及对应的实用方法。
Vite 基础配置
Vite 一种新型前端构建工具,主要基于浏览器原生的 ESM 方式,真正实现了按需加载,节约了项目构建的时间开销。 更多介绍及配置请查看官网 vitejs.dev/config
对应配置文件是 vite.config.ts,位于项目根目录,在项目初始化的时候去读取。
本项目针对公共基础路径、自定义路径别名、服务器选项、构建选项等做了如下基础配置:
ts
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
base: './', // 设置打包路径
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), // 设置 `@` 指向 `src` 目录
'#': fileURLToPath(new URL('./types', import.meta.url)), // 设置 `#` 指向 `types` 目录
}
},
plugins: [
vue(),
vueJsx(),
],
server: {
open: true, // 服务启动自动打开浏览器
host: '127.0.0.1', // 设置服务启动地址
port: 5000, // 设置服务启动端口号
cors: true, // 允许跨域
// 设置代理
proxy: {
'/api': {
target: 'http://xxxxxxx',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
target: 'es2015', // 设置最终构建的浏览器兼容目标
sourcemap: false, // 构建后是否生成 source map 文件
chunkSizeWarningLimit: 2000, // chunk 大小警告的限制
reportCompressedSize: false, // 启用/禁用 gzip 压缩大小报告
outDir: 'dist', //设置打包输出目录,默认dist
assetsDir: 'assets', //设置静态文件输出目录,默认assets
assetsInlineLimit: 4096, // 设置引用资源 base64 内联的最大值,默认4096=4kb
},
})
集成 Element Plus
项目脚手架没有为我们集成 UI组件库 解决方案,需要自己集成,本项目选择了基于 Vue3 的 Element Plus 组件库,具体可看个人使用习惯来选择。
Element Plus 的使用有两种方案:完整引入
和 按需导入
。本项目使用官方推荐的 按需导入。
按需导入需要安装 unplugin-vue-components
和 unplugin-auto-import
这两款插件。
1、安装依赖
sh
pnpm add element-plus
2、vite 配置
sh
pnpm add -D unplugin-vue-components unplugin-auto-import
在 vite.config.ts
中添加按需导入配置,如下:
ts
// vite.config.ts 按需导入
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()], // 自定义解析器,配置 unplugin-vue-components 使用
dts: 'types/auto-imports.d.ts', // 自动生成 'auto-imports.d.ts'全局声明文件
dirs: ['./src'], // 此目录下相关 API 自动导入
}),
Components({
dts: './types/components.d.ts', // 按需导入组件,相关组件声明放置于 components.d.ts
resolvers: [ElementPlusResolver()],
}),
],
3、Volar 支持
在 tsconfig.json
中通过 compilerOptions.type
指定全局组件类型。
json
// tsconfig.json
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
4、使用体验
新建一个测试组件,并使用 element-plus 组件:
ts
// src/views/test_module/element_puls.vue
<template>
<div>
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
<el-row>
<el-select v-model="value" class="m-2" placeholder="Select" size="middle">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
name: 'ElementPlusComponent'
})
const value = ref('')
const options = [
{
value: 'Option1',
label: 'Option1',
},
]
</script>
可以看到,页面中用到了 el-button el-select 组件,但是并没有显式的引入也能正常使用。
集成 Vue Router
Vue Router 是 Vue.js 的官方路由。为 Vue.js 提供富有表现力、可配置的、方便的路由。更多介绍及配置请查看官网 router.vuejs.org
项目脚手架已经为我们集成了 Vue Router
,以及为我们搭建好配置文件和挂载路由配置,接下来看一下具体怎么改造以及使用。
1、路由目录改造
路由相关配置在 router
文件夹下,一个完整的项目路由应该是按【模块/业务功能】去分开定义的,这样才易于维护,并且有路由守卫,有404路由等。本项目搭建的基础路由目录如下:
json
|-- src
|-- router
|-- index.ts // 路由入口文件
|-- error_route.ts // 错误页路由,如:404
|-- guards.ts // 路由守卫文件
|-- modules
|-- modules1 // 模块1
|-- basic // 基础模块
|-- index.ts // 基础模块-入口文件
路由改造涉及到一些页面的变动,改动幅度较大,因篇幅原因,只放出部分关键代码,具体代码移步至仓库查看。
2、路由入口文件改造
ts
// src/router/index.ts
import type { App } from 'vue'
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { BasicRoutes } from './modules/basic/index' // 导入基础模块路由
import { errorRoutes } from './error_route' // 导入错误页路由
import { createRouterGuards } from './guards' // 导入路由守卫
// 组合路由
const constantRouter: RouteRecordRaw[] = [...BasicRoutes, ...errorRoutes]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 禁止尾部斜杠
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
routes: constantRouter
})
// 初始化路由函数,并注入路由守卫
export function setupRouter(app: App<Element>) {
app.use(router)
createRouterGuards(router)
}
export default router
3、路由守卫改造
ts
// src/router/guards.ts
import { Router } from 'vue-router'
const whiteList = ['/home', '/404'] // 白名单列表,不用校验,直接放行
export function createRouterGuards(router: Router) {
// 全局导航前置守卫:beforeEach,还有 afterEach、beforeResolve
router.beforeEach(async (to, _from, next) => {
const hasToken = false // 用于测试
if (hasToken) {
next()
} else {
const isValid = whiteList.find((path) => to.path.startsWith(path))
if (isValid) {
next()
} else {
next(`/home`)
}
}
})
router.onError((error) => {
console.log(error, '路由错误')
})
}
4、错误页路由配置
ts
// src/router/error_route.ts
import { RouteRecordRaw } from 'vue-router'
export const errorRoutes: RouteRecordRaw[] = [
{
path: '/404',
name: '404',
component: () => import('@/views/error_page/404_page.vue'),
meta: {
hidden: true,
},
},
{
path: '/:pathMatch(.*)*', // 匹配不到定义的路由,跳转 404 路由
redirect: '/404',
name: 'not-found',
},
]
此外,项目的入口文件、模块基础路由、tsconfig文件等都做了改动。
5、初始化改造
ts
# src/main.ts
import { setupRouter } from './router'
const app = createApp(App)
setupRouter(app)
6、使用体验
访问首页:
访问 不存在的路由:
集成 Pinia
Pinia 是开箱即用的模块化设计,基于vue3的 组合式 API 构建,支持多个store实例,更加灵活,可读性更高,基于es module 的方式,可直接通过单个store实例对象进行引用。
项目脚手架已经为我们集成了 Pinia
,接下来通过改造它来看一下具体用法:
1、目录改造
json
|-- src
|-- stores
|-- index.ts // srore入口文件
|-- modules
|-- modules1 // 模块1
|-- counter // 计数器模块
|-- index.ts
2、counter 模块编写
ts
// stores/modules/counter/index.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
// 基于组合式API,像写组件一样写store
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
3、counter 模块引用
ts
// src/views/test_module/counter_page/index.ts
<template>
<div class="flex-col flex-y-center">
<p>
count: {{ count }}
<el-button type="primary" @click="onPlusOne">plus one</el-button>
</p>
<p>double count: {{ doubleCount }}</p>
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from '@/stores/modules/counter'
defineOptions({
name: 'CounterPage',
})
// 创建 counter 实例
const counterStore = useCounterStore()
// 引用 counter 实例里的属性 count,具有响应式
const count = computed(() => counterStore.count)
// 引用 counter 实例里的属性 doubleCount,具有响应式
const doubleCount = computed(() => counterStore.doubleCount)
const onPlusOne = () => {
counterStore.increment() // 调用 counter 实例里的方法
}
</script>
4、页面效果
集成 Unocss
UnoCSS - 具有高性能且极具灵活性的即时原子化 CSS 引擎。出现的契机以及原理可以读下作者的文章 重新构想原子化 CSS
原子化 CSS 是一种 CSS 的架构方式,它倾向于小巧且用途单一的 class,并且会以视觉效果进行命名。所有功能可以通过预设和内联配置提供。
1、依赖安装
unocss
- 核心包。@unocss/reset
- 样式重置包。@unocss/runtime
- 运行时包:提供 CDN 构建,在浏览器中运行 UnoCSS 引擎。@unocss/transformer-directives
指令转换器: 可用指令 - @apply、@screen 和 theme()。
sh
pnpm add -D unocss @unocss/reset @unocss/runtime @unocss/transformer-directives
2、unocss 配置
在根目录下新建 unocss.config.ts,用于配置 unocss 的规则,并添加如下配置:
ts
// `uno.config.ts`
import { defineConfig, presetUno, presetAttributify } from 'unocss'
import transformerDirectives from '@unocss/transformer-directives'
export default defineConfig({
presets: [
presetUno(),
presetAttributify(),
],
transformers: [transformerDirectives()], // 使用指令,如 @apply
// 一些实用的自定义规则
rules: [
[/^m-h-(.+)$/, ([, d]) => ({ 'margin-left': `${d}`, 'margin-right': `${d}` })],
[/^m-v-(.+)$/, ([, d]) => ({ 'margin-top': `${d}`, 'margin-bottom': `${d}` })],
[/^p-h-(.+)$/, ([, d]) => ({ 'padding-left': `${d}`, 'padding-right': `${d}` })],
[/^p-v-(.+)$/, ([, d]) => ({ 'padding-top': `${d}`, 'padding-bottom': `${d}` })],
[/^font-s-(.+)$/, ([, d]) => ({ 'font-size': `${d}` })],
[/^wh-(.+)$/, ([, d]) => ({ width: `${d}`, height: `${d}` })],
],
// 一些实用的自定义组合
shortcuts: {
'wh-full': 'w-full h-full', // width: 100%, height: 100%
'flex-center': 'flex justify-center items-center', // flex布局居中
'flex-x-center': 'flex justify-center', // flex布局:主轴居中
'flex-y-center': 'flex items-center', // flex布局:交叉轴居中
'text-overflow': 'overflow-hidden whitespace-nowrap text-ellipsis', // 文本溢出显示省略号
'text-break': 'whitespace-normal break-all break-words', // 文本溢出换行
},
})
3、导入
在 main.ts 文件中,添加如下配置:
ts
import 'virtual:uno.css'
import '@unocss/reset/normalize.css'
4、使用体验
如果给一个元素定义 margin-top: 1rem
。
平时的做法是:先命名一个 class,如 margin-top-1
,然后在样式文件在声明 .margin-top-1 {margin-top: 1rem }
ts
// test
<template>
<el-button class="margin-top-1"></el-button>
</template>
<style scoped>
.margin-top-1 {margin-top: 1rem }
</style>
但是使用 unocss 就不用这么麻烦,因为 unocss 预设了大量的样式,我们直接使用类名就可以达到想要的效果,如下:
鼠标悬停到mt-1
类名上,就可以看到预设的默认值,在浏览器调试面板的 style 中也可以看得到。
这样即避免了命名的繁琐,又减小了打包的体积。查询预设样式的类型,可以在 unocss.dev/interactive 查询。
本项目中使用了 Scss
CSS预处理器, 所以还需安装 对应的依赖包:
sh
pnpm add -D scss
代码规范
集成 ESlint + Prettier
ESLint 是一个用于识别和报告在 ECMAScript/JavaScript 代码中发现的不符合规则的工具,主要用于检测代码,保证代码质量,使代码更加一致并避免错误。
Prettier 用于代码格式的校验。这两种工具经常一起配合使用,达到代码检测、代码格式化的目的。
但两者在使用过程中,会因为规则不同,有出现冲突的可能性,所以需要通过插件加强两者的配合。
项目脚手架已经为我们集成了这两个工具,但目前依然没有达到我们想要的效果,存在以下问题:格式错乱,不提示, 保存不会自动纠正。
该段代码中格式错乱,缩进符长度不统一,编辑器没有任何提示,文件保存的时候也没有进行格式化。
接下来开始改造:
1、相关依赖安装
sh
pnpm add vue-eslint-parser @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
vue-eslint-parser
:基于ESLint,对.vue文件的 ESLint 语法进行检测,使 vue 项目遵循统一的代码规范和风格。@typescript-eslint/eslint-plugin
: ESLint插件,为TypeScript项目提供lint规则。@typescript-eslint/parser
: ESLint解析器,用于将TypeScript代码解析为与ESLint兼容的节点。
2、eslintrc 配置
js
// .eslintrc.cjs
// ...
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
],
plugins: ['@typescript-eslint'],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
// 指定eslint解析器
parser: '@typescript-eslint/parser',
// 允许使用 import
sourceType: 'module',
// 允许解析 jsx
ecmaFeatures: {
jsx: true,
},
},
}
3、prettier.json 配置
json
{
"$schema": "https://json.schemastore.org/prettierrc",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"vueIndentScriptAndStyle": true,
"singleQuote": true,
"quoteProps": "as-needed",
"bracketSpacing": true,
"trailingComma": "es5",
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"arrowParens": "always",
"insertPragma": false,
"requirePragma": false,
"proseWrap": "never",
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "lf",
"rangeStart": 0
}
4、使用效果
配置完成之后,一些文件开始飘红,看下之前的那段代码:
点击保存之后,格式化成功:
完成了 ESlint 和 Prettier 的集成配置,就统一了团队协作的代码规范,不会因为个人的编码习惯不同而导致项目有不同的书写格式。
Husky 配置
Husky 是 Git hooks 的工具,当
git commit
时,pre-commit
钩子会启动,可以让我们在提交代码之前去执行一些脚本。
比如以下代码,声明了一个变量,但没有使用,这种 ESlint
检测出来的错误,我们想要阻止它的提交。但是现有配置没有能够达到这一点,依然能提交成功。
开始改造:
1、安装依赖、初始化
sh
# 安装 husky
pnpm add husky -D
# 生成 .husky 文件夹
npx husky-init install
2、修改 pre-commit
.husky/pre-commit
js
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint
本质上就是执行以下脚本, 进行代码的检测:
json
"script" {
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
}
3、执行效果
配置好了之后,再次提交代码:
可以看到,在提交代码之前,会先去执行 pnpm lint
,检测代码,检测到不符合规范的代码,会自动抛出错误,阻止代码的提交。目的达成。
lint-staged 配置
lint-staged 一个能过滤出 Git 代码暂存区文件(被 git add 的文件)的工具。
了解 Git 工作流的都知道,我们在本地改动的代码存在的区域叫 Workspace - 工作区
,git add 之后,工作区的代码就会存到 Stage - 暂存区
,暂存区的代码通过 git commit 提交至 Repository -本地版本库
(不了解的可以查看之前的一篇文章 Git 常用命令手册,里面画了 git 工作流图)。
在上述【集成 ESlint + Prettier】中,通过 git hooks 在提交代码至本地版本库前去执行一些脚本,这些脚本是针对 工作区和暂存区
的代码都做一个检查,但实际上,我们只能提交 暂存区
的代码,所以没必要对工作区的代码也执行这些脚本,如果工作区改动较大,可能会耗时较长,所以通过 lint-staged 过滤出暂存区的代码。
1、添加配置文件
通过命令行,在 .husky 文件夹下新建 pre-commit 文件,写入配置:
sh
pnpm husky add .husky/pre-commit "pnpm lint-staged --allow-empty $1"
2、添加 lint-staged 命令
在 package.json 里添加如下:
json
{
// ...
"lint-staged": {
"*.{vue,js,ts,jsx,tsx}": [
"eslint --fix"
]
}
}
3、使用体验
在两个文件中分别声明了一个变量,均不被使用,对其中一个文件 执行 git add 后,在进行提交,可以看到:只有暂存区的变更进行了 eslint 校验,检测到不符合规范的代码时,抛出了异常
。目的达到。
提交规范
commilint 配置
配置好 Husky 之后,接下来需要约定 commit 规范。按照约定的规范提交代码,有助于我们分析提交的代码,在后续生成的 changelog 文件和语义发版中根据 commit 信息可以了解大致的改动。
1、安装依赖
sh
pnpm add -D @commitlint/config-conventional @commitlint/cli
@commitlint/cli
:命令行工具@commitlint/config-conventional
:基于 AngularJS提交规范
2、添加钩子函数
sh
pnpm husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
3、创建新配置
创建 commitlint.config.js, 写入配置:
js
{
"extends": ["@commitlint/config-conventional"]
}
4、配置完成,测试
git commit -m 'add commitlint
中的 add commitlint
信息不符合AngularJS提交规范,所以有错误提示:类型和简述不能为空。
外话 :之前本人也自己写了一个
git commit
命令行工具,算是一个脚手架的初尝试。主要是针对提交的 commit 信息去校验是否符合AngularJS
提交规范,不符合则禁止提交,不足之处就是功能不够强大,也不支持 git hooks。大家有兴趣可以看看,当作脚手架入门教程。
总结
本文从技术选型到架构搭建、从代码规范约束到提交信息规范约束、也对项目中相关生态工具链做了使用讲解,一步一步带领大家实现规范的前端工程化环境。
因篇幅较长,难免会出现错误,希望大家多多指正!