【项目-喵内记账-meoney-04】Vue 全局数据管理
大纲链接 §
[toc]
知识点
组件间数据通信实现过程
- 再次封装
recordListModel.ts
- 用
window
来容纳数据
- 用
window
来封装 API
- 消除对
window
的依赖
- 将
model
融合进 store
- 修复
Tags.vue
的 bug
store
的 bug
之值与地址
- 小技巧:把
store2
变成 this.$store2
(不用跟着做)
- 全局状态管理的优点
- 再次封装
recordListModel.ts
解决bug
- 标签组件和记账组件不共享标签数据
- 在记账组件中新增标签的数据不会同步到标签组件中
- 切换标签组件和记账组件时,新增标签的数据丢失
- 没有统一的组件间数据通信管理
Vuex
初体验 - 数据读写
- 在
Money.vue
中使用 Vuex
- 重构
Tags.vue
和 Labels.vue
- 在 TS 里使用
mixin
- 重构
EditLabel.vue
- 在 TS 里使用
computed
要用 getter
语法
- 继续重构
EditLabel.vue
Vuex
小结
抽象一个克隆数据函数模块src/lib/clone.ts
recordListModel.clone()
功能可复用,仅数据深拷贝
1
2
3
4
5
6
|
function clone(data: any) {
return JSON.parse(JSON.stringify(data)) as RecordItem;
}
export default clone;
|
再次封装 recordListModel.ts
- 数据变量
data: [] as RecordItem[]
- 获取数据
fetchRecord() {... return this.data}
- 保存数据
saveRecord(){window.localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));}
- 创建一个数据项目
createItem(record: RecordItem) {...}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import clone from '@/lib/clone';
const localStorageKeyName = 'recordList';
const recordListModel = {
data: [] as RecordItem[],
createItem(record: RecordItem) {
const clonedRecord = clone(record);
clonedRecord.createdAt = new Date();
this.data.push(clonedRecord);
},
fetchRecord() {
this.data = JSON.parse(
window.localStorage
.getItem(localStorageKeyName) ?? '[]') as RecordItem[];
return this.data;
},
saveRecord() {
window.localStorage.setItem(localStorageKeyName,
JSON.stringify(this.data));
}
};
export default recordListModel;
|
对应修改Money.vue
的 API,引入recordListModel.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
|
<template>
<Layout class-prefix="layout">
<Tags :data-source.sync="tags"
@update:selectedTags="pickTags"/>
<FormItem class="form-item"
field-name="备注"
placeholder="在这里输入备注"
@update:inputValue="onUpdateTips"/>
<Types :type.sync="record.type"/>
<Numpad :value.sync="record.amount"
@submit="saveRecord"/>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import Tags from '@/components/Money/Tags.vue';
import FormItem from '@/components/Money/FormItem.vue';
import Types from '@/components/Money/Types.vue';
import Numpad from '@/components/Money/Numpad.vue';
import {Component, Watch} from 'vue-property-decorator';
import recordListModel from '@/models/recordListModel.ts';
import tagListModel from '@/models/tagListModel';
@Component({
components: {
Numpad, Types, FormItem, Tags
}
})
export default class Money extends Vue {
tags = tagListModel.fetch();
recordList = recordListModel.fetchRecord();
record: RecordItem = {
tags: [],
tips: '',
type: '-',
amount: 0,
createdAt: new Date(),
};
pickTags(selectedTags: string[]) {
this.record.tags = selectedTags;
}
onUpdateTips(value: string) {
this.record.tips = value;
}
saveRecord() {
recordListModel.createItem(this.record);
}
@Watch('recordList')
onRecordeChange() {
recordListModel.saveRecord();
}
}
</script>
//...
|
如何解决Money.vue
中对于数据tags
和recordList
各自获取数据时, 重复调用相同功能 的API
同样地,Labels.vue
和EditLabel.vue
中也如此,优化逻辑,避免每次都fetch
,就clone
(JSON.parse
)一次数据
用 window
来容纳数据
在Money.vue
、Labels.vue
和EditLabel.vue
的 入口层main.ts
中,统管数据
window.tagList = tagListModel.fetch();
TS中未声明类型
- 在
custom.ts
中统一声明全局类型
- 将原本在
tagListModel.ts
中声明的类型一并移至custom.d.ts
custom.d.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
|
type RecordItem = {
tags: string[];
tips: string;
type: string;
amount: number;
createdAt?: Date;
}
type Tag = {
id: string;
name: string;
}
type TagListModel = {
data: Tag[];
fetch: () => Tag[];
create: (name: string) => 'success' | 'duplicated';
save: () => void;
update: (id: string, name: string) => 'success' | 'not found' | 'duplicated';
remove: (id: string) => boolean;
}
interface Window {
tagList: Tag[]
}
|
main.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
|
import Vue from 'vue';
import App from './App.vue';
import './registerServiceWorker';
import router from './router';
import store from './store';
import Nav from '@/components/Nav.vue';
import Layout from '@/components/Layout.vue';
import Icon from '@/components/Icon.vue';
import tagListModel from '@/models/tagListModel.ts';
Vue.config.productionTip = false;
Vue.component('Nav', Nav);
Vue.component('Layout', Layout);
Vue.component('Icon', Icon);
window.tagList = tagListModel.fetch();
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
|
- 统一使用
window.tagList = tagListModel.fetch();
容纳数据
- 这样做的好处是,今后不管是属性还是值的类型写错,都可以得到TS报错的提示
- TS的本质就是用类型去检查代码
用 window
来封装 API 1
Money.vue
中tags = window.tagList;
代替tags = tagListModel.fetch();
Labels.vue
中tags = window.tagList;
代替 tagListModel.fetch();
和 tags = tagListModel.data;
- 读取
window.tagList
- 写入
tagListModel.create(name) {...}
- 统一读取和写入,符合最小知识原则
- 封装“增”
window.createTag = (name: string) => {...};
- 封装“改”
window.updateTag = (id: string, object: Exclude<Tag, 'id'>) => {...};
- 返回值和之前声明类型相同
updateTag: (id: string, name: string) => 'success' | 'not found' | 'duplicated';
简写为updateTag: TagListModel['update']
- 封装“查”
window.findTag = (id: string) => {...}
重构main.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
|
import Vue from 'vue';
import App from './App.vue';
import './registerServiceWorker';
import router from './router';
import store from './store';
import Nav from '@/components/Nav.vue';
import Layout from '@/components/Layout.vue';
import Icon from '@/components/Icon.vue';
import tagListModel from '@/models/tagListModel.ts';
Vue.config.productionTip = false;
Vue.component('Nav', Nav);
Vue.component('Layout', Layout);
Vue.component('Icon', Icon);
window.tagList = tagListModel.fetch();
window.findTag = (id: string) => {
// return window.tagList.filter(t => t.id === id)[0] || undefined;
return window.tagList.find(t => t.id === id) || undefined;
};
window.createTag = (name: string) => {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('添加成功');
}
};
window.removeTag = (id: string) => {
return tagListModel.remove(id);
};
window.updateTag = (id: string, name: string) => {
return tagListModel.update(id, name);
};
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
|
重构custom.d.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
|
type RecordItem = {
tags: string[];
tips: string;
type: string;
amount: number;
createdAt?: Date;
}
type Tag = {
id: string;
name: string;
}
type TagListModel = {
data: Tag[];
fetch: () => Tag[];
create: (name: string) => 'success' | 'duplicated';
save: () => void;
update: (id: string, name: string) => 'success' | 'not found' | 'duplicated';
remove: (id: string) => boolean;
}
interface Window {
tagList: Tag[];
findTag: (id: string) => Tag | undefined;
createTag: (name: string) => void;
removeTag: (id: string) => boolean;
updateTag: TagListModel['update'];
}
|
重构Labels.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
|
<template>
<Layout class="labels">
<ul class="tags">
<li v-for="tag in tags" :key="tag.id">
<router-link class="tag" :to="`/labels/edit/${tag.id}`">
<span class="tag-text">{{ tag.name }}</span>
<Icon class="tag-icon" name="money_right"/>
</router-link>
</li>
</ul>
<div class="createTag-wrapper">
<Button class="createTag" @click.native="createTag">新建标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import Button from '@/components/Button.vue';
@Component({
components: {Button}
})
export default class Labels extends Vue {
tags = window.tagList; // [{id: '1', name: '1'}, {id: '2', name: '2'}]
createTag() {
const name = window.prompt('请输入标签名');
if (name) {
window.createTag(name);
} else {
window.alert('没有输入内容,请重新创建标签');
}
}
}
</script>
...
|
重构EditLabel.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
48
49
50
51
52
53
54
55
56
57
58
|
<template>
<Layout>
<div class="headerBar">
<Icon class="left-icon" name="money_right" @click.native="goBack"/>
<span class="title">编辑标签</span>
</div>
<FormItem :inputValue="tag.name"
@update:inputValue="update"
class="form-item" field-name="标签名" placeholder="在这里输入标签名"/>
<div class="button-wrapper">
<Button @click="remove">删除标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import FormItem from '@/components/Money/FormItem.vue';
import Button from '@/components/Button.vue';
@Component({
components: {Button, FormItem}
})
export default class EditLabel extends Vue {
tag = window.findTag(this.$route.params.id);
created() {
if (!this.tag) {
return this.$router.replace('/404');
}
}
update(name: string) {
if (!this.tag) { return; }
if (name === '') {
window.alert('标签名不能为空');
this.$router.back();
return;
}
window.updateTag(this.tag.id, name);
}
remove() {
if (this.tag) {
if (window.removeTag(this.tag.id)) {
window.alert(`成功删除标签:${this.tag.name}`);
this.$router.back();
}
}
}
goBack() {
this.$router.back();
}
}
</script>
...
|
用 window
来封装 API 2
- 同样思路重构
main.ts
、Money.vue
、recordListModel.ts
、custom.d.ts
main.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
|
// import ...
Vue.config.productionTip = false;
Vue.component('Nav', Nav);
Vue.component('Layout', Layout);
Vue.component('Icon', Icon);
// record store
window.recordList = recordListModel.fetchRecord();
window.createRecord = (record: RecordItem) => {
recordListModel.createItem(record);
};
// tag store
window.tagList = tagListModel.fetch();
window.findTag = (id: string) => {...};
window.createTag = (name: string) => {...};
window.removeTag = (id: string) => {...};
window.updateTag = (id: string, name: string) => {...};
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
|
缺点
- 全局变量太多,变量可能互相覆盖
- 对
window
的依赖
消除对 window
的依赖
将变量方法都挂在window.store
上
- 需要在
custom.d.ts
中声明window.store
类型
重构custom.d.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// ...
interface Window {
store: {
tagList: Tag[];
findTag: (id: string) => Tag | undefined;
createTag: (name: string) => void;
removeTag: (id: string) => boolean;
updateTag: TagListModel['update'];
recordList: RecordItem[];
createRecord: (record: RecordItem) => void;
}
}
|
重构main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// import ...
// ...
window.store = {
// record store
recordList: recordListModel.fetchRecord(),
createRecord(record: RecordItem) {...},
// tag store
tagList: tagListModel.fetch(),
findTag(id: string) {...},
createTag(name: string) {...},
removeTag(id: string) {...},
updateTag(id: string, name: string) {...},
};
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
|
解决依赖 window
将变量挂在src/store/index2.ts
上
- 将整个
store2
对象放到index2.ts
里导出
- 可在同一文件模块中引用数据
- TS自动推测
store2
类型
src/store/index2.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
|
import recordListModel from '@/models/recordListModel';
import tagListModel from '@/models/tagListModel';
const store2 = {
// record store
recordList: recordListModel.fetchRecord(),
createRecord: (record: RecordItem) => {
recordListModel.createItem(record);
},
// tag store
tagList: tagListModel.fetch(),
findTag(id: string) {
return this.tagList.find(t => t.id === id) || undefined;
},
createTag: (name: string) => {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('添加成功');
}
},
removeTag: (id: string) => {
return tagListModel.remove(id);
},
updateTag: (id: string, name: string) => {
return tagListModel.update(id, name);
},
};
export default store2;
|
重构Money.vue
、Labels.vue
和EditLabel.vue
对应的方法
重构Money.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
|
// import ...;
// import store2 from '@/store/index2';
@Component({
components: {
Numpad, Types, FormItem, Tags
}
})
export default class Money extends Vue {
tags = store2.tagList;
recordList = store2.recordList;
record: RecordItem = {
tags: [],
tips: '',
type: '-',
amount: 0,
createdAt: new Date(),
};
pickTags(selectedTags: string[]) {
this.record.tags = selectedTags;
}
onUpdateTips(value: string) {
this.record.tips = value;
}
saveRecord() {
store2.createRecord(this.record);
}
}
|
重构Labels.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// import ...;
@Component({
components: {Button}
})
export default class Labels extends Vue {
tags = store2.tagList;
createTag() {
const name = window.prompt('请输入标签名');
if (name) {
store2.createTag(name);
} else {
window.alert('没有输入内容,请重新创建标签');
}
}
}
|
重构EditLabel.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
|
// import...;
@Component({
components: {Button, FormItem}
})
export default class EditLabel extends Vue {
tag = store2.findTag(this.$route.params.id);
created() {
if (!this.tag) {
return this.$router.replace('/404');
}
}
update(name: string) {
if (!this.tag) { return; }
if (name === '') {
window.alert('标签名不能为空');
this.$router.back();
return;
}
store2.updateTag(this.tag.id, name);
}
remove() {
if (this.tag) {
if (store2.removeTag(this.tag.id)) {
window.alert(`成功删除标签:${this.tag.name}`);
this.$router.back();
}
}
}
goBack() {
this.$router.back();
}
}
|
拆分src/store/index2.ts
中的store2
为recordStore.ts
和tagListStore.ts
...
扩展原算符 浅拷贝...recordStore,
- 注意合并话的同名屏蔽问题,重构重命名
src/store/index2.ts
1
2
3
4
5
6
7
8
9
|
import recordStore from '@/store/recordStore.ts';
import tagStore from '@/store/tagStore.ts';
const store2 = {
...recordStore,
...tagStore,
};
export default store2;
|
recordStore.ts
1
2
3
4
5
6
7
8
|
import recordListModel from '@/models/recordListModel.ts';
export default {
recordList: recordListModel.fetchRecord(),
createRecord: (record: RecordItem) => {
recordListModel.createItem(record);
}
}
|
tagListStore.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import tagListModel from '@/models/tagListModel.ts';
export default {
tagList: tagListModel.fetch(),
findTag(id: string) {
return this.tagList.find(t => t.id === id) || undefined;
},
createTag: (name: string) => {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('添加成功');
}
},
removeTag: (id: string) => {
return tagListModel.remove(id);
},
updateTag: (id: string, name: string) => {
return tagListModel.update(id, name);
},
};
|
声明tagListModel
类型,统一写到custom.d.ts
中
1
2
3
4
5
6
7
8
9
10
11
|
// ...
type TagListModel = {
tagList: Tag[];
fetchTags: () => Tag[];
findTag: (id: string) => Tag | undefined;
createTag: (name: string) => 'success' | 'duplicated';
saveTags: () => void;
updateTag: (id: string, name: string) => 'success' | 'not found' | 'duplicated';
removeTag: (id: string) => boolean;
}
...
|
import store2...
两次是同一个store2
吗?
- 可尝试
import
两次store
测试console.log(store === store2)
,结果为true
那import
中的代码执行了几次
- 在
index2.ts
测试console.log("index2 执行了一次")
,查看控制台打印的次数
- 结果为只执行一次
- 即
import
多处的代码只执行一次
将 model
融合进 store
model
和store
功能重合
store
多了没必要的中转,比如createRecord: (record: RecordItem) => { recordListModel.createItem(record); }
model
合并到store
重构recordStore.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
|
import clone from '@/lib/clone';
const localStorageKeyName = 'recordList';
let data: RecordItem[] | undefined = undefined;
const recordStore = {
recordList: data,
fetchRecords() {
data = JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]') as RecordItem[];
return data;
},
saveRecords() {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(data));
},
createRecord: (record: RecordItem) => {
const clonedRecord = clone(record);
clonedRecord.createdAt = new Date();
data?.push(clonedRecord);
recordStore.saveRecords();
}
};
recordStore.fetchRecords();
export default recordStore;
|
- 判断是否存在
data && data.push(clonedRecord);
改为新语法:可选链data?.push(clonedRecord);
- 在默认导出的匿名对象中
export default
不可用this.***()
比如this.saveRecord()
在定义对象的过程中立即来使用该对象
- 需改为具名对象,然后导出改对象
- 在对象外部指向对象的方法,初始化
recordStore.fetchRecord();
let data: RecordItem[] | undefined = undefined;
在recordStore
内使用了闭包:函数使用了外部变量
- 可以消除闭包,直接内部定义
recordList: [] as RecordItem[],
,不用箭头函数
再次重构recordStore.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 clone from '@/lib/clone';
const localStorageKeyName = 'recordList';
const recordStore = {
recordList: [] as RecordItem[],
fetchRecords() {
this.recordList = JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]') as RecordItem[];
return this.recordList;
},
saveRecords() {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(this.recordList));
},
createRecord(record: RecordItem) {
const clonedRecord = clone(record);
clonedRecord.createdAt = new Date();
this.recordList?.push(clonedRecord);
recordStore.saveRecords();
}
};
recordStore.fetchRecords();
export default recordStore;
|
重构tagListStore.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
|
import createId from '@/lib/createId';
const localStorageKeyName = 'recordList';
const tagStore = {
tagList: [] as Tag[],
findTag(id: string) {
return this.tagList.find(t => t.id === id) || undefined;
},
saveTags() {
localStorage.setItem(localStorageKeyName, JSON.stringify(this.tagList));
},
createTag(name: string) {
const names = this.tagList.map(d => d.name);
if (names.indexOf(name) >= 0) {
window.alert('标签名重复了');
return 'duplicated';
}
const id = createId().toString();
this.tagList.push({id, name});
this.saveTags();
window.alert('添加成功');
return 'success';
},
removeTag(id: string) {
let index = -1;
for (let i = 0; i < this.tagList.length; i++) {
if (this.tagList[i].id === id) {
index = i;
break;
}
}
this.tagList.splice(index, 1);
this.saveTags();
return true;
},
updateTag(id: string, name: string) {
const idList = this.tagList.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const nameList = this.tagList.map(item => item.name);
if (nameList.indexOf(name) >= 0) {
return 'duplicated';
} else {
const tag = this.tagList.filter(item => item.id === id)[0];
tag.name = name;
this.saveTags();
return 'success';
}
} else {
return 'not found';
}
},
};
export default tagStore;
|
- 在定义变量时无法调用正在定义的方法
- 需在对象外部调用方法
- 对象的方法可调用对象内部的方法
可以删除src/model
,直接用src/store
代替
全局管理数据
Tags.vue
中this.$emit('update:dataSource', [...this.dataSource, name];
的name
产生的bug
this.dataSource
,即子组件传递事件所必要的外部数据@Prop({required: true}) readonly dataSource!: string[];
- 来自父组件
Money.vue
中:data-source.sync="tags"
Tags.vue
新传来的数据[...this.dataSource, name];
不能赋值给原来的tags
- 因为
tags = store2.tagList;
传的数据不够精确,不是更新整个数组,而是更新name
这一项
修改Tag.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
|
<template>
<div class="tags">
<ul class="current">
<li v-for="tag in tagList"
:key="tag.id"
@click="toggle(tag)"
:class="{selected: selectedTags.indexOf(tag)>=0 }">
{{ tag.name }}
</li>
</ul>
<div class="new">
<button @click="createTag">添加新标签</button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import store2 from '@/store/index2';
@Component
export default class Tags extends Vue {
tagList = store2.fetchTags(); // Tag[]
selectedTags: string[] = [];
toggle(tag: Tag) {
const index = this.selectedTags.map(tag => tag.id).indexOf(tag.id);
if (index >= 0) {
this.selectedTags.splice(index, 1);
} else {
this.selectedTags.push(tag);
}
this.$emit('update:selectedTags', this.selectedTags);
}
createTag() {
const name = window.prompt('请输入标签名');
if (!name) {return window.alert('标签名不能为空');}
store2.createTag(name);
}
}
</script>
...
|
- 将
Tag.vue
组件中的this.dataSource
通知给父组件的事件改为直接操作store2.ts
,使用store2.createTag(name);
- 把用户输入的
name
传给store2.ts
,做到全局管理数据,代替原来的父子组件间通信
- 无需
@Prop({required: true}) readonly dataSource!: string[];
- 而使用
tagList = store2.fetchTags();
- 原来所有的
dataSource
改为tagList
- 比如
<li v-for="tag in tagList" :key="tag.id" @click="toggle(tag)" :class="{selected: selectedTags.indexOf(tag) >= 0}"> {{ tag.name }}</li>
Money.vue
中自己到全局store2.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
|
<template>
<div class="tags">
<ul class="current">
<li v-for="tag in tagList"
:key="tag.id"
@click="toggle(tag)"
:class="{selected: selectedTags.indexOf(tag)>=0 }">
{{ tag.name }}
</li>
</ul>
<div class="new">
<button @click="createTag">添加新标签</button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import store2 from '@/store/index2';
@Component
export default class Tags extends Vue {
tagList = store2.fetchTags(); // Tag[]
selectedTags: string[] = [];
toggle(tag: Tag) {
const index = this.selectedTags.map(tag => tag.id).indexOf(tag.id);
if (index >= 0) {
this.selectedTags.splice(index, 1);
} else {
this.selectedTags.push(tag);
}
this.$emit('update:selectedTags', this.selectedTags);
}
createTag() {
const name = window.prompt('请输入标签名');
if (!name) {return window.alert('标签名不能为空');}
store2.createTag(name);
}
}
</script>
...
|
<Tags @update:selectedTags="pickTags"/>
无需再绑定:data-source="tags"
- 因此无需数据
tags = store2.tagList;
- 而在
Tags.vue
中让tagList = store2.fetchTags();
自己获取数据
数据无需一层一层传递,统一集中处理tagStore.ts
中的数据
store
的 bug
之值与地址
- 原始值类型与引用类型的地址
- 引用类型变化,个引用其地址的变量也变化
- 原始值类型赋值的变量,值改变不会影响(同步到)改变量
- 用
data
获取数据store
只会在created
创建阶段获取一遍,不会监听变更并重新渲染
改为使用computed
- 同时将声明变量为内部数据,挂在
App.vue
的data
函数上,给vue
监听到
- 可以将值和引用类型的变量都放到
computed
里getter
和setter
- 自定义的
store2.ts
并没有在一开始就被vue
监听到
- 而使用
Vuex
即src/store/index.ts
则可以被vue
监听到
小技巧:可以把 store2
变成 this.$store2
进行全局访问
- 在
main.ts
中引入store2
: import store2 from '@/store/index2.ts'
- 放到
Vue
的原型上Vue.prototype.$store2 = store2
- 在
shims-vue.d.ts
中声明接口类型interface Vue {$store: any;}
TypeScript
增强类型以配合插件使用
- 在
custom.d.ts
中声明模块declare module 'vue/types/vue' {interface Vue {$store2: any;}}
- 可以全局使用
this.$store
- 将
custom.d.ts
中的类型声明(比如type RecordItem
)之外的其他(store2
)拆分到global.d.ts
Vuex
就使用了该技巧Vue.use(Vuex);
全局状态管理的优点
全局状态管理(也叫全局数据管理)的好处是什么?
- 解耦:将所有数据相关的逻辑放入
store
(也就是 MVC
中的 Model
,换了个名字而已)
- 数据读写更方便:任何组件不管在哪里,都可以直接读写数据
- 控制力更强:组件对数据的读写只能使用
store
提供的 API
进行(当然也不排除有猪队友直接对 tagList
和 recordList
进行 push
等操作,这是没有办法禁止的)
Vuex
跟我们手写的 store
没有本质区别。
Vuex
官方全局状态管理初体验 - 数据读写
- Vuex 是一个专为
Vue.js
应用程序开发的状态管理模式
- 多个组件共享状态时,单向数据流 的简洁性很容易被破坏
- 多个视图 依赖于 同一状态
- 来自不同视图的行为需要 变更同一状态
Vuex
就是一个用于管理数据的工具(对象),提供了读/写(增删改查)数据的API
核心概念
State
对应 data
,用于存放数据
Getters
对应 computed
,用于监听变更
Mutations
对应methods
,意为改动、变更,用于写数据,调用同步方法,实时更改数据
Actions
对应 调用methods
,必须用于异步操作,调用Mutations
的方法
Modules
对应components
将 store
分割成模块module
,每个模块拥有自己的 state
、mutation
、action
、getter
、module
在 Money.vue
中目前主要使用 Vuex
的 State
和 Mutations
在 Money.vue
中使用 Vuex
- 使用
vue/cli
项目初始化时已选
- 操作
src/store/index.ts
state
属性
- 单一状态树:树型结构数据的对象
- 唯一数据源
- 处处使用数据
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {},
actions: {},
modules: {}
});
export default store;
// console.log(store.state.count);
|
- 可以全局使用
store.state.count
main.ts
引用了src/store/index.ts
state
中不必写方法返回一个值,和data
的用法不同
- 注意
store
不是{state:{...}, mutations: {...}}
选项,而是由这个选项构造出的对象,拷贝了所有选项的属性
mutations
属性
- 其中的方法不传改对象本身为
this
- 而将
state
作为第一个形参this
传给方法,显式地传递
- 在方法中使用数据直接访问
state.***
- 在调用方法时,使用
store.commit('xxx')
,以字符串方式将变更的方法名 提交一个操作,并自动传之前定义时传入的state
给方法
- 给
mutations
中的方法传第二个参数以及之后传的参数
- 在 TS 中需声明之后传参的类型
store.commit(type, [payload...]);
main.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
|
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: () => {
return {
count: 0,
}
},
mutations: {
increaseCount(state) {
state.count += 1;
},
increaseN(state, n) {
state.count += n;
},
},
actions: {},
modules: {}
});
export default store;
console.log(store.state.count); // 0
store.commit('increaseCount'); // +1 操作
console.log(store.state.count); // 1
store.commit('increaseN', 10); // +10 操作
console.log(store.state.count); // 11
|
使用computed: {}
- 不使用只在初始化创建时使用一次的
data
,不会自动计算属性的变化,无法更新 UI
- TS 在装饰器中使用
@Compnent({ components: {...}, computed: {...})
- 在
template
中无法直接访问store
- 需要
methods
中的方法执行store.commit('increaseCount');
作为中转
- 或者使用
$store
(template
中),方法中使用this.$store
- 原理是
Vue.use(Vuex);
在Vue.prototype
(原型),把store
绑定到共用属性上 Vue.prototype.$store = userStore
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
|
<template>
<div>
count: {{ count }}
<button @click="add">+1 +5</button>
<button @click="$store.commit('increaseN', -10)">
使用$store: {{count}} - 10
</button>
dataCount不变: {{ dataCount }}
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator'
import store from '@/store/index.ts';
@Component({
components: {/*...*/},
computed: {
count() {
return store.state.count;
},
},
})
export default class Money extends Vue {
dataCount = store.state.count;
// ...
add() {
// 对应 import store from '@/store/index.ts';
store.commit('increaseCount'); // +1
// 对应 `Vue.use(Vuex);`
this.$store.commit('increaseN', 5); // +5
}
}
</script>
// ...
|
- 读数据
@Component({..., computed: {xxx() {return ...}, ...}, ...
计算属性
- 写数据
- 全局使用:
@eventName="$store.commit(...)"
/ this.$store.xxx
- 局部使用:引用
store
来使用import store from '@/store/index.ts';
- 注意
mutations
中的方法可以调用其中的其他方法fn1() {store.commit('fn2');}
重构Money.vue
将数据分模块写入index.ts
,替换所有store2.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
|
<template>
<Layout class-prefix="layout">
<Tags @update:selectedTags="pickTags"/>
<FormItem class="form-item" field-name="备注" placeholder="在这里输入备注"
@update:inputValue="onUpdateTips"/>
<Types :type.sync="record.type"/>
<Numpad :value.sync="record.amount"
@submit="saveRecord"/>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import Tags from '@/components/Money/Tags.vue';
import FormItem from '@/components/Money/FormItem.vue';
import Types from '@/components/Money/Types.vue';
import Numpad from '@/components/Money/Numpad.vue';
import {Component} from 'vue-property-decorator';
@Component({
components: {Numpad, Types, FormItem, Tags},
computed: {
recordList() {
return this.$store.state.recordList;
}
},
})
export default class Money extends Vue {
record: RecordItem = {
tags: [],
tips: '',
type: '-',
amount: 0,
createdAt: new Date(),
};
created() {
this.$store.commit('fetchRecords');
}
onUpdateTips(value: string) {
this.record.tips = value;
}
pickTags(selectedTags: string[]) {
this.record.tags = selectedTags;
}
saveRecord() {
this.$store.commit('createRecord', this.record);
}
}
</script>
...
|
- 模块的局部状态:对于模块内部的
mutation
和 getter
,接收的第一个参数是 state
模块的局部状态对象
Tags.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
48
49
50
51
52
|
<template>
<div class="tags">
<ul class="current">
<li v-for="tag in tagList"
:key="tag.id"
@click="toggle(tag)"
:class="{selected: selectedTags.indexOf(tag)>=0 }">
{{ tag.name }}
</li>
</ul>
<div class="new">
<button @click="createTag">添加新标签</button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
@Component({
computed: {
tagList() {
return this.$store.state.tagList;
},
}
})
export default class Tags extends mixins(tagHelper) {
selectedTags: Tag[] = [];
created() {
this.$store.commit('fetchTags');
}
toggle(tag: Tag) {
const index = this.selectedTags.map(i => i.id).indexOf(tag.id);
if (index >= 0) {
this.selectedTags.splice(index, 1);
} else {
this.selectedTags.push(tag);
}
this.$emit('update:selectedTags', this.selectedTags);
}
createTag() {
const name = window.prompt('请输入标签名');
if (!name) {return window.alert('没有输入内容,请重新创建标签');}
this.$store.commit('createTag', name);
}
}
</script>
...
|
Labels.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
|
<template>
<Layout class="labels">
<ul class="tags">
<li v-for="tag in tags" :key="tag.id">
<router-link class="tag" :to="`/labels/edit/${tag.id}`">
<span class="tag-text">{{ tag.name }}</span>
<Icon class="tag-icon" name="money_right"/>
</router-link>
</li>
</ul>
<div class="createTag-wrapper">
<Button class="createTag" @click.native="createTag">新建标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import Button from '@/components/Button.vue';
import tagHelper from '@/mixins/tagHelper.ts';
@Component({
components: {Button},
computed: {
tags() {
return this.$store.state.tagList;
}
}
})
export default class Labels extends mixins(tagHelper) {
beforeCreate() {
this.$store.commit('fetchTags');
}
createTag() {
const name = window.prompt('请输入标签名');
if (!name) {return window.alert('没有输入内容,请重新创建标签');}
this.$store.commit('createTag', name);
}
}
</script>
...
|
- 创建前 钩子
beforeCreate() {this.$store.commit('fetchTags');}
获取数据,每次切换标签都触发这个钩子
- 组件中都有相同的
createTag(){...}
,可用mixin
来复用代码
在 TS 里使用 mixin
src/mixins/tagHelper.ts
引入 mixin
的实例
1
2
3
4
5
6
7
8
9
10
11
12
|
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class TagHelper extends Vue {
createTag() {
const name = window.prompt('请输入标签名');
if (!name) {return window.alert('没有输入内容,请重新创建标签');}
this.$store.commit('createTag', name);
}
};
|
重构Tags.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
|
// ...
<script lang="ts">
import {Component} from 'vue-property-decorator';
import {mixins} from 'vue-class-component';
import tagHelper from '@/mixins/tagHelper.ts';
@Component({
computed: {
tagList() {
return this.$store.state.tagList;
},
}
})
export default class Tags extends mixins(tagHelper) {
selectedTags: Tag[] = [];
created() {
this.$store.commit('fetchTags');
}
toggle(tag: Tag) {
const index = this.selectedTags.map(i => i.id).indexOf(tag.id);
if (index >= 0) {
this.selectedTags.splice(index, 1);
} else {
this.selectedTags.push(tag);
}
this.$emit('update:selectedTags', this.selectedTags);
}
}
</script>
...
|
重构Labels.vue
子组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//...
<script lang="ts">
import {Component} from 'vue-property-decorator';
import {mixins} from 'vue-class-component';
import Button from '@/components/Button.vue';
import tagHelper from '@/mixins/tagHelper.ts';
@Component({
components: {Button},
computed: {
tags() {
return this.$store.state.tagList;
}
}
})
export default class Labels extends mixins(tagHelper) {
beforeCreate() {
this.$store.commit('fetchTags');
}
}
</script>
//...
|
- 一处JS 处处JS
- 注意必须写全后缀
import tagHelper from '@/mixins/tagHelper.ts';
重构 EditLabel.vue
注意 store.commit
没有返回值
- 无法通过
this.$store.commit('fn')
来获取返回值,即使在回调函数fn
写了return
- 因为源码中
export interface Commit {(type: string, payload?: any, options?: CommitOptions): void;<P extends Payload>(payloadWithType: P, options?: CommitOptions): void;}
,所有最后的无返回:void
- 在所有
mutations
中的方法都不能写return
,写了无意义
在state
中新增一个数据currentTag: undefined,
- 通过
currenTag
来获取store.commit('setCurrentTag')
的返回值
在custom.d.ts
中声明所有状态的类型RootState
1
2
3
4
5
6
|
//...
type RootState = {
tagList: Tag[],
recordList: RecordItem[],
currentTag?: Tag,
}
|
在src/store/index.ts
中断言状态类型
1
2
3
4
5
6
7
8
|
const store = new Vuex.Store({
state: {
tagList: [],
recordList: [],
currentTag: undefined,
} as RootState,
//...
})
|
在EditLabel.vue
中预想提交'setCurrenTag'
方法,获取this.$store.state.currentTag;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//...
@Component({
components: {Button, FormItem},
computed: {
tag() {
return this.$store.state.currentTag;
}
}
})
export default class EditLabel extends Vue {
created() {
this.tag = this.$store.commit('setCurrentTag', this.$route.params.id);
if (!this.tag) {
this.$router.replace('/404');
}
}
...
}
|
在src/store/index.ts
中mutations
实现'setCurrenTag'
方法
1
2
3
4
5
6
7
8
9
10
11
12
|
//...
mutations: {
//...
/**
* 设置当前标签
* @param state: RootState
* @param id: string
*/
setCurrentTag(state, id: string) {
// state.currentTag = state.tagList.find(t => t.id === id);
state.currentTag = state.tagList.filter(t => t.id === id)[0];
},
|
- 在
src/store/index.ts
中,无法获取this.$route.params.id
- 需有
this.$route.params.id
的页面(view
)中的方法通过commit
第二个参数payload
传递而得到
setCurrentTag
方法代替原来的findTag
在 TS 里使用 computed
要用 getter
语法
存在console.log(this.tag)
,但会报错提示未设置getter
setter
- 在
EditLabel.vue
之前使用@component({...tag() {...}})
错了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//...
@Component({
components: {Button, FormItem}
})
export default class EditLabel extends Vue {
get tag() {
return this.$store.state.currentTag;
}
created() {
this.$store.commit('setCurrentTag', this.$route.params.id);
if (!this.tag) {
this.$router.replace('/404');
}
}
//...
}
|
将之前所有的写法更改
继续重构 EditLabel.vue
在src/store/index.ts
中mutations
里面的方法 只能接受两个参数
Muation
函数 不可为 async函数, 也不能 使用箭头函数来定义, 因为代码需要运行在重新绑定执行的上下文
- 重构
updateTag()
- 重构
removeTag()
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
|
// ...
const store = new Vuex.Store({
state: {
tagList: [],
recordList: [],
currentTag: undefined,
} as RootState,
mutations: {
// ...
updateTag(state, tag: { id: string, name: string }) {
const {id, name} = tag;
const idList = state.tagList.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const nameList = state.tagList.map(item => item.name);
if (nameList.indexOf(name) >= 0) {
window.alert('标签名重复类');
} else {
const tagItem = state.tagList.filter(item => item.id === id)[0];
tagItem.name = name;
store.commit('saveTags');
}
}
},
//...
|
在EditLabel.vue
中
- 注意输入时会触发
input
- 在
FormItem.vue
组件中不可同时写@Watch
和onValueChanged
方法监听变化,会触发两遍
- 重构
created
- 注意
setCurrentTag
默认tagList
是存在的 直接赋值state.currentTag = state.tagList.filter(t => t.id === id)[0];
- 取到后需要获取当前新的
tagList
,需要this.$store.commit('fetchTags');
- 重构
update
- 重构
remove
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
|
//...
@Component({
components: {Button, FormItem}
})
export default class EditLabel extends Vue {
get tag() {
return this.$store.state.currentTag;
}
created() {
this.$store.commit('fetchTags');
this.$store.commit('setCurrentTag', this.$route.params.id);
if (!this.tag) {
this.$router.replace('/404');
}
}
update(name: string) {
if (!this.tag) { return; }
if (name === '') {
window.alert('标签名不能为空');
this.$router.back();
return;
}
this.$store.commit('updateTag', {id: this.tag.id, name});
}
remove() {
if (this.tag) {
// TODO : if (store2.removeTag(this.tag.id))
this.$store.commit('removeTag', this.tag.id);
window.alert(`成功删除标签:${this.tag.name}`);
this.$router.back();
}
}
//...
}
|
- 注意如果
this.$router.back();
放在数据中index.ts
- 引入
router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import router from '@/router
//...
removeTag(state, id: string) {
let index = -1;
for (let i = 0; i < state.tagList.length; i++) {
if (state.tagList[i].id === id) {
index = i;
break;
}
}
if (index >= 0) {
state.tagList.splice(index, 1);
store.commit('saveTags');
router.back();
} else {
window.alert('删除失败');
}
},
//...
|
原来的数据index2.ts
可以删除了
Vuex 小结
- 所有代码
- 所有 commits
- Vuex 依赖
Promise
,特别是action
异步接口
面试问:你是怎么使用 Vuex 的(常用使用套路)
- 声明
const store = new Vuex.Store({/*options*/...})
{/*options*/...}
包括{state: {...}, mutations: {...}, ...}
等
state
中写表示状态的数据变量
mutations
中写表示状态变动的方法
mutations
中的方法通过传参state
作为代替this
指向本状态实例
- 通过
state
可访问本实例的状态数据变量进行操作
- 传且仅可传第二个参数
payload
表示接受其余参数
Muation
函数 不可为 async函数, 也不能 使用箭头函数来定义, 因为代码需要运行在重新绑定执行的上下文
- 在组件中调用状态数据的方法(读数据)
this.$store.state
- 在入口
main.js/ts
中注册了全局可访问的new Vue({router, store, render...}).$mount(...)
- 在
src/store/index.ts
中Vue.use(Vuex);
- 在组件中调用状态变更的方法(写数据)
this.$store.commit('methodInMutations')
- 传且仅可传第二个参数
payload
表示其他参数(可用对象存多个变量,)类似Array.prototype.apply
- 导出默认
export default store
其他注意
- 使用TS的
computed
是使用getter
和setter
函数
扩展功能 保存 vue-router 数据