目录

  1. 可控组件?不可控组件?
    1. 什么是可控和不可控?
    2. 为啥要区分呢?
    3. value? defaultValue? onChange?
    4. propName in this.props?
    5. Independence
    6. 如何使用?
    7. 总结

前言:本人入职之后算是第一次真正去写 React,发现了 React 的组件系统和其他框架的组件系统有些许的不同,这也触发了我对其中组件的可控性的一些思考和总结。

可控组件?不可控组件?

自从前端有了组件系统之后,有一个很常见但是却又被大家忽视的概念,就是可控组件(Controlled Component)和不可控组件(Uncontrolled Component)。

什么是可控和不可控?

官方详细讲解了什么事可控和不可控组件,虽然只是针对 input 组件的 value 属性来讲的。但是对于很多第三方组件库来讲,一个组件不止有一个数据属于可控。比如 Ant Design 的 Select 组件,valueopen 都属于可控的数据,如果你让 value 可控 open 不可控,那这到底是可控组件还是不可控组件呢?

所以从广义来讲使用可控/不可控组件其实不是很恰当,这里使用可控数据不可控数据更加合理一点。一个组件可能可能同时有可控的数据和不可控的数据。

可控数据是指组件的数据被使用者所控制。不可控数据是指组件的数据不由使用者来控制而是由组件内部控制。

之所以会有可控和不可控,主要是跟人奇怪的心理有关。如果把框架比作一个公司,组件比作人,组件之间的关系比作上下级。那么上级对下级的期望就是你既能自己做好分内的事情,也可以随时听从我的命令。这本身就是一件矛盾的事情,一边撒手不管,一边又想全权掌控。遇到这样的上级,下级肯定会疯了吧。

为啥要区分呢?

在 Vue 中,其实都忽视了这两者的区别,我们来看下面这个例子。

1
<input/>

上面是一个最简单 Input 组件,我们来思考一下如下几种使用场景:

  • 如果我只关心最后的结果,也就是输入的值,中间的过程不关心,最简单的方式是用 v-model 或者自己在 change 事件里面获取值并保存下来。

    1
    2
    3
    <input v-model="value"/>
    <!-- OR -->
    <input @change="change"/>

    这种场景是非常普遍,Vue 可以很好的完成,结果也符合人们的预期。

  • 如果我也只是关心结果,但是想要一个初始值。
    也很简单,通过 value 传入一个静态字符串不就好了,或者传入一个变量,因为 Vue 的 props 是单向的。

    1
    2
    3
    <input v-model="value"/> <!-- value 有初始值 -->
    <input value="init string" @change="change"/>
    <input :value="initValue" @change="change"/>

    其中第三个方案并不是非常正确的方式,如果 initValue 在用户输入期间发生了更新,那么他将覆盖用户的数据,且不会触发 change 事件。

  • 我不仅仅关心结果,还关心过程,我需要对过程进行控制。比如说把输入的字符串全部大小写,或者锁定某些字符串。
    熟练的工程师肯定可以写出下面的代码。

    1
    2
    <input v-model="value"/> <!-- watch "value",做修改 -->
    <input :value="value" @change="change"/> <!-- 在 change 中修改数据 -->

    但是这会有问题:

    1. 数据的修改都是在渲染 dom 之后,也就是说你不管怎么处理,都会出现输入的抖动。
    2. 如果通过第二种方法,恰巧你做的工作是限制字符串长度,那么你这样写 change(e) {this.value = e.target.slice(0, 10)} 函数会发现没有效果。这是因为当超过 10 字符之后,value 的值长度一直是 10,vue 没有检测到 value 的变化,从而不会更新 input.value。

出现这个问题最关键的是因为没有很好的区分可控组件和不可控组件,我们来回顾一下上面的某一段代码:

1
<input :value="value" @change="change"/>

你能从这块代码能看出来使用这个组件的用户的意图是什么呢?他是想可控的使用组件还是说只是想设置一个初始值?你无法得知。我们人类都无法得知,那么代码层面就不可能得知的了。所以 vue 对这一块的处理其实是睁一只眼闭一只眼。用户用起来方便,

用一个例子来简单描述一下:上级让你去做一项任务,你询问了上级关于这些任务的信息(props),然后你就开始(初始化组件)工作了,并且你隔一段时间就会向上级汇报你的工作进度(onChange),上级根据你反馈的进度,合理安排其他的事情。看起来一切都很完美。但是有的上级会有比较强的控制欲,当你提交了你的工作进度之后,他还会瞎改你的工作,然后告诉你,按照我的继续做。然后下级就懵逼,当初没说好我要接受你的修改的呀(value props),我这里也有一份工作进度呀(component state),我应该用我自己的还是你的?

对于人来说,如何处理上级的要求(props)和自身工作(state)是一个人情商的表现,这个逻辑很符合普通人的想法,但是对于计算机来说,它没有情商也无法判断究竟应该听谁的。为了克服这个问题,你需要多很多的判断和处理才可以,而且对于一些不变的值,你需要先清空再 nextTick 之后赋值才可以出发组件内部的更新。

最近入职之后,公司用到了 React,我才真正的对这个有所理解。

value? defaultValue? onChange?

如果对 React 可控组件和不可控组件有了解了可以跳过这块内容了。

让我们来看一下 React 如何处理这个的?我们还是拿上面的那三种情况来说:

  • 如果我只关心最后的结果,也就是输入的值,中间的过程不关心

    1
    <input onChange={onChange}/>
  • 如果我也只是关心结果,但是想要一个初始值

    1
    2
    <input defaultValue="init value" onChange={onChange}/>
    <input defaultValue={initValue} onChange={onChange}/>
  • 我不仅仅关心结果,还关心过程,我需要对过程进行控制

    1
    <input value={value} onChange={onChange}/>

当看完了这段你会很清楚的知道什么样的结构是可控,什么结构是不可控:

  • 如果有 value 那么就属于可控数据,永远使用 value 的值
  • 否则属于不可控数据,由组件使用内部 value 的值,并且通过 defaultValue 设置默认值

不论什么情况修改都会触发 onChange 事件。

React 对可控和不可控的区分其实对于计算机来说是非常合理的,而且也会让整个流程变的非常清晰。当然,不仅仅只有这一种设置的方式,你可以按照一定的规则也同样可以区分,但是保证可控和不可控之间清晰的界限是一个好的设计所必须要满足的

propName in this.props?

了解上面的概念之后,我们进入到实战环节,我们怎么从代码的层面来判断当前组件是可控还是不可控呢?

根据上面的判断逻辑来讲:

1
2
3
const isControlled1 = 'value' in this.props // approval 1
const isControlled2 = !!this.props.value // approval 2
const isControlled3 = 'value' in this.props && this.props.value !== null && this.props.value !== undefined // approval 3

我们来观察上面几个判断的方式,分别对应一下下面几个模板(针对第三方组件):

1
2
3
4
<Input value={inputValue} /> // element 1,期望可控
<Input value="" /> // element 2,期望可控
<Input /> // element 3,期望不可控
<Input value={null} /> // element 4,期望???

可以得到如下表格

是否可控 approval 1 approval 2 approval 3
element1 true true true
element2 true false true
element3 false false false
element4 true false false

大家第一眼就应该能看出来方法二其实是不正确的,他无法很好的区分这两种状态,所以直接 pass 掉。

眼尖的同学也会发现为什么 element 4 的期望没有填写呢?这是因为有一条官方的规则没有讲,这条规则是这样的:当设置了 value 属性之后,组件就变成了可控组件,会阻止用户修改 input 的内容。但是如果你想在设置了 value prop 的同时还想让用户可以编辑的话,只可以通过设置 valueundefinednull

在官方的这种规则下面,element 4 期望是不可控组件,也就是说 approval 3 是完全符合官方的定义的。但是这样会导致可控和不可控之间的界限有些模糊。

1
2
<Input value={inputValue} />
// 如果 inputValue 是 string,组件是什么状态?如果是 null 又是什么状态?

所以这里其实我推荐使用 approval 1 的方式,这也是 antd 所采用的。虽然不符合官方的定义,但是我觉得符合人们使用组件的一种直觉。第六感,=逃=

Independence

有了判断的方法,那么我们可以画出一个简单的流程图(Input 组件为例):

图片有点复杂,简单来讲就是每一次需要获取可控数据或者更新可控数据的时候,都需要检测一下当前组件的状态,并根据状态选择是从 props 中获取数据还是从 state 中获取数据已经更新的时候调用的是那个函数等等。图中有一些箭头的方向不是很正确,而且部分细节未画出,大家见谅。

如果只是添加这一个可控的属性 value ,这样写未尝不可,但是如果我们要同时考虑很多属性呢?比如说 Antd Select 组件就同时有 valueopen 两个可控属性,那么整个代码量是以线性方式增长的。这很明显是无法接受的。

于是这里我引入了 Independence 装饰器来做这件事情。架构如下:

我们可以这么理解,一个支持可控和不可控的组件本质上可以拆分成内部一个展示型的无状态受控的组件和外面的包装组件,通过包装(也就是高阶组件的方式)让内部受控组件支持不可控。

这样写其实有如下几个好处:

  1. 组件逻辑复杂度降低,只需要将组件的受控情况
  2. 可以将任意受控组件包装成不受控组件,尤其是对第三方组件的封装上
  3. 组件复杂度降低,代码冗余变少
  4. 非常方便的添加和删除受控属性,只需要修改装饰器即可

如何使用?

目前我简单实现了 Independence 装饰器,代码在网易猛犸开源的组件库 bdms-ui(建设中,组件不全、文档不全、时间不够,敬请期待)中,代码在此

他遵循这样的规范:假如属性名称为 value,那么默认值为 defaultValue,change 事件为 onValueChange。支持通过 onChangeName 修改 change 事件名称,通过 defaultName 修改默认值名称。

另外最简单的使用方式就是通过装饰器了,拿 Select 组件举例。

1
2
3
4
5
6
7
8
9
@Independence({
value: {
onChangeName: 'onChange'
},
open: {} // 使用默认值
})
export default class Select extends Component {
// blahblah,你就可以当受控组件来编写了
}

从此编写可控和不可控的数据从未如此简单。另外 Independence 还实现了 forward ref 的功能。

不过现在功能还比较薄弱,需要经过时间的检验,等完备之后可以封装成一个库。

总结

本文简单讲解了一下什么是可控和不可控,以及提出了一个 React 的解决方案。

这些只是基于我的经验的总结,欢迎大家积极交流。

注释和共享

阅读推荐:本人需要您有一定的 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 年,我完成了哪些。

注释和共享

  • 第 1 页 共 1 页

XGHeaven

一个弱弱的码农


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


Weifang Shandong, China