Vue3加TS的最佳实践

本文最后更新于:2 个月前

总在盼望,总在失望,日子还不都这样。代码也需要雅俗共赏,博客亦如此。

鄙人不善前端,但是又会一点,近日后端写的乏味,便拎起前端端详端详,一日不见如隔三秋的Vue,让我感觉到生疏,Vue3的语法奇奇怪怪,官网上的写法,我是并不能很理解,或者说就是不易于后端程序员理解,加上Ts这个JS的类型系统语言,让我深深的感觉到,眉眼不如初,让我怎敢相认。

但是光说不练,是很难体会到其中的玄妙的,我便开始了时隔好多月的Vue3重逢之旅。

打开VsCode,打开终端,输入vue create vue3_demo。一顿框选,主要记得要选择Vue3以及TS的支持。之后,看着进度条,不停在变动,嘴角疯狂上扬,邪魅一笑,还是内味。

进度条加载完毕,VsCode,文件-> 将文件夹添加到工作区,打开终端,yarn run serve,回车,嘴角继续疯狂上扬,

绿色的美好提示Compiled successfully,项目成功启动,复制url, 浏览器打开,回来了,他们都回来了。

娴熟的打开Home.vue让我看看,这几月没见,变成什么样了。

1
2
3
import { Vue } from 'vue-class-component'

export default class Home extends Vue {}

看到了这两行代码,让我陷入了深思,嗯,这啥玩意,最佳实践嘛,本着实践的态度,试试基本的文档中的新语法,不过有一说一我甚至不知道,新语法的代码我甚至不知道往哪里写,这tm不是欺负老实人嘛。程序员嘛,喜欢学习,搜索vue-class-component,原来是以类的方式写代码啊,Over,Java入门的我写起TS到也不觉得生疏,语法虽有区别,但理念都是一样的。声明变量时需要声明属性,定义方法时需要规定入参格式和返回值格式。况且Ts更多的时候只是一种约束,并不是一种规则。这句话很重要,还记得SpringBoot的一个经典理论,约束大于规则。编程的哲学是通用的。

大致知道了怎么写之后,容我新起一行,坑就如约而至,父子组件传值的时候,我不知道props在什么地方写,还有监听属性等等。这些问题待我一一解答

最佳实践

创建项目

Vue3加Ts必须,VueRouter和Vuex,一般来说项目中都会用到,就都加上。

我建议不启用代码质量检查和ESlint,原因,Vue3和ts能很好的兼容,在vue-class-component的加持下,实际项目中可能百分之九十九的代码都是使用TS编写,Ts天生就有静态检查,再使用EsLint,在开发中反倒是有点徒增功耗。

在Vue的可视化项目创建平台中,勾选这几项就行。如图所示,我并没有打算写保姆教程。
创建项目

路由模式,主要分为两种一种是带#的还有一种就是不带的,看个人喜好,我不讨厌这个#。默认就是带的。css预处理器编译,一般都是选用node-sass。就选这个就行了。

第三方库和CSS组件

第三方组件:element-plus这个组件库在写后台管理系统的时候比较好用,更多的时候是提供一个组件,并不能用来写样式。而且我是打算写一个差不多的前台内容系统,考虑到暗黑模式的兼容,我并不能直接开箱即用里面的组件。

CSS框架:Windi CSS,被称作为下一代的CSS框架,天然支持暗黑模式,弥补上面组件库样式的不足,而且写起来很舒服。

Jquery:JQ这个会引起一点争议,不要提到Jquery就是什么过时啦,都是上个世纪的东西了。Jq一直在更新,Jq适配了Ts, Jq的兼容性很好,JQuery和Vue能一起使用。虽说感觉会很low,但是能抓到老鼠的猫就是好猫,主要原因还是,鄙人不善前端!!。

为了实现一个多行文本输入高度自适应,嗯,就是很尴尬的那种。挺难的,Element实现方式的源码我看了,有点看不懂,直接用Jquery能实现,但是不完美,就是为了追求我认为的完美,挺难的。

Axios:用来通信,性能优异,好处多多。

安装

packegt.json

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

{
"name": "ql_blog",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"@element-plus/icons": "^0.0.11",
"@types/jquery": "^3.5.6",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"element-plus": "^1.1.0-beta.8",
"jquery": "^3.6.0",
"vue": "^3.0.0",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"node-sass": "^4.12.0",
"sass-loader": "^8.0.2",
"typescript": "~4.1.5",
"vue-cli-plugin-windicss": "~1.0.4"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

引入Jquery

项目根目录中有的话添加里面的内容即可,没有的话直接新建即可。

vue.config.js

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
var webpack = require('webpack')

// 这里只列一部分,具体配置参考文档
module.exports = {
// 开发服务器代理
devServer: {
proxy: {
'^/api': {
target: 'http://localhost:8001',
ws: true,
changeOrigin: true
}
}
},
configureWebpack: {
plugins: [
// 引入Jquery
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'windows.jQuery': 'jquery'
})
]
}
}

axios简单的封装

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
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import qs from 'qs'
import { ElMessage } from 'element-plus'

let axiosInstance: AxiosInstance = axios.create({
baseURL: '/api',
// 延迟
timeout: 3000,
});

// axios实例拦截请求
axiosInstance.interceptors.request.use(
(config: AxiosRequestConfig) => {
console.log(config);
removePending(config) // 在请求开始前,对之前的请求做检查取消操作
addPending(config) // 将当前请求添加到 pending 中
config.headers.sa_token = 'ken'
return config;
},
(error: any) => {
console.log('请求错误拦截' + error);

return Promise.reject(error);
}
)

// axios实例拦截响应
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
removePending(response)
return response;
},
// 请求失败
(error: any) => {
if (axios.isCancel(error)) {
console.log('repeated request: ' + error.message)
} else {
// handle error code
// 错误抛到业务代码
error.data = {}
error.data.msg = '请求超时或服务器异常,请检查网络或联系管理员!'
ElMessage.error(error.data.msg)
}
return Promise.reject(error)
}
);

// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map()
/**
* 添加请求
* @param {Object} config
*/
const addPending = (config: AxiosRequestConfig) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&')
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
pending.set(url, cancel)
}
})
}
/**
* 移除请求
* @param {Object} config
*/
const removePending = (config: AxiosRequestConfig) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&')
if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pending.get(url)
cancel(url)
pending.delete(url)
}
}

/**
* 清空 pending 中的请求(在路由跳转时调用)
*/
export const clearPending = () => {
for (const [url, cancel] of pending) {
cancel(url)
}
pending.clear()
}

export default axiosInstance

路由的简单配置

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
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home'
},
{
path: '/home/:type',
name: 'Home',
component: () => import('@/views/Home.vue'),
alias: ['/home'],
props: true
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('@/views/About.vue')
}
]

const router = createRouter({
history: createWebHashHistory(),
routes
})

// 全局前置拦截
router.beforeEach((to, from, next) => {
console.log('全局前置拦截')
console.log(to)
console.log(from)

next()
})
export default router

全局的状态管理

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
import { createStore, Store } from 'vuex'

declare module '@vue/runtime-core' {
interface Photo {
src: string;
}
// declare your own store states
interface State {
dark: boolean,
count: number,
user: {
name: string,
age: number,
photoList: Array<Photo>
}
}

// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
}
}
export default createStore({
state: {
dark: localStorage.getItem('dackModel') === undefined ? false : JSON.parse(localStorage.getItem('dackModel') + ''),
count: 112,
user: {
name: '',
age: 0,
photoList: [{
src: 'test'
}]
}
},
getters: {

},
mutations: {
switchDarkMode(state) {
state.dark = !state.dark
if (state.dark) {
$('html').addClass('dark')
localStorage.setItem('dackModel', JSON.stringify(true))
} else {
$('html').removeClass('dark')
localStorage.setItem('dackModel', JSON.stringify(false))
}
}
},
actions: {
},
modules: {
}
})

项目的main.ts

1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
import 'windi.css'
import store from './store'

createApp(App).use(store).use(router).use(ElementPlus).mount('#app')

少女马尾辫 夜晚 电脑 听音乐


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!