vue

✍️ Tangxt ⏳ 2020-08-27 🏷️ Money 组件

Money.vue 组件(下)

★JS 组件

先用 JS 的姿势来写,写完后再改写成 TS 姿势的!

1)「收入」&「支出」类型切换组件

刚开始搞,就来整个最简单的!

在单文件组件里边,写 JS 区域的模板:

<script>
  export default { data, props, methods, created, /**...**/ }
</script>

<!-- <=> -->

<script lang="js">
  export default { data, props, methods, created, /**...**/ }
</script>

你不写lang="js",默认就是 lang="js"

思路:

  1. 定义这个切换组件的状态:type'+'/'-'则表示组件为「收入/支出」状态 -> 定义组件状态用 data
  2. template里边代表「支出」和「收入」的这俩元素,此时 type 为何值,那么相对应的谁就有下划线,也就是谁会有 selected 这个 class
  3. 组件状态的改变 -> click 元素

代码:Demo

2)代码分析

:class="type === '-' && 'selected'"

咩有一个叫 false 的 class

vue 在背后做了很多事情,如果 && 表达式返回了 false,那么 vue 就会自动把这个 false 值给去掉,毕竟 class 是 false 未免也显得忒奇怪了吧! -> 总之,这是一种简写的写法!正常写法应该是整个三目运算……

@click="selectType('-')"

这还有一种写法,那就是直接 type = '-'就可以了,但是这种写法只能写一行代码,所以这就显得很鸡肋了!而方法调用则可以写多行代码

该「切换组件(Types)」接收外边传过来的值:

透过 props 这个 options API 就可以做到! -> 拿到传过来的值,一般扔到 mounted 里边使用!

如何看 `props`?


接下来看看如何把上边的 JS 代码,改成是 TS 姿势的!

★TS 组件

实现第一个 TS Vue 组件

如何写一个 TS 组件?

1)TS 组件 vs JS 组件

它们俩最大的不同:

TS 组件不用构造选项 -> 为啥不用? -> 因为构造选项是没有类型的,而且可以随便加东西!

写 TS 组件必须要用 Class -> 这是 Vue 规定的!

2)如何写一个 TS 组件?

  1. <script> 添加 lang="ts"
  2. 导出一个 class,而且该 class 继承 Vue -> 这是规定
  3. class 里边定义实例的自有属性 -> 就是在干 data 的活儿
  4. class 里边定义实例的方法 -> 就是在干 methods 的活儿
  5. 安装vue-property-decoratornpm i -S vue-property-decorator -> 在安装vue-property-decorator的过程中,连带着vue-class-component也一起安装了!
  6. 添加装饰器 @Component -> 有了它,就可以把「自有属性+实例方法」自动处理成「data+methods」了 -> 没有它,那就不帮你处理了! -> 总之,它是一个自动化的帮我们处理代码的东西
  7. 为 JS 代码加类型 -> 你用 webstorm,如果代码出现爆红,那么你就得把它去处理成不爆红的状态! 如:为形参加类型 selectType(type: string) {}(是小写的string,可不是大写的String

如何添加生命周期钩子? -> 直接 created() {} 就行了!

代码:

代码

3)vue-property-decorator

文档:kaorun343/vue-property-decorator: Vue.js and Property Decorator

这个包不是尤雨溪写的,话说,为啥不用尤雨溪的呢? -> 因为尤雨溪写的没有这个人写的好!

尤雨溪写的是这个:vuejs/vue-class-component: ES / TypeScript decorator for class-style Vue components. -> 这是 Vue 官方提供的 TS 支持库!但其功能不如 vue-property-decorator 好用,所以我们就用了这个 vue-property-decorator

vue-property-decorator 提供了好几个装饰器(@开头的),当然,我们用的这个 @Component 是 Vue 官方提供的!所以,如果你要看 @Component 的官方文档,那就得看 vue-class-component 的!

透过 CRM 学习法来了解 @Component 的使用:

文档:Overview - Vue Class Component

vue-class-component 使用姿势

CRM 学习法,让你可以不用深究所有的事情,节省大量的时间!当然,你的 JS 基础得扎实才行! -> 「class 语法」 无非就是把 「JS 对象写法」 换成是另一种写法罢了!所以,如果你时间有限的话,其实可以放弃深究 class 的语法细节,以及 装饰器的用法!直接依葫芦画瓢就行了,反正你已经知道这种写法的背后到底做了什么……

★TS 组件 @Prop 装饰器

1)如何写 props?

props

Vetur 提示说 Property 'xxx' does not exist on type 'Types'xxx不存在于Types这个class里边!)

可以看到官方文档翻车了……(或许这是插件的缘故!毕竟效果已经出来了,只是插件检测说这有问题罢了!)

代码:Demo

2)用 @Prop 来写 props

@Prop

你在父组件里边是透过 xxx="Money pass data" 来传值的,但这样的写法只能表示这是在传字符串,因此 Vue 报了这样的错误:

传字符串

即便传的值不符合类型,但最终的运行结果还是出来了! -> Vue 的容错性处理!

所以你得这样来::xxx="666"(加一个:,意味着属性值是 JS 代码)

代码:Demo

3)代码解析

@Prop(Number) xxx: number | undefined;

这行代码在干嘛?为啥写了一个大写的 Number 还得再写一个小写的number

number | undefined -> 是编译时的检查! -> 在你还未运行代码的时候就能告诉你的代码是否有问题

如:

TS 代码检查

透过代码检查,你就明白自己这样写是有潜在的 bug 的! -> 所以我得换一种写法,如先判断一下 this.xxx 是否等于 undefined,如果不等于那就证明 this.xxx 是个 Number 类型的值……

回过头来,整体看一下 @Prop(Number) xxx: number | undefined; 这行代码:

话说,为啥Number是大写,而number 是小写呢?难道不应该都是小写的吗?

Vue 作者也想把 Number 改成是小写的,但因为某种原因(区分运行时和编译时)就放弃了!

Number vs number

补充一点:

你也可以这样写:@Prop(Number) xxx: number = 0;(默认给xxx一个0的默认值),这样你就不用检查 xxx 是否为 undefined了,反之,用原先的姿势你就得处处检查this.xxx是否为undefined -> 我测试了一下给上初始值,结果报了这样的错误:

给 prop 初始值的 bug

@Prop(Number) xxx: number | undefined;这种姿势,应该是在告诉 TS 这个xxx是没有初始值的!(疑问 🤔 ) -> 如果父组件不传xxx,那么this.xxx就是undefined值了!这难道不就是在说初始化值是undefined吗?

4)我们用 TS 写代码好还是不好?

给我工资 3k? -> 我是个没追求的前端,所以我的代码都用 JS 来写 -> 把 lang="ts" 删了 + 去掉所有的 : type1 | type2 -> 瞎写……(webstorm 有智能的编译时弱提示,而 vscode 则没有,只能当代码在浏览器里边跑起来了才知晓!)

给我工资 10k? -> 那我就是个有追求的前端,因此我的代码应该用 TS 来写!

5)TS 类型添加原则

  1. 你这样 type = '-' 写了,那么就不用这样写了: type: string = '-',TS 会自己推导
  2. 函数的返回值类型声明不用写,即不用 string selectType(){} 这样写
  3. 函数的形参需要写上类型声明
  4. @Prop(Number) xxx需要写上类型声明 -> @Prop(Number) xxx: number | undefined;

6)怎样理解这个代码?

this.xxx.yyy

this.xxx不过关,它可能是 undefinedthis.xxx.yyy不过关,因为已知this.xxxnumber类型,所以它是咩有yyy这个方法的

代码改写:

if(this.xxx === undefined) {
  console.log('xxx is undefined')
} else {
  // xxx 一定会有 toString 这个方法!
  console.log(thix.xxx.toString())
}

7)写 TS 的 3 个好处?

  1. 类型提示:更智能的提示 -> 自动提示
  2. 编译时报错:还没运行代码就知道自己写错了 -> 严谨
  3. 类型检查:无法点出错误的属性,如你不能随便写 this.xxx.yyy

在 Vue 的 template 里写的 JS 代码,TS 是不会去检查的!

8)其它装饰器的用法?

自己看 文档 -> CRM 学习法! -> 始终谨记「写法形式上有所不同,不需深究,依葫芦画瓢直接用即可!」

9)小结

  1. 官方的 props 不行,就用了第三方的 @Prop
  2. 搞清楚这行 @Prop(Number) xxx: number | undefined; 代码里边的 Numbernumber 各自代表什么意思! -> 它们的错误展示是这样的:浏览器控制台 Error(运行时) vs 终端 Error(编译时)

★TS 的本质

1)TS 真得很难吗?

TS 的本质:

TS

2)如何配置只要有编译错误,就无法得到编译出来的 JS 文件?

官网回答说:

tsconfig.json"compilerOptions"配置"noEmitOnError": true就行了!

但测试发现,这配置是无效的,TS 还是可以把代码 编译成 JS

当然,我们并不关心这个问题是否能解决,因为这在我看来,这有点画蛇添足的意思了!

3)小结


接下来,就来看看目前写 Vue 单文件组件的几种姿势!

★Vue 单文件组件的三种写法

只针对「Vue 单文件组件」,可不是直接的在某个 new Vue({})里边注册一个 Vue 组件或者全局注册一个 Vue 组件,因为这种姿势忒 low 了!

1)用 JS 对象

export default { data, props, methods, created, ...}

2)用 TS 类

<script lang="ts">

 @Component
 export default class XXX extends Vue{
     xxx: string = 'hi';
     @Prop(Number) xxx: number|undefined;
 }

3)用 JS 类

<script lang="js">

 @Component
 export default class XXX extends Vue{
     xxx = 'hi'
 }

JS 也可以用 class 姿势来写单文件组件! -> 我一直以为只有「JS 对象」那种姿势!

4)用哪个?

优先使用最难的 TS 姿势! -> 因为使用最难的,会学到更多的知识!

最简单的姿势是用「用 JS 对象」

注意:你使用了 class 姿势,那么你就得写上 @Componentextends Vue

★numberPad 模块

1)创建一个写 ts 代码的模板

{
  "vue-ts": {
    "prefix": "!tts",
    "body": [
      "<script lang=\"ts\">",
      "import Vue from \"vue\";",
      "import { Component, Prop } from \"vue-property-decorator\";",
      "",
      "@Component",
      "export default class ${TM_FILENAME_BASE} extends Vue {",
      "}",
      "</script>"
    ],
    "description": "插入 ts 模板"
  }
}

2)该模块的功能分析

看 windows 系统提供的计算器功能

由于 TS 是非常重视数据类型的,所以根据实际情况分析, div.output 的值是 string 类型的:

string

3)代码实现

思路:

功能展示

代码:Demo

4)代码分析

height: 72px

为啥要写高度?为啥不写 min-height

不写高度的话,用户一进来页面,那么那个展示输入情况的框框就会有个从低到高这样一个变化的闪烁情况(由于 JS 渲染 DOM 是需要时间的,所以你给了这个框框初始值也是会有问题的!我们原先就直接给这个框框元素一个字符串100,但现在它是动态变化的,所以我们用了插值表达式!) -> min-height,你不能保证每个字符是否高度一致,可能你输入某个字符的时候,增加了一个 1px ,导致框框闪烁了一下

所以综合起来看,我们就得写死这个高度! -> 当然,这是不得已的情况!


inputContent(event: MouseEvent) {}

我们可以 @click="output += 1" 这样做,但这样忒麻烦了!而且也不好写!所以我们用了这种姿势:@click="inputContent('1')"

可这种姿势也不好,根据「我与重复不共戴天」原则,最后选择了这种@click="inputContent" 不传参数的做法(Vue 会自动传一个事件对象给这个方法,透过这个事件对象,我们可以拿到用户所点击的那个button的值) -> 当然,这inputContent还是重复,所以我们最后可以用事件代理的姿势来搞!

回过头来看这个 event: MouseEvent -> 没有点击事件,只有鼠标事件、键盘事件、UI 事件等等的! -> 点击事件是鼠标事件的儿子!

这行event.target as HTMLButtonElement;代码,为啥要加个 as HTMLButtonElement

对于 TS 而言,面对 e.target.textContent这样的代码,e.target 有可能为空!e.target可能没有textContent,如图片就咩有内容!(不是所有元素都有内容的!

当你面对这种写啥都有意外的情况,你真得有种不想写下去的感觉!幸好,我们可以强制指定类型,而这就是为啥要用 as HTMLButtonElement 的原因了,你添加了这个,那就意味着,这个元素是有内容的,即便 button 元素的内容空字符串也是有内容的!

虽然这样写代码让人蛋疼,但这代码写得很精确,不会有任何 bug!

强制指定类型会经常用到,尤其用在 event.target 上! -> 为啥会这样?因为 Vue2 和 TS 的搭配并没有那么好!照理说,像 event.target 这样代码,tsc 是可以自动识别的,或者 Vue 会在编译模板的时候,告知 tsc 这个元素是个button元素


"0123456789".indexOf(input) >= 0 的读法:你输入的东西在0123456789之间,那就大于等于0

this.output.indexOf(".") >= 0 的读法:output的东西已经有.


你在计算器里边输入的字符个数是有限制的 -> 最多16个字符


const input = button.textContent!;

没有 !,那么你在用 input 的时候,tsc 会说 input 可能为 null

tsc 的毛病

所以我们就强制断言一下 input 不可能是 null,而是一个 stringbutton.textContent as string

如果我们只是把它不为null这种情况给排除掉,那么这还有一种写法,那就是 button.textContent! -> !相当于是as string这样的东西,说白了就是它表示不为null的值,它可以是as numberas boolean之类的东西…… -> !类似于水果这样的东西,它可以是苹果、桃子、李子……,但它就是不是丝瓜、苦瓜之类的东西

总之,!就是在提示我们 button.textContent 不是一个 null 值,而我们之所以能确切知道它不是null,因为我们写的button元素就有是内容的呀!即便内容是个空字符串!

对了,如果 tsc 提示某个变量可能是 undefined,那么你也可以用 !


inputContent方法里边的if……else……逻辑:

output vs input


this.output = this.output.slice(0, -1)

把切割出来的结果作为组件的output状态!

slice API

-1是最后一个字符! -> 正值从左往右正数(索引为 0 是第一个字符),负值从右往左倒数(索引为 -1 是最后一个字符,-2 就是倒数第二)

为啥是从 0 开始的?难道不应该是第一个字符 1 这样吗? -> 因为这是外国人的习惯,外国人在说接触地面上的那一层,是说 Ground Floor,然后接着上一层才是 First Floor,而我们中国则是 1 楼,然后 2 楼这样子……

5)重复的东西出现了,但很难消除?

重复难以消除

6)小结

★notes 模块 - v-model

1)代码实现

思路:

用户输入的内容需要让我们的 JS 代码过一手再显示到输入框! -> 所谓的「双向绑定」

代码:

代码

注意,如果是原生 HTML,我们是通过 event.target 拿到 input 框的输入值的!而我们自定义的 input 组件(也就是Notes组件),input元素直接就是透过 Notes组件的状态 value 拿到用户输入的值的!

template里边拿到事件对象用 $event(不需要 TS 类型),方法里边则是形参event(需要 TS 类型)

为啥不用 change 事件? -> 失去焦点才会触发change事件!

关于形参 event 是什么类型的事件? -> 需要你自己去查找,如 input 事件就是 KeyboardEvent

2)`v-model`

如果你的代码出现这种形式的代码:

脱糖

那么你可以把那两句话(:value+@input)简写成 v-model

v-model

我们一般用 value 作为 input 组件(也就是Notes组件)的状态!

v-model 是 vue 从 angular 1 抄过来的! -> 所以,不要问为啥要这样写!这是历史原因决定的,请不要问「为什么」!

v-model 的存在让我们少写了很多代码!话说,我们可以拿到用户输入的值,但是为啥还有把拿到的值再赋值给 input元素 的 value 属性呢? 这难道不是自动就能赋值的吗? 为啥还要这样多此一举? -> 难道这是为了保存用户输入的状态?还是说我们要把input元素的值的状态交给这个 Notes 组件来管控? -> Notes 组件看上去就像是一个 input 元素一样!

为啥要添加一个 value 状态

所谓的双向绑定应该就是:

单向数据流

v-model 给了我们双向绑定的错觉,因为它让我们忽视了 @input 的作用!

假的双向绑定

★tags 模块

1)代码实现

思路:

思路

代码:Demo

2)代码分析

方方在写 data 的时候,直接先写 dataSourceselectedTags,然后知道这个dataSource的数据是来自于外部的,所以就追加了 @Prop() -> 总之,先不考虑外部的数据,先把这个组件应该有的状态给写上!

  1. 标签不是写死的,是外部告诉 Tags 组件的!
  2. Tags 组件默认有「所有的标签dataSource」,以及选中的标签selectedTags(默认咩有任何一个标签被选中)
  3. selectedTags: string[] = [] -> 表示「我是一个字符串数组,只不过我目前是一个空的数组,而且我这里边只能装字符串,不能装其它东西!」 -> 如果你直接这样 selectedTags = [],那么 tsc 就认为这里边能装任何数据类型的东西!
  4. @Prop() dataSource: string[] | undefined -> @Component即便不传参数也不用加(),而@Prop不管传不传参都要加(),而这是语法规定! -> 如果给@Prop()传参,那么你得写成这样:@Prop(Array) dataSource: string[] | undefined,话说,Arraystring[]为啥不对应上?即应该写成 @Prop(string[])才对的呀!因为给@Prop的参数是 JS 内置的构造函数呀!这个参数是运行时检查的! -> 为啥方方不给@Prop传参?因为这个项目是我们自己在做给人家看的,而且又用上了 TS,所以不用给!当然,如果你这个项目是 UI 轮子库,给其它前端开发者是使用的话,那么你就得给上参数Array -> 为什么不这样写 @Prop() dataSource: string[] = []?因为如果真这样写,那就违反原则了,毕竟这个 dataSource 是外部的数据,我们这个Tags组件给dataSource一个值,那就很不好了!所以用了一个 undefined ,表示外部可能没有传数据过来,那就是接收undefined值了!当然,我们这样做了,在后续的代码里边,如果用到了 dataSource,那么就得判断一下它是否为undefined值!
  5. 如果组件名字叫Tags了,那么prop的名字就不要再叫tags了! -> 不要这样写 <Tags :tags="" />,因为这显得很奇怪!
  6. 子组件用 TS 写,而父组件也可以用 JS 写 -> 父组件(JS) vs 子组件(TS)
  7. 渲染标签 -> 遍历dataSource
  8. 状态驱动视图渲染! -> 我们的 JS 只为改变状态而生!
  9. 我们用了对象写法来添加class,原先的做法是 xxx && 'selected',现在的做法是{selected: xxx} -> xxxture就添加selected这个class,否则就不添加! -> 如果元素被selected了,那就点亮它!
  10. 点一下标签,就高亮,再点一下,就熄灭 ,如开灯关灯一般…… -> 方法起名 toggle(开关之意),根据方法行为决定方法的起名!

toggle

3)新增标签功能

目前不做复杂的,只做个最简单的!

代码实现:

思路 -> 子父通信!

代码:Demo

代码解析:

创建标签的方法起名 -> 本来叫createTag的,但这整个组件本身就叫Tags,所以直接就是create了 -> 一般起名原则是「动词+名词」(所以不能起newTag),特殊情况直接一个「动词」

手机上的 window.prompt 效果很好!而 PC 上就不太好了!

window.prompt("请输入标签名")的返回值就是用户输入的东西:

window.prompt

可以直接回车确定!

如果用户咩有输入内容,该 API 返回值默认就是一个空字符串 ''

如果用户输入了内容,我们就得更改 dataSource 的值,但是 Tags 组件是不能自己直接更改它的,因为 dataSource 是外部传过来的,不是Tags 组件自己的!

为啥 dataSource 要加 readonly

readonly

上边的代码 tsc 并没有提示爆红,因为我们是直接往数组里边追加元素,并没有更改dataSource 的地址,而 tsc 功能有限,所以就没有提示爆红了 -> 如果你直接这样 this.dataSource = [],那么就会报错——「你不能把一个值赋值给一个 const or readonly 的变量

总之,为Propreadonly,是为了防止我们写弱智代码——组件更改来自外部的数据!

理论来说,我们直接在 Tags 组件里边 update dataSource 是可以的,Vue ,并没有阻止我们这种做法,但 Vue 不推荐!因为这是一种不规范的行为,子组件改外部数据,很容易导致数据混乱,让代码难以维护!

作为一个职业素养良好的前端程序员来说,我们不应该让组件更改来自外部的数据,于是,我们就用了$emit 这个 API:

$emit & .sync

.sync 的本质就是拿到子组件传过来的数据,然后把这数据赋值给父组件的tags状态,tags一变,那么子组件的dataSource就能实时更新 -> 这是一个响应式过程!


关于标签的删除功能 -> 在「标签页」里边做!「记账页」可没有删除标签的功能!

这个记账页面做到现在,基本的功能已经完成了,至于还有其它的什么功能,目前还想不到!

下一篇,主要讲「如何把记账页面里的 4 个模块整合起来 -> 把它们的数据收集起来,然后把这些数据提交到数组里边或数据库里边!」

★了解更多

➹:读懂 TS 中联合类型和交叉类型的含义 - 全栈修仙之路

★总结

★Q&A

1)slice 和 splice 的区别?

测试 splice & slice

补充:

var s34 = [1,2,3,4,5]
s34.splice(1,3,7,8,9) // [2, 3, 4]
s34 // [1, 7, 8, 9, 5]

➹:slice()与 splice()的用法和区别你清楚吗?_积少成多-CSDN 博客

➹:JavaScript Array splice vs slice - Stack Overflow

➹:JavaScript slice()、splice()、split() 傻傻分不清 - by Peggy Chan - Medium

2)`@Prop`(这里到底要不要写类型)?

上节视频里我给大家留了一个坑。我说先不写@Props(这里的类型),只写冒号后面的类型,示例如下:

@Prop() xxx!: boolean; 

这种偷懒的写法会在很后面造成一个 bug。应该写成

@Prop(Boolean) xxx!: boolean;

为什么呢?原因是这样的:

  1. 左边的 Boolean 是跟 Vue 说 xxx 的类型是 Boolean(运行时类型)
  2. 如果不写左边的 Boolean,那么 Vue 就不知道 xxx 的类型是什么了,默认就把 xxx 当作字符串了
  3. 因此,当你给 xxx 传值时,Vue 会将其自动转换成字符串。而与我们的预期「xxxboolean」不相符,这就是 bug。

等你遇到 prop 类型错误的时候,你就会想起我说的这番话了,目前你就看一眼这篇文章就行了。

只有等你才坑了,你才会记住这个知识点 :)

xxx!:的意思:Assert that xxx is non-null -> 对xxx属性进行非空断言

➹:ts属性后面的感叹号有什么用处? – 七月时光