Vue3+vite+ts搭建项目

新公司待了差不多三个月了,公司前端一直使用基于Vue2封装的一个基础前端框架进行开发,主要是框架集成了一些公司封装的后台管理功能,年后尤大宣布将Vue3作为默认版本,恰好年后一直没什么项目做,就学习一下Vue3顺便把公司的框架拿Vue3重写一下吧。

因为选择了Vue3+Ts+Vite,所以基础配置的搭建跟之前的Vue-cli是有些不同的,所以还是记录一下搭建整个项目基础的过程。

初始化项目

查看Vite的官方文档,我们可以使用yarn create vite来创建项目,然后根据提示完成项目的初始化即可。

当然也可以直接输入自己要用的模板来创建项目,比如我们要创建一个vue+ts的项目,也可以直接通过yarn create vite --template vue-ts生成项目。

关于eslint的配置不在此过多描述,根据部门要求创建即可。

配置vite.config.ts

vite.config.ts与之前的vue.config.js是比较相似的,但也有一些不同之处,只介绍一些常用功能的配置。

配置基础路径

vue.config.js中我们可以配置publicPath来配置基础路径,在vite中我们通过配置base来实现。

假设我们在开发环境中基础路径配置为’/‘,但是在生产环境中配置为相对路径,可以如下配置:

1
2
3
4
5
6
7
8
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const isProduction = process.env.NODE_ENV === 'production'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
base: isProduction ? './' : '',
})

上面我们还引入了defineConfigvue,前者是为了有良好的代码提示,后者则必须要配置

如果配置了base路径,那么要注意你的路由是否能正常匹配

路由创建的时候也可以在createWebHistory(base)中传入一个base路径

路径配置别名

大家使用vue开发都喜欢给src目录配置别名为@,一般都是定义一个resolve方法,用来返回路径。

在vite中我们也可以直接从path模块中引入resolve,不过在这个时候代码编辑器可能会给你一个错误提示。

所以我们需要先安装@types/node供我们正常使用node的模块

1
yarn add @types/node

现在我们就可以引入resolve方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: [
{
find: '@',
replacement: resolve(__dirname, 'src')
},
{
find: '@coms',
replacement: resolve(__dirname, 'src/components')
},
]
},
})

按照之前的套路,现在应该就已经完成了,但是现在并不可以,我们还需要在tsconfig.json中进行对应的配置

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"baseUrl": ".", // paths 路径解析起点
"paths": {
"@/*": ["src/*"], // 别名路径设置
"@coms/*": ["src/components/*"]
}
}
}

到此我们就可以使用配置的别名进行引入了。

配置proxy代理

代理的配置跟之前相比是差不多的,不过proxy不再允许直接配置为字符串了,必须配置为一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server: {
//服务器主机名
// host: "localhost",
//端口号
port: 5000,
//设为 true 时若端口已被占用则会直接退出,
// strictPort: true,
// cors: true, // 默认启用并允许任何源
open: true, // 在服务器启动时自动在浏览器中打开应用程序
//https.createServer()配置项
//https: false,
proxy: {
'/testApi': {
target: 'http://192.168.1.1:8080', //代理接口
changeOrigin: true,
rewrite: (path) => path.replace(/^\/testApi/, '')
}
}
}

配置CSS预处理器

我们在开发过程中一般不会直接使用css,而是使用预处理器如scss/sass/less等等,虽然vite对他们有原生支持,但是必须要手动安装依赖,以scss为例:

1
2
yarn add dart-sass --dev
yarn add sass --dev

配置全局css样式文件

配置好样式的预处理器之后,我们可以对全局样式进行配置。

将这个全局样式文件注入到项目中,在任何组件中都可以直接使用而不需要任何引入。

假设我们将样式文件目录为 /src/style/main.scss,可以在vite中进行如下配置将样式注入全局

1
2
3
4
5
6
7
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/style/main.scss";'
}
}
},

配置路由

路由需要我们手动进行添加,目前对vue-router@4版本对ts有不错的支持。

1
yarn add vue-router@4  // 可以自行指定版本号,版本不低于4即可

在src目录下新建一个router目录,创建index.ts对路由进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import type { App } from 'vue';
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
},
]

export const router = createRouter({
// 如果用哈希模式则history: createWebHashHistory()
history: createWebHistory(),
// vue-router4, x=>left, y=>top
scrollBehavior: () => ({ top: 0 }),
routes
})

之前对scrollBehavior配置使用的x,y在新版本中对应为left,top,其他的配置应该都能看懂

动态路由

现在大家在做路由权限的时候大多会将路由信息存储在后台,在用户登录后返回用户对应的路由信息,然后动态的生成路由,所以之前可能会有这个操作:

1
2
3
4
5
6
7
8
9
10
let routers = userMenus.map( item => {
return {
path: item.path,
component: () => import('@/views/' + item.component),
name: item.name
}
})
routers.forEach( route => {
router.addRoute( route )
})

但是在vite中是不允许你这样动态引入路由组件的,需要使用vite提供的import.meta.glob来实现

例如:

1
const modules = import.meta.glob('../views/*.vue')

modules就能读取到views下的所有vue文件,Object.keys(modules)就是所有vue文件的路径

所以我们在动态引入组件的时候可以这样:

1
component: modules['../views/' + item.component + '.vue']

import.meta.*

前边在配置动态路由的时候我们看到了import.meta.glob,与之相对还有一个import.meta.globEager也是用来引入文件的。

还有一些别的方式也是常用的:

环境变量

Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。

.env文件也有多种,我们可以查看一下官方文档。

.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略

我们可以基于此在不同的环境中配置不同的变量

比如创建.env.development文件配置开发环境变量

1
2
// 开发环境变量 - .env.development
baseURL=devURL

创建.env.production文件配置生产环境变量

1
2
// .env.production
baseURL=proURL

然后我们可以在任何地方通过import.meta.env.baseURL来读取这个变量,并且会根据你当前所处环境的不同读取不同的文件。

静态资源

在之前我们想在页面上加载一张本地图片可能会有这种操作

1
2
3
4
5
6
7
<template>
<img :src="imgUrl" />
</template>
<script>
.....
imgUrl: require('@/imgs/img.png')
</script>

但是现在vite也不在允许这种操作了,而是提供了新的方案

我们可以定义如下函数,然后传入图片的名称就可以得到图片的URL了

1
2
3
4
5
6
7
/**
* @description 获取图片url
* @param { string } name - 图片的完整名称,带后缀名
*/
export function getImageUrl( name: string ) {
return new URL(`../imgs/${name}`, import.meta.url).href
}

然后在页面中使用:

1
2
3
4
5
6
7
<template>
<img :src="imgUrl" />
</template>
<script>
...
const imgUrl = getImageUrl('img.png')
</script>

状态管理

在vue3项目中我们一般不会使用vuex来作为状态管理工具了,而是使用pinia来完成这个功能

在此不过多介绍pinia的使用,可以查看一下文档,用起来比较简单。

我们可以在store目录下新建index.ts生成store实例:

1
2
3
4
import { createPinia } from 'pinia';
const store = createPinia();
// 导出store,供外部使用pinia时传入store和挂载到vue
export { store };

然后我们可以创建某一模块功能的store:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// loginStore.ts
import { defineStore } from "pinia";
import { store } from '@/store'
export const useLoginStore = defineStore( 'login', {
state: () => ({
msg: '123'
}),
getters: {},
actions: {
/**
* @description 获取路由权限
*/
getNav () {

},
/**
* @description 获取用户信息
*/
getUserInfo () {

}
}
})

export function useLoginStoreWithOut() {
return useLoginStore(store);
}

正如你所见,pinia是没有mutation的。

我们可以在组件中使用它:

1
2
3
4
5
<script setup lang='ts'>
import { useLoginStore } from '...'
const loginStore = useLoginStore()
console.log(loginStore.msg) // '123'
</script>

大家可能已经发现了在loginStore.ts文件中我们还定义了一个函数useLoginStoreWithOut,这是因为如果在组件外使用pinia,需要手动传入store实例,具体细节可以查看文档。

事件通信

Vue3官方推荐使用mitt等来实现事件通信,以mitt为例:

创建一个工具方法EventBus.ts:

1
2
3
4
5
6
7
8
import mitt from 'mitt'

// Events 的key是可触发/监听的事件名, value是发送/接收的数据的类型
type Events = {
change: string
}
const emitter = mitt<Events>()
export default emitter

在组件中就可以引入emitter来实现通信

1
2
3
4
5
<script setup lang='ts'>
// 组件发送
import emitter from './EventBus'
emitter.emit('change','hello world')
</script>
1
2
3
4
5
6
7
<script setup lang='ts'>
// 组件接收
import emitter from './EventBus'
emitter.on('change', (str) => {
console.log(str)
})
</script>

Mock数据

前后端分离项目,Mock数据总是不能少的,基于Mockjs进行简单实现一下即可。

1
2
3
// main.ts
// 开发环境且启用Mock时引入Mock
process.env.NODE_ENV === 'development' && config.ifMock && import('@/mock/index')

在/mock/index下进行Mock拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /mock/index.ts
import Mock from 'mockjs'
Mock.setup({
// 请求延迟时间 ms
timeout: "100-300"
})

const modules = import.meta.globEager('./**/*.ts')
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
// 自定义的工具函数请以_开头,非_开头的都默认为mock数据
if (key.includes('/_')) {
return;
}
// 每个mock数据的文件务必 export default 一个列表
mockModules.push(...modules[key].default);
});
mockModules.forEach( item => {
Mock.mock(item.url, item.method, item.response)
})

在/mock目录下其他文件配置相关mock接口及返回数据

1
2
3
4
5
6
7
8
9
10
11
12
// /mock/mock数据
export defualt [
{
url: '/login',
method: 'post',
response: {
code: 200,
message: '',
data: null
}
}
]

如果在/mock目录下需要配置一些工具函数等不是mock数据等内容,命名以_开头,比如_util.ts

作者

胡兆磊

发布于

2022-02-11

更新于

2022-10-23

许可协议