vue

✍️ Tangxt ⏳ 2020-09-17 🏷️ Vuex

11-Vue 全局数据管理(上)

★前言

上节课遗留的问题:改了这个页面的数据,另一个页面没有同步更新这个数据 (各自为政)-> 解决办法 -> 全局数据管理!

方方的一个观点(人的主观想法,不一定对):

全局数据管理

前端把这个全局数据管理搞得超级复杂,而目前还咩有把这个东西回归到最朴素的状态! -> 为啥咩有回归? -> 因为前端还咩有搞清楚全局数据管理到底是什么!

➹:在 2016 年学 JavaScript 是一种什么样的体验? - 知乎

★再次封装 recordListModel

1)提取 data

任何操作表的 data 都交给recordListModel来做 -> 之后的统计页面拿数据也方便!

代码:Demo

2)封装 create

createItem比较好听!

代码:Demo

💡:第一次使用any类型?

把经常用的clone方法封装到lib里边的clone.ts

any 类型

其它模块文件使用这个clone方法,直接import一下就好了!

3)小结

把获取数据,新增数据,保存数据的操作都封装到recordListModel里边去了!

接下来做一下全局数据管理 -> 解决页面之间数据不同步的问题!

★用 window 来容纳数据

1)页面之间数据不同步的原因

数据同步

我的已知认识是这样的:

Money.vue里边导入了tagListModelLabels.vue里边也导入了tagListModel -> 这两个tagListModel是同一个地址!

我的测试:

把这两个tagListModel各自扔到window上,看看它们是否全等?

经过测试,它们确实是全等的!

tagListModel

既然如此,那么问题来了,我们在标签页创建了一个标签,就会往tagListModel里边追加了一个标签,可记账页却没有同步更新? -> 所以,这是怎么一回事呢?

我猜测这是 Vue 不能检测数组项修改的原因!

我们知道 tagListModel.data的数据结构是这样的 [{},{}],我们在创建标签的时候用了 tagListModel.data.push({ id, name: name }),而这个push显然不是 Vue 更改过的 push 方法!所以这就导致了数据不同步更新的问题!

我用了Vue.set()测试一下,结果还是如此:

Vue set 也不行


按照老师的解释 -> 我们代码是在把同一个 JSON 字符串分别做成了对象 1 和对象 2 -> 解决姿势是 -> 弄成是同一个对象!

这两个 data 是不同的


测试:

测试

两次 fetch

再次测试在不同模块里边导入相同的模块:

测试

我在tagListModel.ts里边,写了行 console.log(1),然后再不同文件模块里边各写一次import tagListModel from "@/models/tagListModel";,结果只log一次1

2)解决 bug

思路:

思路

main.ts 里边搞个全局变量 -> 往上提(Money.vue -> App.vue -> main.ts

代码:Demo

💡:你直接这样写:window.tagList = tagListModel.fetch();会报错?

为啥tsc会报错? -> 因为window根本就咩有tagList属性,所以你不能这样做!

我们写 JS 可以随意加,但是tsc是不允许的!

所以,我们得声明一下这个Window

// custom.d.ts
interface Window {
  tagList: Tag[];
}

为啥要这样做?有种脱裤子放屁的感觉……

其实这是为了约束你写代码,保证不会写出taglsit这样的无用属性,以及不会写出'666'这样的数据类型 -> 总之,你要记住,tsc就是在用类型检查你的代码写得有没有错!

💡:标签多了,创建标签button就看不见了?

创建标签

我直接用 fixed 固定定位加水平居中!


以上这个操作就是「全局数据管理」 -> 就是这么简单! -> 也许你听说过 vuex ,觉得它很厉害,但其实它也就那样!

接下来,就把我们这个「全局数据管理」封装得更厉害一点!

★用 window 来封装 API 1

1)对「全局数据」的操作

不能直接push数据,而是通过某些 API 来管理这些数据! -> 全局暴露 API

2)最小知识原则

最小知识原则

我们写代码的时候,对一个全局数据的操作,理应只有一种姿势去操作!

所以我们需要封装一个createTag

封装

如果使用 Promise 封装的话,那么在使用 window.createTag的时候,那就可以这样了:

window.createTag(name).then(()=>{}).catch((e)=>{
  console.log(e)
})

代码:Demo

3)封装查找、更新、删除标签操作

思路:

思路

代码:Demo

💡:window.updateTag = (id: string, name: string) => {}

如果传多个属性?如,还有createdAt等这样的属性,那么就得这样写了:

type Tag = {
  id: string;
  name: string;
  // …… 
};
// 这样做必选传 id,因为 Tag 的类型定义并咩有写 ?
window.updateTag = (id: string, object: Tag) => {}

如果除了id都可以传呢?

那就这样写:

window.updateTag = (id: string, object: Exclude<Tag, 'id'>) => {}

表示Tag里边,除了'id'的所有东西 -> 也就是说,你不能传id

不管怎样,为了简单起见,就直接id: string, name: string 这样定义形参了!

💡:类型声明一样,能否简写?

类型一样

可以简写的,你可以这样做:

interface Window {
  updateTag: TagListModel['update']
}

表示updateTag的类型,跟update的类型完全一模一样!

当然,我们重复写两遍也是没问题的!毕竟如果TagListModel改了就麻烦了!

💡:变量只用了一遍,那就简化它?

简化?

{
  created() {
    this.tag = window.findTag(this.$route.params.id);
    if (!this.tag) {
      this.$router.replace("/404");
    }
  }
}

变量只用了一遍,那么就不用加局部变量了!

💡:不能在data里边拿到id吗?

细节

猜测,Vue 是在 created 的时候,才会构造出this.$route这个对象!

当然,我们在beforeCreated里边,也是可以拿到这个id的!

总之,你不能在给data的赋值里边,透过this.$router拿到id -> 因为此时可能没有$route对象(具体确切情况,得分析源码)!


以前我们是对 DOM 树增删改查,现在则是对数组(tagList)增删改查!

接下来封装另外一个东西,目前,我们有两个数据(标签数据和记账数据),一个是tagList(已封装),另一个是recordList -> 以相同的形式来封装记账数据!

★用 window 来封装 API 2

如何封装 recordList? -> 先看看我们的代码哪里有用到recordList,然后其封装思路与tagList完全一模一样!

代码:Demo

1)封装步骤

  1. 抽离用到recordList的代码到main.ts
  2. 对原先用到recordList的代码改成是使用window.xxx的 -> 发现爆红
  3. Window的类型声明
  4. 看看recordListModel有哪些操作 -> 封装到window旗下!

💡:不需要手动save()

对比之前的tagList封装,发现并咩有类似save的代码!

save

变化都被封装了,我们在createRecord的时候,就顺道save了! -> 不需要你手动去调用saveAPI -> create的时候就意味着save代码到localStorage里边去了!

💡:看起来无意义的代码:window.createRecord = (record: RecordItem) => recordListModel.create(record);

window.creatRecord的时候,本质上就是在直接recordListModel.create -> 有种挂羊头卖狗肉的感觉 -> 这看起来有点智障(为了使用window而用window) -> 之后会重新封装

2)整体看看整个数据管理的封装操作

封装变化的

3)我们是否再继续优化封装好的代码?

目前我们看到的问题:

  1. 用了过多的全局变量 -> 如果用了 500 个数据,那么岂不是需要在window旗下搞很多个全局变量? -> 我们现在只用了两个数据,就搞了 7 个全局变量了! -> 这全局变量多了,就得考虑变量名是否冲突了,是否覆盖了……
  2. 严重依赖window -> 有些环境是咩有window对象的,如Node.js,所以我们不应该依赖window对象,而是自己搞一个对象!

如何解决这两个问题? -> 请看下个

★消除对 window 的依赖

解决这两个问题特别简单……

1)解决「全局变量过多」

全局变量太多,那就把变量挂到一个变量身上 -> window.store -> 这样一来全局变量就不多了!

window.store

💡:关于改代码?

程序员很不喜欢改功能已经被写好的代码,但一旦你喜欢上了改代码,那么你的代码会越来简洁,不然,你的代码会越来越丑!

注意,在改代码途中,千万不要多删了代码

💡:如果你用了this,那么你就不要用箭头函数了!

💡:在编辑代码的过程中,我们只管红线警告,黄色警告则不管!

2)解决「严重依赖 window」

这个也非常好解决,不要window就行了!

做法:

  1. store/里边新建一个index2.ts -> 区分默认的index.ts
  2. store这个全局对象整个扔到index2.ts里边去 -> 导出store -> 不用window了! -> 全局咩有store对象了!
  3. 其它模块文件想要这个store,直接import一下就好了!
  4. 改代码 -> 运行代码测试功能看看是否更改正确(没有写单元测试,只能手动测试了!) -> 这是一个重复自己的过程,每次改了代码都得重复做一样的操作,当然,用了单元测试就不用重复自己了!

代码:Demo

💡:在使用store的过程中,其类型不需要声明,因为tsc可自动推测!

写 TS 很多时候,也不需要写类型声明 -> 因为可以自动推测类型!

💡:在改代码的时候,关注红线就可以了!

改红线

💡:目前发现有一个 bug?

keep-alive


可以看到,store就是个仓库,就是个数据库!(不需要先解释store是什么,而是你捣鼓一个store出来,然后告诉自己这就是store

话说,目前的这个store是否还有再优化的可能?

分两个文件

3)再优化 store

代码:Demo

💡:文件的命名,方便搜索?

文件的命名

  1. recordStore
  2. tagStore

💡:如何把对象弄进来?

...

做法:

做法

...recordStore -> 是浅拷贝 -> 只是把地址赋值过来了!

引用值

可以看到,我们目前的代码组织,是非常容易扩展的! -> 只要有独一无二的数据,就在store目录旗下创建个xxxStore.ts,然后把ModelCRUD操作扔进去!

💡:多文件导入一个store,这个store是否是单例?

单例

index2.ts这个模块文件被其它模块文件import了一万遍,也只会加载一遍!


我们自己弄的 store

目前,我们已经会搞全局数据管理,接下来看看 Vue 里边引以为傲的全局数据管理 —— vuex -> 看看 vuex 比我们弄的全局数据管理高在哪里?

★将 model 融合进 store

storemodel的功能没啥区别,都是存储数据的仓库,我们这种代码:

bug

根本就没有必要这样组织!

代码:Demo

1)把 model 里边的代码合到 store 里边

💡:在定义对象的时候不能使用对象?

定义对象

测试一下:

代码

💡:关于属性的命名?

如果你这样写:

命名

所以你得改成是独一无二的名字,如fetchRecords……

💡:闭包的体现?

闭包

用了闭包后,任何人都无法访问这个data变量,除非你通过recordStore.recordList去引用这个data

但这并不简洁,有种逻辑交叉的调调!

💡:关于 tagList: [] as Tag[]

你给tagList初始值,必须说明这是一个什么类型的数组,不然tsc会报错!

💡:重构技巧?

先找最简单的重构,然后再找最复杂的 -> 不然,先找最复杂的,会让人感到蛋疼,因为复杂的会涉及到比较多的东西!而这些比较多东西还没有重构,所以导致这个很复杂的 API 会报很多错误!

如果都不简单,那就随你的便吧!

💡:为啥叫saveTags,而不是saveTag

数组保存加 s

这是我们目前创建的store所带有的 API -> 它们都是从tagStorerecordStore里边拷贝而来的!

同理,fetchRecordsfetchTags也是如此 -> 拿到的都是数组!

💡:不叫init

不加 init

我们透过调用fetchRecordsrecordList值,是因为我们不能这样写:recordList:tagStore.fetchTags() -> 一旦这么写了,就会报错!

也不能这样:recordList:this.fetchTags()

this

💡:一种新语法?

this.recordList && this.recordList.push(record2)

// or -> 加?号是说,? 前边有东西,那就走 push,没有东西,那就走下一行代码,不要管 push 了! -> 可选链语法!

this.recordList?.push(record2)

用这种?.语法,需要更高版本的TypeScript,还有 Webstorm、VS Ccode……


modelstore合并之后,就不存在src/models这个目录了! -> 该目录旗下咩有东西,那就可以删掉它!

★修复 Tags.vue 的 bug

bug 情况:

bug

解决 bug 的思路:看看点击新建标签之后,我们的代码到底做了什么! -> 数据是全局的 -> 我们不用传数据给子组件! -> 简单来说,我们不用一层层地去传数据了!

代码:Demo

全局数据管理

有了全局数据管理之后,就不用考虑「爷 -> 父 -> 孙 -> 父 -> 爷」这样传递数据了!直接手递手给全局,然后全局再手递手给一个个组件!

★store 的 bug 之值与地址

bug

为什么要这样做?

值类型与地址类型

监听 store

这个监听操作,还有另一种姿势——放到全局的App.vue里边,这样其它子组件就不需要监听store了:

全局监听变化

这个全局监听我没怎么懂,这是根组件实例呀!其它子组件实例的computed属性为啥就能收到这个信号呢? -> 难道仅仅只是借用 API?只要是个 Vue 实例就行了?不管是 子组件实例,还是父组件实例都行?

原先我们在组件类里边是这样做的:

原先做法


不管是简单类型的值,还是地址值都放在computed里边,而不是看到地址值,就放到组件类里边,非地址值就放到@Component(computed:{})里边!

总之,我们就是为了统一管理,让我们写代码的时候,无须留意这个来自store的值到底是简单类型的值,还是引用类型的值! -> 不用思考!

原理:利用了computed的能力——被监听的值发生了变化,就会去更新计算属性的值!

💡:在测试这个bug的时候,用的还是非常实用的 +1 button

★小技巧:把 store2 变成 this.$store2

我们每次使用store,都要import store from "@/store/index2";一下

那么我们能否不import一下吗? -> 可以的

做法如下:

第一步:

①

第二步:声明类型(复制 官方文档

// 1. 确保在声明补充的类型之前导入 'vue'
import Vue from 'vue'

// 2. 定制一个文件,设置你想要补充的类型
//    在 types/vue.d.ts 里 Vue 有构造函数类型
declare module 'vue/types/vue' {
// 3. 声明为 Vue 补充的东西
  interface Vue {
    $myProperty: string
  }
}

做法

再次使用store时,不用import store了,直接this.$store2.recordList就行了!

$store2: any -> 建议,不要用any,而是把 API 接口写得更清楚一点,这样在为$store增加 API 的时候,就不会写错代码了! -> $store2: { recordList: RecordItem[]……}

不过,这种做法就牵扯到.d.ts文件的潜规则,那就是如果这个文件里边没有import语句,那么默认所有 .vue 文件全部都是自动引入.d.ts文件的! -> 反之,如果有import,那么你在某个.vue文件里边,用到了RecordItem等这样的类型,那么你就得手动引入这个.d.ts文件了!

当然,这也是可以解决的:

解决 bug

★全局状态管理的优点

全局状态管理(也叫全局数据管理)的好处是什么?

  1. 解耦:将所有数据相关的逻辑放入 store(也就是 MVC 中的 Model,换了个名字而已)
  2. 数据读写更方便:任何组件不管在哪里,都可以直接读写数据
  3. 控制力更强:组件对数据的读写只能使用 store 提供的 API 进行(当然也不排除有猪队友直接对 tagListrecordList 进行 push 等操作,这是没有办法禁止的)

下节课我们学习 Vuex,你会发现,Vuex 跟我们手写的 store 没有本质区别。

★总结

你必须得有这样的角度去看待模块化:

看模块化的角度

不要管import很多遍,你的脑海里就是import一次,相当于这个文件无中生有的多了一段xxx代码! -> 一般我们会用const声明某段xxx代码! -> 其它模块文件用的都是同一份xxx代码,内存地址也是一样的!

说白了,我们会把多个模块打包成一个文件!然后交给浏览器去解析执行! -> 千万不要认为import一次模块文件,那么这个模块文件就都会加载执行一遍哈!

这节课涉及到的知识点:

  1. 全局数据管理/全局状态管理是啥
  2. store 是啥
  3. store 怎么模块化
  4. 如何封装

★Q&A

1)在 JS 里边,如何拿到对象的内存地址?

问题缘由:

问题缘由

tagListModel自己维护了一个data状态,我们通过这个对象提供的 API 去操作这个data!而这个data也是每个模块公有的值!

➹:请问 JavaScript 里面怎么获取某个变量的内存地址?并打印出来 - 的回答 - SegmentFault 思否

➹:javascript - 有没有一种方法可以在 JavaScript 中获取对象的内存地址和对象的哈希值 - IT 工具网

2)global 对象存在吗?为啥我们可以在 TS 里边,直接定义一个 Window 接口?

不存在 global 这个变量,但存在 window = { global 的键值对 } -> 可存在一个叫Window的构造器呀!只是我们无法 new 罢了!

➹:JS/JavaScript 中的概念区分:global 对象、window 对象、document 对象

3)多个文件导入同一个模块,那么这个模块是否是一样的?

无论执行环境和实现如何,js 代码本身在 import 完成之前是不会被执行的,所以就好像在“执行”之前存在一个静态的编译期一样。

为什么题主的那一种写法会报错呢,因为在 import 完成之前,代码是不被执行的。

那么为什么代码一定要等 import 完成才会执行,是因为 import 需要时间,而如果要在代码运行的中途支持这种写法,那么脚本执行就必须被阻塞,但这样的阻塞在浏览器中会导致用户界面阻塞,所以这样的阻塞必须被“提前”,于是就形成了“import 之前代码不被执行”的操作。

实际上 es6 是支持动态 import 的,import 函数的签名是 string -> Promise,用 Promise 就可以避免阻塞带来的问题了

➹:多个文件 import 的相同模块里的对象,是否永远都是同一个对象? - 知乎

➹:Module 的语法 - ECMAScript 6 入门

➹:深入理解 ES6 模块机制 - 前端

➹:代码静态分析的原理是什么? - 知乎

➹:如何理解 es6 中的 import 是静态编译执行的?(一说是编译期执行的)? - 知乎

4)`position: fixed` 定位的元素如何实现水平居中?

.demo {
    position:fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

➹:position:fixed 定位的元素,如何实现绝对居中,并且宽度自适应内容的宽度 - SegmentFault 思否

5)TS 接口声明语法?

➹:对象的类型——接口 · TypeScript 入门教程

➹:🔌 TS 的 Interface 了解一下?