vue

✍️ Tangxt ⏳ 2020-09-06 🏷️ 标签页、标签编辑页

10-标签页+标签编辑页

★model.js & model.ts

1)为什么要 MVC 思想重构代码?

因为目前的 代码,在数据层面上是很混乱的!

所以我们需要对页面做代码分层 -> 用 MVC 思想过来搞 -> 关于这个思想,你记得越模糊越好!如 M 负责数据层, V 负责视图层, C 负责其它的业务逻辑!

目前代码有视图层 template ,也有控制器(一个个更新组件状态的方法),但唯独就是差了个 Model ,不过,实际上,我们的 Money.vue 是有 Model 层面上的代码的!

如:

初始化数据,从数据库里边拿数据:

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

保存数据到数据库里边:

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

MVC 这个思想可以影响一个前端工程师的整个生涯,因为你只要写网页,那么就会用到 MVC,毕竟它实在是太经典了,而你写页面,不可能不会用到数据

2)代码实现

如何封装一个model

思路:

思路

我们 push 一笔账( {} )到 recordList[] 结构的表)里边, @Watch 是可以 watchrecordList 的变化的!

代码:Demo

3)代码解析

1、本来的做法 model.js

创建了一个model.jssrc这个根目录下)-> 把获取数据和保存数据的代码扔进去:

js 姿势的 model

接下来我们要在 ts 里边导入 model.js,可是 ts 里边真得用 js吗?

测试发现,我们不能通过 import导入这个model.js,因为这种姿势与ts不相容!

而用了require这种姿势却是可行的!

require

require一开始是不支持export defalut xxx这种defalut姿势的,之后为了兼容就导出了带有defalut的!

所以,如果你在require的时候,就require('xxx.js').default这样搞!

而前端界里边有一个大佬呼吁大家不要用 export defalut xxx,而是用具名的姿势:export {model}(就是导入列表姿势啦!) -> 引入的时候就 require('xxx.js').model(表示我要这个xxx文件里的model

可以用析构语法:const {model} = require('model.js')

总之,不管你用 export defalut model 也好,还是用 export {model}也罢,都随你的便! -> 个人建议用后一种,因为语义性更强!

我们在ts里边引入js -> 使用导入进来的model -> 在用 vscode 写代码时是没有提示它有哪些 API 的!(webstorm 可以猜出model的类型信息) -> 这就是为啥不用model.js而用model.ts的原因!

对了,用了这种姿势,你还得测试一下使用了导入进来的modelMoney.vue是否正常运行!(一定要测试代码是否正常运行,这一点贼重要

小结:不推荐使用 JS,因为这是懦夫行为,当然万不得已你可以这样!

接下来看看,如何把 JS 改写成 TS!

2、model.ts

.js后缀改写成.ts后缀 -> 这一步就完成了 99%的工作,剩下的工作就是为变量加: 类型了!

🤔:原先代码的失误

我们之前局部定义了一个Record类型,但该类型是 TS 默认就有的高级类型,所以我们是不能把天生自带的,定义成自己的

总之,Record局部声明你随便改没问题,但全局声明就有问题了,毕竟Record这个名字已经被占用了!不然就会冲突

所以我们改成是一个不冲突的名字就可以了,如RecordItem

由于现在要用RecordItem,而之前我们都是用 Record的,所以有什么快捷方式能快速让Record变为RecordItem? -> 编辑器都提供了一个叫「重构」的功能!

💡:如何让一个自定义类型能在所有文件里边使用?

创建src/custom.d.ts-> ts发现.d.ts这个后缀文件,就知道这是一个全局的声明文件

把需要全局使用的类型声明扔到这个文件里边……

全局的类型声明

一个小技巧:

查看类型声明

你写到每个RecordItem类型都会关联到这个custom.d.ts文件!-> 关于custom这个名字,不是固定的,你也可以叫xxx.d.ts,但是custom这个名字语义性更好,表示这是我们自定义的类型!

💡:用了model.ts就可以用import语法了吗?

当然可以:

// Money.vue
import model from "@/model";

// model.ts-> 可以使用默认导出了!
export default model;

当然,你也可以用有名字的导出,不过,这就需加{}了:

// Money.vue
import {model} from "@/model";

// model.ts -> or export model -> 推荐带 {} 导出,因为可以一眼看清楚输出了哪些变量。
export {model};

简单区分一下默认导出和有名字导出:

💡:如何确定fetch的返回值类型?或者说整个model.ts里边的类型?

确定类型

随便搞个 x 变量,就能得出一个个很长的代码的返回值类型了!

可以看到这是一个any类型

有两种姿势可以改变x的类型:

姿势有两种

关于姿势二,注意不要把类型强制错了(出错了,就是你活该!),因为 tsc 就是根据你强制出来的类型来判断JSON.parse()返回值的类型的!

TS 很简单,把类型说清楚就完事儿了!-> 说不清楚,那你就用回 JS 吧!

为什么要指定model.fetch()的返回值类型?也就是给返回值一个as xxx这样的断言?

TS 自动推测类型

方便了我们之后不用再写:RecordItem[]了,不然,每个变量都得写一遍 :RecordItem[]……

最开始的地方写对了,那么后边用到了就会自动推测,千万不要为了图个方便,就不给model.fetch()的返回值一个断言(as RecordItem[]),不然,你每次需要用到model.fetch()的返回值,都得加上 :RecordItem[]

💡:为啥要把 JSON.parse(JSON.stringify(xxx)) 这个操作也封装到model.clone了?

因为搞个副本对象这种需求,有很多种姿势可以做到,而我们并不关心是用哪种姿势做到的,反正你调用一下model.clone能完成这种需求就可以了,至于clone的背后用了啥,一点都不关心,既然不关心,那就封装一下……这样代码看起来就很语义化了

封装的意义

感觉封装就是为了让我们写的代码语义性更强!更能读懂代码在做什么!-> 你想想 JSON.parse(……)好懂,还是clone()好懂?-> 就像是你叫「名字」好懂人家叫谁,还是叫「头发短的、小眼睛、大嘴巴」好懂人家叫谁?

后边会把这个model.ts给删掉!

★Labels.vue 之 HTML

1)需求与代码实现

看着设计稿怼就行了!

标签页

标签页里的每个标签都是可以 click 的,click之后就跳到标签的标签的「编辑页」,注意,标签名是不可以重复的(我们没有用id来区分所有标签)

点击「新建标签」button -> 与 Money 页面的「新增标签」功能是完全一样的!

点击「删除标签」button -> 回到「标签页」列表

需求图示

代码:Demo

2)标签页面

写 HTML -> 写 CSS -> 写 JS

💡:为啥要用一个span标签包裹一个文本?

span

💡:页面的背景色写在哪儿?

写在 body 里边!

★Labels.vue 之 CSS

1)写 CSS 顺序

  1. 文本、icon -> 字体大小
  2. 文本所处盒子的min-heightpadding等 -> 有关布局
  3. 边框、icon、文本 -> 颜色
  4. icon -> 布局
  5. button -> 背景色、文本颜色、布局、盒子模型

元素上下之间的间距用padding,因为margin会有塌陷,不过padding也会影响background的范围……当然也可以结合起来使用!

💡:一个你使用scss,但一直误会的点?

scss 选择器

&这个符号的作用,可以把嵌套的写法编译成平级的写法:

#main {
  color: black;
  a {
    font-weight: bold;
    &:hover { color: red; }
  }
}

编译为

#main {
  color: black; }
  #main a {
    font-weight: bold; }
    #main a:hover {
      color: red; }

如果咩有&,那就是存粹的后代选择器了!

总之,被嵌套的也有可能是爸爸级元素!

★新建标签功能

💡:标签存在哪儿?

之前我们用了一个model来专门存储一笔账!而现在要存储的东西是「标签」呀!

为啥要存储标签呢?

我们在Money.vue里边是这样写的:

tags = ['', '', '', '', '彩票'];

显然,这标签是写死的!不管你新建了多少个标签,页面一刷新,新建的标签都会统统消失掉!

所以我们搞了一个叫 tagListModel 的东西

之前,我们只有一个model,所以就创建了一个叫model.ts文件,而现在还需要model,所以现在的目录结构变成这样了:

model 目录

本来文件名是没有后缀Model的,但不加后缀的话,在使用的时候会有名字冲突! -> 所以就随便加了一个后缀!

💡:tagListModel.ts如何写?

结构基本与recordListModel.ts一样,所以 clone 一份 recordListModel.ts里边的内容粘贴过来,然后修改就好了!

结构很像 -> 意味着重复 -> 之后会试着重构一下,如把这两个model合成一个! -> 而目前我们先把功能完成再说!

💡:每个组件的template里边,基本不会出现写死的数据?

数据有了,动态生成 DOM 结构

💡:往tagListModel对象里边封装了很多操作?

fetch(从localStorage里边拿到['11','222'])、create(创建标签,如'3333')、save(把['11','222']保存到localStorage)等这样的操作

💡:对象有个属性data,可它的类型该怎么声明呢?

这样做?

语法

我们想要这个data的类型是元素是字符串的数组,而不是元素是对象的数组之类的……

所以,我们搞了个类型声明定义:

type tagListModel = {
  data: string[];
  fetch: () => string[];
  create: (name: string) => "success" | "duplicated"; //联合类型
  save: () => void;
};

=>左边(name: string)里边的内容是输入的类型(输入咩有就是一个空的()),=>右边的string[]则是输出的结果的类型 -> void是不返回

这可不是箭头函数语法哈!

不要问为什么要这样写,因为语法就这样规定的!你看着方方写的代码依葫芦画瓢就行了,下次遇到同样的场景,就照着这个姿势去处理就对了! -> 当你写多了,你也就感觉也就那样了,这语法没啥奇怪的!

💡:为啥要在tagListModel对象里边写个data属性?

因为tagListModel想自己维护data,如果你Labels页面想要操作data,那么你就得用tagListModel提供的 API 来操作!而不是让页面直接操作用户数据!

这种处理,跟recordListModel对比是不同的:

data 处理异同

tagListModel这样做的好处:

好处

更像 MVC 了,Labels组件的tags状态值,只是视图渲染的数据源!而 C 则是负责调度M去操作data

再一次理解 MVC

别忘了数据分为 UI 数据 和 用户数据

💡:方方喜欢把创建的东西return回去!

习惯

💡:用户在创建出来的标签重复了,我们该如何提示用户「标签重复了」?

新手做法:创建 成功/失败 就返回ture/false -> 根据返回值提示用户

老手做法(考虑很多):比如为啥创建失败?需要把失败原因给返回出去!

姿势有两种:

姿势一:

抛出错,捕获错误

姿势二:

按照方方的经验来说,最好是返回一个数字或者返回一个对象!

如:

返回数字这种姿势可行,但数字这种姿势语义性不好呀! -> 很容易遗忘……

所以我们用了字符串姿势来表示错误原因!如返回'duplicate'就是重复错误,而'success'表示创建成功!

返回对象的姿势是这样的:

{
  // 错误码
  code: number,
  // 错误码所代表的含义
  message: string
}

这种返回值也是 ok 的 -> 为了简单起见,返回字符串!

注意,返回字符串这种姿势,很容易会写错单词,所以我们需要统一规定一下:

联合类型

当你拼错单词的时候,tsc就会爆红:

为啥要用联合类型

联合类型是字符串的子类型 -> 也就是说,不是所有的字符串,而是就那两个字符串! -> 这是不属于那 7 大类型里边的其中一个,类似于枚举

★EditLabel.vue & Vue Router

1)需求与代码实现

编辑标签

顶部导航栏 -> 方便回退到「标签页」 -> 如果咩有这个那就只能点击底部的那三个导航了,而这贼其不方便!

代码:

2)代码解析

💡:我们要编辑一个标签需要用到它的id

id一般是数据库自动生成的,在这里我们自己造一个假的id(方便起见,把标签的name当作id)!

正常的id是一串数字或者一串数字字符串!而我们这里则是非数字的字符串

之后会改成id生成器!

💡:数组 API map的用法?

map

💡:在「标签页」里边点击了某个标签,然后就会跳转到「标签编辑页」,那么这个「标签编辑页」是如何知道我们刚刚点击了哪个标签?

获取另一个页面的数据

路由配置一下:

{ path: "/labels/edit/:id", component: EditLabel }

:id是占位符 -> 可以是123等这样的值!

话又说回来,我们该如何获取路由信息?也就是如何获取那个:id值?

用 Vue 提供的钩子 created 来获取 -> 为什么? -> 因为我们需要用到this呀!

this.$route.params // {id: '1'}

一般跟路由相关的信息是放在$route里边的(拿到路由路径信息之类的),而跟路由器相关的信息则放在$router里边(处理转发之类的)!

注意,如果你写的是:fuck,那么你就得这样做了:

this.$route.params // {fuck: '1'}

所以:id并不是固定的写法 -> 它相当于是给这个值一个名字!

方方的一个习惯,如果不确定一个东西是哪儿来的,那就把这个东西改成fuck! -> 如我们不确定parms所返回的{id:'1'}里边的id是哪儿来的,于是就用了:fuck测试一下! -> 得出结论:{id:'1'}里边的id来自于路由配置里边的:id

透过这些 API ,「标签编辑页」就知道用户要编辑的是哪一个标签了!

话说,params为啥会有id这个参数?不需要声明它的类型吗?

id

Dictionary类型意味着 params这个 {} 里边可以是任意的键值!

为什么this会有个$route的东西? -> 因为我们安装了一个库 vue-router

Vue 实例多了两个信息

这两个属性 API 的作用 -> 无非就是在收集一些信息罢了!我们无须关心它们的源码实现!

💡:this.$router.push("/404") & this.$router.replace("/404") 的区别?

前者跳转后,不能点击<-回退到上一个页面,而后者则可以点击<-回退到上一个页面!

replace的效果:

replace 的效果

💡:如何把「标签页」和「标签编辑页」关联起来?

代码:Demo

很简单,对「标签页」里边的每个item使用 router-link 包裹一下就好了

目前,每个item是这样的结构:ol > li

我们不能改成这样:ol > router-lin1k -> 因为ol的儿子只能是li,所以我们改oldiv就好了

所以,最后改成这样了:

router-link

图中那个class是在a标签上的

样式作用的位置

总之,我们把div > router-link 看成是原先的 ol > li 就好了! -> 在样式方面都是用class来管控的,而不是用标签,所以更改标签基本上对样式无影响!

💡:#/labels是标签,tags也是标签,那么在命名方面能否统一起来?

💡:如何找到对称的 Icon

我们很难找到相匹配的 Icon,所以我们可以把right.svg上传到 iconfont.cn ,然后借助它的 svg 编辑器旋转修改!

我的做法是这样的(不用上传):

拷贝

旋转处理:

旋转处理

方方的一个习惯:

在上传文件给iconfont.cn的时候,首先把源文件拷贝一份到桌面(桌面上的文件是不重要的!可以随便删!),然后再上传

💡:EditLabel.vue用到了类似Money.vue里边的那个Notes.vue,那么我们能否把这个Notes组件封装成更为通用的组件呢?

Money页面需要用到输入框,EditLabel页面同样也需要用到输入框

对于这整个记账应用来说,输入框都应该是同一个组件才对!而不是拷贝一份Notes组件代码,修改一下,扔到EditLabel.vue里边直接用就完事儿了……

★如何封装通用组件

1)做法

  1. EditLabel 里边使用 Notes 组件
  2. 发现有问题 -> 字段提示信息不是我们想要的
  3. Notes组件 -> 用字段提示信息是由外界提供的
  4. Label.vue出问题了 -> 修复

代码:Demo

2)代码解析

💡:@Prop({required:true}) fieldName!: string;

你这样定义fieldName属性,那么在传值时,你得这样来:field-name

这个字段是必填的 -> 加!是表示我不需要初始值,因为用户必填有一个值!

如果不加!,你必须给个初始值!不然,tsc会报错!

总之,!是非null和非undefined的类型断言!

另一种理解姿势:

明确的赋值断言

➹:【前端资讯】TypeScript 2.7 发布 - 知乎

💡:@Prop() placeholder?: string

?用于属性定义时,表示该字段有可能不存在!而属性读取时,该值可能为空值(nullorundefined),所以需要做判断!

如果你用!,那么意味着你不传placeholder字段,它就是默认的undefined值!而我们并不想要这样,所以就用?

我测试了一下,不管用!还是?都是一样的!用户不传placeholder,那么这个Notes组件的placeholder属性就是undefined值了! -> 我还以为用了?,用户不传placeholder,那么这个Notes组件就咩有这个placeholder属性!实际上,不管传不传都会有这个属性!

➹:对比理解 Typescript 中的 as、问号与叹号 - 掘金

💡:关于Notes.vue这个名字?

其实这个Notes名字不怎么合适,因为它的功能就是个输入框,所以可以叫做EditItem or InputItem 之类的……

重命名这个Notes.vue(功能必须先完成,而且还得先提交一份代码,然后再考虑重命名):

在 VSCode 里边,全局搜索Notes这个名字,然后替换成FormItem就好了!(只改使用到的组件名以及class名,其它的方法名,字段名还是用notes

3)删除按钮

Button也改成通用组件! -> 为啥要改? -> 因为「标签页」有个「新建标签」的Button,而「标签编辑页」也有个叫「删除标签」的Button

💡:新建了一个Button.vue,可是原生标签是button呀!难道这不会冲突吗?

不会的!因为一个是大写B,一个是小写b -> 最终会编译这个Button组件标签!

💡:改用为Button组件标签后,如何触发它的点击事件?

我们监听了大Button的点击事件,但用户在页面上点击的是编译过后的小button -> 用户点击小button的时候,触发大Button的点击事件!

button

<button class="button" @click="$emit('click',$event)">
  <slot />
</button>

Button

<Button class="createTag" @click="createTag">新建标签</Button>

话说,如果还要监听其它事件呢? -> 那我岂不是还需要写很多次$emit()

尤雨溪发明了另一种语法:

<!-- template -->
<button class="button">
  <slot />
</button>

<!-- use -->
<Button class="createTag" @click.native="createTag">新建标签</Button>

这种姿势也是可以的!.native相当于是一种语法糖! -> 一种封装操作!

不过,这种姿势很少人知道,所以一般都会用上边那一种,即内部通知外部触发click事件!

总之,不管怎样,整体上看这个Button组件标签,我们都可以把它当作像使用普通的button原生标签这样去使用它!

一个权衡:Button很容易会被使用到,那么我们是否可以全局引用它? -> 随便……


始终谨记:先搞定 HTML,再去写 CSS,最后再搞 JS

★EditLabel.vue 之 CSS

代码:Demo

1)写 CSS

💡:HTML 结构?

HTML 结构

HTML 代码重复 2 次及以上,就应该考虑组件化!

💡:navBar的布局思路?

思路一:

左边Icon绝对定位,而title则是绝对居中 -> 为啥不左中右布局呢?因为右边没东西呀!不然,就很不对称了! -> 但这有个弊端,那就是Icon上下不好居中!

思路一

思路二:

flex布局! -> 还是用了左中右布局,因为Icon不好上下居中!

💡:关于方方写样式时的一个比较喜欢的习惯?

选择器先吃

💡:方方在 CSS 的时候,其大脑在干什么?

加 border

从中可以看到Icon并咩有上下对齐! -> 利用 flex 提供的那几个属性可以实现上下居中!

💡:为什么会有个没有内容的标签:<div class="rightIcon"></div>

右 Icon

💡:为什么方方写的 CSS 代码经常性的出现一个classxxx-wrapper嵌套div

因为不想直接在:

<FormItem field-name="标签名" placeholder="请输入标签名" />

上给背景色,而是给它的爸爸一个背景色!

为啥不叫container? -> 因为这单词比wrapper长!

💡:一个不成文的规定?

高度一致

让一个元素看起来变高(用背景色衬托),可以给它的爸爸加个上下padding,如Money.vue里边的Input框就是这样做的!

💡:button元素上下距离的处理?

上下距离

具体测试效果:

距离测试

★EditLabel.vue 功能实现

写这个代码之前,一定要先把之前写的代码给提交了! -> 你做了一件事情就要commit一次,而不是做了多件事之后再提交!

1)如何获取标签名然后展示之?

功能:把用户选择的标签展示到「标签编辑页」的输入框

功能

思路:EditLabel.vueFormItem.vue的父组件,所以我们只需要传一个valueFormItem就行了

💡:我们要让Input框里边的内容从外部传进来?(把用户选中的标签名传进来)

代码:Demo

💡:为什么要写单元测试?

你会经常性地改代码,改完之后,还得测试一下项目是否有报错! -> 如何测试呢? -> 让项目运行一下

而有了单元测试,就不用项目运行了,直接跑测试代码就行了!

💡:为什么不推荐用v-model

我们给FormItem.vue一个Prop

@Prop({ default: "" }) value!: string

结果在测试(输入内容)的时候报错了:

测试报错

为啥会报错呢?(具体来说应该是警告)

错误原因

所以对于Prop,那就不要用v-model了,如果是data,那倒是可以使用!

消除警告:

消除警告

为了稳妥起见,我们对Prop加了个readonly,以防他人对该value属性在子作用域直接赋值!

@Prop({ default: "" }) readonly value!: string;

如果你不加的话,那么只能让控制台提示你这儿有错误了,如果加上的话,tsc 就能直接提示了有错误了!

我测试了一下,加了readonly,但是还是原来的v-model,结果tsc并没有爆红! -> 为啥呢? -> 因为 tsc 检查不到 template 里边的内容!

但不管怎样,加上readonly是很有必要的! -> 因为你有可能在方法里边出现对value赋值的情况! -> 而一旦出现了,就会爆红!

代码:Demo

💡:如何展示标签名?

代码:Demo

再次解释一下这样的代码:tag?: { id: string; name: string } = undefined

它表示tag这个属性是可选的,如果你要给tag值,而且你没有给?,那么你就不能给undefined值了!

总之,你加了?就相当于是把tag当作是union类型,即相当于你这样{ id: string; name: string } | undefined tag 类型!

所以这就是我们可以给tag赋值一个undefined作为初始值的原因!

给?

2)用户修改标签名

改标签名

代码:Demo

💡:对于@update:value="update",为啥不这样写update($event)

你不写 Vue 默认会传个事件对象!

不过,在这里第一个参数值,是内部抛出来的值!

💡:tagListModel.ts里边的update(id,name)的逻辑?

  1. 拿到idname(修改的值)
  2. 如果表里边咩有这个id,那就不用更新name了! -> 返回'not found'
  3. 如果表里边有这个id,而且这个name与表里的name重复了,那就返回'duplicated'
  4. 如果表里边有这个id,而且这个name与表里的name不重复,那就修改这个id所对应的name值,然后save一下表 -> 返回'success'

注意,我们只是更改id所对应的name -> 不会更改id

这个代码写得不是很好,还有优化的可能!

💡:为啥要判断this.tag是否存在?

判断是否存在

3)用户删除标签

删除标签

本质是删除一个数组元素(标签表[{id,name},{},{}]

效果

思路:还是交给Model层去处理 -> VC层才不会去管数据处理呢!

💡:tagListModel.remove(this.tag.id)的逻辑?

要删谁? -> 传个id就知道要删谁了!

删除代码

4)两个 bug

💡:id重复的问题

id 重复

我们原先在创建标签的时候,判断标签名重复是根据name值来判断的,而不是根据id! -> 原先的做法是很取巧的!

要解决这个问题很简单,生成随机id

💡:路由返回的问题

路由

路由代码是这样的:

this.$router.back()

这个问题不需要解决,因为用户一般都是从「标签页」进入到「标签编辑页」的!可不会打开谷歌页面 -> 敲下「标签编辑页」的地址 -> 回车!

路由 bug 复现

★ID 生成器

代码:Demo

1)ID 的原则

一般来说,每个数据都应该有id -> 咩有id,就咩有办法定位到这个数据!

有以下两个原则:

  1. 一旦给了 id,就不要修改 -> 如果修改了,那么原先可以访问到的id就找不到标签了!
  2. id 不能重复! -> 如果重复,就不知道同样的id所对应的到底是哪个标签了!

总之,请遵循以上这两个原则!

2)如何造一个 id ?

在 JS 里边,Number类型的值就是有边界的(超出边界可视为爆栈),即达到多少位数字后,计算就不准确了! -> 当然,这并不是我们应该考虑的问题!因为标签不可能有那么多个!更何况如果真有那么多的话,还有BigInt这个数据类型呢!

数字

💡:关于src旗下的lib目录?

lib目录 -> 存放着自己写给自己使用的库!

💡:为啥要在localStorage里边存id

以防页面刷新后,id重置为0

关于这行代码:

let id: number = parseInt(window.localStorage.getItem("_idMax") || "0") || 0;

第一个字符串"0"是保底值,保底第一次存储时,从0开始!

第二个数字0是为了防止其它人手贱搞了个'xxx'的值,如parseInt('xxx' || '0')的返回值是NaN

💡:什么时候生成id

临近用的时候再生成id

id 生成时机

💡:点击「删除标签」按钮,可以自动回到「标签页」?

回退


目前,我们已经搞定了标签的查看、新建、编辑、删除!

3)目前发现的 bug?

💡:标签页与记账页 -> 标签同步的问题

标签同步

需要用到vuex

💡:原先用了「衣服」这个标签作为一笔账的标签,但是现在我改了这个标签名,我是否要同步更新一下?

修改标签

★了解更多

➹:ts 中的 Record – 七月时光

★Q&A

1)`export` 一个变量 与 `export` 一个`{}`的区别?

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

等价姿势:

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

// 导出列表
export { firstName, lastName, year };

import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

export命令后面,使用大括号指定所要输出的一组变量-> 我很好奇这是不是在导出一个对象?其实这并不是,因为你有见过这样的语法吗?

// 重命名导出
export { variable1 as name1, variable2 as name2, , nameN };

如果是一个对象的话,那就不能用 as name1 这样的语法了,所以 export 后边跟着的东西是特殊的语法

而对于「导出列表」这种姿势,外部的import的姿势则是 import {xxx} from 'x.js'

千万不要认为 {xxx}是一种解析赋值呀!-> 这只是样子上看上去是一样罢了!(require那种姿势才是解析赋值)

总之,存在两种 exports 导出方式:

  1. 命名导出(每个模块包含任意数量
  2. 默认导出(每个模块包含一个

➹:export - JavaScript - MDN

➹:export default function 和 export function 的区别_export__开猿笔记💤

➹:Module 的语法 - ECMAScript 6 入门

2)TS 中的问号?

➹:巧用 ES 系列 4: TypeScript 中的问号 ? 与感叹号 ! 是什么意思? · Issue #9 · e2tox/blog