--愿你内心有种不灭的火焰,将你与别人区分开来--

0%

前端数据层的技术选择

0x00 开篇

这个世界上的有绝对理想主义者,也有绝对现实主义者,这个世界不是非黑即白的,我们常常游走在两者的边缘,即灰色地带。就想苦苦追寻的真理一样,我们一直在探索所谓的最佳实践,企图找到一个可以解决所有问题的宇宙终极规律,但是最后很有可能死在交叉的十字路口,技术也是如此,从来都没有绝对的银弹,所有的选择都是不断博弈的结果,所谓的真理,也许只是找到那个最佳的平衡点,适合自己的,能有效解决当下问题的就是好的,战术上必须存在最佳实践,但战略上可能都是哲学问题(卖个关子,升华一下主题)。

本文的产生是基于这样一个出发点,Redux 用起来太繁琐,大把大把的 Action,Reducer 写得头都大,样板代码太多,即不好写,也不好看,还不好 Copy(重构/迁移/维护)。。。

难道除了 Redux 就没别的选择了吗?于是开始了寻找 Redux 替代品(或者造个轮子)的漫漫长路,于是有了本文。

0x01 前菜

不是前菜的前菜,也可以是总结,前端数据流哲学 这篇之前已经在内部分享过,本文前置是希望大家对前端数据流这个概念先有个具象的认识,开胃菜。

  1. 数据流管理模式,比较热门的分为三种:

    1. 函数式、不可变、模式化 => 典型实现:Flux(Redux);
    2. 响应式、依赖追踪 => 典型实现:Mobx;
    3. 响应式,和 Mobx 区别是以流(stream)的形式实现 => 典型实现:Rxjs、xstream;
  2. 历史:Redux => Mobx => Rxjs
    angular.js 开辟了新天地,并且带来了数据驱动思想,React 借鉴前人理念并将自己定位为 View 层,数据层增强的框架不断涌现,从 Flux、Reflux、到 Redux。

    Redux 概念太超前了,一步到位强制把副作用隔离掉了,但自己又没有深入解决带来的代码冗余问题,让人又爱又恨,于是一部分人把目光转向了 Mobx,这个响应式数据流框架,这个没有强制分离副作用,所以写起来很舒服的框架。

    Mobx 渐渐成长起来,于此同时,另一个更偏门的领域正刚处于萌芽期,就是 Rxjs 为代表的框架,和 Mobx 公用一个 Observable 名词,但是很多人连 Mobx 都没搞清楚,更是很少人会去了解 Rxjs。

    又与此同时 Vue 来了,并且带着如果 ”React + Mobx 很好用,那为什么不用 Vue?“ 的 flag 来了。

  3. 各有所长

    1. Redux (不多赘述)
    2. Mobx
      Mobx 是一个非常灵活的 TFRP(Transparent Functional Reactive Programming) 框架,是 FRP(Functional Reactive Programming) 的一个分支,将 FRP 做到了透明化,也可以说是自动化。
      Mobx 带来的概念从某种角度看,与 Rxjs 很像,他们都自带 Observable,自动订阅 + 自动发布,没什么比这个更高效了。
    3. Rxjs
      Rxjs 是 FRP 的另一个分支,是基于 Event Stream 的, 它和 TFRP 有着本质的区别,同时 Rxjs 对 View 层的辅助没有 Mobx 那么智能,但是 Rxjs 其数据流(Stream)处理能力非常强大,当把前端的一切都转为stream后,剩下的一切都由无所不能的 Rxjs 做数据转换,你会发现,副作用已经在数据源转换这一层完全隔离了,如果再加上虚拟 dom 的点缀,那就是 cycle.js 了。
      不过,Rxjs 除了带来了 cycle.js 还带来了 Redux-Observable,如果说 Redux-saga 解决了异步,那么 Redux-Observable 就是解决了副作用,同时赠送了 Rxjs 数据处理能力。
  4. 思考
    可能在不远的未来,布局和样式工作会被 AI 取代,但是数据驱动下数据流选型应该比较难以被 AI 取代。未来的框架可能会朝着 view 与数据流完全隔离的方式演化,这样不但根本上解决了框架 + 数据流选择之争,还可以让框架更专注于解决 view 层的问题。

0x02 单向数据流、Flux、Redux

Flux 架构是为了面对传统 MVC 架构中混乱的数据流的问题而出现的解决方案。

Flux 将一个应用分为四个部分:

  • View: 视图层;
  • Action(动作):视图层发出的消息(比如mouseClick);
  • Dispatcher(派发器):用来接收Actions、执行回调函数;
  • Store(数据层):用来存放应用的状态,一旦发生变动,就提醒 View 要更新页面;

其的核心就是一个简单的约定:视图层组件不允许直接修改应用状态,只能触发 Action。应用的状态必须独立出来放到 Store 里面统一管理,通过侦听 Action 来执行具体的状态操作,也就是所谓的 单向数据流,从此解决了 MVC 架构中 View 和 Model 之间不可调和的混乱。当然单向数据流并非是 Flux 的首创,但是确实是其发扬光大的。

单向数据流的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”。 同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性。

Flux 通过一套严格(繁琐)的规范来限制数据流向,保证了程序在走向复杂化的过程中不会失去控制。但是缺点也很明显,代码量会相应的上升,数据的流转过程变长,从而出现很多类似的样板代码。 Flux 会迫使你把代码拆分的非常别扭,这也是为什么 Redux 天生自带繁琐的根本原因。

附录: Flux 架构入门教程
附录: Flux架构模式
附录: 浅析 Facebook Flux 架构

Redux

Redux 实际上相当于 Reduce + Flux,和 Flux 相同,Redux 也需要你维护一个数据层来表现应用的状态,而不同点在于 Redux 不允许对数据层进行修改,只允许你通过一个 Action 对象来描述需要做的变更。同时在 Redux 中,去掉了 Dispatcher,转而使用一个纯函数来代替,这个纯函数接收原 state tree 和 action 作为参数,并生成一个新的 state tree 代替原来的。而这个所谓的纯函数,就是 Redux 中的重要概念 —— Reducer。

Redux 是 Flux 架构的一种具体实现,它也在众多的类 Flux 框架中脱颖而出,除了单向数据流外,也承载着复杂应用中组件之间通信的问题。

Redux 而对整个应用状态的表达可以使用如下公式:

1
store := store.dispatch(Actions.reduce(Reducer, initState))

这个表达式的含义是:在初始状态上,依次叠加后续的变更,所得的就是当前状态,这也是 Redux 的核心理念。

单纯从状态变更的角度来看,使用 Redux,相当于把整个应用都实现为命令模式,一切变动都由命令驱动。

附录1:命令式编程、声明式编程、响应式编程;
附录2:函数响应式编程 FRP

Redux 的限制

Redux 带来了函数式编程、不可变性思想等等,为了配合这些理念(继承自 Flux 架构),开发者必须要写很多“模式代码(boilerplate)”,繁琐以及重复是开发者不愿意容忍的。当然也有很多 hack 旨在减少 boilerplate,但目前阶段,可以说 Redux 天生就附着繁琐;

Redux 本身是 prue 的,其在面对复杂的异步逻辑时根本无能为力,同时为了保持自身的 prue 性,将异步处理、副作用这些脏活累活都交给了 middleware,比如 Redux-thunk,Redux-suga,Redux-Observable 等;

Redux 只有一个单一的 Store 状态树,无法存在多个 Store 实例(Flux 架构是可以存在多 Store的);

0x03 Mobx

Mobx 小专栏

MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。
我为什么从Redux迁移到了Mobx
Mobx 思想的实现原理,及与 Redux 对比

一个简单的例子 mobx-react,例子中的源码

综上:

选择 Mobx 的原因:

  1. Redux 繁琐,流程较多,需要配置/创建 Store,编写 Reducer,Action,如果涉及异步任务,还需要引入 Redux-thunk 或 redux-saga 等中间件编写额外代码,Mobx 流程相比就简单很多,并且不需要额外异步处理库;

  2. 面向对象编程:Mobx 支持面向对象编程,我们可以使用 @Observable,@observer,以面向对象编程方式使得 JavaScript 对象具有响应式能力,同时 Mobx 对 Typescript 的支持也比 Redux 更好;Redux 推荐遵循函数式编程,当然 Mobx 也支持函数式编程;

  3. Redux 全局只能存在一个 Store,而 Mobx 则不受此限制;

  4. 拥有比Redux 更加精准的数据更新机制

不选择 Mobx 的原因:

  1. 过于自由:Mobx 提供的约定及模版代码很少,这导致开发代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一;
  2. 依然没有彻底的解决副作用的问题(这是目前几乎所有框架所面临的共同问题):
    Redux 将副作用交给了 middleware,Vue 则是通过把 Action 和 Mutation 分开来实现, 目前主流的框架对异步 Action 的最终行为都是只能通过多个 Action 组合(异步 Action + 同步 Action)使用来完成。

    Mobx 同样跳不出这个诅咒, 即时使用了 async 函数,其执行过程中也是不能被追踪的,即使这个 async 函数也被标记为 Action,若果在该函数内操作了数据,还会被误判是在 Action 外修改了数据。(不过 Mobx 对异步的处理要比其它框架友好的多,,通过runInAction Mobx 工具函数,或者 MST 已经可以曲线救国, Mobx State Tree 利用了Generator,使异步操作可以在一个 Action 函数内完成并且可以被追踪。但这又是在捆绑了 Generator 的前提下实现的)

    当然,最根本的原因还是由于 JavaScript 的限制,异步操作天生就难以被追踪。

0x04 Rxjs

Rxjs 其函数响应式的编程方式可以说是为我们打开了编程思维的另一扇大门,但是 Rxjs 的学习曲线非常陡峭。尤雨溪也曾说,Rxjs 完全可以胜任 Vuex 的工作,但是绝大多数的前端应用还没有复杂到需要使用 Rxjs,杀鸡焉用宰牛刀咧! 不过这些这并不妨碍我们去认识 Rxjs。

如果对 Rxjs 还不太了解,请参考下面的阅读清单:

初识

RxJS 入门指引和初步应用

RxJS是一个强大的Reactive编程库,提供了强大的数据流组合与控制能力,从事件到流,它被称为 lodash for events,倒不如说是lodash for stream更贴切,它提供的这些操作符也确实可以跟lodash媲美。

使用 RxJS 就将所有的输入都装换为数据流(stream):

1
2
3
4
5
6
数据       |>
用户操作 |>
网络响应 |> => Observable
定时器 |>
Worker |>
缓存 |>

数据流是一种可观察的序列,可以被订阅(subscribe),也可以被用来做一些转换操作,可以对若干个数据流进行组合。 由于 RxJS的抽象程度很高,所以,可以用很简短代码表达很复杂的含义,这对开发人员的要求也会比较高,需要有比较强的归纳能力。

然后作者列出了几个场景示例,有兴趣的可以理解下示例五,测试下自己的抽象归纳能力。

来个实际的例子

LeetCode 中国区招聘笔试题用 RxJS 处理复杂的异步业务 使用 RxJS 6+,实现一个 Autocomplete 组件的基本行为,需满足以下要求:
(1) 用户停止输入 500ms 后,再发送请求;
(2) 如果请求没有返回时,用户就再次输入,要取消之前的请求;
(3) 不能因为搜索而影响用户正常输入新的字符;
(4) 如果用户输入超过 30 个字符,取消所有请求,并显示提示:您输入的字符数过多。

当然,这个只是一个题纲而已,我们不妨将这个问题更细化一下,然后试着用传统的方式去解决:

问题 (1),这个太简单了,加个 debounce 就可以了;
问题 (2) 这个稍微有点难,首先我们的 http 需要加入 abort 机制了,然后需要在业务层去做加入各种判断,到这里 lodash 已经无能为力了,因为它无法处理异步逻辑;
甚至我们还可以将问题 (3) 和问题 (2) 合起来考虑, 发出多个异步事件之后,每个事件所耗费的时间不一定相同。如果前一个异步所用时间较后一个长,那么当它最终返回结果时,有可能把后面的异步率先返回的结果覆盖,这个就很蛋疼了。但是在 Rxjs 中,一个 flatMapLatest 就搞定。

解决了 问题 (3),问题(4) 就不是什么困难了。

综上,我们发现将每个问题拆开来看,使用传统的方式来处理,虽然有些复杂,但是依然有解,可如果将这些问题通通考虑进去,就会发现非常棘手,因为同步和异步逻辑相互交融,同时还要考虑用户体验。

我们看看 RxJS 是怎么解决的: 楼上的答案:探索 RxJS - 做一个 github 小应用 demo地址

上面几篇文章算是对 Rxjs 有一个直观认识,但只能算是小试牛刀,如果要用一句来描述 Rxjs,那么就是 其文简,其意博,其理奥,其趣深

进阶

想要深入的理解 Rxjs,请继续打怪升级:

参考文章:30 天精通 RxJS系列
书籍推荐:陈墨大佬的深入浅出RxJS

Rxjs 学得差不多了,怎样在实战中应用起来呢?

Vue: Vue-rx
React: Rxjs-hooks
Redux: Redux-Observable

OK,对于 Rxjs 你已经有了一些项目的实践经历了,不妨再玩点进阶的,考虑使用 Rxjs 进行状态管理,甚至直接干掉 Redux,Mobx,开辟一个端数据层,再造一个前端 ORM 不?(牛先吹起来,理想还是要有的,万一实现了呢?)

0x05 使用 Rxjs 打造前端数据层

本方案只是列出来,供大家参看,这条路并不好走,下面很多点直到目前我也 get 不到,但不妨碍我们去了解一下这套东西,将来当我们也面临问题的时候,至少不慌,因为我们知道还有一条路没有走过。

老套路,我们先来看几篇文章:

文章一: 复杂单页应用的数据层设计

作者首先阐述了 Teambition 一个在线项目协作工具(类似的产品还有 Trello)中所面对的复杂业务场景:

  • 存在全业务的细粒度变更推送(websocket) => 需要在前端聚合数据;
  • 前端聚合 => 数据的组合链路长;
  • 视图大量共享数据 => 数据变更的分发路径多;

面对这样一种复杂场景,数据层如何设计才能让它要提供的接口,使得视图层用起来也简便呢?如此引发出作者对主流框架(React, Vue, Angular)如何处理数据层的思考,几乎所有现存方案都是不完整的,从而开始探索 Rxjs, xstream 面对复杂业务场景的辅助能力,正好可以满足之前的诉求:

以下是这类库的特点:

  • Observable,基于订阅模式;
  • 类似 Promise 对同步和异步的统一;
  • 查询和推送可统一为数据管道;
  • 容易组合的数据管道;
  • 形拉实推,兼顾编写的便利性和执行的高效性;
  • 懒执行,不被订阅的数据流不执行;

Redux 这类东西出现的初衷为了提供一种单向数据流的思路,防止状态修改的混乱。但是在基于数据管道的这些库中,数据天然就是单向流动的。
由于Redux更多地是一种理念,它的库功能并不复杂,而 Rx 是一种强大的库,我们可以用 Rx 依照 Redux 的理念作实现,但反之不行。

附录:
流动的数据——使用 RxJS 构造复杂单页应用的数据逻辑
单页应用的数据流方案探索
上一篇文章的思路:精读民工叔单页数据流方案
基于 Rxjs 的前端数据层实践

0x06 Redux + Rxjs(Redux-Observable)

相比 Redux-saga,Redux-Observable 的革新将会更加彻底(从 Generator 转换到 Observable),Redux-Observable 基于 Rxjs,使得对异步操作的处理变得更加优雅。 Redux-Observable 相当于在局部构建了一个 Observable,但是依然基于 Redux 体系,而且 Rxjs 的学习成本很高。

附录:
使用 Redux-Observable 实现组件自治

0x07 Mobx + Rxjs

目前来看,社区对这整套架构的实现很少(至少目前公开的没有找到),这将是一套轻巧灵活而又强大的架构,试想一下,我们可以将状态的管理和全局的通信交给 Mobx 打理,而将所有的事件行为(UI 交互事件,网络事件等)移交给 Rxjs。

ref

Redux 有什么缺点?

你需要Mobx还是Redux?

Mobx 思想的实现原理,及与 Redux 对比

React 的数据管理方案:Redux 还是 Mobx?

MobX React: Refactor your application from Redux to MobX

从时间旅行的乌托邦,看状态管理的设计误区

Redux、Mobx、rxjs这三款数据流管理工具在你项目中是如何取舍的?

我为什么从Redux迁移到了Mobx

精读《dob - 框架实现》

Using MobX with React Hooks and TypeScript

元编程

元编程(英语:Metaprogramming),又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。

编写元程序的语言称之为元语言。被操纵的程序的语言称之为“目标语言”。一门编程语言同时也是自身的元语言的能力称之为“反射”或者“自反”。

Proxy

案例一:构建 getter/setter

管控对象属性的读取而不是需要使用闭包创建私有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// before
let obj = {
_id: undefined,
get id() {
return this._id;
},
set id(v) {
this._id = v;
}
};

// use proxy
const handler = {
get (target, attr) {
console.log('Getter');
return Reflect.get(target, attr);
},
set (target, attr, value) {
console.log('Set');
return Reflect.set(target, attr, value) ;
}
}

const objProxy = new Proxy(obj, handler);

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。

Reflect 设计的目的:

  • 将 Object 对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到 Reflect 对象上(Reflect.defineProperty)。未来新的方法将只部署在 Reflect 上

  • 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty 在无法定义属性时,会报错,而Reflect.defineProperty 则会返回 false。

  • 让 Object 的命令式操作都变成函数行为。

  • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

Proxy 与 Object.defineProperty 实现双向绑定

defineProperty 实现双向绑定的缺点:

  • 只能检测对象属性的变化,对数组的变化无能为力;
  • 也是因为只能检测属性的变化,双向绑定属于显示的声明属性;

Proxy 实现双向绑定:

  • 可以直接检测对象,而非仅限于对象属性;
  • 可以直接检测数组,而不需要通过 hack 某些数组方法;

Proxy 的缺点可能就是兼容性了,因为无法通过 polyfill 磨平。

ref

wiki-元编程

ES6 元编程

ES6精华:Proxy & Reflect

proxy%Reflect

TypeScript Tips

显示赋值断言

TypeScript 2.7 引入了一个新的控制严格性的标记 --strictPropertyInitialization,我们必须要确保每个实例的属性都会初始值,可以在构造函数里或者属性定义时赋值。

但有时用户比 TypeScript 更加了解类型,我们无法在给一个值赋值前使用它,但如果我们已经确定它已经被赋值了,这个时候类型系统就需要我们人的介入

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
let x: number;
initialize();
console.log(x + x);
// ~ ~
// Error! Variable 'x' is used before being assigned.

function initialize() {
x = 10;
}
```

添加 `!` 修饰: `let x!: number`,则可以修复这个问题。

[TypeScript 2.7 记录](https://segmentfault.com/a/1190000014913104)

## null 和 undefined

TypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null。

默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

但是如果开始了 `--strictNullChecks` 标记,null 和 undefined 只能赋值给 void 和它们各自。

[基本类型](https://www.tslang.cn/docs/handbook/basic-types.html)

## 在泛型约束中使用类型参数

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj 上,因此我们需要在这两个类型之间使用约束。

```typescript
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

TS 泛型

infer 的实践

关于 infer

Record 的实践

先看看 Record 的实现:

1
2
3
4
5
6
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};

我们可以理解为,使用类型 K 中的属性值作为 key,使用 T 作为对 value 的约束来构造一个新的类型。

举例1

如果有如下对象,如何使用 TS 进行类型定义:

1
2
3
4
5
const AnimalMap = {
cat: { name: '猫', title: 'cat' },
dog: { name: '狗', title: 'dog' },
frog: { name: '蛙', title: 'wa' },
};

解决方案:

1
2
3
4
5
6
7
8
9
10
11
type AnimalType = 'cat' | 'dog' | 'wa';
interface AnimalDescription {
name: string,
title: string
}

const AnimalMap: Record<AnimalType, AnimalDescription> = {
cat: { name: '猫', title: 'cat' },
dog: { name: '狗', title: 'dog' },
wa: { name: '蛙', title: 'wa' },
}

ref

Typescript 那些好用的技巧

  • 如何处理第三方库类型相关问题.

TypeScript-infer

ReturnType

ReturnType 可以获得函数的返回类型。

1
2
3
4
5
function sayName(name: string): string {
return name;
}

type ResultType = ReturnType<typeof sayName>;

我们看下 ReturnType 的实现:

1
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

上面 T extends U ? X : Y 的形式为条件类型(Conditional Types),即,如果类型 T 能够赋值给类型 U,那么该表达式返回类型 X,否则返回类型 Y

所以对于 ReturnType 我们有,如果传入的 类型 T 可以赋值给 (..args: any) => R,则返回 R

但是这里类型 R 从何而来?讲道理,泛型中的变量需要外部指定,即 ReturnType<T,R>,但我们不是要得到 R 么,所以不能声明在这其中。这里 infer 便解决了这个问题。表达式右边的类型中,加上 infer 前缀我们便得到了反解出的类型变量 R,配合 extends 条件类型,可得到这个反解出的类型 R。这里 R 即为函数 (...args: any) => R 的返回类型。

简而言之,infer 可以获得在 extends 中待推断的变量类型。

infer 实例

获取 Promise 类型中返回值的类型。

1
2
3
type PromiseVale<T> = T extends Promise<infer R> ? R : never;
type ResultType = PromiseVale<Promise<string>>;
// ResultType === string

获取函数的参数

1
2
3
4
5
6
7
8
9
type FuncArgsType<T extends (...args: any) => any> = T extends (...args: infer A) => any ? A : never;

// 测试
function sayName(weight: number, height: number): number {
return weight * height;
}

type ArgsType = FuncArgsType<typeof sayName>;
// type ArgsType = [number, number]

一道快手面试题

用 TypeScript 实现一个 caller 函数,接收一个函数作为第一个参数,其返回值类型、其它参数类型由接收函数的参数决定。

1
2
3
4
5
6
7
8
9
10
11
12
type FuncArgsType<T extends (...args: any) => any> = T extends (...args: infer U) => any ? U : never;

function caller<T extends (...args: any) => any>(func: T, ...args: FuncArgsType<T>): ReturnType<T> {
return func(args);
}

function sayName(name: string) {
return name;
}

const name = caller(sayName, 'jack'); // ok
const name = caller(sayName, 34); // error: Argument type 34 is not assignable to parameter type string

ref

TypeScript infer 关键字
infer 介绍

cycle.js 初识

lenses

lens 是一对可组合的 getter 和 setter 纯函数,它会关注对象内部的一个特殊字段,并且会遵从一系列名为 lens 法则的公理。将对象视为整体,字段视为局部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const lens = (getter, setter) => {
return ({
get: (obj) => getter(obj),
set: (value, obj) => setter(value, obj)
})
}

const aGetterLens = (obj) => obj['a'];
const aSetterLens = (value, obj) => ({
...obj,
a: value
});
const aLens = lens(aGetterLens, aSetterLens);

const obj = { a: 1 };

aLens.get(obj);
aLens.set('jack', obj)

cycle.js 结合 React

使用 rxjs 在 react 中实现 mvi 架构的小例子

其他

关于 MVI 的分形(fractal)架构和基于洋葱模型(onion)的状态管理机制的,当然这些都是从 cycle.js 得来的。

Cycle.js 状态管理模型

Cycle.js 基础21讲

cycle.js 官方文档

关于 Cycle.js 知乎上有些回答: RxJS/Cycle.js 与 React/Vue 相比更适用于什么样的应用场景?:

尤雨溪: “先说观点,React/Vue 和 Cycle 一起用是不太合理的,因为 Cycle 本身定位是框架,定义了整个应用的代码组织方式和开发范式,那就是无论是用户事件处理还是服务端数据同步,统统用 Rx 来做,Cycle 自己也提供了偏好的 view layer(基于 virtual-dom 的 DOM driver)。总的来说 Cycle 的范式侵入性很强,属于要么不用要用就得全盘接受 Rx for everything 的理念。我本身对于这个理念持保留态度,同时目前还没有看到过大型 Cycle 应用的例子,那么自然对于 Cycle 到底好不好用,也是持保留态度。

另一方面,在 React/Vue 应用中部分使用 Rx 是完全没有问题的。思路上来说就是把 React/Vue 组件的 local state 当做一个『中介』,在一个 Rx Observable 的 subscribe 回调里面更新组件状态。通过简单的绑定库支持,可以完全把 component state 作为一个实现细节封装掉,实现 Observable -> view 的声明式绑定”

cycle.js 结合 React

Cycle.js 作者本人阐述如何在 React 中使用

在 React 中实践 MVI 架构模式

ref

对 Cycle.js 的一些思考

Lenses:可组合函数式编程的 Getter 和 Setter

haskell 的 lens 机制

An Introduction Into Lenses In JavaScript

Lenses with Immutable.js

书:可组合式编程

mvi => cycle.js => lens => haskell => ramda => monad

Cycle.js 基础21讲

cycle.js 官方文档

Cycle.js 状态管理模型

tsconfig 详解

如果目录下项目存在一个 tsconfig.json 文件,那么意味着这个项目是 TypeScript 项目的根目录,tsconfig.json 指定了用来编译这个项目的根文件和编译选项。

tsconfig 配置项目

tsconfig.json 几个基本的顶级属性如下:

1
2
3
4
5
6
7
8
{
"extend": "",
"compilerOptions": {},
"files": {},
"include": {},
"exclude": {},
"compileOnSave": true
}

extend

tsconfig.json 文件可以利用 extends 属性从另一个配置文件里继承配置。

compileOptions

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
{
"compilerOptions": {
/* 基本选项 */
"target": "es3", // 设定编译后代码所遵循的 ECMAScript 版本 `'ES3' (default)` `ES5` `ES2015` `ES2016` `ES2017` `ESNEXT`
"module": "commonjs", // 设定编译后代码所遵循的模块规范 `commonjs(default)` `amd` `system` `umd` `es2015`
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定编译后 js 的输出目录
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}

module

module 设定 TypeScript 编译所遵循的模块规范,当你遇到如下错误的时候:

1
2
3
import lodash from "lodash";

SyntaxError: Cannot use import statement outside a module

有可能是在 node.js 项目中设置了非 common.js 的导出规范,那么请正确设定当前运行环境所对应的 module 规范。

相关 issues

rootDir outDir outFile

專案輸出 X 輸出設定

moduleResolution

moduleResolution 共有两种可用的模块解析策略:NodeClassic。 若未指定,那么在使用了 --module AMD | System | ES2015 时的默认值为Classic,其它情况时则为 Node

请参考TS模块解析

  • 相对 vs. 非相对模块导入
  • TypeScript在 package.json里使用字段”types”来表示类似”main”的意义 - 编译器会使用它来找到要使用的”main”定义文件。

@types,typeRoots和types

编译器默认会包含所有可见的 @types 包,包括 ./node_mouels../node_mouels ../../node_mouels 下的。

如果指定了 typeRoots,只有 typeRoots 下面的包才会被包含进来:

1
2
3
4
5
{
"compilerOptions": {
"typeRoots" : ["./typings"]
}
}

如果指定了 types,只有被列出来的包才会被包含进来。 比如:

1
2
3
4
5
{
"compilerOptions": {
"types" : ["node", "lodash", "express"]
}
}

files/include/exclude

“files”指定一个包含相对或绝对文件路径的列表。 “include”和”exclude”属性指定一个文件glob匹配模式列表。

  • 如果”files”和”include”都没有被指定,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts, .d.ts 和 .tsx),排除在”exclude”里指定的文件。

  • 使用”outDir”指定的目录下的文件永远会被编译器排除,除非你明确地使用”files”将其包含进来(这时就算用exclude指定也没用)。

  • 使用”include”引入的文件可以使用”exclude”属性过滤。 然而,通过 “files”属性明确指定的文件却总是会被包含在内,不管”exclude”如何设置。

“exclude”默认情况下会排除node_modules,bower_components,jspm_packages和目录。

compileOnSave

设置 compileOnSave 标记,可以让 IDE 在保存文件的时候根据tsconfig.json 重新生成文件。

附优秀的开源项目配置

  • vue-cli 的 tsconfig 配置

下面是通过 vue-cli 创建的 vue typescript 项目的 tsconfig.json 配置。

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
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
  • create-react-app 的 tsconfig 配置
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
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
}

import json

TS 2.9 以后的版本直接导入 json 文件,只需在 tsconfig.json 添加如下代码:

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true
}
}

// use
// import data from './package.json'

TS 2.9 以前的版本需要通过typings.d.ts文件定义,然后在文件中使用通配符导入:

1
2
3
4
5
6
7
8
// typings.d.ts
declare module "*.json" {
const value: any;
export default value;
}

// use
// import * as package from './package.json'

ref

tsconfig-json

tsconfig 编译选项

專案輸出 X 輸出設定

TS模块解析

  • 相对 vs. 非相对模块导入

vue-cli

create-react-app

import-json-into-typescript

JavaScript隐式类型转换

基本数据类型

ECMAScript 一共定义了七种 build-in types,其中六种为 Primitive ValueNullUndefinedStringNumberBooleanSymbol。而最后一种 Object build-in type 与通常意义上的 JavaScript 中 Object 并不一样,总的来说,只要不属于 Primitive Value 的值,就属于 Object 类型,比如数组、对象、日期、正则、函数。

装箱转换

每一种基本类型 number, string, boolean, symbolObject(build-in type) 中都有对应的类。所谓装箱转换,正是把基本类型转换为对应的对象,他是类型转换中一种相当重要的种类。

JavaScript 语言设计上试图模糊对象和基本类型之间的关系,比如,我们可以直接在基本类型上使用对象的方法:

1
console.log('abc'.charAt()); // a

甚至我们在原型上添加方法,都可以应用于基本类型。

实际上是 . 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

拆箱转换

在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

ToPrimitive

ToPrimitive 用于将 Object 转为 Primitive Value

对于我们平常遇到的 Object,其处理逻辑是:

  • 调用 Object.valueOf,如果结果是 Primitive Value,则返回;
  • 调用 Object.toString,如果结果是 Primitive Value,则返回;
  • 都不是,返回 TypeError

普通对象和数组的这两个方法返回的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = [12]
var b = {a: 123}

// [12]
a.valueOf()

// '12'
a.toString()

// {a: 123}
b.valueOf()

// '[object Object]'
b.toString()

如上,两者的 valueOf 返回的都不是 Primitive Value (返回了自身,还是 Object 类型)。那么,根据规范,两者调用 ToPrimitive 返回的将是一个 字符串

显示类型转换

ToBoolean

这个方法用于将不是 Boolean 类型的值转换为 Boolean 类型。

  • Undefined 返回 false
  • Null 返回 false
  • 所有 Object(引用类型/包装类型) 类型都会被转换为 true;
  • Number 类型中,0,NaN 会被转换为 false,其它都为 true
  • 只有空字符串 '' 为 false,其它都为 true

ToNumber

其它类型转换为 Number 类型。

  • Undefined 返回 NaN
  • Null 返回 0
  • Boolean 类型,true 为 1; false 为 0
  • String 类型,如果满足数字语义则转为数字,否则转换为 NaN
  • Object 类型,先转换为 Primitive Value 再递归调用自身 ToNumber 来转换。
1
2
3
4
5
6
7
8
// '56' ==> 56
Number([56])

// ',56' ==> NaN
Number([,56])

// '55,56' ==> NaN
Number([55, 56])

ToString

  • Number 返回 对应数值字符串
  • Boolean 返回字符串 “true” 或者 “false”
  • Undefined 返回 “undefined”
  • Null 返回 “null”

隐式类型转换

了解了上面的知识,可以开始进入我们的正题了,在 JavaScript 中可以触发隐式类型转换的操作有:

  • 四则运算: +, -, *, /
  • 比较运算符: ==, <, >, >=, <=
  • 判断语句: if, while
  • Native调用: console, alet 输入时会自动转换成 String 类型
  • 逻辑非 !,将直接调用 ToBoolean 方法,然后取反返回。

比较运算符

非严格比较(==)

  • 如果 Type 相同,等价于 A === B
  • 特别的, undefined == null
  • String == Number,则把 String 转换成 Number
  • Boolean 值的,将其它类型转换为 Boolean 类型,将 Boolean 转换成 Number
  • Object String/Number/Symbol,将 Object 转换成 Primitive Value
  • 否则,返回 false
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
// '12' ==> 12;
// 返回 true
12 == '12'

// 转 boolean: [] == 0
// 转 object: '' == 0
// 转 string: 0 == 0
// 返回 true
[] == false

// 转 object: '45' == 45
// 转 string: 45 == 45
// 返回 true
[45] == 45

// 单目: {} == false
// 转 boolean: {} == 0
// 转 object: '[object Object]' == 0
// 转 string: NaN == 0
// 返回 false
{} == !{}

// 单目:[] == fasle
// 转 boolean: [] == 0
// 转 array: "" == 0
// 转 string: 0 == 0
// 返回 true
[] == ![]

[] == []
[] == 0

严格比较 (===)

  • 类型不同,直接返回 false
  • Number 类型判断:有 NaN 就 false;
  • 特别的 +0 === -0;
  • 最后调用 SameValueNonNumber

另外 != 和 !== 则是指出了 A != B 与 !(A == B) 是完全等价的。在判断 !=/!== 时,其实就是在判断 ==/===.

不等关系

  • 两边操作数调用 ToPrimitive 转换为 Primitive Value
  • 由于 Primitive Value 出来有 StringNumber 两种结果,分别有不同的比较规则;

    1. 如果两边的值都是 String,则 按 code unit 比较,
    2. 如果一边是 Number,则将另一边也转换为 Number;注意 Number 需要处理 +0/-0/NaN/Infinity 等情况
1
2
3
4
5
6
7
8
// 注意转换后为 '45' < '46'
// 按字符串规则比较 最终比较的是 '5'.charCodeAt() < '6'.charCodeAt() => 53 < 54
// 返回 true
[45] < [46]

// 同理 [10] < [9] 最后进行的是 '10' < '9' 的比较,是字符串之间的笔记,不会转换为数字间的比较,
// 其实最终比较的是 '1'.charCodeAt() < '9'.charCodeAt() => 49 < 57.
[10] < [9]

练习题

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
// 每个表达式是 true 还是 false 呢?为啥呢?

// 初阶
!{}
12 == '12'
'false' == false
null == undefined

// 高阶
[] == []
[] == false
[] === false
[45] == 45

// 终阶
[45] < [46] ?
[10] < [9] ?
{} == !{}
{} != {}
-0 === +0
NaN === NaN
NaN != NaN

// 转换条件 转换后类型 结果
[]+[] // String “”
[1,2]+[3,4] // String “1,23,4”
[]+{} // String “[object Object]”
[1,2] + {a:1} // String “1,2[object Object]”
{}+[] // Number 0
{}+[1] //Number 1
{a:1}+[1,2] // Number NaN
{a:1}+{b:2} // Chrome - String “[object Object][object Object]” (背后实现eval)
{a:1}+{b:2} // Firefox - Number NaN
true+true // Number 2
1+{a:1} // String “1[object Object]”

reference

JavaScript 中的隐式类型转换的规范

JavaScript 运算符规则与隐式类型转换详解

JavaScript类型:关于类型,有哪些你不知道的细节?

深入浅出弱类型JS的隐式转换

JavaScript字符串间的比较

ecma-sec-relational-operators

前言

我想将我的学习过程全部记录下来,技术,工作,生活,思维片段,所有能记的都要记下来,终生学习这个理念不单要植入自己的脑子还要形成肌肉记忆。

记录这件事情一直在做,但是做得还不够好,单纯的记录其实意义不大,如果能分享出去,并因此而获取一些正向的反馈,然后再激励自己去 学习 => 记录 => 分享 => 获得正向反馈 形成一个无限重复下去的闭环,这将是一件非常有意思的事情。

背景

起因是我的 Hexo 博客有很久没有更新了,因为每次写完一篇文章要再部署到博客网站,太麻烦了。

我所有的内容都放在了 GitHub:my-notes 这个仓库下,然后本地使用 VS Code 去整理和编辑, Git + GitHub + VS Code 这套组合拳足以秒杀市面上的大部分笔记管理软件。

当然好的软件自有其存在的价值,比如我会使用:

  1. ulysses 去记录我零散的思维片段,其在移动端的编辑体验非常棒;
  2. 有道云笔记将在微信或者在网络中看到的有价值的资源一键发送到有道云笔记进行临时备份;
  3. 滴墨书摘将实体书或者图片中(如:网易云音乐图片中的歌词,海报文案…)的文字自动提取出来进行归类;
  4. 从 Kindle 中导出的读书笔记。

以上种种,所有搜集来的素材,等空下来的时候(周末)再整理和细分到 my-notes 下。

然后我会用 docsify 把 my-notes 中的内容放在我的 笔记:文档网站 中展示出来;最后,将一些想要分享的文章提出来再放到我的 博客:质数的一 博客。如果有兴趣话,再同步更新到简书, 掘金,segmentfault,知乎各个平台。

tip: 如果你是个技术极客,或者愿意再折腾一下,那么在 VSCode 下再集成 Cacher(Gitss 的第三方管理平台) 的插件,当然前提是你需要把素材(代码片段)放到 Cache 下,只需要 Shift + Option + I,素材(代码片段)就会自动插入到当前段落下。

Cacher 官方链接

docsify 和 Hexo 数据迁移

docsify 和 Hexo 的目录结构是不一样的,docsify 中的文章按照目录树的结构去组织的,这也是选择 docfisy 的原因,my-notes 本身就是一个 project,而 docsify 可以直接将整棵 project 树呈现出来。但是 Hexo 需要将所有的文章全部放在 source/_posts 目录下,并且 Hexo 的文章格式和 docsify 有差异的,这意味着在两者之间需要有一次文章格式的转换,于是需要将 docsify 中的树状目录转换为一维的形式,全部放在一个目录下去。

docsify 下的文章目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tree .
├── 算法
│   ├── 算法入门
│   │   ├── (0)Linux-C-编程.md
│   │   ├── (1)冒泡排序.md
│   │   ├── (10)-堆之队列的优化.md
│   │   ├── (2)快速排序.md
│   │   ├── (3)去除排序中的重复元素.md
│   │   ├── (4)队列.md
│   │   ├── (5)栈.md
│   │   ├── (6)Floyd最短路径算法.md
│   │   ├── (7)Dijkstra最短路径算法.md
│   │   ├── (8)邻接链表的非链表化实现.md
│   │   └── (9)树-二叉树-完全二叉树.md
│   └── 算法收藏夹
│   └── 红黑树探索笔记.pdf
├── 网络
│   └── HTTP-请求and响应.md
....more items

Hexo 下的文章目录结构:

1
2
3
4
5
6
7
8
9
ls hexo-blog/source/_posts
(0)Linux-C-编程.md JS对象(2)值传递与引用传递.md http(2)模块之服务器端.md
(1)Linux进程基础.md JS事件(3)EventUntil对象.md http模块(1)之客户端.md
(1)冒泡排序.md JS高阶(3)数组去重与排序.md jQuery(1)选择器.md
(10)-堆之队列的优化.md JS对象(3)经典对象创建与继承模式.md jQuery(2)事件.md
(2)Linux进程空间.md JS对象(4)对象方法.md jQuery(3)DOM属性与内容.md
(2)快速排序.md JS对象(5)对象属性.md jQuery(4)DOM节点操作.md
(3)Linux多线程与同步.md JS设计模型(1)单例模式.md jQuery(5)动画.md
...more items

嗯,需要一个脚本去完成这件事情,关于此部分的内容可以参考我的博文 博客: Hexo博客迁移与Node.js目录遍历

自动生成 docsify 文章导航目录

使用 docsify 还有一个问题是,每次添加新的文章都需要在 _sidebar.md 文件中添加一个新的文章链接, 内容大概如下:

1
2
3
4
5
6
7
8
9
10
11
bat _sidebar.md
1 │ - [README](/README)
2 │ - **migration_hexo**
3 │ - [将Hexo部署到自有服务器](/migration_hexo/将Hexo部署到自有服务器)
4 │ - [搭建Hexo博客站点](/migration_hexo/搭建Hexo博客站点)
5 │ - **前端笔记**
6 │ - **CSS**
7 │ - **CSS-Secrets**
8 │ - [字体排版](/前端笔记/CSS/CSS-Secrets/字体排版)
9 │ - [形状](/前端笔记/CSS/CSS-Secrets/形状)
16 │ - [FlexBox](/前端笔记/CSS/CSS3/FlexBox)

每添加一篇文章都要往这个文件里手动添加一个新的文章导航链接,非常麻烦,而且很容易出错,这种需要机械重复劳作的事情,显然也是通过一段代码就可以解决的 auto_generate_docsify_sidebar.js。该脚本会自动遍历当前工程下的所有目录然后根据所在的路径和文件名称自动生成 _sideBar.md 文件。

自动部署

Ok, 解决了 docsify 和 Hexo 之间的文章格式,接下就是部署的问题了,N 久以前我是将 Hexo 直接部署到 GitPages 中的,此部分内容可以参考我的博文 博客:Hexo系列之站点搭建篇 ,由于墙的存在以及种种原因,访问速度以及部署体验并不好,所以我又决定将 Hexo 部署我的腾讯云服务器中去,此部分内容可以参考我的博文 博客:Hexo系列之部署篇,嗯,当你可以将 Hexo 自动化部署到服务器之后,你会觉得实现 docsify 的自动化部署将会是件多么简单的事情,此部分内容可以参考 docsify 官方文档

那么,尽情享受码字的快感吧!

Hexo系列之部署篇

配置 Nginx

服务器端使用 Nginx 作为 Web 服务器,如果没有安装 Nginx,请安装 Nginx。

然后专门为 Hexo 创建一个部署目录 /wwwroot/hexo,然后在 Nginx 中写入配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /etc/nginx/nginx.conf
server {
listen 80;
listen [::]:80;
server_name iojo.xyz;
root /wwwroot/hexo;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
index index.html;
}
}

服务器上搭建hexo博客

配置 Git

初始化一个公共仓库

1
2
3
mkdir /wwwroot/hexo-repo
cd /wwwroot/hexo-repo
git init --bare blog.git

使用--bare参数,Git 就会创建一个裸仓库,裸仓库没有工作区,我们不会在裸仓库上进行操作,它只为共享而存在。

配置 Git hooks

在 blog.git/hooks 中添加一个 post-receive 的 hook,这个 hook 会在整个 Git 操作过程完结以后被运行,借助 hexo-deploy-git 在每次 push 完成自动化部署。

1
touch /wwwroot/hexo-repo/blog.git/hooks/post-receive

编辑该文件,写入如下内容:

1
2
#!/bin/sh
git --work-tree=/wwwroot/hexo --git-dir=/wwwroot/hexo-repo/blog.git checkout -f

设置该文件的可执行权限:

1
chmod +x post-receive

添加 Git 用户

为了安全,为服务器添加一个单独的 Git 用户来运行 Git 服务。

添加证书登录

将本地 ~/.ssh/id_rsa.pub 中的文件上传到服务器的 /home/git/.ssh/authorized_keys 中。

1
2
# 复制 ssh
cat ~/.ssh/id_rsa.pub | pbcopy

如果使用的是腾讯云服务器,请参考官网添加 ssh 的说明。

改变 blog.git 目录的拥有者为 git 用户

1
sudo chown -R git:git blog.git

禁用 git 用户的 shell 登录权限

出于安全考虑,我们要让 git 用户不能通过 shell 登录,编辑 /etc/passwd

1
2
3
vi /etc/passwd
# 将 git:x:1001:1001:,,,:/home/git:/bin/bash 改为
git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell

如此 git 用户可以通过 ssh 正常使用 git,但是无法登录 shell。

至此,服务器端的配置已经完成了。

自动化部署 Hexo

修改 hexo 目录下的 _config.yml 找到 deploy, 修改为:

1
2
3
4
5
6
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
type: git
repository: git@123.206.74.118:/wwwroot/hexo-repo/blog.git
branch: master

然后每次添加文章以后只需要执行:

1
hexo clean && hexo g && hexo d

VSCode 基础篇

高频快捷键

Cmd + Shift + P 切换命令面板

Ctrl + \` 切换 VS Code 集成终端

Ctrl + Option + O 在 VS Code 集成终端中打开当前文件

Cmd + shift + c 在系统终端中打开当前目录

常用快捷键

文件跳转

Cmd + Tab 在当前打开的文件中切换

Cmd + P 搜索当前工程下的文件

行跳转

Ctrl + G 输入目标行

符号(Symbols)跳转

cmd + shift + O 工具栏输入框里将会自动输入 @,此时会列出当前文件中的所有符号。如果输入紧接再输入一个 :,则会将所有符号进行分类。

JavaScript 中的小技巧:如果同时打开了多个 .js 文件,可以使用 Cmd + T 搜索这个文件中的符号。

定义(Definition)和实现(implementation)跳转

F12 跳转到函数定义处
Cmd + F12 跳转到函数实现的位置
Shift + F12 可以打开函数的引用预览
Ctrl + - 跳回到上一个操作时的位置

代码补全、快速修复、重构

Cmd + . 可以调出快速修复的列表来。比如写 CSS 时,将 padding 写成了 pading,可通过快速修复列表进行修改

Cmd + . 把一段代码抽取出来装换成一个单独的函数。需要先选中一段代码,然后会出现一个黄色的小灯泡,这时按下 Cmd + . 就出现将代码提取到全局还是当前函数的提示

F2 修改一个函数或者变量的名字

mac 默认是没有开启 F1~F12 的功能的,需要在 系统偏好设置 => 键盘 => 勾选 将F1、F2等键用作标准功能建

代码折叠,小地图和面包特性

Cmd + Option + [ 折叠当前光标所在的最内层代码
Cmd + Option + ] 展开当前光标所在的最外层代码
Cmd + K && Cmd + [ 从当前光标所在位置向最外层递归折叠
Cmd + K && Cmd + ] 从当前光标所在位置向最外层递归展开
Cmd + K && Cmd + O 折叠当前编辑器的所有可折叠的代码
Cmd + K && Cmd + J 展开当前编辑器的所有可展开的代码

通过关键词注释控制代码折叠

详情可参考 VS Code 官方文档

小地图

通过小地图我们可以看见整个文件的缩略版,但是很多时候我们只是需要看个大概的结构,而没必要看清每个字符。

Cmd + P 打开命名面板,搜索 Open Settings, 找到 editor.minimap.renderCharacter, 将其关闭,如此,所有的字符都会被渲染成色块

面包屑导航

面包屑主要是展示目前的代码在整个工程里的路径,同时你还能够看出这个代码所在位置的结构层级并且可以快速跳转。

要打开这个功能,Open Settings => breadcrumbs.enabled,找到后将它打开。

搜索和替换-单文件内

Cmd + F 自动将当前光标所在位置的单词填充到搜索框中,接着按下 Shift + Enter 可以在所有搜索结果中快速跳转。但是,Cmd + F 的聚焦点是在搜索框,不能直接修改目标字段。

Cmd + G 可以不断在搜索结果之间自上而下地循环跳转,这时我们只需直接打字就能对代码进行修改了。

Cmd + Shift + G 自下而上的搜索。

此外在进入到搜索状态是时候,可以通过以下快捷键切换响应的功能:

Cmd + Option + C 大小写敏感切换 (Case)
Cmd + Option + W 全单词匹配 (Word)
Cmd + Opiton + R 正则表达式匹配 (Regular Expression)

替换

Cmd + Option + F 直接调出替换窗口
Tab / Shift + Tab 在替换输入框和搜索输入框之间相互跳转
Cmd + Option + Enter 替换全部内容
Cmd + Shift + 1 替换第一个匹配结果
Cmd + Shift + L 将选中的单词替换为小写
Cmd + Shift + U 将选中的单词替换为大写

搜索-多文件

Cmd + Shift + F 打开多文件搜索

专注模式

Cmd + B 打开或者关闭侧边栏

Cmd + J 打开或者关闭面板

Cmd + K && Cmd + Z “切换禅模式”或者在 Cmd + Shift + P 中输入Toggle Zen Mode

多行编辑

Alt + Shift + i: vim 下选中多行,然后进入多行编辑模式