vue

vue实战

Posted by アライさん on 2022年09月23日

一、基础结构

vue组件基础结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<!-- HTML 代码 -->
</template>

<script>
// JavaScript 代码
</script>

<style scoped>
/* CSS 代码 */
</style>
```

## 使用typescript
```shell
npm install -D typescript ts-node
```
```json
"scripts": {
"dev:ts": "ts-node src/ts/index.ts",
"build": "tsc src/ts/index.ts --outDir dist --target es6"
},

dev:ts 代表用了 ts-node 来代替原来在用的 node ,因为使用 node 无法识别 TypeScript 语言。
typescript 这个包是用 TypeScript 编程的语言依赖包
ts-node 是让 Node 可以运行 TypeScript 的执行环境

tsc src/ts/index.ts –outDir dist –target es6
代表打包成js(比如html中使用),输出到src的同级目录dist中, –target es6可以省略,代表js规范。
执行npm run build,就可以在dist中看到相应的js文件。

使用npm run dev:ts,可以测试typescript的可运行性。

执行npm run build,会将代码打包到dist目录,之后怎么修改,dist里的代码都不变,直到再次build。

在 Webpack ,可以使用 process.env.NODE_ENV 来区分开发环境( development )还是生产环境( production )。
在 Vite ,还可以通过判断 import.meta.env.DEV 为 true 时是开发环境,判断 import.meta.env.PROD 为 true 时是生产环境。

注意事项:

  • 在.gitignore 文件里添加 node_modules 忽略。

文件

  • vite.config.ts脚手架配置文件
  • .editorconfig文件,代码风格,比如缩进和空格。vsCode需要安装EditorConfig扩展。
  • .prettierrc格式化代码。”semi”:true代表结尾需要分号。vsCode需要安装Prettier扩展。
  • .eslintrc.js ESLint代码检查工具。
    需要在开发依赖中安装相关依赖。





二、创建项目

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
npm init vue@3

//或者用Awesome Starter的脚手架创建模版项目
npm create preset
```

<br>
<br>
<br>
<br>
<br>

# 三、setup
setup在props解析之后,创建组件之前执行。
是组合式API的入口,业务代码可以直接放在里面。

**使用setup的时候,无法使用this来获取Vue实例。**
```typescript
<template>
<p class="msg">{{ msg }}</p>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
const msg = 'Hello World!'
//context暴露三个属性,attrs、slots、emit
setup(props, context) {
// 业务代码写这里...
onBeforeMount(() => {
//一些声明周期触发
})

return {
// 需要给 template 用的数据、函数放这里 return
// 只在函数中调用,不需要rerurn
msg,
}
},
})
</script>

<style scoped>
.msg {
font-size: 14px;
}
</style>





四、生命周期

  • setup 组件创建前执行
  • setup 组件创建后执行
  • onBeforeMount 组件挂载到节点上之前执行
  • onMounted 组件挂载完成后执行
  • onBeforeUpdate 组件更新之前执行
  • onUpdated 组件更新完成之后执行
  • onBeforeUnmount 组件卸载之前执行
  • onUnmounted 组件卸载完成后执行
  • onErrorCaptured 当捕获一个来自子孙组件的异常时激活钩子函数

被包含在<keep-alive>中的组件,会多出2个生命周期钩子:

  • onActivated 被激活时执行
  • onDeactivated 切换组件后,原组件消失前执行





五、ref与reactive的使用

读取任何 ref 对象的值都必须通过 xxx.value。
ref是一个对象,所以const定义后,依旧可以修改value。

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
const msg = ref<string>('Hello World!');
const uids = ref<number[]>([ 1, 2, 3 ]);

// 声明对象的格式
interface Member {
id: number,
name: string
};

// 定义一个成员对象
const userInfo = ref<Member>({
id: 1,
name: 'Tom'
});


<template>
<!-- 挂载DOM元素 -->
<p ref="msg">
留意该节点,有一个ref属性
</p>
<!-- 挂载DOM元素 -->

<!-- 挂载子组件 -->
<Child ref="child" />
<!-- 挂载子组件 -->
</template>
```

<br>

**reactive只用于对象、数组。**
```typescript
// 定义一个成员对象
const userInfo: Member = reactive({
id: 1,
name: 'Tom'
});
// 定义一个成员对象数组
const userList: Member[] = reactive([
{
id: 1,
name: 'Tom'
},
{
id: 2,
name: 'Petter'
},
{
id: 3,
name: 'Andy'
}
]);
```

例子:
```typescript
import { defineComponent, reactive, toRefs } from 'vue'

interface Member {
id: number,
name: string,
age: number,
gender: string
};

export default defineComponent({
setup () {
// 定义一个reactive对象
const userInfo = reactive({
id: 1,
name: 'Petter',
age: 18,
gender: 'male'
})

// 定义一个新的对象,它本身不具备响应性,但是它的字段全部是ref变量
const userInfoRefs = toRefs(userInfo);

// 2s后更新userInfo
setTimeout( () => {
userInfo.id = 2;
userInfo.name = 'Tom';
userInfo.age = 20;
}, 2000);

// 在这里结构toRefs对象才能继续保持响应式,使用时直接使用{{id}}、{{name}}
return {
...userInfoRefs
}
}
})
```

<br>
<br>
<br>
<br>
<br>

# 六、watch
```typescript
import { defineComponent, reactive, watch } from 'vue'

export default defineComponent({
setup() {
// 定义一个响应式数据
const userInfo = reactive({
name: 'Petter',
age: 18,
})

// 2s后改变数据
setTimeout(() => {
userInfo.name = 'Tom'
}, 2000)

watch(userInfo, () => {
console.log('监听整个 userInfo ', userInfo.name)
})

/**
* 也可以监听对象里面的某个值
* 此时数据源需要写成 getter 函数
*/
watch(
// 数据源,getter 形式
() => userInfo.name,
// 回调函数 callback
(newValue, oldValue) => {
console.log('只监听 name 的变化 ', userInfo.name)
console.log('打印变化前后的值', { oldValue, newValue })
}
)


const handleWatch = (
newValue: string | number,
oldValue: string | number
): void => {
console.log({ newValue, oldValue })
}

// 然后定义多个监听操作,传入这个公共函数
watch(userInfo, handleWatch)
// watch(index, handleWatch)

},
})
```

watchEffect:
```typescript
export default defineComponent({
setup() {
const foo = ref<string>('')

setTimeout(() => {
foo.value = 'Hello World!'
}, 2000)

function bar() {
console.log(foo.value)
}

// 可以通过 watchEffect 实现 bar() + watch(foo, bar) 的效果
watchEffect(bar)
},
})





七、computed

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
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
setup() {
// 定义基本的数据
const firstName = ref<string>('Bill')
const lastName = ref<string>('Gates')

// 定义需要计算拼接结果的数据
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

//通过实现set和get,来实现对computed的value的修改
const foo = computed({
// 这里需要明确的返回一个值
get() {
// ...
},
// 这里接收一个参数,代表修改 foo 时,赋值下来的新值
set(newValue) {
// ...
},
})

// 2s 后改变某个数据的值
setTimeout(() => {
firstName.value = 'Petter'
}, 2000)

// template 那边在 2s 后也会显示为 Petter Gates
return {
fullName,
}
},
})
```
**computed的优势是会缓存数据,不用每次都执行一遍操作。**
**computed的变量获取依旧要通过.value,并且是只读。**

<br>
<br>
<br>
<br>
<br>

# 八、动态修改style
## 使用:class
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
## 使用 :style 动态修改内联样式  
```html
<template>
<p
:style="[style1, style2]"
>
Hello World!
</p>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
setup () {
const style1 = {
fontSize: '13px',
'line-height': 2,
}
const style2 = {
color: '#ff0000',
textAlign: 'center',
}

return {
style1,
style2,
}
}
})
</script>

使用v-bind

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
<template>
<p class="msg">Hello World!</p>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup () {
const fontColor = ref<string>('#ff0000')

return {
fontColor,
}
}
})
</script>

<style scoped>
.msg {
color: v-bind(fontColor);
}
</style>
```

<br>
<br>
<br>
<br>
<br>

# 九、路由

```html
<template>
<!-- 登录 -->
<Login v-if="route.name === 'login'" />

<!-- 注册 -->
<Register v-else-if="route.name === 'register'" />

<!-- 带有侧边栏的其他路由 -->
<div v-else>
<!-- 固定在左侧的侧边栏 -->
<Sidebar />

<!-- 路由 -->
<router-view />
</div>
</template>
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 { useRouter } from 'vue-router'
const router = useRouter();

// 跳转首页
router.push({
name: 'home',
params: {
id: 123
}
})
//等价于,router-link默认是a标签,可以手动指定tag
// <router-link
// tag="span"
// class="link"
// :to="{
// name: 'article',
// params: {
// id: 123
// }
// }"
// >
//</router-link

// 返回上一页
router.back();
```

配置404页面
```typescript
const routes: Array<RouteRecordRaw> = [
{
path: '/:pathMatch(.*)*',
name: '404',
component: () => import(/* webpackChunkName: "404" */ '@views/404.vue')
}
]
```

路由守卫

import { createRouter } from ‘vue-router’

// 创建路由
const router = createRouter({ … })

// 在路由跳转前触发
router.beforeEach((to, from) => {
// …
})

// 在导航被确认前,同时在组件内守卫和异步路由组件被解析后
router.beforeResolve(async to => {
// 如果路由配置了必须调用相机权限
if ( to.meta.requiresCamera ) {
// 正常流程,咨询是否允许使用照相机
try {
await askForCameraPermission()
}
// 容错
catch (error) {
if ( error instanceof NotAllowedError ) {
// … 处理错误,然后取消导航
return false
} else {
// 如果出现意外,则取消导航并抛出错误
throw error
}
}
}
})

//跳转完成后
router.afterEach( (to, from) => {
// 上报流量的操作
// …
})

// 暴露出去
export default router

1
2
  
单独的路由跳转守卫,在跳转之前

{
path: ‘/home’,
name: ‘home’,
component: () => import(/* webpackChunkName: “home” */ ‘@views/home.vue’),
// 在这里添加单独的路由守卫
beforeEnter: (to, from) => {
document.title = ‘程沛权 - 养了三只猫’;
}
}

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
  
<br>
<br>
<br>
<br>
<br>

# 十、组件通信
## 1、通过Props
```html
<template>
<Child
title="用户信息"
:index="1"
:uid="userInfo.id"
:user-name="userInfo.name"
/>
</template>
```
接收
```typescript
export default defineComponent({
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
index: Number,
userName: String,
//两种类型之一
uid: 【Number,String]
}
})
```

## 2、通过emits
```html
<template>
<Child
@update-age="updateAge"
/>
</template>
<script>
import { defineComponent, reactive } from 'vue'
import Child from '@cp/Child.vue'

interface Member {
id: number,
name: string,
age: number
};

export default defineComponent({
components: {
Child
},
setup () {
const userInfo: Member = reactive({
id: 1,
name: 'Petter',
age: 0
})

// 定义一个更新年龄的方法
const updateAge = (age: number): void => {
userInfo.age = age;
}

return {
userInfo,

// return给template用
updateAge
}
}
})
</script>
```
子元素调用
```typescript
export default defineComponent({
emits: [
'update-age'
],
setup (props, { emit }) {

// 2s 后更新年龄
setTimeout( () => {
emit('update-age', 22);
}, 2000);

}
})
```

## 3、eventbus
```shell
npm install --save mitt
```
在libs文件夹下建立bus.ts
```typescript
import mitt from 'mitt';
export default mitt();
```
启用接收eventbus
```typescript
import { defineComponent, onBeforeUnmount } from 'vue'
import bus from '@libs/bus'

export default defineComponent({
setup () {
// 定义一个打招呼的方法
const sayHi = (msg: string = 'Hello World!'): void => {
console.log(msg);
}

// 启用监听
bus.on('sayHi', sayHi);

// 在组件卸载之前移除监听
onBeforeUnmount( () => {
bus.off('sayHi', sayHi);
})
}
})
```
发送eventbus
```typescript
import { defineComponent } from 'vue'
import bus from '@libs/bus'

export default defineComponent({
setup () {
// 调用打招呼事件,传入消息内容
bus.emit('sayHi', '哈哈哈哈哈哈哈哈哈哈哈哈哈哈');
}
})
```
<br>
<br>
<br>
<br>
<br>

# 十一、Vuex
src/store/index

```typescript
import { createStore } from 'vuex'

export default createStore({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
```

<br>
<br>
<br>
<br>
<br>

# 十一、Pinia
```shell
npm install pinia
```
**scr/main.ts**中
```typescript
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 Pinia
import App from '@/App.vue'
createApp(App)
.use(createPinia()) // 启用 Pinia
.mount('#app')
```

**Pinia的核心也是store**
基本定义:
```typescript
// src/stores/index.ts
import { defineStore } from 'pinia'
//如果有多个store,可以分模块管理。useMessageStore、useUserStore等
export const useStore = defineStore('main', {
state: () => {
return {
message: 'Hello World',
//通过as指定类型。等同<string[]>[]
randomMessages: [] as string[],
}
},

actions: {

// 异步更新 message
//调用store.updateMessage('New message by async.').then((res){})
async updateMessage(newMessage: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
// 这里的 this 是当前的 Store 实例
this.message = newMessage
resolve('Async done.')
}, 3000)
})
},
// 同步更新 message
updateMessageSync(newMessage: string): string {
// 这里的 this 是当前的 Store 实例
this.message = newMessage
return 'Sync done.'
},
},

// 定义一个 fullMessage 的计算数据
getters: {
fullMessage: (state) => `The message is "${state.message}".`,
// 这个 getter 返回了另外一个 getter 的结果
emojiMessage(): string {
return `🎉🎉🎉 ${this.fullMessage}`
},
// 定义一个接收入参的函数作为返回值。这种get没有相应性
//调用const signedMessage = store.signedMessage('Petter')
signedMessage: (state) => {
return (name: string) => `${name} say: "The message is ${state.message}".`
},
},
})
```
基本使用:
```typescript
import { defineComponent } from 'vue'
import { useStore } from '@/stores'

export default defineComponent({
setup() {
// 像 useRouter 那样定义一个变量拿到实例
const store = useStore()

// 通过计算拿到里面的数据
// 直接通过 message.value = 'New Message.';就可以更新
const message = computed({
// getter 还是返回数据的值
get: () => store.message,
// 配置 setter 来定义赋值后的行为
set(newVal) {
store.message = newVal
},
})

// 传给 template 使用
return {
message,
}
},
})
```
<br>
<br>

也提供```storeToRefs```把state 的数据转换为 ref 变量。
它会忽略掉 Store 上面的方法和非响应性的数据,只返回 state 上的响应性数据。
```typescript
import { defineComponent } from 'vue'
import { useStore } from '@/stores'

// 记得导入这个 API
import { storeToRefs } from 'pinia'

export default defineComponent({
setup() {
const store = useStore()

// 通过 storeToRefs 来拿到响应性的 message
const { message } = storeToRefs(store)
//const { message } = toRefs(store) //跟 storeToRefs 操作都一样,只不过用 Vue 的这个 API 来处理



console.log('message', message.value)

//要用时直接赋值即可
//message.value = 'New Message.'

return {
message,
}
},
})

批量修改
patch为增量修改。修改的内容会补充到state中

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
//传入对象修改,替换掉state中的message和randomMessages,其他不变
store.$patch({
message: 'New Message',
randomMessages: ['msg1', 'msg2', 'msg3'],
})

//或者传入函数修改
store.$patch((state) => {
state.message = 'New Message'
// 数组改成用追加的方式,而不是重新赋值
for (let i = 0; i < 3; i++) {
state.randomMessages.push(`msg${i + 1}`)
}
})



//重新覆盖所有state的方式修改
store.$state = {
message: 'New Message',
randomMessages: ['msg1', 'msg2', 'msg3'],
}


//直接重置的方式修改
store.$reset()





//监听store的变化,进行回调。会在组件销毁时销毁
store.$subscribe((mutation, state) => {
})





//监听store的变化,手动销毁
// 定义一个退订变量,它是一个函数
const unsubscribe = store.$subscribe((mutation, state) => {
// ...
}, { detached: true })

// 在合适的时期调用它,可以取消这个订阅
unsubscribe()
```
<br>
<br>
<br>

## 多store

### 1、在scr/stores目录下建立多个文件,每个store一个。

src
└─stores
│ # 入口文件
├─index.ts
│ # 多个 store
├─user.ts
├─game.ts
└─news.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
### 2、每个store文件中,导出use方法。  
```typescript
//每个store的id必须唯一
export const useUserStore = defineStore('user', {
})
```
### 3、在index.ts里统一输出。
```typescript
export * from './user'
export * from './game'
export * from './news'
```
### 4、使用时只需要从stores导入。
```typescript
import { useUserStore, useGameStore } from '@/stores'
export default defineComponent({
setup() {
// 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到)
const userStore = useUserStore()
const { userId, userName } = storeToRefs(userStore)

// 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表
const gameStore = useGameStore()
const gameList = ref<GameItem[]>([])
onMounted(async () => {
gameList.value = await gameStore.queryGameList(userId.value)
})

return {
userId,
userName,
gameList,
}
},
})
```
<br>
<br>
<br>

## store之间相互调用
```typescript
import { defineStore } from 'pinia'

// 导入用户信息的 Store 并启用它
import { useUserStore } from './user'
const userStore = useUserStore()

export const useMessageStore = defineStore('message', {
state: () => ({
message: 'Hello World',
}),
getters: {
// 这里我们就可以直接引用 userStore 上面的数据了
greeting: () => `Welcome, ${userStore.userName}!`,
},
})
```

<br>
<br>
<br>
<br>
<br>

# 十二、补遗

使用了script-setup
```typescript
<!-- 使用 script-setup 格式 -->
<template>
<Child />
</template>

<script setup lang="ts">
//无需再defineComponent和return,
import Child from '@cp/Child.vue'

defineProps({
name: {
type: String,
required: false,
default: 'Petter'
},
userInfo: Object,
tags: Array
});

const msg: string = 'Hello World!';

// 获取 emit
const emit = defineEmits(['chang-name']);

// 调用 emit
emit('chang-name', 'Tom');

const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>