阅读推荐:本人需要您有一定的 React 基础,并且想简单了解一下 Hook 的工作方式和注意点。但是并不详细介绍 React Hook,如果想有进一步的了解,可以查看官方文档。因为项目比较简单,所以我会比较详细的写出大部分代码。建议阅读文章之前请先阅读目录找到您关注的章节。

目录

  1. React Hook + Parcel
    1. 环境搭建
    2. useState 第一个接触的 Hook
    3. useEffect 监听开始和结束事件
    4. 其他 Hook
    5. 我们来用 Emotion 加点样式
    6. 收尾
    7. 总结复盘 —— 性能问题?
    8. 总结

React Hook + Parcel

几天前,我女票和我说他们新人培训需要一个《真心话大冒险》的界面,想让我帮她写一个。我说好呀,正好想到最近的 React Hook 还没有玩过,赶紧来试试,于是花了一个晚上的时间,其实是俩小时,一个小时搭建项目,一个小时写。

Demo: http://souche-truth-or-dare.surge.sh (因为女票是大搜车的)

环境搭建

首先我们创建一个文件夹,做好初始化操作。

1
2
3
mkdir truth-or-dare
cd truth-or-dare
npm init -y

安装好依赖,react@next react-dom@next parcel-bundler emotion@9 react-emotion@9 babel-plugin-emotion@9

React Hook 截止发稿前(2018-12-26)还处于测试阶段,需要使用 next 版本。

emotion 是一个比较完备的 css-in-js 的解决方案,对于我们这个项目来讲是非常方便合适的。另外因为 emotion@10 的最新版本对 parcel 还有一定的兼容性问题,见 issue。所以这里暂时使用 emotion@9 的旧版本。

1
2
npm i react@next react-dom@next emotion@9 react-emotion@9
npm i parcel-bundler babel-plugin-emotion@9 -D

创建 .babelrc 文件或者在 package.json 中写入 Babel 配置:

1
2
3
4
5
{
"plugin": [
["emotion", {"sourceMap": true}]
]
}

创建 src 文件夹,并创建 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>真心话大冒险</title>
</head>
<body>
<div id="app"></div>
<script src="./index.jsx"></script>
</body>
</html>

index.jsx 文件

1
2
3
4
import * as React from 'react'
import { render } from 'react-dom'

render(<div>First Render</div>, document.getElementById('app'))

最后添加如下 scriptspackage.json

1
2
3
4
{
"start": "parcel serve src/index.html",
"build": "rm -rf ./dist && parcel build src/index.html"
}

最后我们就可以 npm start 就可以成功启动开发服务器了。在浏览器中打开 localhost:1234 即可。

parcel 已经内建了 Hot Reload,所以不需要进行额外的配置,开箱即用。是不是觉得非常简单,有了它,手动搭建项目不再困难。当然了,TS 也是开箱即用的,不过这次我这个项目真的很小,就不用 TS 了。

useState 第一个接触的 Hook

我们创建一个 App.jsx 开始我们真正的编码。先简单来看一下

1
2
3
4
5
6
7
8
9
10
11
export default function App() {
const [selected, setSelected] = useState('*')
const [started, setStarted] = useState(false)

return (
<div>
<div>{selected}</div>
<button>{started ? '结束' : '开始'}</button>
</div>
)
}

我们就完成了对 Hook 最简单的使用,当然了现在还没有任何交互效果,也许你并不明白这段代码有任何用处。

简单讲解一下 useState,这个函数接受一个参数,为初始值,可以是任意类型。它会返回一个 [any, (v: any) => void] 的元组。其中第一个 State 的值,另一个是一个 Setter,用于对 State 设置值。

这个 Setter 我们如何使用呢?只需要在需要的地方调用他就可以了。

1
<button onClick={() => setStarted(!started)}>{started ? '结束' : '开始'}</button>

保存,去页面点击一下这个按钮看看,是不是发现他会在 结束开始 之间切换?Setter 就是这么用,非常简单,如果用传统的 Class Component 来理解的话,就是调用了 this.setState({started: !this.state.started}) 。不过和 setState 不同的是,Hook 里面的所有数据比较都是 ===(严格等于)。

useState 还有很多用法,比如说 Setter 支持接收一个函数,用于传入之前的值以及返回更新之后的值。

useEffect 监听开始和结束事件

接下来,我们想要点击开始之后,屏幕上一直滚动,直到我点击结束。

如果这个需求使用 Class Component 来实现的话,是这样的:

  1. 监听按钮点击事件
  2. 判断是开始还是结束
    • 如果是开始,那么就创建一个定时器,定时从数据当中随机获取一条真心话或大冒险并更新 selected
    • 如果是结束,那么就删除之前设置的定时器

非常直接,简单粗暴。

用了 Hook 之后,当然也可以这样做了,不过你还需要额外引入一个 State 来存储 timer,因为函数组件无法持有变量。但是如果我们换一种思路:

  1. 监听 started 变化
    • 如果是开始,那么创建一个定时器,做更新操作
    • 如果是结束,那么删除定时器

好像突然变简单了,让我们想象这个用 Class Component 怎么实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class App extends React.Component {
componentDidUpdate(_, preState) {
if (this.state.started !== preState.started) {
if (this.state.started) {
this.timer = setInterval(/* blahblah*/)
} else {
clearInterval(this.timer)
}
}
}

render() {
// blahblah
}
}

好麻烦,而且逻辑比较绕,而且如果 componentDidUpdate 与 render 之间有非常多的代码的时候,就更难对代码进行分析和阅读了,如果你后面维护这样的代码,你会哭的。可是用 useEffect Hook 就不一样了。画风如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function App() {
// 之前的代码

// 当 started 变化的时候,调用传进去的回调
useEffect(() => {
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne())
}, 60)

return () => clearInterval(timer)
}
}, [started])

return (
// 返回的 View
)
}

当用了 React Hook 之后,所有的逻辑都在一起了,代码清晰且便于阅读。

useEffect 从字面意义上来讲,就是可能会产生影响的一部分代码,有些地方也说做成副作用,其实都是没有问题的。但是副作用会个人一种感觉就是这段代码是主动执行的而不是被动执行的,不太好理解。我觉得更好的解释就是受到环境(State)变化影响而执行的代码。

为什么这么理解呢?你可以看到 useEffect 还有第二个参数,是一个数组,React 会检查这个数组这次渲染调用和上次渲染调用(因为一个组件内可能会有多次 useEffect 调用,所以这里加入了渲染限定词)里面的每一项和之前的是否变化,如果有一项发生了变化,那么就调用回调。

当理解了这个流程之后,或许你就能理解为什么我这么说。

当然了,第二个参数是可以省略的,省略之后就相当于默认监听了全部的 State。(现在你可以这么理解,但是当你进一步深入之后,你会发现不仅仅有 State,还有 Context 以及一些其他可能触发状态变化的 Hook,本文不再深入探究)

到现在,我们再来回顾一下关于定时器的流程,先看一下代码:

1
2
3
4
5
6
7
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne())
}, 60)

return () => clearInterval(timer)
}

理想的流程是这样的:

  • 如果开始,那么注册定时器。——Done!
  • 如果是结束,那么取消定时器。——Where?

咦,else 的分支去哪里了?为啥在第一个分支返回了取消定时器的函数?

这就牵扯到 useEffect 的第二个特性了,他不仅仅支持做正向处理,也支持做反向清除工作。你可以返回一个函数作为清理函数,当 effect 被调用的时候,他会先调用上次 effect 返回的清除函数(可以理解成析构),然后再调用这次的 effect 函数。

于是我们轻松利用这个特性,可以在只有一条分支的情况下实现原先需要两条分支的功能。

其他 Hook

在 Hook 中,上面两个是使用非常频繁的,当然还有其他的比如说 useContext/useReducer/useCallback/useMemo/useRef/useImperativeMethods/useLayoutEffect

你可以创建自己的 Hook,在这里 React 遵循了一个约定,就是所有的 Hook 都要以 use 开头。为了 ESLint 可以更好对代码进行 lint。

这些都属于高级使用,感兴趣的可以去研究一下,本片文章只是入门,不再过多讲解。

我们来用 Emotion 加点样式

css-in-js 大法好,来一顿 Duang, Duang, Duang 的特技就好了,代码略过。

收尾

重新修改 src/index.jsx 文件,将 <div/> 修改为 <App/> 即可。

最后的 src/App.jsx 文件如下:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import React, { useState, useEffect } from 'react'
import styled from 'react-emotion'

const lists = [
'说出自己的5个缺点',
'绕场两周',
'拍一张自拍放实习生群里',
'成功3个你说我猜',
'记住10个在场小伙伴的名字',
'大声说出自己的名字“我是xxx”3遍',
'拍两张自拍放实习生群里',
'选择另一位小伙伴继续游戏',
'直接通过',
'介绍左右两个小伙伴',
]

function chooseOne(selected) {
let n = ''
do {
n = lists[Math.floor(Math.random() * lists.length)]
} while( n === selected)
return n
}

const Root = styled.div`
background: #FF4C19;
height: 100vh;
width: 100vw;
text-align: center;
`

const Title = styled.div`
height: 50%;
font-size: 18vh;
text-align: center;
color: white;
padding: 0 10vw;
font-family:"Microsoft YaHei",Arial,Helvetica,sans-serif,"宋体";
`

const Button = styled.button`
outline: none;
border: 2px solid white;
border-radius: 100px;
min-width: 120px;
width: 30%;
text-align: center;
font-size: 12vh;
line-height: 20vh;
margin-top: 15vh;
color: #FF4C19;
cursor: pointer;
`

export default function App() {
const [selected, setSelected] = useState('-')
const [started, setStarted] = useState(false)

function onClick() {
setStarted(!started)
}

useEffect(() => {
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne(selected))
}, 60)

return () => clearInterval(timer)
}
}, [started])

return (
<Root>
<Title>{selected}</Title>
<Button onClick={onClick}>{started ? '结束' : '开始'}</Button>
</Root>
)
}

总结复盘 —— 性能问题?

最近刚刚转正答辩,突然发现复盘这个词还挺好用的,哈哈哈。

虽然这么短时间的使用,还是有一些自己的思考,说出来供大家参考一下。

如果你仔细思考一下会发现,当使用 useEffect 的时候,其实每次都是创建了一个新的函数,但并不是说每次都会调用这个函数。如果你代码里面 useEffect 使用的很多,而且代码还比较长,每次渲染都会带来比较大的性能问题。

所以解决这个问题有两个思路:

  1. 不要在 Hook 中做太多的逻辑,比如说可以让 Hook 编写一些简单的展示组件,比如 Tag/Button/Loading 等,逻辑不复杂,代码量小,通过 Hook 写在一起可以降低整个组件的复杂度。

  2. 将 Effect 拆分出去,并通过参数传入。类似于这个样子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function someEffect(var1, var2) {
    // doSomething
    }

    export function App() {
    // useState...
    useEffect(() => someEffect(var1, var2), [someVar])
    // return ....
    }

    虽然这也是创建了一个函数,但是这个函数创建的速度和创建一个几十行几百行的逻辑的函数相比,确实快了不少。其次不建议使用 .bind 方法,他的执行效率并没有这种函数字面量快。

    这种方式不建议手动来做,可以交给 babel 插件做这部分的优化工作。

其实作为一个开发者来说,不应该太多的关注这部分,但是性能就是程序员的 XX 点,我还是会下意识从性能的角度来思考。这里只是提出了一点小小的优化方向,希望以后 React 官方也可以进一步做这部分的优化工作。

已经有的优化方案,可以查看官方 FAQ

总结

经过这个简短的使用,感觉用了 Hook 你可以将更多的精力放在逻辑的编写上,而不是数据流的流动上。对于一些轻组件来说简直是再合适不过了,希望早点能够正式发布正式使用上吧。

另外 parcel 提供了强大的内置功能,让我们有着堪比 webpack 的灵活度却有着比 webpack 高效的开发速度。

好的,一篇 1 小时写代码,1 天写文章的水文写完了。以后如果有机会再深入尝试。

注释和共享

不知何时,曾经我们认为的东西便会被打破,如果我们不坚持着去学习,那么我们终将会被社会所淘汰。于是我决定写《后知后觉系列》来记录一下我曾经跟不上的知识和关键点,内容不一定复杂,内容含量不一定高,也许别人已经写过一个一样的教程了,但是希望你能从我的笔记中获取你认为重要的东西,在纷繁复杂的工作中留下一个真正极客的世界,希望某一天这些东西都能够运用到工作当中。——XGHeaven

记得先看一下目录,找到你喜欢好奇的内容去针对性阅读,毕竟我不是来写教程的。

目录

  1. position: sticky 这究竟是一个什么鬼?
    1. 正确的使用姿势
    2. 举个栗子 - 通讯录列表头部
    3. 拓展思考
      1. 如何检测是否已经被固定?
      2. 那能不能实现表格头/列固定呢?

position: sticky 这究竟是一个什么鬼?

最近公司在用 Regular 封装一个表格组件,需要实现固定表头的功能。这个是几乎所有的组件库都会实现的一个效果,所以实现方式有很多种:

  1. 因为 thead/tr 的 position 属性是无效的,所以需要单独用 div 创建一个表头。然后设置这个表头的 position: absolute,同时 top: 0。同时这种模式下,需要用户指定每一列的宽度,保证自制的表头和下面原生的表格一一对应起来。如果不指定的话,也可以等待 dom 渲染完成之后,再测量宽度。比如 Ant Design 就是使用的这种方式。
  2. 因为上面那种方案的难点在于无法很好的保证自制表头和原生表格宽度的一致性,所以我们组的大佬提出了使用原生 thead,监听 scroll 事件,设置 transform 属性使得表头进行偏移,从而实现 fixHeader 的问题,这种方式解决了第一个的问题,但是需要手动监听 scroll 事件,在快速滚动的情况下,可能会有一定的性能问题。而且不够优雅。如果后面的表格内容中有 position: relative 的元素,会覆盖到表头。

不管是哪种方式,我总感觉不是很完美,于是我就在思考,除了手动更新的方式,难道就没有一些比较好的方式去做。然后我就去翻看了 github 的固定表头的方式,顿时豁然开朗。于是就延伸出了这篇文章,position: sticky 属性。

Pay Attention:后面所讲的内容就不怎么和表格固定表头相关,如果你对表格固定表头或者固定列有一定问题,可以查看网易考拉的这篇文章 《一起来聊聊table组件的固定列》

当第一眼看到这个熟悉的时候,第一句话就是“我 CA”,这 TMD 是什么鬼属性,position 什么时候有了这个属性。于是去看了 MDN 的介绍,可以理解为,这个属性是实现固定顶部最简单的实现方式

他其实是一种 position:relativeposition: fixed 的结合体,一定要配合 top/right/bottom/left 的属性一起才有作用,设置对应方向的最小值。当大于最小值的时候,他就像 relative 一样,作为文档流的一部分,并且 top/right/bottom/left 属性也会失效。否则当小于设置的值的时候表现的像 fixed,只不过这个 fixed 不再现对于窗口,而是相对于最近的可滚动块级元素。

如果你看过其他关于 sticky 的文章,大部分都会以黏贴的意思来解释他,那么很明显,确实也是这个意思,如果你觉得看了其他教程能够清楚的话,那么可以不用看我这篇了,如果你没看懂的话,可以来我这里看看。

废话少说,我们先来看一下如何正确使用 sticky。

正确的使用姿势

以下的代码预览请使用最新 Chrome 查看,或者支持 position: sticky 的浏览器查看。部分网站不支持 iframe,可以去我的 Blog 查看

  1. position: sticky 只相对于第一个有滚动的父级块元素(scrolling mechanism,通过 overflow 设置为 overflow/scroll/auto/overlay 的元素),而不是父级块元素。

  2. position: sticky 只有当设置对应的方向(top/right/bottom/left),才会有作用,并且可以互相叠加,可以同时设置四个方向。

  3. 即使设置了 position: sticky,也只能显示在父级块元素的内容区域,他无法超出这个区域,除非你设置了负数的值。

  4. position: sticky 并不会触发 BFC,简单来讲就是计算高度的时候不会计算 float 元素。

  5. 当设置了 position: sticky 之后,内部的定位会相对于这个元素

  6. 虽然 position: sticky 表现的像 relative 或者 fixed,所以也是可以通过 z-index 设置他们的层级。当这个元素的后面的兄弟节点会覆盖这个元素的时候,可以通过 z-index 调节层级。

    See the Pen position: sticky 通过 z-index 调节层级 by Bradley Xu (@xgheaven) on CodePen.

当你懂了这几个之后,其实这个属性就用起来就很简单了。

举个栗子 - 通讯录列表头部

no code no bb,直接上代码。

拓展思考

如何检测是否已经被固定?

最常见的需求就是,当还在文档流当中的时候,正常显示,但是当固定住的时候,添加一些阴影或者修改高度等操作。要想实现这个效果,第一反应可能就是手动监听 scroll 事件,判断位置,这当然是没有问题的,但是随之而来的确实性能的损耗。

最好的方式是使用 IntersectionObserver,这是一个可以监听一个元素是否显示在视窗之内的 API,具体内容见阮老师的《IntersectionObserver API 使用教程》。基本原理就是在一段滚动的头部和尾部分别添加两个岗哨,然后通过判断这两个岗哨的出现和消失的时机,来判断元素是否已经被固定。

例子详见此处

那能不能实现表格头/列固定呢?

理想很丰满,显示很骨感,因为 thead/tbody 对 position 无爱,所以也就不支持 sticky 属性,所以我们还是要单独创建一个头部。

后来经过网友提醒,自己又去研究了一下,发现还是有办法做到固定表头和列的。

首先针对 Firefox,它本身就支持 thead/tbody 的 position 属性,所以可以直接通过对 thead/tbody 设置 position 来实现。而对于 Chrome 浏览器来讲,可以通过设置 thead 内的 th 来实现。具体见 Demo.

See the Pen position sticky 通过设置 td 来实现固定表头 by Bradley Xu (@xgheaven) on CodePen.

然后好像就没有了,谢谢观看水水的《后知后觉系列》

注释和共享

目录

  1. Trouble 1: U 盘启动找不到启动盘
    1. 问题描述
    2. 解决方案
  2. Trouble 2: 安装的时候明明插着网线但是却提示无网络
    1. 问题描述
    2. 解决方案
  3. Trouble 3: CentOS 安装的时候一直卡死在设置安装源
    1. 问题描述
    2. 解决方案
  4. 安装完毕

自己家里有一台 Dell 服务器,之前一直跑着 FreeNAS,后来发现自己对 NAS 的需求并不是很高,所以我决定装回 Linux,之前用的 Debian,虽然 Debian 很好,但是没法只使用 root 用户,所以我又回归了 CentOS 系统,但是问题并没有我想的那么简单。

另外,这是一篇总结文章,所以没有图,见谅。

Trouble 1: U 盘启动找不到启动盘

这里所说的找不到启动盘并不是说无法进入引导界面,而是说进入了引导界面但是无法正确加载安装镜像。具体表现请往下看

问题描述

相信大家装过系统的都知道,在 Windows 上面有很多 ISO 刻录到 U 盘的工具,这些工具非常好用,所以我就理所当然的用 ISO to USB 这个软件刻录启动 U 盘。

我刻录的是 CentOS-7-DVD 的版本,大约 4.2G,里面包含了必要的安装包。

U 盘刻录完成,插入电脑,启动,一切正常,进入选择界面,选择 Install CentOS 7,之后问题来了,出现了 failed to map image memory 提示,之后等了一段时间之后就一直出现 warning:dracut-initqueue timeout-starting timeout script 的提示,最终显示启动失败,进入恢复模式,显示 dracut:/# 终端提示符,不管怎么重启都不行。

当然,Google 当然是有结果的,《安装CentOS7.4出现failed to map image memory以及warning:dracut-initqueue timeout的解决办法》 指出了原因和解决方案。

解决方案

出现这个原因是因为找不到启动盘,解决方案其实也很简单,就是手动设置一下就可以了。

首先找到你的 U 盘是哪个硬盘符,方法就是在恢复模式下运行 ls -l /dev | grep sd,可以看到一系列的文件,一般情况下 sda 是硬盘,sdb 是 U 盘,sda1 是硬盘上第一个分区,同理 sda2 是第二个分区。如果你有多个硬盘,那么 U 盘可能是 sdc, sdd 等等,找到你的 U 盘启动盘的分区,我的是 sdb1。

然后在选择界面的时候按 e,然后将 inst.stage2=hd:LABEL=CentOS\x207\x20x86_64.check 修改为 inst.stage2=hd:/dev/sdbx(你u盘所在),之后修改结束之后按 Ctrl+x 退出就可以正常进入安装界面了。

那么有人会说每次启动都要这么做么?答案是的,但实在是太麻烦了,不急,这个也是有办法解决的,办法请看 Trouble 3 的解决方案。

Trouble 2: 安装的时候明明插着网线但是却提示无网络

问题描述

进入安装界面之后,会有一个网络的选项,一直提示未连接,不管你怎么设置。无论是你拔插网线还是禁用启用都不行。

解决方案

这是因为 CentOS 默认的网络是不自动连接的,你可以在设置界面,选择 General ,也就是第一个标签页,把启动时自动连接网络的选项勾选,然后保存。然后就会自动去获取 ip 地址然后可以上网了。

这一点的设计实在是太 SB 了,不知道为啥要设计成这个样子。

Trouble 3: CentOS 安装的时候一直卡死在设置安装源

问题描述

当我们成功进入安装界面的时候,你会发现他的安装源是无效的,需要你自己去设置。
理论上来讲使用的是 DVD 的镜像,是自带了很多包的,不会出现这种情况。
而且出现这种情况的时候,是无法选择本地的安装源的,只能填写网络。
但是不管你填写的是官方的安装源还是阿里、网易的安装源,都会一直卡死在设置安装源这个环节。

解决方案

经过 Google,得知原因是因为刻录 U 盘的方式不对。《CentOS7安装时的新问题》 给出了原因和解决方案。

引用之

绝望中,无意间看到 Centos 百科(https://wiki.centos.org/zh/HowTos/InstallFromUSBkey) 上的一段话:“由于 CentOS 7 安装程序的映像采用了特殊的分区,而截至 2014 年 7 月,大部份Windows 工具都不能正确地转移,因此利用 USB 存储器开机时会导致不能预知的结果。(暂时)已知不适用的工具包括 unetbootin 和 universal usb installler。已确定能正确运作的有 Rufus、Fedora LiveUSB Creator、Win32 Disk Imager、Rawrite32 及 dd for Windows。如果采用 Windows 7 以上的版本,请先卸下该 USB 存储器(其中一个方法是在执行工具程序前把存储器格式化),否则 indows 可能会拒绝写入该存储器,出现 can’t write to drive 错误及取消行动。”

可以得知,CentOS 的 ISO 镜像的刻录方式比较特殊,大部分软件都无法很好的兼容,但是 dd 可以非常好的兼容,所以这里我切换了刻录软件,使用 Rufus 进行刻录,并且选择 DD 模式。

然后重启启动,哈哈哈,不仅仅这个问题解决了,还完美解决了 Trouble 1 无法找到启动盘的问题。

安装完毕

记得之前安装的时候,也没有遇到这么多麻烦。这次既然遇到了就记录下来。

之后我就可以开心的玩耍了,装了 Docker,切换安装源到网易,安装 epel 等等。

注释和共享

从接触到 node 环境来说,其中一个不可或缺的一部分便是 npm 包管理,但是由于官方的 npm 有各种各样的问题,于是催生了很多不同的版本,这其中的曲折也许只有过来人才知道。

放弃 npm?

上古时代

在上古版本(应该是 npm3 以前的版本,具体我也记不清了),npm 的安装策略并不是扁平化的,也就是说比如你安装一个 express,那么你会在 node_modules 下面只找到一个 express 的文件夹。而 express 依赖的项目都放在其文件夹下。

1
2
3
4
5
6
7
8
- app/
- package.json
- node_modules/
- express/
- index.js
- package.json
- node_modules/
- ...

这个带来的问题或许 windows 用户深谙其痛,因为在这种安装环境下,会导致目录的层级特别高,而对于 windows 来说,最大的路径长度限制在 248 个字符(更多请见此),再加上 node_modules 这个单词又特别长,所以你懂得,哈哈哈。解决方案啥的自己去搜索吧,反正估计现在也没人会用上古版本了。

除了 windows 用户出现的问题以外,还有一个更严重的问题,就是模块都是独立的,比如说位于 express 下面的 path-to-regexpconnect 下面的 path-to-regexp 的模块是两个不同的模块。
那么这个会带来什么影响呢?其实在使用上,并没有什么太大的影响,但是内存占用过大。因为很多相同模块位于不同模块下面就会导致有多个实例的出现(为什么会加载多个实例,请查看 Node 模块加载)。你想想,都是同样的功能,为什么要实例这么多次呢?不能就加载一次,复用实例么?

上古时代的 npm 的缺点可以说还是很多的:

  • 目录嵌套层级过深
  • 模块实例无法共享
  • 安装速度很慢,这其中有目录嵌套的原因,也有安装逻辑的问题。因为 npm 是请求完一个模块之后再去请求另一个模块,这就会导致同一个时刻,只有一个模块在下载、解析、安装。

软链时代

后面,有人为了解决目录嵌套层次过高的问题,引入的软链接的方案。

简单来说,就是将所有的包都扁平化安装到一个位置,然后通过软链接(windows 快捷方式)的方式组合到 node_modules 中。

1
2
3
4
5
6
7
8
9
10
11
12
- app/
- node_modules
- .modules/
- express@x.x.x/
- node_modules
- connect -> ../../connect@x.x.x
- path-to-regexp -> ../../path-to-regexp@x.x.x
- ... -> ../../package-name@x.x.x
- connect@x.x.x/
- path-to-regexp@x.x.x/
- ...others
- express -> ./.modules/express@x.x.x

这样做的好处就是可以将整体的逻辑层级简化到很少的几层。而且对于 node 的模块解析来说,可以很好的解决相同模块不同位置导致的加载多个实例,进而导致内存占用的情况。

基于这种方案,有 npminstall 以及 pnpm 这个包实现了这种方案,其中 cnpm 使用的就是 npminstall,不过他们实现的方式和我上面讲的是有差异的,具体请看。简单来讲,他们没有 .modules 这一层。更多的内容,请看 npminstall 的 README。

总的来讲这种解决方案有还有以下几个好处:

  • 兼容性很好
  • 在保证目录足够简洁的情况下,解决了上面的两个问题(目录嵌套和多实例加载)。
  • 安装速度很快,因为采用了软连接的方式加上多线程请求,多个模块同时下载、解析、安装。

那么缺点也是挺致命的:

  • 一般情况下都是第三方库实现这个功能,所以无法保证和 npm 完全一致的行为,所以遇到问题只能去找作者提交一下,然后等待修复。
  • 无法和 npm 很方便的一起使用。最好是要么只用 npm,要么只用 cnpm/pnpm,两者混用可能会产生很奇葩的效果。

npm3 时代

最大的改变就是将目录层级从嵌套变到扁平化,可以说很好的解决了上面嵌套层级过深以及实例不共享的问题。但是,npm3 在扁平化方案下,选择的并不是软连接的方式,而是说直接将所有模块都安装到 node_modules 下面。

1
2
3
4
5
6
- app/
- node_modules/
- express/
- connect/
- path-to-regexp/
- ...

如果出现了不同版本的依赖,比如说 package-a 依赖 `package-c@0.x.x的版本,而package-b依赖package-c@1.x.x` 版本,那么解决方案还是像之前的那种嵌套模式一样。

1
2
3
4
5
6
7
8
9
- app/
- node_modules/
- package-a/
- package-c/
- // 0.x.x
- package-b/
- node_modules/
- package-c/
- // 1.x.x

至于那个版本在外面,那个版本在里面,似乎是根据安装的先后顺序有关的,具体的我就不验证了。如果有人知道的话,欢迎告诉我。

在这个版本之后,解决了大部分问题,可以说 npm 跨入了一个新的世界。但是还要一个问题就是,他的安装速度依旧很慢,相比 cnpm 来说。所以他还有很多进步的空间。

yarn 的诞生

随着 Node 社区的越来越大,也有越来越多的人将 Node 应用到企业级项目。这也让 npm 暴露出很多问题:

  • 无法保证两次安装的版本是完全相同的。大家都知道 npm 通过语义化的版本号安装应用,你可以限制你安装模块的版本号,但是你无法限制你安装模块依赖的模块的版本号。即使有 shrinkwrap 的存在,但是很少有人会用。
  • 安装速度慢。上文已经讲过,在一些大的项目当中,可能依赖了上千个包,甚至还包括了 C++ Addon,严重的话,安装可能要耗时 10 分钟甚至到达半个小时。这很明显是无法忍受的,尤其是配合上 CI/CD。
  • 默认情况下,npm 是不支持离线模式的,但是在有些情况下,公司的网络可能不支持连接外网,这个时候利用缓存构建应用就是很方便的一件事情。而且可以大大减少网络请求。

所以,此时 yarn 诞生了,为的就是解决上面几个问题。

  • 引入 yarn.lock 文件来管理依赖版本问题,保证每次安装都是一致的。
  • 缓存加并行下载保证了安装速度

那个时候我还在使用 cnpm,我特地比较了一下,发现还是 cnpm 比较快,于是我还是继续使用着 cnpm,因为对于我来说足够了。但是后面发现 yarn 真的越来越火,再加上 cnpm 长久不更新。我也尝试着去了用 yarn,在尝试之后,我彻底放弃了 cnpm。而且直到现在,似乎还没有加入 lock 的功能。

当然 yarn 还不只只有这么几个好处,在用户使用方面:

  • 提供了非常简洁的命令,将相关的命令进行分组,比如说 yarn global 下面都是与全局模块相关的命令。而且提示非常完全,一眼就能看明白是什么意思。不会像 npm 一样,npm --help 就是一坨字符串,还不讲解一下是什么用处,看着头疼。
  • 默认情况安装会保存到 dependencies,不需要像 npm 一样手动添加 -S 参数
  • 非常方便的 yarn run 命令,不仅仅会自动查看 package.json 中 scripts 下面的内容,还是查找 node_modules/.bin 下的可执行文件。这个是我用 yarn 最高的频率。比如你安装了 yarn add mocha,然后就可以通过 yarn run mocha 直接运行 mocha。而不需要 ./node_modules/.bin/mocha 运行。是我最喜欢的一个功能
  • 交互式的版本依赖更新。npm 你只能先通过 npm outdated 看看那些包需要更新,然后通过 npm update [packages] 更新指定的包。而在 yarn 当中,可以通过交互式的方式,来选择那些需要更新,那些不需要。
  • 全局模块的管理。npm 管理全局模块的方式是通过直接在 /usr/lib/node_modules 下面安装,然后通过软连接连接到 /usr/local/bin 目录下。而 yarn 的做法是选择一个目录,这个目录就是全局模块安装的地方,然后将所有的全局模块当做一个项目,从而进行管理。这个好处就是,你可以直接备份这个目录当中的 package.json 和 yarn.lock 文件,从而可以很方便的在另一个地方还原你安装了那些全局模块。至于这个目录的问题,通过 yarn global dir 命令就可以找到,mac 下是在 ~/.config/yarn/global/,linux 我没有测试过。

可以说 yarn 用起来非常舒服,但是唯一的缺点就是不是 npm 官方出的,更新力度、兼容性都会差一些。但这也阻挡不住 yarn 在 Node 社区的火热程度。很快,大家纷纷从 npm 切换到 yarn 上面。

重拾 npm 5

在受到 yarn 的冲击之后,npm 官方也决定改进这几个缺点,于是发布了和 Yarn 对抗(这个词是我意淫的)的 npm5 版本。

  1. 引入了 package-lock.json,并且默认就会添加,和 yarn.lock 是一样的作用,并且取代之前的 npm shrinkwrap。
  2. 默认情况下,安装会自动添加 dependencies,不需要手动书写 -S 参数
  3. 提升了安装速度,和之前有了很大的进步,但是和 yarn 相比,还是略微慢一些

至此,yarn 和 npm 的差距已经非常非常小了,更多的差距体现在用户体验层面,我使用 yarn 的功能也只剩下全局模块管理、模块交互式更新和 yarn run 这个命令了。

但是后面推出的 npx 让我放弃了使用 yarn run 这个命令。不是说 npx 比 yarn 有多好,而是说 npm 集成了这个功能,也就没必要再去使用第三方的工具了。而且 npx 还支持临时安装模块,也就是那种只用一次的命令,用完就删掉了。

后面我又发现了 npm-check 这个工具,我用它来替代了 yarn 的交互式更新。

然而 npm6 的出现加入了缓存,并且又进一步提升了速度,可以说直逼 yarn。

于是 yarn 对我来说只剩下一个全局模块管理的功能了。我的整个开发流程以及从 yarn 切换回 npm 上面了。或许后面的日子我也会让 npm 来接管全局模块管理,从而放弃使用 yarn。但是我还是会装 yarn,毕竟有一些老项目还是用 yarn 的。

总结

我经历了从 npm -> cnpm -> yarn -> (npm + npm-check + npx) 的一个循环,也见证了 npm 社区的一步步发展。而且 yarn 的更新频率也非常慢,可能一个月才更新一次,这也让我逐渐放弃使用 yarn。

有的时候感觉,第三方的终究是第三方,还是没有原生的好用和方便,而且用起来安心。

注释和共享

本人是一名 Node.js 实习生,在进入大搜车之后,有幸见识到 Akyuu.js 这个框架。但是这个框架是使用 Express + Callback 的方式,我不是很喜欢。在我的推荐以及社区的发展下,组长决定用 TS + Async/Await 来试一试。于是我也去了解了一下 TS 的后端框架有哪些,结果经过别人推荐,找到了 Nest.js 这个想法几乎和我一模一样的框架。

框架简介

因为我这个不是教程向,所以就不细讲,可以查看 Nest.js 官网。从我的感性角度来讲,简单说一下以下几个特点:

  • 去中心化路由。所有的路由通过装饰器与 Controller 绑定。简单、明了,学习成本低。
  • TypeScript/Rx.js 加持。智能补全,代码分析,静态类型等等优点。如果你只是个人用用的话,可能会觉得很全。但是放在企业当中使用,是非常大的优点。
  • 依赖注入。从 Angular 那里学习而来,但是进行了一些简化,但是完全够用。比如说简化掉了 deps。
  • 模块思想。Node 社区的后端框架,其实都被 Express 导向到了中间件的模式。而 Nest.js 却从 Angular 当中吸取到了模块的思想。不同的 Service、Controller、Component 组成不同的模块。模块之间可以相互依赖,也可以独立存在,这大大减少了测试和逻辑的复杂度。
  • 易于扩展。以往的框架,你能做的就是编写业务逻辑,而其他的你都很难去做到。于是传统的后端框架不得不引入了一套插件机制来增强框架的扩展性。但是 Nest.js 将插件的功能直接内置到了框架当中。传统的插件在这里可以认为就是一个模块,通过加载不同的模块来添加不同的功能。
  • Express 基石。有人会说,不是现在 Koa 才是更好的模型么?洋葱模型可以解决更多复杂的问题。没错,我不反对这个言论。但是我想说的是,Express 还是最简单最通用的方式,因为他不赖 Generator/Promise,只需要你又一个 Node.js 运行环境,支持 Callback 就可以了。(话说应该没有不支持 Callback 的 Node.js 环境吧,哈哈哈)不管怎么样,Express 的覆盖面还是比 Koa 要广不少。
  • 条条大路通罗马。那么有人就问了,那我要实现洋葱模型怎么办呢?我想说,办法总是会有的。而在 Nest.js 当中,通过 Interceptor ,可以很好的实现洋葱模型。也就是说你可以通过 Interceptor 来记录请求的耗时。
  • 同步代码。这里所说的同步代码并不是单单指的是 async/await。在很多支持 async/await 的框架中,如果你想返回值,如果是 Express ,你还是需要调用 resp.send(await getValue()),而 koa 也是需要调用 ctx.body = await getValue()。但是在 Nest.js 中,只需要 return await getValue() 即可。实现真正的同步编写业务逻辑代码。
  • 逻辑分层。其实很多功能,都是可以通过中间件来实现的。但是不同类型的功能有不同的需求,如果只是通过中间件来实现,势必会导致有一些重复的代码。于是 Nest.js 里面引入了 Pipe/Interceptor/Guard/ExceptionFilter 等逻辑层。不同的层里面处理相似的事情,比如说 Pipe 处理的是输入数据的转换。而 Interceptor 来实现洋葱模型。Guard 用于权限校验等拦截任务。ExceptionFilter 用来处理错误数据。这种分层带来的好处就是可以让代码更加清晰,主需要思考这个层需要做的事情,而不需要站在中间件的层面去考虑这个事情。
  • Validation。自带校验,而且和 TS 结合的非常完美,使用起来很舒服,请看教程
  • 输入参数的转换。这个其实是一个很方便的方面。有的时候你需要将输入的参数转换成一个类,这个时候你就可以通过 Validation 进行转换。你要是不想用自动转换,可以通过传统的手动转换的方式。
  • 测试功能完美。由于采用了依赖注入,所以测试简直简单的不得了。而且官方也提供了一系列测试工具,也能很好的解决单元测试的问题。

Nest.js 企业化当中的问题

  • 目录无约束。在企业当中,不对目录进行约束会导致代码越来越乱。从而降低了代码可维护性。
  • 没有配置管理功能。在框架开发中,配置往往是一个很重要的功能。比如说配置数据库的连接,配置监听的端口。
  • 没有进程管理。虽然有提供 @nestjs/cli,但是这个提供的仅仅是一个项目的创建的能力。
  • 部分文档讲解不详细,会提高入门的门槛。

不过总的来说,前面几点也正是 Nest.js 灵活性的保证。但是我们真正在开发当中,还是需要一种合理的约束来保证开发的统一。

Nest.js 企业化的尝试

那么我们这里针对上面的几个问题,尝试采用一些方式来进行约束。

目录结构

我们对项目指定如下的规则:

  • 全部通过 TypeScript 书写,并且全部位于 src 目录下
  • 入口文件是 main.ts 如果没有特殊情况,不动这个文件
  • 配置放在 src/config 文件夹下
  • 所有的 Service/Controller/Logic/Component 等都挂载到 MainModule 下。
  • 其中 module 文件夹存放自定义的 Module,或者说希望独立成模块但是还没有完全独立出来的。其中目录结构和这个项目目录结构类似
  • boot 文件夹是项目启动代码的时候执行的,这部分在 Nest.js 当中没有给出。我这里打算添加这个功能,但是还没有想好具体的实现形式,所以待定。
  • interface/enum 等数据随着对应的 service 导出。不另做说明。比如说 car.service.ts 除了可以导出 CarService 类以外,还可以导出 CarType enum。
  • dest 文件夹是编译之后的文件,可以直接输入 node dest/main.js 运行。
  • 命名规则
    • 所有的文件除了 main.ts 和类文件以外,都要添加类型后缀,比如说 user.model.ts car.controller.ts google.logic.ts。但是比如说只是一个 Car 类,那么可以直接命名成 car.ts
    • 不允许通过 export default 导出数据。一方面是为了方便导入的时候保证命名的统一,另一方面可以随时导出 interface/enum 等内容。
    • 所有的测试文件后缀名都以 .spec.ts.test.ts 结尾。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|-- dest
|--- ...
|-- src
|-- config
|-- controller
|-- model
|-- service
|-- logic
|-- component
|-- boot
|-- module
|-- module-name
|-- config
|-- index.ts
|-- module-name.module.ts
|-- main.ts
|-- main.module.ts

配置管理

我目前初步的想法是通过提供一个 ConfigModule 暴露出一个 ConfigService 来提供配置的获取和查看。

在某些情况下,可能需要多级配置,模块级别的配置,应用级别的配置。那么 ConfigService 可以在获取配置的时候自动合并这些规则。

进程管理

现在已经是 18 年了,不用 Docker 你真的对得起自己么?很明显是对不起的。所以进程管理这一块,我们就交给 Docker 来处理。包括启动、停止、重启、日志等,都交给 Docker。

于是启动命令就可以简化成 node dest/main.js 即可。

那么你可能会想到,如果一个 Docker 环境给你分配了两个 u,那岂不是会浪费一个 u。理论上是的,那么你就可以通过 pm2 啊啥的自己去管理吧,哈哈哈,不管。

Iron.js

说了这么多,把上面的内容都沉淀下来,我得要给他取个名字,于是我就取成了 Iron。为啥叫 Iron 呢?因为 Iron Man。那为啥因为 Iron Man 呢?因为他制作的盔甲可以自由拆分,自动拼合。非常适合我们这个项目的形态。

不过这个项目什么时候能沉淀下来,看我心情了。不过定个时间线吧,就在 4 月底,争取搞定。

因为这里面最大的问题就是配置的问题,需要深入依赖注入,所以会麻烦一些。但是其他的方面更多的只是一种约束吧。

这就是我用 Nest.js 一周以来的心得。暂时就想到这么多,更多的内容等我后面再分析吧。

写完睡觉,答应女票了,啦啦啦~

注释和共享

2018 年终总结

发布在 年终总结

2017 年,有开心,也有失落。也不知道该从何说起,就随便写写了。高中语文就没学好过,所以可能写成流水账了。请各位看官多多包涵。

实习

如今步入大四,最大的心事就是找实习找 Offer 了。先后经历了两次阿里面试的失利,至于失利的过程,我之前有吐槽过,就不多说了,而且听说今年的面试难度提升到了社招,心痛。饿了么/七牛都有投递过,但是都没有进。如果各位看官想招 Node.js 或者 前端 的校招生的话,请联系我哦~ 请在各大社交媒体请搜索 XGHeaven 即可

终于在 CNode 上面找到了一家美国 AR 公司,Integem。

在里面主要是用 Electron 做客户端,技术栈就是 Vue 全家桶,不多说了。反正整体干下来的感觉其实和自己接了一个外包的感觉差不多,而且设计师设计的页面参差不齐。最可气的是,假设页面 A 和页面 B 相同的部分,没准一个就有边框,另一个就没有。没准一个字体是 12px,另一个可能就是 16px。真是受不了,一开始,我还是让他确认一下到底哪个设计图是对的,到最后,只能用我的佛系心态对待这个设计图,懒得问了。切个图,也是切的乱七八糟,我还是自己来好了。

在里面差不多干了 5 个月,再加上学校要求每个人都要去跟着导师完成一个实践项目,于是我就离开了。

再之后,我在逛实习的时候,突然发现大搜车在招 Node.js 实习生,而且标注的是有大牛带。大搜车……为啥听着这么耳熟啊,不管了,报了再说。于是我就去面试了。面试我的是一个胖胖的留着胡子的人(死月罪过,当时并不知道那是你),于是和他相聊甚欢,最后还记得,当时看到他的 15寸 macbook pro 后面贴着一个 bad apple 的一个贴纸。当时顺口就问了一句,这个是 bad apple 么?当时之所以会问这个,因为我还记得很早以前,看过一个人的博客,里面讲到了他在花瓣网工作,而且很详细的讲解了图片主题色的提取和 Node.js NAN API,感觉受益匪浅,于是我当时在想,这是哪个学校的大牛,竟然这么厉害。后来看到他已经在花瓣工作了。。。不过我记得他很喜欢二次元。。。于是面试的时候,我在想会不会是他,于是就问了句贴纸,以为他会聊起二次元,结果他随便应了一句就过去了。我见状就没再继续问下去了。后面安慰道自己说,没事,那个人应该不会在大搜车。面试我这个人感觉还是很厉害的~~至少有一点,我说我提过 issue 给 node,他能马上就打开 github 看。这一点让我很敬佩,因为大部分面试都是会听你描述,看简历上面写的,而不是当场去查看。举个例子,你跟面试官说你博客写了很多高品质的文章,大部分面试官会直接问你有什么,他不会自己去看。而好的面试官会一边问你,一边自己打开看。。。我是这么觉得的。反正不管当时是不是月老,我已经决定来这家公司了。

哈哈哈,后面等进大搜车之后,剧情反转。那个人其实就是死月。当时看到他在 QQ 群里面的时候,你知道我的心里有多么开心么!!但是,有一个噩耗,就是他在我去的前几周已经跳槽去蚂蚁金服了,哭 (((T____T))) 我的大牛啊,你怎么走了啊~啊~啊~啊~

不过还好,也认识了挺多大牛的,不过还是没见到过我心仪的 朴灵/不四/狼叔 -_-。

现在在大搜车呆了有两个月了吧,那就简单总结一下干了啥吧:

  • 完成了一个图片上传服务,里面包含了公共的图片上传,以及大风车的头像上传,真正的编码时间也就两个星期,但是真的发布上去,却花了一个月。
  • 现在准备一个请求限制框架,讲道理这是很简单的一个工作,但是我看时间很充裕,于是我就想写大,看看能不能独立成一个库,开源骗 star。
  • 期间还要各种小东西,修修补补。

刚进大搜车,按理来说,带我的人应该是小山,但是那个时候他请了几天假,于是就鹏飞暂时带着我。让我看了 Akyuu.js 和帕秋莉网关。之后其实所有的时间都是跟着鹏飞,我师父小山感觉不喜欢多说话,平时也没有太多的共同语言。想平时打打游戏联络一下感情,但是看他很忙的样子,就放弃了。最近才发现,原来小山也看二次元,哈哈哈。反正就这样,和小山半亲近半陌生。和鹏飞一开始也聊的挺多,后面等公司的事情知道的差不多了,也交流的不多了。

而且由于我比较慢热,再加上我进入公司比较晚,没有参加过团建,和大家都不是很熟。就和组内的坐在旁边的外加组内的实习生比较熟。

说一下,我在大搜车实习的感受吧。

  • 代码层面
    • 更加理性的对待 callback 和 promise,因为在之前,我是极力反对使用 callback 的,所以当第一眼看到公司的代码的时候,我懵逼了,怎么全是 callback。于是经过和鹏飞的交流以及自己的领悟,终于放下执念。其实 callback+async 和 promise 没啥区别么,哈哈哈。
    • 尝试先写文档,后写代码。我平时兴起的时候,直接就开始撸,从来不打草稿。小项目可以,但是当项目大了之后,就呵呵哒了。
    • 了解了 Node 的 PR 流程。
    • 其他的好像还真的没有了,什么代码规范,git flow,框架的使用等等,我基本都了解。但是又重新复习了并精进了一下。毕竟之前看时候只是看了几眼,大体明白了内容。正好趁着这次实习,运用一下,看看自己理解的哪里有问题。
  • 交际层面
    • 首先我是一个慢热的,也就是说我不是很擅长去找别人交流,但是别人来找我交流,我是很乐意的。所以说,我当初进入公司之前的幻想,就是大家都在交流着各种新技术,新框架,新事物,当一个人抛出问题的时候,大家会一起去解决研究。结果进来之后,我发现,好像群里半天都不会有任何消息。于是我就努力去带动气氛,有什么好玩的东东都尝试发到群里,结果还是很难带起氛围。不知道是大家太忙了,还是我发的信息太简单。
    • 还记得在学校的时候,社团的技术群,可能一个人发现了新东西,于是群里的大佬一起去尝试,评论,总结。和群里的人撕逼那个语言好,撕逼什么框架好用。我们只追求方便好用,并不怎么在乎稳定性这种东西。哎,这种感觉好难在找回来了。不知道头哥能不能看到我写的,我不知道头哥你想的团队是不是我想的那样,但是感觉大家仅仅为了业务而工作,死气沉沉,不觉得失去了乐趣么?
    • 再讲讲开源的东东,我不知道用我们大学生的思维来思考对不对。至少我会很讨厌所有的公司沉淀出来的产品,比如说阿里的 egg。怎么解释呢?你可以理解假设公司内部的开发版本按照 master 的一条线进行,如果开源了,我就从 master checkout 一个新的分支,然后做一些开源的修改。我为什么不喜欢这种呢?因为这种所有的功能的设计实现就是严重依赖业务的,他只能做到的是在这个业务的情况下尽可能去兼容其他的业务格式而抽离出来的核心。而我真正想要的是什么呢?是社区驱动,一个产品可以由一个公司来开发,但是设计一定还给社区。而且我也不喜欢所谓的二次封装的框架,二次封装的框架我建议内部使用,而不是开源。除非你的二次封装能够提供很多功能。
  • 反正总的来讲,我感觉从知识层面,我获取的很少,没获取到太多新的概念/知识。但是实践层面我获取的还是挺多的。不知道这是不是以后工作的常态。

大学

这一年其实大学生活没有太多的东西,主要是在实习当中度过的。

正是因为如此,我也越发怀念当初的学习生活。回头看看学弟当中的大佬,不由自主的感叹自己好像虚度了大学生活。

不顾了,下学期就准备毕业设计了,希望自己能珍惜最后的时光吧。

2018 展望

看过了死月的总结,我发现其实有一点挺好的,一年给自己定一些目标,来年看看目标有没有实现。

那我也来展望一下好了:

  • 学习
    • 争取研读 Node 源码,至少要把死月那本书给啃完
    • 争取 Github 每周都有贡献,希望能长久的维护一个项目
    • 争取写一些有意思,有难度的代码,比如说《如何写出一个 Babel》
    • 坚持写文章吧(这个有点难),至少保证每两周一篇高质量的
    • 了解一些其他方面的内容,下一年总结一下了解了啥。
  • 坚持锻炼身体,把体重控制在 65 以下,争取练出胸肌(其实我有,只不过有点萎缩了)腹肌二头肌(当然,这些都练出来之后我就不限制体重了)。
  • 争取学会做几个菜(当然指的是在实习期间了),暂时只考虑用电饭煲来做。
  • Minecraft 开新坑,等 1.13 发布~如果有小伙伴想入坑的请联系我,最好有正版,因为我是想单人开坑的。
  • 不知道为啥,自从实习之后,感觉有点思春。。。特别想找人聊聊非技术方面的事情。。没妹子陪,我都不想去电影院看电影了。。。所以,不求找女票,感觉自己现在还不是很适合去当男票,其实是找不到合适的,哈哈哈。只求可以找到有空可以出去看个电影,聊个天,而且臭味相投的妹子就好了。不过目测是完不成这个目标了。
  • 既然没有女票,那就多花一些时间在学习上。但是不要死学习,做程序员路上的书呆子。
  • 恶习
    • 争取改掉拖沓的毛病,有任务赶紧去做,有事情提前安排
    • 争取每天刷牙洗脸(也就是让自己早起,哈哈哈)

先这么多吧,看看 2019 年,我完成了哪些。

注释和共享

Reflect-Metadata 详解

发布在 Jacascript

引言

在 ES6 的规范当中,就已经存在 Reflect API 了。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。具体的关于这个 API 的内容,可以查看这个教程

然而我们在这里讲到的,却是 Reflect 里面还没有的一个规范,那么就是 Reflect Metadata

Metadata

想必对于其他语言的 Coder 来说,比如说 Java 或者 C#,Metadata 是很熟悉的。最简单的莫过于通过反射来获取类属性上面的批注(在 JS 当中,也就是所谓的装饰器)。从而可以更加优雅的对代码进行控制。

而 JS 现在有装饰器,虽然现在还在 Stage2 阶段。但是 JS 的装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。

所以,后文当中我就使用 TypeScript 来进行讲解,因为 TypeScript 已经完整的实现了装饰器。
虽然 Babel 也可以,但是需要各种配置,人懒,不想配置那么多。

但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上。

于是 Reflect Metadata 应运而生。

Reflect Metadata

Relfect Metadata,简单来说,你可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来。当然你也可以通过反射来添加这些信息。 就像是下面这个例子所示。

1
2
3
4
5
6
7
8
9
10
11
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('hello', 'world')
public hello(): string {
return 'hello world'
}
}

Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'
// 这里为什么要用 new A(),用 A 不行么?后文会讲到

是不是很简单,那么我简单来介绍一下~

概念

首先,在这里有四个概念要区分一下:

  1. Metadata Key {Any} 后文简写 k。元数据的 Key,对于一个对象来说,他可以有很多元数据,每一个元数据都对应有一个 Key。一个很简单的例子就是说,你可以在一个对象上面设置一个叫做 'name' 的 Key 用来设置他的名字,用一个 'created time' 的 Key 来表示他创建的时间。这个 Key 可以是任意类型。在后面会讲到内部本质就是一个 Map 对象。
  2. Metadata Value {Any} 后文简写 v。元数据的类型,任意类型都行。
  3. Target {Object} 后文简写 o。表示要在这个对象上面添加元数据
  4. Property {String|Symbol} 后文简写 p。用于设置在那个属性上面添加元数据。大家可能会想,这个是干什么用的,不是可以在对象上面添加元数据了么?其实不仅仅可以在对象上面添加元数据,甚至还可以在对象的属性上面添加元数据。其实大家可以这样理解,当你给一个对象定义元数据的时候,相当于你是默认指定了 undefined 作为 Property。 下面有一个例子大家可以看一下。

大家明白了上面的概念之后,我之前给的那个例子就很简单了~不用我多说了。

安装/使用

下面不如正题,我们怎么开始使用 Reflect Metadata 呢?
首先,你需要安装 reflect-metadata polyfill,然后引入之后就可以看到在 Reflect 对象下面有很多关于 Metadata 的函数了。因为这个还没有进入正式的协议,所以需要安装垫片使用。

啥,Reflect 是啥,一个全局变量而已。

你不需要担心这个垫片的质量,因为连 Angular 都在使用呢,你怕啥。

之后你就可以安装我上面写的示例,在 TypeScript 当中去跑了。

类/属性/方法 装饰器

看这个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}

const objs = [A, new A, A.prototype]
const res = objs.map(obj => [
Reflect.getMetadata('name', obj),
Reflect.getMetadata('name', obj, 'hello'),
Reflect.getOwnMetadata('name', obj),
Reflect.getOwnMetadata('name', obj ,'hello')
])
// 大家猜测一下 res 的值会是多少?

想好了么?再给你 10 秒钟

10
9
8
7
6
5
4
3
2
1

res

1
2
3
4
5
[
['A', undefined, 'A', undefined],
[undefined, 'hello', undefined, undefined],
[undefined, 'hello', undefined, 'hello'],
]

那么我来解释一下为什么回是这样的结果。

首先所有的对类的修饰,都是定义在类这个对象上面的,而所有的对类的属性或者方法的修饰,都是定义在类的原型上面的,并且以属性或者方法的 key 作为 property,这也就是为什么 getMetadata 会产生这样的效果了。

那么带 Own 的又是什么情况呢?

这就要从元数据的查找规则开始讲起了

原型链查找

类似于类的继承,查找元数据的方式也是通过原型链进行的。

就像是上面那个例子,我实例化了一个 new A(),但是我依旧可以找到他原型链上的元数据。

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}

const t1 = new A()
const t2 = new A()
Reflect.defineMetadata('otherName', 'world', t2, 'hello')
Reflect.getMetadata('name', t1, 'hello') // 'hello'
Reflect.getMetadata('name', t2, 'hello') // 'hello'
Reflect.getMetadata('otherName', t2, 'hello') // 'world'

Reflect.getOwnMetadata('name', t2, 'hello') // undefined
Reflect.getOwnMetadata('otherName', t2, 'hello') // 'world'

用途

其实所有的用途都是一个目的,给对象添加额外的信息,但是不影响对象的结构。这一点很重要,当你给对象添加了一个原信息的时候,对象是不会有任何的变化的,不会多 property,也不会有的 property 被修改了。
但是可以衍生出很多其他的用途。

  • Anuglar 中对特殊字段进行修饰 (Input),从而提升代码的可读性。
  • 可以让装饰器拥有真正装饰对象而不改变对象的能力。让对象拥有更多语义上的功能。

API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Reflect {
// 用于装饰器
metadata(k, v): (target, property?) => void

// 在对象上面定义元数据
defineMetadata(k, v, o, p?): void

// 是否存在元数据
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean

// 获取元数据
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any

// 获取所有元数据的 Key
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]

// 删除元数据
deleteMetadata(k, o, p?): boolean
}

大家可能注意到,针对某些操作,会有 Own 的函数。这是因为有的操作是可以通过原型链进行操作的。这个后文讲解。

深入 Reflect Metadata

实现原理

如果你去翻看官网的文档,他会和你说,所有的元数据都是存在于对象下面的 [[Metadata]] 属性下面。一开始我也是这样认为的,新建一个 Symbol('Metadata'),然后将元数据放到这个 Symbol 对应的 Property 当中。直到我看了源码才发现并不是这样。请看例子

1
2
3
4
@Reflect.metadata('name', 'A')
class A {}

Object.getOwnPropertySymbols(A) // []

哈哈,并没有所谓的 Symbol,那么这些元数据都存在在哪里呢?

其实是内部的一个 WeakMap 中。他正是利用了 WeakMap 不增加引用计数的特点,将对象作为 Key,元数据集合作为 Value,存到 WeakMap 中去。

如果你认真探寻的话,你会发现其内部的数据结构其实是这样的

1
WeakMap<any, Map<any, Map<any, any>>>

是不是超级绕,但是我们从调用的角度来思考,这就一点都不绕了。

1
weakMap.get(o).get(p).get(k)

先根据对象获取,然后在根据属性,最后根据元数据的 Key 获取最终要的数据。

End

因为 Reflect Metadata 实在是比较简单,这里就不多讲解了。更多内容请查看 Spec

题外话

其实看了源码之后还是挺惊讶的,按照一般的套路,很多 polyfill 会让你提供一些前置的 polyfill 之后,当前的 polyfill 才能使用。但是 reflect-metadata 竟然内部自己实现了很多的 polyfill 和算法。比如 Map, Set, WeakMap, UUID。最惊讶的莫过于 WeakMap 了。不是很仔细的阅读了一下,好像还是会增加引用计数。

注释和共享

Boostnote 是一款通过 Electron 构建的桌面笔记应用。它支持离线存储,无需注册、Markdown 编辑、像 Github Gist 的代码片段的管理等等内容。全球 198 个国家和地区的程序员们在使用这款应用。

Boostnote 就如同器名字一般,最主要的一个用途便是 Markdown 笔记编辑器。你所有的 Markdown 笔记拥有自动保存的功能,并且支持多种展示格式。拥有半实时的预览,所以你可以及时的检查最后的格式是否是和你所输入的一致。

Latex 公式编辑器也内置在 Boostnote 当中,你可以很轻松的插入各种公式在你的笔记中。

不管是笔记还是代码片段都可以通过标签的方式进行管理。

对于代码片段来说,他支持高达 100 多种语言的高亮,其中包括 JavaScript, Python, HTML 和 CSS。当然你也可以在一份代码片段当中存储多段代码,比如说你可以同时存储 HTML,CSS,JS 代码在一份当中。而且不管你是用什么样子的缩进(tab/空格)或者缩进的程度(2个字符,4个字符,8个字符)都可以在文档中进行配置。

最后,将笔记导出成普通文本(.txt) 或者 Markdown(.md) 也是支持的功能。

外观(UI)

丰富的快捷键可以让你让更快的浏览、搜索笔记,以及更快的执行一些重要操作。

对于 Boostnote 的外观来说,你有很多种不同的主题可以选择。当然,编辑器的高亮也是有很多种可以选择的,你可以根据你自己的喜好自由搭配。你可以在 Preferences > UI > Theme 查看支持的主题。

下载

Boostnote 目前是开源的,你可以通过官网下载。不过要注意的是,存储使用的是亚马逊的 s3,所以你需要能够翻墙,否则无法下载。

它目前是支持全平台(Windows, MacOS, Linux,IOS,Android)

注释和共享

vue-lever 是一个使用装饰器模式的插件来帮你管理 true/false 状态

最近一直在写 vue 相关的东西,因为毕竟是前端,所以经常会遇到一种情况就是说,我需要维护一个 true/false 状态,比如说:当按钮点击的时候,显示加载字样,然后等加载完毕之后完成显示。

首先我们考虑出现这种情况的时候,如果我们一行一行的书写,那将是非常麻烦的。

1
2
3
4
5
6
7
8
9
10
11
12
13
new Vue({
data() {
return { loading: false }
}
methods: {
asyncAction() {
this.loading = true
this.$http.doing() // return a promise
.then(() => this.loading = false)
.catch(() => this.loading = false))
}
}
})

我们需要在每一个退出的情况下将 loading 状态设置为 false,这至少意味着你要写 3 遍,非常冗余。而且很容易忘记。

我们可以抽象出来,可以发现每当这种函数运行的时候,首先将相关的变量设置成 true,然后等待异步操作完成或者失败之后,再将变量设置回之前的值。

完成这个操作最简单的方式就是代理,在用户的函数调用之前设置相应的变量,在用户的函数调用完成之后,或者如果函数是异步操作,那么通过返回一个 promise 来表示异步操作。

既然讲到了代理模式,那么在 JS 中有很多,不过我们在这里通过一个 ES6 的新语法,装饰器。

话不多说,上代码

1
2
3
4
5
6
7
8
9
10
11
import Lever from 'vue-lever'
// import others

new Vue({
methods: {
@Lever.Lever('loading')
asyncAction() {
return this.$http.doing() // return a promise
}
}
})

这个功能和上面的那段代码是一模一样的,是不是感觉用了装饰器之后就变得特别简单了呢?

不过这里有一点需要注意的是,为了方便和隔离,我将所有的变量全都放在 levers 这个变量下面,也就是说你需要通过 levers.loading 来使用,而不是 loading

不过现在暂时没有支持回调函数的方式,也就是你必须要返回一个 promise,否则是不行的。

其次我们这里还有手动模式,也就是如果你不想用装饰器的话,你可以通过 this.$lever(name, value),来更改状态,其中 name 为变量名,value 为 true/false 值。当然这里也提供两个 alias,this.$lever.t(name)this.$lever.f(name)。方便设置变量为 true/false。

还有一些其他的参数,大家可以去 Github 网站查看。这篇文章就写到这里。

注释和共享

XGHeaven

一个弱弱的码农


杭州电子科技大学学生一枚


Weifang Shandong, China