危险

为之则易,不为则难

0%

02_Vue3 周边

今日目标

✔ 能够掌握 Vue Router4。

✔ 能够掌握 Vuex4 和 Pinia。

Vue Router4

Vue 升级 3.x 之后,配套的 Vue Router 也升级为 4.x 的版本,Vue Router4 的语法和 3 的版本基本一致,但是有一些细微的修改。

1
2
# vue@2 + vue-router@3 + vuex@3    options api
# vue@3 + vue-router@4 + vuex@4 composition api

基本使用

  1. 创建项目。
1
npm create vite
  1. 安装 vue-router
1
2
# 如果安装时出现错误,请先把当前正在运行的项目停止后(ctrl + c)再安装
yarn add vue-router
  1. 配置路由并导出路由实例,router/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'

// 创建路由实例
const router = createRouter({
// 创建history模式的路由
// history: createWebHistory(),
// 创建hash模式的路由
history: createWebHashHistory(),
// 配置路由规则
routes: [
{ path: '/home', component: () => import('../pages/Home.vue') },
{ path: '/login', component: () => import('../pages/Login.vue') },
],
})

export default router
  1. 引入 router 实例,main.js
1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')
  1. 组件中指定路由入口和出口,App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<ul>
<li>
<router-link to="/home">首页</router-link>
</li>
<li>
<router-link to="/login">登陆</router-link>
</li>
</ul>

<!-- 路由出口 -->
<router-view></router-view>
</template>

route 与 router

🥱 由于组件中无法访问 this,因为无法使用 this.$route 与 this.$router。

  1. 通过 useRoute() 可以获取 route 信息,Home.vue
1
2
3
4
5
6
7
8
9
10
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
console.log(route.path)
console.log(route.fullPath)
</script>
<template>
<div>Home</div>
</template>
  1. 通过 useRouter() 可以获取 router 信息,Login.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

const handleClick = () => {
router.push('/home')
}
</script>
<template>
<div>
<h3>Login</h3>
<button @click="handleClick">Go Home</button>
</div>
</template>

Vuex4

基本使用

  1. 安装依赖包,yarn add vuex

  2. 创建 Vuex 实例并导出,store/index.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
import { createStore } from 'vuex'

const store = createStore({
state: () => ({
money: 100,
}),
mutations: {
changeMoney(state, money) {
state.money += money
},
},
actions: {
changeMoneyAsync(context, money) {
setTimeout(() => {
context.commit('changeMoney', money)
}, 1000)
},
},
getters: {
double(state) {
return state.money * 2
},
},
})

export default store
  1. 关联 store,main.js
1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
const app = createApp(App)

app.use(router)
app.use(store)
app.mount('#app')
  1. 在组件中使用 Vuex,Home.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { computed } from '@vue/reactivity'
import { useStore } from 'vuex'

const store = useStore()
const money = computed(() => store.state.money)
const double = computed(() => store.getters.double)
</script>
<template>
<p>money: {{ money }}</p>
<p>double: {{ double }}</p>
<button @click="store.commit('changeMoney', 1)">change money</button>
<button @click="$store.dispatch('changeMoneyAsync', 2)">change money async</button>
</template>

🤔 mapState、mapMutations、mapActions、mapGetters 等辅助方法需要配合 Options API 才能使用,可见 Vuex4 在 Vue3 项目中并不好用。

Pinia

image-20220213181825857

基本介绍

Pinia 是应用与 Vue.js 的轻量级状态管理库。

为什么学习 Pinia?

  • Pinia 和 Vuex4 一样,也是 Vue 官方的状态管理工具(作者是 Vue 核心团队成员)。

  • Pinia 相比 Vuex4,对于 Vue3 的兼容性更好。

  • Pinia 相比 Vuex4,具备完善的类型推导。

  • Pinia 同样支持 Vue 开发者工具,最新的开发者工具对 Vuex4 支持不好。

  • Pinia 的 API 设计非常接近 Vuex5 的提案

Pinia 核心概念?

  • state: 状态。

  • actions: 修改状态(包括同步和异步,Pinia 中没有 mutations)。

  • getters: 计算属性。

基本使用与 state

目标:掌握 Pinia 的使用步骤。

  1. 安装 pinia。
1
2
3
yarn add pinia
# or
npm i pinia
  1. 挂载 Pinia,main.js
1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'
import App from './App.vue'

// #1
import { createPinia } from 'pinia'
// #2
const pinia = createPinia()

// #3
createApp(App).use(pinia).mount('#app')
  1. 创建模块,store/counter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineStore } from 'pinia'
// 创建 store,命名规则:useXxxxStore
// 参数 1:store 的唯一表示
// 参数 2:对象,可以提供 state actions getters
const useCounterStore = defineStore('counter', {
// 推荐函数:避免服务端渲染导致的数据状态污染
// 箭头函数:为了更好的 TS 类型推导
state: () => {
return {
count: 0,
}
},
getters: {},
actions: {},
})

export default useCounterStore
  1. 在组件中使用,App.vue
1
2
3
4
5
6
7
<script setup>
import useCounterStore from './store/counter'
const counter = useCounterStore()
</script>
<template>
<div>{{ counter.count }}</div>
</template>

actions 的使用

😀 在 Pinia 中没有 mutations,只有 actions,不管是同步还是异步的代码,都可以在 actions 中完成。

  1. 在 actions 中提供方法并且修改数据,store/counter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { defineStore } from 'pinia'

const useCounterStore = defineStore('counter', {
state: () => {
return {
count: 0,
}
},
actions: {
increment(count) {
this.count += count
},
incrementAsync(count) {
setTimeout(() => {
this.count += count
}, 2000)
},
},
})

export default useCounterStore
  1. 在组件中使用,App.vue
1
2
3
4
5
6
7
8
9
10
11
<script setup>
import useCounterStore from './store/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<p>count: {{ counter.count }}</p>
<button @click="counter.increment(3)">increment</button>
<button @click="counter.incrementAsync(4)">increment async</button>
</div>
</template>

getters 的使用

😀 Pinia 和 Vuex 中的 getters 基本是一样的,也带有缓存的功能。

  1. 在 getters 中提供计算属性,store/counter.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
import { defineStore } from 'pinia'

const useCounterStore = defineStore('counter', {
state: () => {
return {
count: 0,
}
},
actions: {
increment(count) {
this.count += count
},
incrementAsync(count) {
setTimeout(() => {
this.count += count
}, 2000)
},
},
getters: {
double() {
return this.count * 2
},
},
})

export default useCounterStore
  1. 在组件中使用,App.vue
1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import useCounterStore from './store/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<p>count: {{ counter.count }}</p>
<p>double: {{ counter.double }}</p>
<button @click="counter.increment(3)">increment</button>
<button @click="counter.incrementAsync(4)">increment async</button>
</div>
</template>

storeToRefs 的使用

🤔 如果直接从 Pinia 中解构数据,会丢失响应式,使用 storeToRefs 可以保证解构出来的数据也是响应式的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { storeToRefs } from 'pinia'
import useCounterStore from './store/counter'
const counter = useCounterStore()
// !数据解构会丢失响应式
// const { count, double } = counter
// 方法可以正常解构
const { increment, incrementAsync } = counter
// 被 storeToRefs 包裹后的 counter,解构出来的数据是响应式的
const { count, double } = storeToRefs(counter)
</script>
<template>
<div>counter: {{ count }}</div>
<div>double: {{ double }}</div>
<button @click="increment">add 1</button>
<button @click="incrementAsync">async add 1</button>
</template>

Pinia 模块化

🤔 在复杂项目中,一般来说应该每一个功能模块对应一个 store,最后通过一个根 store 进行整合。

  1. user 模块,store/user.js
1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from 'pinia'

const useUserStore = defineStore('user', {
state: () => {
return {
name: 'ifer',
age: 18,
}
},
})

export default useUserStore
  1. 关联 user 和 counter 模块到根 store,store/index.js
1
2
3
4
5
6
7
8
9
import useCounterStore from './counter'
import useUserStore from './user'

export default function useStore() {
return {
user: useUserStore(),
counter: useCounterStore(),
}
}
  1. 在组件中使用,App.vue
1
2
3
4
5
6
7
8
9
10
<script setup>
import { storeToRefs } from 'pinia'
import useStore from './store'
const { user } = useStore()
const { name, age } = storeToRefs(user)
</script>
<template>
<div>name: {{ name }}</div>
<div>age: {{ age }}</div>
</template>

TodoMVC

静态结构

main.js

1
2
3
4
5
6
import { createApp } from 'vue'
import './styles/base.css'
import './styles/index.css'
import App from './App.vue'

createApp(App).mount('#app')

App.vue

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
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus />
</header>

<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked />
<label>Taste JavaScript</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Rule the web" />
</li>
</ul>
</section>

<footer class="footer">
<span class="todo-count"><strong>0</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
</template>

styles/base.css

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
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}

.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}

.learn a:hover {
text-decoration: underline;
color: #787e7e;
}

.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}

.learn h3 {
font-size: 24px;
}

.learn h4 {
font-size: 18px;
}

.learn h5 {
margin-bottom: 0;
font-size: 14px;
}

.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}

.learn li {
line-height: 20px;
}

.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}

#issue-count {
display: none;
}

.quote {
border: none;
margin: 20px 0 60px 0;
}

.quote p {
font-style: italic;
}

.quote p:before {
content: '“';
font-size: 50px;
opacity: 0.15;
position: absolute;
top: -20px;
left: 3px;
}

.quote p:after {
content: '”';
font-size: 50px;
opacity: 0.15;
position: absolute;
bottom: -42px;
right: 3px;
}

.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}

.quote footer img {
border-radius: 3px;
}

.quote footer a {
margin-left: 5px;
vertical-align: middle;
}

.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, 0.04);
border-radius: 5px;
}

.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, 0.04);
}

.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, 0.6);
transition-property: left;
transition-duration: 500ms;
}

@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}

.learn-bar > .learn {
left: 8px;
}
}

styles/index.css

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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
html,
body {
margin: 0;
padding: 0;
}

button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}

:focus {
outline: 0;
}

.hidden {
display: none;
}

.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}

.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}

.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}

.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}

.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}

.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}

.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}

.toggle-all + label:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}

.toggle-all:checked + label:before {
color: #737373;
}

.todo-list {
margin: 0;
padding: 0;
list-style: none;
}

.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}

.todo-list li:last-child {
border-bottom: none;
}

.todo-list li.editing {
border-bottom: none;
padding: 0;
}

.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}

.todo-list li.editing .view {
display: none;
}

.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}

.todo-list li .toggle {
opacity: 0;
}

.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}

.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}

.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #4d4d4d;
}

.todo-list li.completed label {
color: #cdcdcd;
text-decoration: line-through;
}

.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
color: #af5b5e;
}

.todo-list li .destroy:after {
content: '×';
}

.todo-list li:hover .destroy {
display: block;
}

.todo-list li .edit {
display: none;
}

.todo-list li.editing:last-child {
margin-bottom: -1px;
}

.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}

.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
float: left;
text-align: left;
}

.todo-count strong {
font-weight: 300;
}

.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}

.filters li {
display: inline;
}

.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}

.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}

.clear-completed:hover {
text-decoration: underline;
}

.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}

.info p {
line-height: 1;
}

.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}

.info a:hover {
text-decoration: underline;
}

/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}

.todo-list li .toggle {
height: 40px;
}
}

@media (max-width: 430px) {
.footer {
height: 50px;
}

.filters {
bottom: 10px;
}
}

列表展示

  1. 引入 Pinia,main.js
1
2
3
4
5
6
7
8
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import './styles/base.css'
import './styles/index.css'

const pinia = createPinia()
createApp(App).use(pinia).mount('#app')
  1. 新建 todo 模块,store/modules/todos.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
import { defineStore } from 'pinia'

const useTodosStore = defineStore('todos', {
// 注意这儿的小括号 ({ ... })
state: () => ({
list: [
{
id: 1,
name: '吃饭',
done: false,
},
{
id: 2,
name: '睡觉',
done: true,
},
{
id: 3,
name: '打豆豆',
done: false,
},
],
}),
})

export default useTodosStore
  1. 创建根 store,store/index.js
1
2
3
4
5
6
7
8
import useTodosStore from './modules/todos'

export default function useStore() {
return {
// Tip: 只有配置了 Pinia 之后,这个 useTodosStore 才能被执行调用
todos: useTodosStore(),
}
}
  1. 渲染列表,components/TodoMain.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import useStore from '../store'

const { todos } = useStore()
</script>

<template>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li :class="{ completed: item.done }" v-for="item in todos.list" :key="item.id">
<div class="view">
<input class="toggle" type="checkbox" :checked="item.done" />
<label>{{ item.name }}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>
</template>

修改状态

  1. 在 actions 中提供方法,store/modules/todos.js
1
2
3
4
5
6
actions: {
changeDone(id) {
const todo = this.list.find((item) => item.id === id)
todo.done = !todo.done
},
},
  1. 在组件中注册事件,components/TodoMain.vue
1
<input class="toggle" type="checkbox" :checked="item.done" @change="todos.changeDone(item.id)" />

删除任务

  1. 在 actions 中提供方法,store/modules/todo.js
1
2
3
4
5
actions: {
delTodo(id) {
this.list = this.list.filter((item) => item.id !== id)
},
},
  1. 在组件中注册事件,components/TodoMain.vue
1
<button class="destroy" @click="todos.delTodo(item.id)"></button>

添加任务

  1. 在 actions 中提供方法,store/modules/todo.js
1
2
3
4
5
6
7
8
9
actions: {
addTodo(name) {
this.list.unshift({
id: Date.now(),
name,
done: false,
})
},
},
  1. 在组件中注册事件,components/TodoHeader.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import { ref } from 'vue'
import useStore from '../store'
const { todos } = useStore()
// # 用于收集数据的变量
const todoName = ref('')
const add = (e) => {
if (e.key === 'Enter' && todoName.value) {
todos.addTodo(todoName.value)
// 清空
todoName.value = ''
}
}
</script>

<template>
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus v-model="todoName" @keydown="add" />
</header>
</template>

全选反选

  1. 在 getters 中提供计算属性,在 actions 中提供方法,store/modules/todos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
const useTodosStore = defineStore('todos', {
actions: {
checkAll(value) {
this.list.forEach((item) => (item.done = value))
},
},
getters: {
// 根据所有的单选按钮,先确定全选按钮的状态
isCheckAll() {
return this.list.every((item) => item.done)
},
},
})
  1. 在组件中使用,components/TodoMain.vue
1
<input id="toggle-all" class="toggle-all" type="checkbox" :checked="todos.isCheckAll" @change="todos.checkAll(!todos.isCheckAll)" />

底部功能

  1. 在 getters 中提供计算属性,store/modules/todos.js
1
2
3
4
5
6
7
8
9
10
11
12
const useTodosStore = defineStore('todos', {
actions: {
clearCompleted() {
this.list = this.list.filter((item) => !item.done)
},
},
getters: {
leftCount() {
return this.list.filter((item) => !item.done).length
},
},
})
  1. 在组件中使用,components/TodoFooter.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import useStore from '../store'

const { todos } = useStore()
</script>
<template>
<footer class="footer">
<span class="todo-count"><strong>{{ todos.leftCount }}</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed" @click="todos.clearCompleted">Clear completed</button>
</footer>
</template>

筛选功能

  1. 提供数据,store/modules/todo.js
1
2
3
4
state: () => ({
filters: ['All', 'Active', 'Completed'],
active: 'All',
}),
  1. 提供 actions,store/modules/todo.js
1
2
3
4
5
actions: {
changeActive(active) {
this.active = active
},
},
  1. 在 footer 中渲染,components/TodoFooter.vue
1
2
3
4
5
<ul class="filters">
<li v-for="item in todos.filters" :key="item" @click="todos.changeActive(item)">
<a :class="{ selected: item === todos.active }" href="#/">{{ item }}</a>
</li>
</ul>
  1. 提供计算属性,store/modules/todo.js
1
2
3
4
5
6
7
8
9
showList() {
if (this.active === 'Active') {
return this.list.filter((item) => !item.done)
} else if (this.active === 'Completed') {
return this.list.filter((item) => item.done)
} else {
return this.list
}
},
  1. 组件中渲染,components/TodoMain.vue
1
2
3
<ul class="todo-list">
<li :class="{ completed: item.done }" v-for="item in todos.showList" :key="item.id"></li>
</ul>

持久化

  1. 订阅 store 中数据的变化,components/TodoMain.vue
1
2
3
4
const { todos } = useStore()
todos.$subscribe(() => {
localStorage.setItem('todos', JSON.stringify(todos.list))
})
  1. 获取数据时从本地缓存中获取,store/modules/todos.js
1
2
3
state: () => ({
list: JSON.parse(localStorage.getItem('todos')) || [],
}),