是时候学习 vue3 了

适用日期:2021-7

一、使用 vite 新建 Vue3 应用

Vite requires Node.js version >=12.0.0.

创建 vue3 项目

使用 @vue/cli 创建 vue2/3 项目

1
vue create xxxx 

使用 vite 创建 vue3 项目

1
2
3
4
5
6
7
8
9
10
11
##创建工程
npm init vite-app xxxx
yarn create vite-app xxxx
## 进入工程目录
cd xxxx
## 安装依赖
npm install
yarn install
## 运行
npm run dev
yarn dev

进入 vue3 项目

1
cd <项目名称>

基本命令

1
2
3
yarn install //安装依赖
yarn dev //启动
yarn run build //发行

其他问题:

在执行 import App from './App.vue' 时,会提示找不到模块“./App.vue”

解决办法是在项目目录(src 同级)下添加 vue-shims.d.ts 文件,内容如下

1
2
3
4
declare module '*.vue' {
import Vue from 'vue';
export default Vue
}

二、安装 vue-router 和 vuex

1
yarn add vue-router@next vuex@next

在 src 目录下新建 router/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createRouter, createWebHistory, Router, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path:'/',
name:'Hash',
component:()=>import('@view/Login.vue')
}
]
// 路由模式
const router = createRouter({
history: createWebHistory(''),
routes,
scrollBehavior(to, from, savedPosition) {
return {
el: '#app',
top: 0,
behavior: 'smooth',
}
},
})
export default router

在 src 目录下新建 store/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'

export interface State {
count: number
}

export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
state() {
return {
count: 0
}
},
mutations: {
increment(state : State) {
state.count++
}
}
})

三、CSS 预处理器

安装最新版less

1
yarn add less less-loader --dev

兼容 vue2 的版本(vue2 无法使用最新版 less)

1
yarn add less less-loader@5 --save-dev

四、配置路径别名

1
yarn add @types/node --dev

vite.config.ts 配置如下

1
2
3
4
5
6
7
8
9
10
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
...
resolve:{
alias:{
"@":path.resolve(__dirname,"./src")
}
}
})

别名配置后,你会发现在 import .ts 后缀的文件时会有红色波浪线
An import path cannot end with a '.ts' extension.
原来是 import ts 时,需要在 tsconfig.json 配置别名,此处配置的别名需在 vite.config.ts 中有相同别名对应存在,具体配置如下:
tsconfig.json

1
2
3
4
5
6
7
8
"compilerOptions": {
"paths": {
"@/*": ["src/*"],
"@view/*":["src/views/*"],
"@utils/*":["src/utils/*"],
"@api/*":["src/api/*"]
}
}

vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
resolve:{
alias:{
"@": path.resolve(__dirname, "./src"),
"@css": path.resolve(__dirname, "./src/assets/css"),
"@c": path.resolve(__dirname, "./src/components"),
"@img": path.resolve(__dirname, "./src/assets/img"),
"@style" : path.resolve(__dirname, "./src/style"),
"@view": path.resolve(__dirname, "./src/views"),
"@utils":path.resolve(__dirname, "./src/utils"),
"@api":path.resolve(__dirname, "./src/api"),
}
},

五、配置服务器

vite.config.ts 配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default defineConfig({
...
server:{
port:3000,
proxy:{
'/api':{
target:"http://localhost:8080",
changeOrigin:true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
...
})

说明

target 是你需要代理的地址,比如你的请求地址是

http://localhost:8080/product/list

那么 target 里面应该这么写:

target:'http://localhost:8080/'

编写请求的地方:

1
2
3
4
5
6
7
8
import { request } from '@/utils/request'
function getProductList(parameter){
return request({
method:'get',
url:'api/product/list',
parameter:parameter
})
}

上述配置会将你的 url 去掉 api 然后拼接上 target 去请求服务器

六、Vue3 模板

点击 vscode 左下角的齿轮,选择 User Snippets->New Global Snippets file
复制下列模板代码进文件

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
28
29
30
31
32
33
34
35
36
37
38
39
{
"Print to console": {
"prefix": "vue",
"body": [
"<!-- $0 -->",
"<template>",
" <div></div>",
"</template>",
"",
"<script lang='ts'>",
"import { defineComponent, ref, reactive, toRefs, onBeforeMount, onMounted} from 'vue'",
"interface DataProps {}",
"export default {",
" name: '',",
" setup() {",
" console.log('1-开始创建组件-setup')",
" const data: DataProps = reactive({",
"",
" })",
" onBeforeMount(() => {",
" console.log('2.组件挂载页面之前执行----onBeforeMount')",
" })",
" onMounted(() => {",
" console.log('3.-组件挂载到页面之后执行-------onMounted')",
" })",
" const refData = toRefs(data);",
" return {",
" ...refData,",
" }",
"",
" }",
"};",
"</script>",
"<style lang='less' scoped>",
"</style>",
],
"description": "Log output to console"
}
}

七、配置 env

vite 中的 env 配置,文档中有讲,这里从简总结。

首先在项目根目录(src同级)处新建 .env.production.env.development,然后用 key=value 的方式填写你需要使用的环境变量,换行区分。

1
NODE_ENV = development

在两个文件中分别配置 NODE_ENV='development'NODE_ENV='production'。vite 会根据你使用 package.json 中的哪个 script,来确定项目的启动方式 MODE,然后通过 MODE 选择加载对应 .env.[mode] 文件的环境变量。让我们可以通过全局变量 import.meta.env.MODE 获取当前项目的 MODE

我们通常不会把配置变量写在 .env.[mode] 文件中,而是在 src 新建 config 文件夹,里面创建 index.ts 存储变量,后面再使用 import.meta.env.MODE 导出对应的配置变量对象。

八、配置 axios

网传 ts 配置 axios 有两种方式

第一种

建一个 axios.ts 放在 utils 文件夹,在其中创建 axios 实例,配置拦截器,然后直接 export 出一个 axios 实例。这种方式的配置,可以直接在 api.ts 中导入实例直接使用。

第二种

也是建一个 axios.ts 放在 utils 文件夹,不同的是 export 出的并不是一个 axios 实例,而是一个带 init 方法的 axios 自定义类。

这里介绍第一种:
utils 文件夹的 axios.ts

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* @description [ axios 请求封装]
*/
// import store from "@/store";
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
import { reactive } from '@vue/reactivity'
import { ElMessage } from 'element-plus'
// 根据环境不同引入不同api地址
import { config } from "@/config";
const service = axios.create({
baseURL: config.baseApi, // url = base url + request url
timeout: 5000,
withCredentials: false // send cookies when cross-domain requests
// headers: {
// // clear cors
// 'Cache-Control': 'no-cache',
// Pragma: 'no-cache'
// }
})
// Request interceptors
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 加载动画
if (config.loading) {
// Toast.loading({
// message: "加载中...",
// forbidClick: true
// });
}
// 在此处添加请求头等,如添加 token
// if (store.state.token) {
// config.headers['Authorization'] = `Bearer ${store.state.token}`
// }
return config;
},
(error: any) => {
Promise.reject(error);
}
)
// Response interceptors
service.interceptors.response.use(
async (response: AxiosResponse) => {
// await new Promise(resovle => setTimeout(resovle, 3000))
// Toast.clear();
const res = response.data;
if (res.code !== 0) {
// token 过期
if (res.code === 401){
ElMessage(res.message)
// 警告提示窗
return;
}
if (res.code == 403) {
ElMessage(res.message)
return
}
// 若后台返回错误值,此处返回对应错误对象,下面 error 就会接收
return Promise.reject(new Error(res.msg || "Error"))
}
// 注意返回值
else return response.data
},
(error: any) => {
// Toast.clear();
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = "请求错误(400)"
break
case 401:
error.message = "未授权,请登录(401)"
break
case 403:
error.message = "拒绝访问(403)"
break
case 404:
error.message = `请求地址出错: ${error.response.config.url}`
break
case 405:
error.message = "请求方法未允许(405)"
break
case 408:
error.message = "请求超时(408)"
break
case 500:
error.message = "服务器内部错误(500)"
break
case 501:
error.message = "服务未实现(501)"
break
case 502:
error.message = "网络错误(502)"
break
case 503:
error.message = "服务不可用(503)"
break
case 504:
error.message = "网络超时(504)"
break
case 505:
error.message = "HTTP版本不受支持(505)"
break
default:
error.message = `连接错误: ${error.message}`
}
} else {
if (error.message == "Network Error") {
error.message = "网络异常,请检查后重试!连接到服务器失败,请联系管理员"
}
}
ElMessage(error.message)
// store.auth.clearAuth()
// store.dispatch("clearAuth")
return Promise.reject(error)
}
)

function request(config:AxiosRequestConfig){
let result = reactive({})
service(config).then(response=>{
Object.assign(result,response.data)
}).catch(e=>{
console.error(e)
})
return result
}

export { service, request }

在 api 文件中使用配置好的 axios
api 文件夹的 article.ts

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
import { request } from '@/utils/axios';

export function article (parameter: any) {
return request({
url:'/api/article',
method:'get',
params:parameter
})
}

export function author (parameter: any) {
return request({
url:'/api/article/author',
method:'get',
params:parameter
})
}

export function commentList (parameter: any) {
return request({
url:'/api/article/commentList',
method:'get',
params:parameter
})
}

这里还导出了一个 request 函数,因为 vue3 有 reactive,即使异步请求之前返回了空的 result,后续回调依然可以给这个已经返回的 result 赋值。这样我们就可以在 component 中这样获取值。

1
2
3
4
5
6
7
8
9
setup(){
const data = reactive({
article:article({}),
author:author({}),
commentList:commentList({}),
})
const refData = toRefs(data);
return { ...refData }
}

九、配置 mock

1
2
yarn add mockjs
yarn add vite-plugin-mock -D

新建 mock 目录(src同级),在 mock 目录中新建 test.ts 作为 mock 数据文件
mock/test.ts

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
28
29
30
31
32
33
34
35
36
37
38
import { MockMethod } from 'vite-plugin-mock'
export default [
{
url:'/api/article',
method:'get',
response:() =>{
return {
code:0,
data:{
id:'1',
category:'人文',
body:'halo',
recommendCount:(Math.random()*100).toFixed(0),
againstCount:(Math.random()*100).toFixed(0),
pre:'NOR Flash驱动程序',
next:'移植DM900C网卡驱动',
commentCount:(Math.random()*100).toFixed(0),
visitCount:(Math.random()*100).toFixed(0),
}
}
}
},
{
url:'/api/article/author',
method:'get',
response:() =>{
return{
code:0,
data:{
id:'1',
name:'quinoa',
followCount:(Math.random()*100).toFixed(0),
fansCount:(Math.random()*100).toFixed(0),
}
}
}
}
] as MockMethod[]

utils 目录新建 mockProdServer.ts 文件

1
2
3
4
5
6
7
8
9
10
//  mockProdServer.ts
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
// 逐一导入您的mock.ts文件
// 如果使用vite.mock.config.ts,只需直接导入文件
// 可以使用 import.meta.glob功能来进行全部导入
import testModule from 'mock/test';

export function setupProdMockServer() {
createProdMockServer([...testModule]);
}

vite.config.ts 中配置 vite-plugin-mock
vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plugins: [
vue(),
viteMockServe({
mockPath:'./mock',
supportTs:true,
watchFiles:true,
localEnabled:true,
prodEnabled:command !=='serve' && prodMock,
// 这样可以控制关闭mock的时候不让mock打包到最终代码
injectCode:`
import { setupProdMockServer } from '@utils/mockProdServer';
setupProdMockServer();
`
})
]

在 vite.config.ts 中关闭代理服务器,就可以使用 ts 文件模拟的 mock 数据了。

需要注意的是如果启动项目时出错

1
2
3
9:30:31 ├F10: AM┤ [vite:mock] mock reload error Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
events.js:292
throw er; // Unhandled 'error' event

这是目前可能会遇到的 esbuild 的 bug(2021-4至2021-7),在当前 blog 项目中使用如下命令可解决

1
node ./node_modules/esbuild/install.js