vue

✍️ Tangxt ⏳ 2020-09-04 🏷️ LocalStorage

09-保存至 LocalStorage

★前言

涉及到的知识点:

  1. .sync 怎么用
  2. TS 怎么用
  3. window.localStorage 怎么用
  4. 数据迁移是什么

★收集四个组件的 value

1)代码实现

思路:

思路

我们要做的就是如何把这 4 个子组件的 data 上交给 父组件 Money ,其中需要注意的是,除了「备注」的数据,其余的都是父组件传给子组件的!

如果我们当初不考虑模块化,那么父子通信的问题也就无须考虑了! -> 似乎组件标签上,只能写自定义事件,而像元素标签则可以写原生事件!

我是用什么视角来看待父子组件的?或者说我眼中这样的代码:

<template>
  <Layout class-prefix="layout">
    <NumberPad :value.sync="record.amount" />
    <Types :value.sync="record.type" />
    <Notes @update:value="onUpdateNotes" />
    <Tags :data-source.sync="tags" @update:value="onUpdateTags" />
  </Layout>
</template>

在我脑海里的呈现是怎样的?

看父子组件

可以看成是鸡蛋白和蛋黄的关系,细胞质和细胞核的关系

细胞通信

代码:Demo

2)代码解析

原先我们把 Money.vue 分成了 4 个部分,而现在我们需要把这 4 部分给整合起来、互相关联起来! -> 把这些子组件的数据收拢到父组件上! -> 把用户填的东西都收集起来!

1、 `Tags.vue`

父组件(Money)区域监听子组件标签 Tags -> 一旦某个 tag 被用户选中,父组件就能拿到这个 tag 的内容


题外话:

把从 node_modules 里边导入的包写到上边去


我们用了 class 姿势写组件,那么局部注册组件,就得这样来:

@Component({
  components: {
    NumberPad,
    Types,
    Notes,
    Tags,
  },
})

不然,你在 class 里边这样:

class Money extends Vue {
  components = {
    Tags,
    Notes,
    Types,
    NumberPad
  }
}

那岂不是在写data

回过头来,当用户在子组件的这个界面区域,选中了一些标签,那么你就 $emit 一下,该 API 会抛一个信号并带着一个实参(选中的一个个标签)向上传递给父组件 -> 父组件执行 callback搞一些事情……

图解这个子父通信过程

自定义的事件名 update:value 是约定俗称的,当然,你也可以用 xxx 流之类的名称!

关于 callback 的命名 -> 某件事情发生了,那我就会回应点什么 -> on+你要回应的事儿 -> 如onUpdateTags,子组件update:value了,那我就在这个事情上(on),回应你,如UpdateTags

好的命名,让你不用写注释! -> 见名知义!


接下来看看如何获取用户输入的备注……

2、`Notes.vue`

跟上边的做法基本一致……

一个细节点,方方本来是这样起事件名的:

事件命名

之后就统一改成 value 了:

每个组件都有个 value 属性

统一把用户输入的数据,都叫做 value -> 不用操心是取type(来自 Types 组件的数据),还是 tag((来自 Tags 组件的数据))这样画蛇添足的命名了……

@Watch('value')

callback的第一个参数是新值 value,第二个参数则是旧值 oldValue

value发生一丁点的更新,那就$emit一下!

你在写代码的时候,看到文档是这样写的: val,那么你就改成 value -> 不推荐用简写,你无须跟着文档这样写,文档傻逼那时它的事儿…… -> 这样你的脑海中就不会有潜意识的把 val 转化成 value 的念头了!

3、`NumberPad.vue`

amount:总金额

本来是同上的做法,但为了代码简洁,用了 .sync 语法 -> 让我们少写了一些东西:

多了一些东西::value.sync="record.amount"

子组件抛上来的 data 会赋值给 record.amount

而这就是 .sync 给我们代码带来的 「糖」!


ok:触发事件的位置 -> 用户体验 -> 用户输入完数据,点 ok 按钮再触发事件,而不是一边输入一边触发事件,所以就不用@Watch了 -> 如果用户狂点ok按钮,那就是用户的事儿了,我们无须去处理(或者你可以给一个反馈给用户,比如「fuck!」)

作为一个前端,你在写代码的时候也要考虑用户交互的事儿了! -> 虽然这是产品经理的活儿……

4、`Types.vue`

为什么不在 selectType方法里边触发事件?

触发事件的代码位置

所以我们就用了 @Watch

watch

第一次进来会触发吗? -> 自己去测试一下,测试不通过,那就再改!

5、`Money.vue`

我们要把从每个子组件里边得到数据统一给记录起来! -> 所以为Money组件创建了一个叫record的状态!

话说,record的类型是啥?普通对象{}这样? -> 这是不行滴,因为在 TS 里边你不能直接说它就是一个对象,你得说这个对象有哪些东西才行 -> 把 record 的类型明确写出来 -> 如何写?

type Record = {
  tags: string[];
  notes: string;
  type: string;
  amount: number;
};

tags: string[];这个;可以是,,甚至可以不用写 -> 总之,有 3 种姿势可选(很宽容的做法) -> 一般都是用 ;(格式化插件自动帮我们加;,不需要我们手动添加)

TS 的类型声明和 JS 的变量声明特别像,如 var record = { tags: ['1','2']}

TS 的类型声明 -> 不要告诉我值是多少,我只关心类型 -> 只写类型,不写值

写这个类型声明的时候,你可以先用 JS 写一个有值的 record 出来,然后再抽象成类型声明的形式:

类型声明

至此,我们已经知道了如何在 TS 里边声明一个类型了!

总之,record是一个复杂类型,我们不能直接这样 record: Object,因为tsc并不知道record它有哪些属性 -> 所以就用了我们写的自定义类型 Record

关于给record初始值,如果你直接这样 record: Record = {},那么就会报错——「大哥,你得声明一下 Record 里边的四个属性呀! 」(根据类型声明,你这一个属性都不能少

所以我们就这样给初始值了:

export default class Money extends Vue {
  record: Record = {
    tags: [],
    notes: "",
    type: "-",
    amount: 0,
  };
}

在 Java 里边的定义一个接口类,只写方法签名,不写方法的实现代码! -> 而 TS 只写类型,不写值!

话说,我不写某个属性的初始值行吗? -> 也是可以的,你直接对某个属性的类型声明加个 就行了:

type Record = {
  tags?: string[];
  notes: string;
  type: string;
  amount: number;
};

这样一来,你就可写 tags 属性,也可不写 tags 属性了!

如果你觉得这样写很麻烦,转为用 JS 姿势,那就是懦夫行为,当然,如果你急着找工作,那你可以用 JS -> TS 的类型声明真得很简单,记住那七种数据类型就好了!

话说,TS 不是可以自动推测类型吗?我们不写 Record 这个类型声明,直接像写 JS 那样给record初始值,难道不行吗? -> 理论是可以的,但测试发现报错了! -> 总之,你不要省类型声明那几个字符,就跟变量命名不用简写一样,多写几个字不会死的……况且,你少写几个字,是很容易出 bug 的!


如何更新 record

直接对record旗下的一个个属性赋值就好了

更新 record

webstorm -> 代码高亮抽风情况(电脑性能不好所致) -> 剪切后重新粘贴,webstorm 会重新检查代码!


如何知道我们的代码对record的收集是正确的? -> 打 log -> 在 template里边用个插值表达式就好了!

测试一下:

测试

思考用户的行为(站在别人的角度思考我们代码这样的交互是否可行) -> 假如用户不选标签、也不输入备注? -> 无关紧要,这两个都是可选项 -> 默认选项的设置:0是一个,'-'也是一个(支出出现的频率高,所以就用了'-'作为默认值,而不是'+'

3)小结

如何监听四个组件的变化 -> 把这四个组件的值收集起来,然后放到父组件的record里边去!

话说,我们该如何把record存到数据库里边去?

在解决这个问题之前,还得解决一个问题,那就是我们要把来自父组件record的初始值传给子组件,而不是让子组件自己决定自己的初始值,如Types组件的type状态:

type 的状态应该用 prop 来声明

需求变更,如默认是 '+'而不是'-',那么你就得改两处地方了 -> 这是靠记忆知道要改两处地方,但人类记忆一般都是不可靠,所以你很有可能改了 Types 这处的,忘记了Moneyrecord这处也要改!

所以我们需要让这两处变成是一个变量 -> 也就是 Types 里边的 type 值必须是从外界输出过来的! -> 总之,默认值是外界给的,不能是 Types 这个组件自己定的!

想到这里,我们修改了Types代码 -> 代码更少了,更简单了,而这是因为 Types 组件不用去操心自己的 data了,毕竟数据是来自于外部的,外部说是啥,那就是啥,Types组件没有资格直接赋值更改它,只能在修改值的时候,通知外部改值,然后再让外部改完值后再传回来!

更改的结果:@Prop() readonly value: string -> 然而这样报错了!(tsc说你没有初始化) -> 可我们能确定这值是从外边传进来的呀! -> 我们能确保不需要初始化

那就改成这样:@Prop() readonly value: string = '-'? -> 可这不是原先那种情况吗?而且,我加了readonly,表示 value 是不能写,只能读的!

所以,我们该如何给value一个默认值?

两种姿势:

export default class Types extends Vue {
  @Prop({default: '-'}) readonly value!: string
  // or 
  @Prop() readonly value!: string
}

前者表示,如果父组件没传初始值,那就用 '-',但我们能确保它是传了的,因为这代码是我们自己写的呀!所以我们用的是后者!


更新 Type 组件 value 状态的位置:在 selectType 里边,而不是@Watch里边! -> 因为 value 现在是 prop 了!value的变化是交给父组件去改的!所以你去监听 value 是毫无意义的!

回到 Money ,再看一下 .sync 的用法:

.sync

话说,record.type的状态是如何变化的? -> recore.type = $event$event是子组件$emit传过来的参数) -> .sync就是把这个参数赋值给省了,还有把那个@update:value也给省了 -> 整体上看,子组件想要的值手递手传给了父组件,然后父组件再传回给子组件

.sync 图示

.sync这个语法糖的效果就像是父子之间的双向绑定一样……

v-model一样,这都是伪双向绑定,本质都是单向数据流,因为这背后都存在一个事件作为第三方,就像是儿子要爸爸打钱,需要经过妈妈的同意才行…… -> 但这俩东西的存在,确实实打实地让我们的代码不用出现事件监听器代码,而这就给人一种是双向绑定的错觉了!

话说,v-model.sync 有啥区别?

v-model.sync还有的区别:在一个组件上,只能使用一个v-model,而.sync则可以多次使用

总之,.sync的使用场景很广泛,而且相较于v-model来说更方便…… -> 万金油


关于一致性的追求:

一致性

因为父组件并没有打算给子组件 NotesTags 一个初始化值 -> 也就是没有传value属性,所以这也就不打算用.sync了,总之,让这俩东西自己去维护用户输入的值,而父组件只管接收这俩东西传上来的数据就行了!

如果后边有需求,那你就自己改成是.sync的姿势吧!如用户在记一笔账的时候,默认就有'食'这个标签,这个时候,我们就可以像改 Types 组件那样,改Tags组件了!


一句话概括 .sync,那就是父组件拿到子组件的最新值,然后再手递手传回给子组件!

★使用 .sync

1)那个 `NumberPad.vue` 的改造

父组件区域:

<!-- record.amount -> number -->
<NumberPad :value.sync="record.amount" />

子组件的映射代码:

子组件的改造

0来自于外界,把来自于外界的0字符串化! -> 注意,我们改 output 的状态,除了ok 方法要告诉外界子组件要改 output,其余的方法都是直接改 output,所以这一点看起来是有点不一致的,但为了方便起见还是这样做了!

这样的做法都是为了让NumberPad的默认值由外边控制,而不是自己内部写死!

:value.sync 这个 value 的命名是方方的命名习惯,你可以改成是 :x.sync,那么事件就是 update:x


接下来看看如何把 record 的值存到数据库里边去!

★使用 LocalStorage

有数据库吗? -> 用浏览器提供的 LocalStorage 功能!

1)代码实现

思路:

当用户点击 ok 的时候,我们就把 record{}) 的副本值 记录到 recordList[{},{}……]) 里边去 -> 把 recordList 字符串化 扔到 localStorage 里边去!

代码:Demo

2)代码解析

window.localStorage.setItem("recordList", JSON.stringify(this.recordList))

localStorageItem存储的值是字符串!所以我们需要把[{},{}……]这样的值给序列化一下!

效果就像这样:

JSON.stringify([{name:'frank'}]) // -> "[{"name":"frank"}]"

localStorage


onRecordListChange这个名字,方方不喜欢加d -> onRecordListChanged


为什么是这样:

{
  saveRecord() {
    const record2 = JSON.parse(JSON.stringify(this.record));
    this.recordList.push(record2);
    console.log(this.recordList);
  }
}

而不是这样:

{
  saveRecord() {
    this.recordList.push(this.record);
    console.log(this.recordList);
  }
}

因为如果你选择后边这种做法,那么你每次push都是在push同一个引用了! -> 而这样的结果就是[]里边的一个个{}都一样的!

要序列化一下每条记录再 push

所以,我们就需要把record深拷贝一下 -> 把得到的record2(副本) pushrecordList里边去!

对象 -> 字符串化 -> 对象化

recordList 存储的是每条记账的记录!

recordList


2)处理一些小 bug

代码:Demo


每次点击 ok :把output 里边的内容重置为 '0' -> 而不是还有上一次记账所输入的金额!


每次刷新这个Money页面,recordList都重置为[],而这显然是不合理的,所以我们就这样干了:

const recordList: Record[] = JSON.parse(
  window.localStorage.getItem("recordList") || "[]"
);

Money组件的recordList状态的初始值来自于localStorage! -> 拿到值是字符串值,所以我们需要把这个值 JSON.parse 一下!

recordList的数据 -> 需要交给「统计」页面去统计!


我们需要把一条账的创建时间给记录下来:

记录每条记账的时间

可选需求:用户可以自己改时间记一笔账 -> 我们这里的时间都是随时记的!不关心这笔账是几点花出去的!

题外话,类型声明语法:

type Record = {
  tags: string[];
  notes: string;
  type: string;
  amount: number;
  // or createdAt: Date || undefined
  createdAt?: Date;
};

Date是拓展类型,不只是那七种类型! -> 简单来说,我们除了可以写数据类型以外,还可以写 这样的东西!(在 JS 里边 叫「构造函数」)

注意,类 和 类型 是不同的 -> 类型是更大的数据划分,如 JS 的七大数据类型(string | object | ……)!其中 object 可以细分成不同的 ,如Date | RegExp | ……(这是大写字母开头的)

总之,「类」是比「类型」更小的一个概念!,因为「类」是对 object 这个「类型」的分类,而「类型」则是所有数据的分类!


在写代码时的提示问题:

代码提示

为啥不提示? -> 因为 ts 认为 record2 是一个 any 类型,即 ts 不知道 record2 是啥类型,也就是任意类型啦!

所以,我们就这样做了:record2: Record -> 当你再写 record2. 的时候,vscode 就会提示你它有 5 个属性!


用户先前的记录是咩有时间这个字段的,而现在我们添加了时间:

数据库升级

问题来了:网站的数据库升级了(多了一个时间字段),而用户的数据库并咩有升级,我们需要通知用户「把数据库清空一下」? -> 这些数据都是重要的呀! -> 简单来说,这涉及到一个数据库的版本问题

所以我们最好再追加一个有关数据库版本的变量!(发现用户的数据库版本不对,那就在保存一笔账前,先把用户的数据库给升级了,再保存到localStorage里边去)

数据库变化了,如何做数据迁移?

★数据迁移策略

1)代码

大概了解那个意思

代码

2)迁移的复用

一次挨着一次的升级,而不是一步登天

迁移策略

关于升级的策略:

0.0.1 -> 0.0.2 -> 0.0.3 -> 0.0.4…… -> 0.0.10

不是

0.0.1 -> 0.0.10

这样一来问题就变得简单很多了!

3)小结

为了避免数据库迁移的问题,一定要在想数据的时候一次性想好! -> 也就是一张表里边有哪几个字段,一定要一次性想好!


至此,保存数据库一块内容就搞定了!

下一篇,对数据库的操作(读和写)进行封装!

我们不想每次「读」的时候,需要写这么一长串的代码:

const recordList: Record[] = JSON.parse(
  window.localStorage.getItem("recordList") || "[]"
);

「写」的时候,也要写这么一长串的代码:

window.localStorage.setItem("recordList", JSON.stringify(this.recordList));

★总结

父组件 vs 子组件:

如何看待父子组件

★Q&A

1)解析 和 分析的区别?

解析表达的意思更为严谨,是内在的分析。分析是外在、可识别的区分。

分析父组件,解析子组件

➹:词语 解析 和 分析 的区别是什么_百度知道

2)在组件标签上只能写自定义事件,而原生事件只能写在元素标签上?

我们想要在一个组件的根元素上直接监听一个原生事件,那么可以这样:

v-on + .native

<base-input v-on:focus.native="onFocus"></base-input>

所以我们在组件标签上也可以写原生事件,但这并不好!因为如果根元素不是input的话,那就 GG 了!

➹:自定义事件 — Vue.js

3)Cookie、localStorage、sessionStorage?

三者的异同:

三者异同

➹:详说 Cookie, LocalStorage 与 SessionStorage - 咀嚼之味

➹:Cookie、LocalStorage、SessionStorage 差異 - by Peggy Chan - Medium

➹:Window.localStorage - Web APIs - MDN