# Vue3项目的创建与使用
# 前言
本次培训的内容是Vue3项目的创建与使用,创建Vue3项目主要是使用Vue3 (opens new window)+Vite (opens new window)+Typescript (opens new window)+Pinia (opens new window)来创建项目,相比较Vue2全家桶来说,对于刚接触Vue3的同学来说可能有些陌生,并且因为涉及到Ts,这也是Vue3不容易上手的原因,所以想通过此次培训来让大家对Vue3的写法、项目的构建和如何使用ts都有一个初步的了解和认识。下面就让我们看看如何去构建一个Vue3项目。
# 一、构建项目
Vue3官方推荐使用Vite (opens new window)来构建项目,而不再使用webpack。
使用Vite构建项目Vue官方和Vite都提供了构建命令,两者构建项目都可以,区别就是Vue官方提供的构建命令更针对与Vue开发,我们使用Vue官方提供的构建命令就可以了。
# 1、创建项目
Vue3创建项目只需安装好Node后直接执行创建项目的命令即可,选择基于TS+组合式API进行开发,不再使用选项式。
npm create vue@latest
执行后,根据提示进行选择,把TS选项选上。
√ Project name: ... vue-project
√ Add TypeScript? ... No / (Yes)
√ Add JSX Support? ... No / (Yes)
√ Add Vue Router for Single Page Application development? ... No / (Yes)
√ Add Pinia for state management? ... No / (Yes)
√ Add Vitest for Unit Testing? ... (No) / Yes
√ Add an End-to-End Testing Solution? » (No)
√ Add ESLint for code quality? ... (No) / Yes
Scaffolding project in ./<your-project-name>...
Done.
前几个选项都默认安装就行了,而Eslint和Prettier选项对于没有严格要求的项目可以直接选择Yes,它只会进行简单的代码检查,不会有严格的风格要求。对于团队来说不建议使用这种,默认选择No不添加,在项目创建好后手动配置Eslint来满足需要的风格指南。
> cd vue-project
> npm install
> npm run dev
# 2、IDE支持
推荐使用Visual Studio Code进行开发,在VSCode中通过Volar (opens new window) 和TypeScript Vue Plugin (opens new window)两个插件对代码进行类型检查。在开发中可以很大程度的帮助检查和提示类型出错,如果想要更严格的类型检查,可以通过Eslint+TS相关的代码检查插件进行控制。
Volar是官方的 VSCode 扩展,提供了 Vue 单文件组件中的 TypeScript 支持,Volar 取代了我们之前为 Vue 2 提供的官方 VSCode 扩展 Vetur。如果之前已经安装了 Vetur,需要在Vue3项目中禁用。TypeScript Vue Plugin针对*.vue 文件进行类型检查。
# 开启Volar Takeover 模式
在添加Volar插件后~~,Volar 提供了一个叫做“Takeover 模式”的功能。在这个模式下,Volar 能够使用一个 TS 语言服务实例同时为 Vue 和 TS 文件提供支持,要开启这个模式需要在VSCode中禁用默认的TS语言服务。~~
在当前项目的工作空间下,用Ctrl + Shift + P (mac:Cmd + Shift + P) 唤起命令面板。输入built,然后选择“Extensions:Show Built-in Extensions”。在插件搜索框内输入typescript (不要删除 @builtin 前缀)。点击“TypeScript and JavaScript Language Features”右下角设置,然后选择禁用。重新加载工作空间。Takeover 模式将会在你打开一个 Vue 或者 TS 文件时自动启用。
# 3、项目结构设计
项目目录创建好后,构建工程化项目,比如常见的后台应用。
├── public # 资源文件
│ └── favicon.ico
├── sr
│ ├── api # Api ajax 等
│ ├── assets # 本地静态资源
│ │ ├── images # 项目图片
│ │ ├── css # reset、common样式
│ │ ├── font # 字体库
│ │ ├── iconfont # iconfont库
│ │ └── less/sass # 公共less/sass文件
│ ├── components # 业务公共组件
│ ├── config # 基础配置
│ ├── constants # 常量文件
│ ├── directive # 自定义指令
│ ├── hooks # hook函数
│ ├── json # json文件
│ ├── layouts # 基础布局
│ ├── mock # mock假数据
│ ├── router # Vue-Router
│ ├── stores # pinia
│ ├── types # 声明文件
│ ├── utils # 工具库
│ ├── views # 业务页面入口和常用模板
│ ├── App.vue # Vue 模板入口
│ └── main.ts # Vue 入口 TS
├── .env # 环境配置文件
├── .env.development
├── .eslintignore # eslint忽略文件
├── .eslintrc.cjs # eslint配置
├── .gitignore # git忽略
├── env.d.ts # 全局声明文件
├── .gitignore # git忽略
├── index.html # Vue 入口模板
├── package.json # 包管理
├── README.md
├── tsconfig.app.json # ts配置
├── tsconfig.json # ts配置
├── tsconfig.node.json # ts配置
└── vite.config.ts # vite配置
# 二、Vite相关配置
以前基于webpack的配置现在全部改为基于Vite的配置,所以很多配置都有所改变,最主要的改变就是环境变量、静态资源、config配置等。
# 1、vite.config.ts
Vue3中关于项目的配置全部写在vite.config.ts中,很多基础配置和以前的vue.config.js差不多,具体API可以参考 配置 Vite (opens new window)。
// vite.config.ts
import path from 'path'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import eslintPlugin from 'vite-plugin-eslint'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
// https://vitejs.dev/config/
export default defineConfig({
// base: '/',
// 本地服务启动配置、跨域配置
// server: {
// host: 'localhost',
// port: 8080,
// proxy: {
// '/api': {
// target: 'http://192.168.100.14:3351',
// ws: false,
// changeOrigin: true
// }
// }
// },
// 插件配置
plugins: [
vue(),
vueJsx(),
// 配置vite在运行的时候自动检测eslint规范
eslintPlugin({
include: ['src/**/*.ts', 'src/**/*.js', 'src/**/*.vue', 'src/*.ts', 'src/*.js', 'src/*.vue']
}),
// 自定义组件名称
VueSetupExtend()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
// less、sass配置
css: {
preprocessorOptions: {
// 配置less全局使用
less: {
modifyVars: {
hack: `true; @import (reference) "${path.resolve('src/assets/less/main.less')}";`,
},
javascriptEnabled: true
}
}
}
})
# 2、环境变量
Vite在环境变量.env等文件里配置变量统一使用 VITE_ 进行开头。
// .env
VITE_ENV=production
VITE_APP_API_BASE_URL=/api
// .env.development
VITE_ENV=development
VITE_APP_API_BASE_URL=http://192.168.100.1
获取时通过一个特殊的 import.meta.env 对象上暴露环境变量。不在是以前的process.env对象。
// 创建 axios 实例
const request: AxiosInstance = axios.create({
// API 请求的默认前缀
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
timeout: 1000 * 60 * 10 // 请求超时时间
})
// 判断环境是开发环境时
if (import.meta.env.VITE_ENV === 'development') {
console.log(1111)
}
在HTML中也能使用环境变量,import.meta.env 中的任何属性都可以通过特殊的 %ENV_NAME% 语法在 HTML 文件中使用。
<h1>当前模式: %VITE_ENV %</h1>
<p>当前接口地址: %VITE_APP_API_BASE_URL%</p>
# 3、静态资源处理
在HTML中引入assets和public是和以前一样的,主要是在js中动态引入方式不同,以前通过requier,现在需要通过 new URL() 方式引入。
<template>
<div v-for="(src, idx) in imageSrc" :key="idx">
<img :src="src" style="width: 100%" />
</div>
</template>
<script setup lang="ts">
const imageSrc = [
new URL('@/assets/images/banner01.png', import.meta.url).href,
new URL('@/assets/images/banner02.png', import.meta.url).href,
new URL('@/assets/images/banner03.png', import.meta.url).href,
new URL('@/assets/images/banner04.png', import.meta.url).href,
new URL('@/assets/images/banner05.png', import.meta.url).href
]
const imgUrl = new URL('./img.png', import.meta.url).href
</script>
<style lang="less" scoped>
</style>
图片地址不能直接支持动态参数,不能整个使用变量替换,仅支持模板字符串写法。
function getImageUrl(name) {
return new URL(`./dir/${name}.png`, import.meta.url).href
}
# 4、使用插件
plugins插件配置就相当于以前webpack的各种loader一样,针对各种文件或者内容进行处理,然后在打包,Vite虽然构建速度快,但是它的插件生态却不如webpack完善。插件在vite.config.ts中进行引入和配置。
import path from 'path'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
// 引入插件
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import eslintPlugin from 'vite-plugin-eslint'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
// https://vitejs.dev/config/
export default defineConfig({
// 插件配置
plugins: [
vue(),
vueJsx(),
// 配置vite在运行的时候自动检测eslint规范
eslintPlugin({
include: ['src/**/*.ts', 'src/**/*.js', 'src/**/*.vue', 'src/*.ts', 'src/*.js', 'src/*.vue']
}),
// 自定义组件名称
VueSetupExtend()
]
})
# 三、TS使用指南
在项目中全部使用ts文件进行开发,不在使用js。开发TS项目时,需要配置TS相关的内容,Vite为我们预先在tsconfig.json文件中配置了,我们只需在其中修改即可。并且Volar插件提供了对TS的代码校验,在开发中需要进行类型校验的都会给与提示和补全,根据这些提示进行修改可以很大程度帮助我们解决报错和标红。
# 1、ts配置文件
tsconfig.json就是ts的配置文件,放在根目录,它指定了用来编译该项目的根文件和编译选项,所有ts相关的配置都放在其中,在Vite中配置文件被拆出tsconfig.app.json和tsconfig.node.json两个文件进行引入,在 tsconfig.app.json中是基础配置。
在tsconfig.app.json配置文件中 include 和 exclude 选项可以指定该项目需要使用ts进行编译的文件和不需要编译的文件,compilerOptions中是其他相关配置。如果在项目中新建的文件无法被ts所识别第一考虑就是此配置中添加include。
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.json", "src/**/*.d.ts", "./.eslintrc.cjs", "./vite.config.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
# 2、声明文件
在开发时,我们可以为变量或者函数定义类型和返回值进行类型约束,定义什么就约束什么,这是针对当前所开发的代码,当我们引入第三方库或者插件,使用其变量、函数或实例化类时ts识别不了。这些插件或库就需要提供一个以 .d.ts 为后缀的声明文件来声明类型编写类型声明,让ts可以识别进行类型检查。
# 1、安装声明文件
一般流行的第三方库都支持ts的类型声明,有的插件会自动安装好对应的声明文件或者直接在安装包里提供声明文件,如果默认没有声明文件,可以去@types organization (opens new window) 上去搜对应的声明文件,搜索加上@types,通过 npm install @types/模块名 -D 去下载其社区为其声明的文件。安装好后在使用插件需要标注类型时,一般都会有相关的类型提示,如果可以就去node_modules里去找,安装的声明文件包在@/node_modules/@types里,如果@/node_modules/@type里没有就直接去对应的包里去找 .d.ts 声明文件。
# 2、自定义声明文件
如果一个插件没有提供声明文件,或者声明文件没有我们需要的类型声明,我们可以根据已有的类型声明导入组装自定义新的声明文件,或者直接为其创建声明文件。自定义的声明文件可以像插件一样发布到npm插件库中去。
添加声明文件或全局公共类型标注文件,一般约定在src目录下创建types文件夹来保存文件,创建好后,在项目根目录下有一个 env.d.ts 文件,这个文件是全局声明文件的入口,可以在此文件中直接引入其他 .d.ts 文件,然后就会全局生效。env.d.ts在tsconfig.json中通过include引入解析。
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="vite/client" />
/// <reference types="./src/types/global.d.ts" />
/// <reference types="./src/types/api.d.ts" />
/**
* 全局声明文件
* 在此自定义一些全局声明或者引入声明文件
* 在此通过reference引入types中的声明文件
* <reference types="./src/types/api.d.ts" />
* */
// store声明
// declare module 'store'
自定义一些常见的声明
// 声明全局...
declare var a:number
declare function fn(params:number):void {}
declare class Vue {}
declare enum Color {}
// 申明外部 npm 插件模块
declare module 'splitpanes'
declare module 'js-cookie'
declare module '@wangeditor/editor-for-vue' {
import { DefineComponent } from 'vue'
const Editor: DefineComponent<{}, {}, any>
const Toolbar: DefineComponent<{}, {}, any>
export { Editor, Toolbar }
}
// 声明一个模块,防止引入文件时报错
declare module '*.json'
declare module '*.png'
declare module '*.jpg'
declare module '*.scss'
declare module '*.ts'
declare module '*.js'
// 声明文件,*.vue 后缀的文件交给 vue 模块来处理
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// 声明文件,定义全局变量
/* eslint-disable */
declare interface Window {
isMobile: boolean
}
# 四、开发指南
如何在vue中写ts、ref/reactive、props、emit如何用ts标注类型、公共组件事件传递、获取this、代码风格指南。
# setup 语法
在 <script setup>
组合式 API 语法糖,使用ts只需要在上面添加 lang="ts" 即可。
<template>
<!-- 启用了类型检查和自动补全 -->
{{ count.toFixed(2) }}
</template>
<script setup lang="ts">
// 启用了 TypeScript
import { ref } from 'vue'
const count = ref(1)
</script>
# ref/reactive类型声明
为ref和reactive添加类型校验,定义基础数据类型时一般会自动推导,可以不用为其标注类型,但定义引用类型时最好是加上类型标注。针对是使用ref还是reactive,网上争论还是很多,因为reactive的局限性比较多,赋值或解构操作都会使其失去响应性,一般我们推荐以ref类型居多,reactive辅助。在定义基础类型和数组我们使用ref来定义,对象的话看情况,如果这个对象会被重新赋值则还是继续使用ref,如果只是维护一些字段就使用reactive,但不要为了方便把所有的数据都塞到一个reactive对象中去,组合式API就是为了拆分细粒度,不同的逻辑放在不同位置,全都塞到一起反而本末倒置,这也是vue官方所不推崇的。
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { Ref } from 'vue'
const title = ref<string>('首页')
const status: Ref<undefined | number> = ref('2020')
// 简写
const title = ref('首页')
const status = ref<undefined | number>(0)
// 不给初始值会自动转为包含 undefined 的联合类型
// 推导得到的类型:Ref<number | undefined>
const status = ref<number>()
// 定义对象
interface StateData {
name: string;
count: number;
type: undefined | '';
date?: string;
[key: string]: any
}
const state = reactive<StateData>({
name: 'xxx',
count: 0,
type: undefined,
data: '2023-01-01',
status: 0
})
// 定义数组
const tableData = ref<StateData[]>([])
tableData.push(state)
</script>
如果定义的数据类型在多个文件中使用,我们可以把它提出来,放在全局公共的types文件中,如果在当前页面多个组件中使用,可以在index.vue目录创建 types.ts 文件来保存公共的数据类型进行导出,就像局部组件和公共组件一样,然后在通过import type 进行引入。
// types.ts
// 筛选条件
export interface FilterData {
name: string
age: number
class: string
type: undefined | ''
date: string
status: undefined | ''
}
// table数据
export interface TableInfo {
key: string
name: string
salary: number
address: string
email: string
type: number | undefined
}
然后在页面中引入
import type { FilterData, TableInfo } from './types'
# 传参
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import type { TableInfo } from '../types'
// 接收参数
const props = withDefaults(
defineProps<{
id?: string
recordData: TableInfo
}>(),
{
recordData: () => ({} as TableInfo)
}
)
</script>
# 事件传递
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import type { TableInfo } from '../types'
// 定义事件
const emit = defineEmits<{
CLOSE_EVENT: [formData?: TableInfo]
}>()
// 弹出层取消操作
const closeDialog = () => {
emit('CLOSE_EVENT')
}
</script>
# 公共组件事件传递
Vue2 通过再创建一个Vue实例当做桥梁来进行组件之间的相互传参,Vue3不再支持此方法,我们通过引入mitt插件来实现兄弟组件之间的事件传递。
安装:
npm install --save mitt
在 src 文件下创建 mitt 文件,然后分别创建index.ts 和 types.ts,types主要用于对提交事件进行类型标注。
// mitt/index.ts
/**
* 配置mitt公共组件传参
* @desc 统一管理全局mitt事件,如不需要可注释
* @desc 定义全局唯一Key,以MITT_开头,以区分其他常量
* @example 调用示例
* global?.$Bus.emit('MITT_GET_USERINFO', { name: 'xxx' })
* global?.$Bus.on('MITT_GET_USERINFO', (params) => { })
* global?.$Bus.off('MITT_GET_USERINFO')
* */
import mitt from 'mitt'
import type { Events } from './types'
// const emitter = mitt()
const emitter = mitt<Events>()
export default emitter
在types中定义事件名称,对所有的事件名进行统一管理。
/**
* mitt传参类型定义
* @desc 统一管理全局mitt事件,并进行类型标注
* @desc 定义全局唯一Key,以MITT_开头,以区分其他常量
* */
interface UserInfo {
name: string
}
export type Events = {
'MITT_GET_USERINFO': UserInfo,
'MITT_GET_NUMBER'?: number
}
在main.ts中引入:
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'
import emitter from '@/mitt'
// TS注册
// 全局属性必须要扩展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
export interface ComponentCustomProperties {
$Bus: typeof emitter
}
}
const app = createApp(App)
app.use(createPinia())
app.use(router)
// 挂载到全局属性上
app.config.globalProperties.$Bus = emitter
app.mount('#app')
组件中使用:
<script setup lang="ts">
// A组件发送事件
import useGlobalProperties from '@/hooks/globalProperties'
const { global } = useGlobalProperties()
const sendEmit = () => {
global?.$Bus.emit('MITT_GET_USERINFO', { name: 'xxx' })
}
</script>
<script setup lang="ts">
// B组件接收事件
import { ref, onMounted, onUnmounted } from 'vue'
import useGlobalProperties from '@/hooks/globalProperties'
const { global } = useGlobalProperties()
// 接收事件
const sendMsg = ref({
name: ''
})
onMounted(() => {
global?.$Bus.on('MITT_GET_USERINFO', (params) => {
sendMsg.value = params
})
})
// 卸载事件
onUnmounted(() => {
global?.$Bus.off('MITT_GET_USERINFO')
})
</script>
# 代码风格指南
<script setup lang="ts" name="Home">
/**
* @desc 注释
* @author xxx
* */
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAppStore } from '@/stores/modules/app'
import useMouse from '@/hooks/mouse'
import useGlobalProperties from '@/hooks/globalProperties'
import type { FormInstance } from '@arco-design/web-vue/es/form'
import type { UserInfo } from '../types'
import ArcoIcon from '@/components/ArcoIcon'
import Ellipsis from '@/components/Ellipsis/index'
const router = useRouter()
const appStore = useAppStore()
const { global } = useGlobalProperties()
// 接收参数
const props = defineProps<{
title: string
likes?: number
}>()
// 定义事件
const emit = defineEmits<{
CLOSE_EVENT: [id?: number, name: string]
}>()
const formRef = ref<FormInstance>()
const title = ref<string>('首页')
const msg = ref<string>('hello world!')
const userInfo = reactive<UserInfo>({
name: 'xxx',
age: 18
})
// 增删改
const updateDialog = reactive({
visible: false,
id: ''
})
const addData = () => {
updateDialog.visible = true
}
const editData = (id: string) => {
updateDialog.visible = true
updateDialog.id = id
}
const delData = (id: string) => {
updateDialog.visible = true
updateDialog.id = id
}
// 其他功能模块
...
// 其他功能模块
...
</script>
# 五、其他项目配置
# 引入Pinia
# Aixos请求封装
# 自定义组件名
Vue3默认是以文件名当作组件的name的,如果我们想自定义组件name,需要使用插件 vite-plugin-vue-setup-extend (opens new window) 来帮助我们完成。
安装:
npm i vite-plugin-vue-setup-extend -D
在vite.config.ts中引入:
import path from 'path'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [
vue(),
vueJsx(),
// 自定义组件名称
VueSetupExtend()
]
})
在组件上定义name:
<template>
</template>
<!-- If you want the include property of keep-alive to take effect, you must name the component -->
<script setup lang="ts" name="Home">
</script>
<style lang="less" scoped>
</style>