这是《【Buff叠满】基于Vite+Vue3+TS+AntDV4+unocss+pinia的项目开发底层》的第二篇文章
上篇文章《从0到1实践,企业级前端开发底层规范搭建(2024版) 》已经把底子打好,现在要开始起灶台摆桌子了。
写在开头
Vue3 + TypeScript + Vite项目开发底层,集成Eslint + Prettier + StyleLint + Husky + Lint-stahed + CommitLint规范检验,目标是搭建一套好用的前端开发常规底层,支撑10万+代码行项目高效开发。
分支 | 备注 | 进度 |
---|---|---|
master | 主分支 | |
release/basic | 基础底层(Vue3 + TypeScript + Vite + Eslint + Prettier + StyleLint + Husky + Lint-stahed + CommitLint) | 已完成 |
release/advanced | 高级版底层(unocss + ant-design-vue4.x + Vue-router + pinia + Axios + Less + unplugin-auto-import + vue-global-api) | 已完成 |
release/pro | Pro版底层(含有各种常用页面+业务组件) | 计划中 |
环境
- IDE:
VSCode
- NodeJs:
18+
- 包管理工具:
PNPM
路径别名和TSConfig配置
路径别名纯粹是想少写些代码,并让IDE知道对应的路径在哪,能正确跳过去,不报错
安装依赖
js
pnpm add @types/node -D
修改vite.config.ts文件
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
// 设置别名
alias: {
'@': path.resolve(__dirname, 'src'),
Assets: path.resolve(__dirname, 'src/assets'),
Components: path.resolve(__dirname, 'src/components'),
Utils: path.resolve(__dirname, 'src/utils'), // 工具类方法(新创建的)
},
},
})
修改tsconfig.json文件
tsconfig的更多配置项可参考《现代Typescript高级教程》解读TSConfig
js
{
"compilerOptions": {
"target": "ESNext", // 将代码编译为最新版本的 JS
"module": "ESNext", // 使用 ES Module 格式打包编译后的文件
"moduleResolution": "node", // 使用 Node 的模块解析策略
"lib": ["ESNext", "DOM", "DOM.Iterable"], // 引入 ES 最新特性和 DOM 接口的类型定义
"skipLibCheck": true, // 跳过对 .d.ts 文件的类型检查
"resolveJsonModule": true, // 允许引入 JSON 文件
"isolatedModules": true, // 要求所有文件都是 ES Module 模块。
"noEmit": true, // 不输出文件,即编译后不会生成任何js文件
"jsx": "preserve", // 保留原始的 JSX 代码,不进行编译
"strict": true, // 开启所有严格的类型检查
"esModuleInterop": false, // 兼容ES模块, 允许使用 import 引入使用 export = 导出的内容
"allowSyntheticDefaultImports": true, // 配合esModuleInterop使用
"allowJs": true, //允许使用js
"baseUrl": ".", //查询的基础路径
"paths": {
"@/*": ["src"],
"Assets/*": ["src/assets/*"],
"Components/*": ["src/components/*"],
"Utils/*": ["src/utils/*"]
} //路径映射,配合别名使用
},
//需要检测的文件
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [
{
"path": "./tsconfig.node.json"
}
] //为文件进行不同配置
}
修改tsconfig.node.json文件
js
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
试一下
在utils下创建一个tools.ts文件
js
// 生成自增的长度为n的数组,从0开始
export const generateArray = function (n: number): number[] {
return [...Array(n).keys()]
}
在App.vue中引入和使用
js
<script setup lang="ts">
import HelloWorld from 'Components/HelloWorld.vue' // 使用Components别名
import { generateArray } from 'Utils/tools' // 使用Utils别名
console.log(generateArray(20))
</script>
</script>
运行看看,没毛病
配置vue-global-api自动引入Vue API
Vue3开发几乎都离不开ref、reative、computed等等Vue API,每次都需要import { ref } from 'vue'
其实有点浪费时间
antfu开发的vue-global-api
就可以解决这个问题帮我们自动引入。
安装依赖
js
pnpm add vue-global-api -D
在main.ts
中导入 vue-global-api
注册全局 API
js
// main.js
import 'vue-global-api'
在.eslintrc.cjs
中增加配置
js
// .eslintrc.js
module.exports = {
extends: [
'vue-global-api'
]
};
试一下
在HelloWorld.vue文件中,直接使用ref
js
<script setup lang="ts">
const count = ref(0)
console.log(count)
</script>
运行看看,没毛病!
配置路由
安装依赖
js
pnpm add vue-router
配置拦截器
src目录下创建config文件夹,里头创建interceptors文件夹用于存放各种拦截器,比如我们先创建router.ts用于路由守卫和拦截。
js
import { RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
export async function routerBeforeEachFunc(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
// 路由进入前的操作
next()
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function routerAfterEachFunc(to: RouteLocationNormalized, from: RouteLocationNormalized) {
// 路由进入后的操作
}
创建views
src目录下创建views文件夹,用于存放页面,页面结构跟之后的路由结构保持一致,便于维护。这里我们创建一个HomePage页面,里头新建index.vue文件。
js
<template>
<div>HomePage</div>
</template>
<script setup lang="ts"></script>
注意,这里eslint会报错,说组件文件名需要是多个单词词组
emmm,我觉得没必要,所以我们可以在.eslintrc.cjs里配置下规则,不去检测这个问题
js
"rules": {
"vue/multi-word-component-names": 'off'
}
创建routes
src目录下创建routes文件夹,用于存放所有路由。这里我们创建一个index.ts存放通用路由。
js
export default [
{
path: '/',
name: 'Home',
component: () => import('Views/HomePage/index.vue'), // 在vite.config.ts和tsconfig.json配置好路径别名Views
},
{ path: '/home', redirect: '/' },
]
创建路由实例
src目录下创建plugins文件夹,用于存放所有的插件注入文件。这里我们创建一个router.ts文件用于创建router实例。
js
import { App } from 'vue'
import { routerBeforeEachFunc, routerAfterEachFunc } from 'Config/interceptors/router' // 在vite.config.ts和tsconfig.json配置好路径别名Config
import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
type RouterModule = {
default: RouteRecordRaw[]
}
// 导入所有路由文件
let Routes: Array<RouteRecordRaw> = []
const modules = import.meta.glob('Routes/*.ts', { eager: true }) // 在vite.config.ts和tsconfig.json配置好路径别名Routes。eager: true表示同步导入
for (const path in modules) {
const module: RouterModule = modules[path] as RouterModule
Routes = Routes.concat(module.default)
}
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes: Routes,
})
export async function setupRouter(app: App) {
// 创建路由守卫
router.beforeEach(routerBeforeEachFunc)
router.afterEach(routerAfterEachFunc)
app.use(router)
// 路由准备就绪后挂载APP实例
await router.isReady()
}
export default router
注册路由
在入口文件main.ts中使用导出的注册路由方法。
js
import { createApp } from 'vue'
import 'vue-global-api'
import './style.css'
import App from './App.vue'
import { setupRouter } from 'Plugins/router'
const app = createApp(App)
async function setupApp() {
// 挂载路由
await setupRouter(app)
app.mount('#app')
}
setupApp()
安置route容器
修改App.vue文件
js
<template>
<router-view />
</template>
试一下
运行看看,没毛病!
配置Ant-Design-Vue 4.x
这里我们选用Ant Design Vue 4.x作为组件库,并安装配置自动按需引入,提升开发效率,顺便引入配套的图标库@ant-design/icons-vue
。
安装依赖
js
// 安装Ant Design Vue 和 配套icons
pnpm add --save ant-design-vue@4.x @ant-design/icons-vue
// 安装组件自动引入插件
pnpm add -D unplugin-vue-components
修改vite.config.ts文件
js
// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码
// Ant Design Vue 4.x 自动按需引入组件
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
// Ant Design Vue 4.x 自动按需引入组件
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
],
})
试一下
删除src目录下的style.css文件,删除main.ts中对此文件的引入,删除App.vue中的所有style,在Antdv文档中找一个Layout示例代码复制进HomePage/index.vue文件
js
<template>
<a-layout class="layout">
<a-layout-header>
<div class="logo" />
<a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="horizontal" :style="{ lineHeight: '64px' }">
<a-menu-item key="1">nav 1</a-menu-item>
<a-menu-item key="2">nav 2</a-menu-item>
<a-menu-item key="3">nav 3</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout-content style="padding: 0 50px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>Home</a-breadcrumb-item>
<a-breadcrumb-item>List</a-breadcrumb-item>
<a-breadcrumb-item>App</a-breadcrumb-item>
</a-breadcrumb>
<div :style="{ background: '#fff', padding: '24px', minHeight: '280px' }">Content</div>
</a-layout-content>
<a-layout-footer style="text-align: center"> Ant Design ©2018 Created by Ant UED </a-layout-footer>
</a-layout>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const selectedKeys = ref<string[]>(['2'])
</script>
<style scoped>
.site-layout-content {
padding: 24px;
min-height: 280px;
background: #fff;
}
#components-layout-demo-top .logo {
float: left;
margin: 16px 24px 16px 0;
width: 120px;
height: 31px;
background: rgb(255 255 255 / 30%);
}
.ant-row-rtl #components-layout-demo-top .logo {
float: right;
margin: 16px 0 16px 24px;
}
[data-theme='dark'] .site-layout-content {
background: #141414;
}
</style>
运行看看,没毛病!(浏览器默认的8px margin)
此时,根目录下会出现一个components.d.ts文件,里面就放着自动按需导入的组件列表,直呼高级! (注意:Antdv的icon需要按需自行引入)
配置Axios
安装依赖
js
pnpm add axios
配置拦截器
在config/interceptors文件夹下新建axios.ts文件,存放请求拦截器。
js
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
export function requestSuccessFunc(requestObj: AxiosRequestConfig) {
// 自定义请求拦截逻辑,可以处理权限,请求发送监控等
// ...
return requestObj
}
export function requestFailFunc(requestError: AxiosRequestConfig) {
// 自定义发送请求失败逻辑,断网,请求发送监控等
// ...
return Promise.reject(requestError)
}
export function responseSuccessFunc(responseObj: AxiosResponse) {
// 自定义响应成功逻辑,全局拦截接口,根据不同业务做不同处理,响应成功监控等
// ...
return responseObj
}
export function responseFailFunc(responseError: AxiosResponse) {
// 响应失败,可根据 responseError.status 来做监控处理
// ...
return Promise.reject(responseError)
}
配置axios默认config
在config文件夹下新建index.ts文件,存放各种配置,这里我们写上axios的简单配置。
js
// axios 默认配置
export const AXIOS_DEFAULT_CONFIG = {
timeout: 50000,
maxContentLength: 2000,
}
创建axios实例
在plugins文件夹下新建axios.ts文件
js
import axios from 'axios'
import { AXIOS_DEFAULT_CONFIG } from 'Config/index'
import { requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc } from 'Config/interceptors/axios'
// 创建axios实例
const axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)
// 注入请求拦截
axiosInstance.interceptors.request.use(requestSuccessFunc, requestFailFunc)
// 注入失败拦截
axiosInstance.interceptors.response.use(responseSuccessFunc, responseFailFunc)
export default axiosInstance
试一下
在HomePage/index.vue文件中,发一个请求
js
// 测试接口请求
import $api from 'Plugins/axios'
$api({
url: window.location.href,
method: 'get',
}).then((res) => {
console.log(res)
})
运行看看,没毛病!
请求API管理
Axios配置好后,马上把API也封装好。
配置接口默认config
在config/index.ts中加入接口配置,目前只有mockBaseUrl配置,以后业务需要就会多起来。
js
// API 默认配置
export const API_DEFAULT_CONFIG = {
mockBaseUrl: '/API', // 本地开发mock接口地址前缀
}
封装API
在plugins下新建api.ts文件,封装API
js
import axios from './axios'
import { API_DEFAULT_CONFIG } from 'Config/index'
import type { AxiosRequestConfig } from 'axios'
// 根据当前环境设置baseUrl
const mockBaseUrl = import.meta.env.DEV ? API_DEFAULT_CONFIG.mockBaseUrl : ''
// 封装API请求
const API = (option: AxiosRequestConfig) => {
option['url'] = mockBaseUrl + option.url
if (option.method?.toLowerCase() === 'get') {
option['params'] = option.data
}
return axios(option)
}
export default API
管理API
在src目录下新建api文件夹分模块存放api接口,例如新建一个user.ts文件存放user相关接口。
js
import type { AxiosRequestConfig } from 'axios'
import $api from 'Plugins/api'
/** 登录 POST */
interface LoginDto {
username: string
password: string
}
// 这里约定所有的接口方法名前加个"$"前缀,跟普通方法名区分开
export async function $authLogin(data: LoginDto, options?: AxiosRequestConfig) {
return $api({
url: '/user/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data,
...(options || {}),
})
}
配置开发环境端口和代理
在vite.config.ts中,加入serve配置。
js
// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码
export default defineConfig({
server: {
port: 3000, // 本地开发服务端口
proxy: {
'/API': {
target: 'http://127.0.0.3:62000', // 要代理的地址
changeOrigin: true,
followRedirects: true, // Cookie支持重定向
rewrite: (path) => path.replace(/^\/API/, ''),
},
},
},
试一下
在HomePage/index.vue文件中,发一个请求
js
// 测试接口请求
import { $authLogin } from 'API/user' // 这里记得配置路径映射别名
$authLogin({
username: 'test',
password: 'test',
}).then((res) => {
console.log(res)
})
运行看看,虽然500(因为就没后端服务),但是没毛病!
配置Pinia
安装依赖
js
pnpm add pinia
创建pinia实例
在src目录下新建store文件夹,里面创建1个index.ts文件
js
import { createPinia } from 'pinia'
import type { App } from 'vue'
// 创建store实例
const store = createPinia()
// 挂载到app上
export function setupStore(app: App<Element>) {
app.use(store)
}
export { store }
在 main.js 中引用
js
import { createApp } from 'vue'
import 'vue-global-api'
import App from './App.vue'
import { setupRouter } from 'Plugins/router'
import { setupStore } from 'Store/index'
const app = createApp(App)
async function setupApp() {
// 挂载pinia状态管理
setupStore(app)
// 挂载路由
await setupRouter(app)
app.mount('#app')
}
setupApp()
创建1个store
在src/store下创建modules文件夹用于存放不同模块的store,比如user.ts存放用户相关的数据
js
import { defineStore } from 'pinia'
import { $authLogin } from 'API/user'
// 第一个参数是该 store 的唯一 id
const userStore = defineStore('user', {
state: () => {
return {
isLogined: 0, // 0 未知 1 登录 2 未登录
username: 'test',
email: 'test@test.com',
}
},
actions: {
authLogin() {
return $authLogin().then(
(res) => {
this.isLogined = 1
Object.assign(this.username, res.data.username)
return res
},
(rej) => {
this.isLogined = 2
console.log('未登录')
return rej
}
)
},
},
// other options...
})
export default userStore
使用store
在HomePage/index.vue文件中,引用user store
js
// 测试Store
import useUserStore from 'Store/modules/user'
const userStore = useUserStore()
console.log(userStore.username)
userStore.authLogin().then((res) => {
console.log(res)
})
运行看看,没毛病!
配置Unocss(可选)
尝一口antfu大佬做的Unocss,真香!如果你问Unocss是什么,可以查阅这篇文章《不喜欢原子化CSS得我,还是在新项目中使用了Unocss》。因为是推荐,这里不再介绍如何安装使用,有需要点击上述链接学习即可!
配置Iconify(可选)
结合unocss,自动按需引入图标,贼优雅。
安装依赖
js
pnpm add @unocss/preset-icons @iconify/json unplugin-icons -D
修改tsconfig.json文件
创建uno.config.ts,会被自动导入
js
import { defineConfig, presetIcons, presetUno, presetAttributify } from 'unocss'
import UnocssIcons from '@unocss/preset-icons'
export default defineConfig({
presets: [
presetUno(),
presetAttributify(),
presetIcons({
// 图标默认样式
extraProperties: {
display: 'inline-block',
height: '1em',
width: '1em',
},
/* options */
}),
UnocssIcons(),
],
})
修改tsconfig.json文件
js
// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// unocss
import Unocss from 'unocss/vite'
// Icons 自动按需引入图标库
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
IconsResolver(),
],
}),
Unocss(),
Icons({ autoInstall: true }), // 自动安装
],
})
使用
我们可以在Iconify网站上搜寻我们想要的Icon。
点击某个Icon,然后切到Unocss,即可便捷复制代码应用到项目中
有2种使用方式,一种是以class名的方式,一种是标签名的方式
html
<div class="i-openmoji:home-button" />
<i-openmoji:home-button />
打开控制台可以看到,class名的方式,Icon会以background的形式展现,标签名的方式就会被转换成SVG。
注意
只有class名的方式会带上extraProperties自定默认样式
配置Less(可选)
CSS预编译器少不了,Less、Scss、stylus。但每个团队可能选择不一样,这里简单演示Less安装。因为Vite自带less-loader能力,所以安装less依赖就行。
安装依赖
js
pnpm add less -D
使用
html
// template
<div class="my-card">
<div class="card-item">Card 1</div>
<div class="card-item">Card 2</div>
<div class="card-item">Card 3</div>
</div>
// style
<style scoped lang="less">
.my-card {
display: flex;
justify-content: space-between;
.card-item {
max-width: 300px;
height: 300px;
font-size: 32px;
text-align: center;
color: #fff;
background-color: #333;
flex: 1;
line-height: 300px;
}
}
</style>
运行看看,没毛病!
打包优化
最后把打包产物优化下,首先build看看现状
js
pnpm run build
可以看到所有资源都在assets下。这里我主要思路是把资源分在不同目录下,把console跟debugger行去掉。
至于文件压缩,gzip我认为交给服务器就行了,打包阶段压缩会加大打包产物总体积,增加打包和传输上线时间,而且服务器压缩是按需+缓存的,图片压缩也同理,处理方式是设计压缩好再交付给前端,保证质量。
修改vite.config.ts文件
js
// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码
export default defineConfig({
build: {
target: 'ESNext',
minify: 'esbuild',
// rollup 配置
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
assetFileNames: '[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等
},
},
},
esbuild: {
drop: [
'console', // 如果线上需要打印,就把这行注释掉
'debugger',
],
},
})