本文主要是以「飞书」小程序为准,兼容「微信」小程序,如果没有了解过「飞书」的同学,可以点击此处去官网了解

什么是 NFC

近距离无线通信(英语:Near-field communication,NFC),又简称近距离通信近场通信,是一套通信协议,让两个电子设备(其中一个通常是移动设备,例如智能手机)在相距几厘米之内进行通信。

近场通信技术由非接触式射频识别(RFID)演变而来,由飞利浦半导体(现恩智浦半导体,缩写 NXP)、诺基亚和索尼共同于2004年研制开发,其基础是RFID及互连技术。近场通信是一种短距高频的无线电技术,在13.56MHz频率运行于20厘米距离内。其传输速度有106 Kbit/秒、212 Kbit/秒或者424 Kbit/秒三种。

NFC 其实在刚诞生的时候我就一直在关注,但是不仅仅应用少,而且搭载的设备也少,甚至小米还出现过前一代搭载 NFC 后一代却不搭载的神奇情况。除此之外,使用起来也是特别复杂,想当初,要用 NFC 去实现刷公交卡,你需要去换一个特殊的 SIM 卡才能够支持(当初不理解,现在想来大概率是因为安全问题)。

在现在,随着安卓厂商的不懈努力,现在不论是应用还是设备的安装率都已经逐渐普及开来。从最初 NFC 也就能在支付宝中扫银行卡快速输入卡号,到现在的公交刷卡、X Pay,甚至传输文件,华为甚至给这个东西换了个名字叫做一碰系统(率感无语)。

除此之外,还能推进智能化的发展。比如以后家庭中加入了一个新设备,那么不再需要繁杂的联网过程,直接扫一下机器身上的 NFC 识别码就可以直接将设备加入到家庭网络中。或者说华为路由器上的一碰连接 Wifi 我觉得就是一个极好的应用。当家里来客人的时候,就不再需要一个人一个人的输入密码了。

NFC 技术一览

运行模式

NFC 现在主要有三种运行模式,分别是卡模拟模式(Card Emulation Mode)、主机模拟模式(Host Emulation Mode)、读卡写卡模式(Reader/Writer Mode)、P2P 模式(P2P Mode)

卡模拟模式

  • NFC手机可以模拟成为一张非接触卡,通过 POS 机(非接触读卡器)的 RF 来供电,即使 NFC 手机没电也可以工作。
  • 现在很常见的比如 Apple Pay,BYD NFC 钥匙,都是能够实现在断电情况下的刷卡

主机模拟模式

  • 该模式与卡模拟模式很类似,只不过卡模拟无需供电或者说无需 App 的参与就可以完成,但是主机模拟模式是不行的,他是通过将所有的消息转发给应用,由应用去决定该模拟什么内容,也就说该返回什么内容
  • 现阶段很多支付钱包,比如云闪付、京东闪付等等都是通过该模式实现的。

读卡写卡模式

  • NFC手机可以通过触碰NFC标签(Tag),从中读取非接触标签中的内容,采集数据并发送到对应的应用进行处理。
  • 最常见的应用其实就是华为的一碰系列,除此之外,支付宝支持直接读取信用卡、储蓄卡的卡号。

P2P 模式

  • 两个NFC设备可以近距离内互相连接,直接传递数据,实现点对点数据传输。
  • 例如协助快速建立蓝牙连接、交换手机名片和数据通信等。
  • 最常见的是手机互传、Android Beam。

协议标准类型

因为 NFC 的发展过程的原因,曾经出现过多个协议,甚至每家公司都有不同的协议内容。但现在主要是有一下几个协议标准:

ISO / IEC

主要定义了一下几个协议:

  • ISO/IEC 18092 / ECMA-340— (NFCIP-1)

    Near Field Communication Interface and Protocol-1

  • ISO/IEC 21481 / ECMA-352— (NFCIP-2)

    Near Field Communication Interface and Protocol-2

除此以外,还有一个协议标准很常用,是 ISO-14443 协议,其实这个协议是 RFID 的协议,和上面的唯一区别就是上面的多了一些其他模式的标准,比如点对点模式。

ISO-14443 协议有两个子类,分别是 Type-A 和 Type-B,这两个在 Android 也被称为 NFC-A 和 NFC-B。

但不幸的是,这些协议也不能免费看,要花钱的

NFC Forum

NFC Forum 是一个在 2004 年创建的非盈利行业协会,其成员来自NFC生态系统的各个部分。另外我主要关注了下国内公司,比较知名的有小米、中国移动通讯。

但是你想要从该组织获取任何关于 NFC 相关的技术标准,首先你的公司要成为该组织的成员才行,因为字节根本不在该组织,所以没法从这里获得一手的信息。

不过办法也是有的,该协会的创办者 NXP 公司网站上是有相关的数据资源,后文的参考此资源。

其他

不用管.jpg

沟通协议

以 NFC-A 为例

整个 NFC 卡片其实内部就是一个有限状态机,根据当前不同的状态需要不同的操作。这个图看起来很复杂,其实主要额外包含了两个操作:

  • 密码校验
    • 这个是说 NFC 卡是经过加密的,只有在密码校验通过之后,才能够进行相关的操作。
    • 有一点特殊的是,NFC 的密码长度其实是固定的,即 32 位,4 个字节。
  • 防冲突
    • 之所有有这个设计,是因为在使用过程中,可能会出现同时扫描到多个 NFC 设备的情况,此时就需要通过 READY1/READY2 两个状态来选择正确的 NFC 设备进行操作。
    • 每个卡片都有一个唯一 UID,长度为 7 字节,而每次操作只能选择 4 个字节,所以不得不拆分成两个状态两步去操作。

当没有上面两个操作的时候,可以简单的执行 IDLE -> ACTIVE -> HALT 的状态流程,也就是说连接、操作(也就是读写)、关闭。

存储设计

NFC 在存储上设计了页的概念,一个页表示 4 个字节,以页为最小单位进行操作。所以 NFC 卡片的存储容量其实都是 4 的整数倍。

这里以 NTAG213 180 字节的存储结构为例

这里只需要关注两点:

  • 用户数据存储的空间是从第四页开始
  • 用户可存储空间其实只有 144 字节

只需要记住这两点,在开发 NFC 需求的时候,不要去修改非用户空间的数据,不要存储过长的内容。

设备准备

在有了上面的基础之后,别急你还是不能开始开发 NFC,因为你还缺少至关重要的一个东西,设备

遗憾的是,不是所有的设备都有 NFC 硬件的,也不是说有了 NFC 硬件就能用的

  • 苹果设备只有升级到 iOS 13 以上才能具有开发 NFC 读卡器的能力,不能写入,除此之外,几乎没有其他的 NFC 能力可以使用。机圈也会叫做阉割版 NFC。(暂时没有能力去调研 NFC 的能力一定是需要硬件支持还是说只是软件限制)
  • 安卓设备理论上可以使用几乎所有的 NFC 能力,机圈内叫全功能 NFC,包括读卡、写卡、卡模拟、P2P 等模式。但是不同的手机有着不同的操作系统的限制,所以要选择一个合适的操作系统(原生安卓、类原生安卓是最推荐的)。

所以,请准备好一台安卓手机!

NFC 卡片

在有了设备之后,还要选择正确的 NFC 卡片,因为不是任意一个 NFC 卡片都是可以用的,比如工牌、银行卡等等。这是因为 NFC 卡片是带有加密的,在操作之前必须要通过验证才能操作,所以建议去淘宝买一些可读可写无加密的 NFC 贴纸用于测试。

不过你在淘宝上买的可能是写着 NTAG213 的型号,其实这个是 NXP 出的一款设备,但是支持兼容 ISO NFC-A 协议以及 NFC Forum Type 2 协议,所以大家可以放心使用。

当然了,你也可能看到 NTAG215/NTAG216,这两个都没有任何区别,只是存储空间不同而已。

小程序 API

此时在有了上述的基础知识后,别急,还要了解下小程序的 API 才能更好的开发。

就目前来说,所有与 NFC 相关的操作都被封装到了 NfcAdaptar 类中,通过 tt.getNFCAdaptar() 获取 nfcAfaptar 对象。

具体的 API 参数细节请参考「飞书开放平台

NFC 整体流程

  • 注册 NFC 发现事件回调 nfcAdaptar.onDiscovered
  • 开启 NFC 扫描 nfcAdaptar.startDiscovered
  • NFC 卡片贴近设备
  • 触发回调,通过回调可以获得 NFC 支持的协议
    • 回调参数内的 techs 字段可以用于判断当前卡片支持的协议
  • 根据协议去读写 NFC 卡片内容 nfcAdaptar.getNfcA()
  • 关闭 NFC 扫描,关闭事件监听 nfcAdaptar.offDiscovered / nfcAdaptar.stopDiscovered()

可以发现这个流程非常容易理解,也非常容易操作。那么接下来我们看下重头戏

读写 NFC 卡片

这里以 NFC-A 协议为主

通过 nfcAdaptar.getNfcA() 获取操作 NFC-A 卡片的操作类实例 nfca,流程如下

  • 连接卡片 nfca.connect()
  • 读写卡片 nfca.transceive()
  • 读写完成之后关闭连接 nfca.close()

关键点来了,NFC 卡片的读写不和其他的 IO 设备类似,有专门的 read 和 write 函数。对 NFC 来说,通过给 NFC 卡片发送不同的指令来做到完成不用的操作。

这些指令都在 NTAG213 文档中有写,这里简单列一下常用的数据

命令 代码 功能 参数
Read 0x30 一次读取四个页的数据 Addr:1B
Write 0xA2 一次写入一个页的数据 Addr:1B Data:4B
Fast Read 0x3A 一次读取多个页的数据 StartAddr:1B StopAddr:1B

比如我要读取第四页的数据,可以写如下代码

1
2
3
4
5
6
7
nfca.transceive({
data: new Uint8Array([0x30, 0x04]).buffer, // 必须要传入 ArrayBuffer
success: (res) => {
// res.data 是 ArrayBuffer,转成数组方便查看
console.log(Array.from(new Uint8Array(res.data))
}
})

根据协议,其实还要传 <CRC:2B>,也就是校验位,不过这个操作已经由 Android 去做掉了,所以就不需要传了,也不需要去了解校验算法

结语

至此,NFC 开发算是入门了,不过这里要注意不同的 NFC 卡片不同的协议会有不同的读写方式,这里要根据你们各自具体的卡片来看。而且有的还有密码保护,还需要额外再走校验的逻辑。

Refs

注释和共享

概要

本文主要讲解了下我平时在工作开发中遇到的关于 Hooks 的一些缺点和问题,并尝试配合 Mobx 解决这些问题的经历。我觉得两者的配合可以极大的降低开发过程中有可能出现的问题以及极大的提高开发体验,而且学习成本也是非常的低。如果你对 Hooks 以及 Mobx 有兴趣,想知道更进一步的了解,那么这篇文章适合你。这篇文章会介绍如下内容,方便你决定是否要仔细阅读,节省时间:

  • 本文不会介绍太过于基础的内容,你需要对 Mobx 以及 Hooks 有基础的了解
  • 本文介绍了平时开发中的一些最佳实践,方便小伙伴们对两者有更加深入的认识
  • 如果你使用过一部分 Mobx,但是不太了解如何和 Hooks 更好的合作,可以尝试来看看

另外 Hooks 本身真的就是一个理解上非常简单的东西,所以本文也不长,我也不喜欢去写什么万字长文,又不是写教程,而且读者看着标题就失去兴趣了。

Hooks 究竟有什么问题?

首先,在这里我不再说 Hooks 的优点,因为他的优点用过的人都清楚是怎么回事,这里主要讲解一下他存在的缺点,以及如何用 Mobx 来进行改进。

  • 依赖传染性 —— 这导致了开发复杂性的提高、可维护性的降低
  • 缓存雪崩 —— 这导致运行性能的降低
  • 异步任务下无法批量更新 —— 这也会导致运行性能的降低

换句话说,造成这种原因主要是因为 Hooks 每次都会创建一个全新的闭包,而闭包内所有的变量其实都是全新的。而每次都会创建闭包数据,而从性能角度来讲,此时缓存就是必要的了。而缓存又会牵扯出一堆问题。

说到底,也就是说没有一个公共的空间来共享数据,这个在 Class 组件中,就是 this,在 Vue3 中,那就是 setup 作用域。而 Hooks 中,除非你愿意写 useRef + ref.current 否则是没有办法找到共享作用域。

而 mobx 和 Hooks 的结合,可以很方便在 Hooks 下提供一个统一的作用域来解决上面遇到的问题,所谓双剑合并,剑走天下。

Hook1 useObserver

在传统的使用 mobx 的过程中,大家应该都知道 observer 这个 api,对需要能够响应式的组件用这个包裹一下。同样,这个 api 直接在 hooks 中依旧可以正常使用。 但是 hooks 并不推荐 hoc 的方式。自然,mobx 也提供了 hookify 的使用方式,那就是 useObserver

1
2
3
4
5
6
const store = observable({})
function App() {
return useObserver(() => {
return <div>{store.count}</div>
})
}

看到这里,相信使用过 mobx 的应该可以发现,useObserver 的使用几乎和 Class 组件的 render 函数的使用方式一致。事实上也确实如此,而且他的使用规则也很简单,直接把需要返回的 Node 用该 hooks 包裹后再返回就可以了。

经过这样处理的组件,就可以成功监听数据的变化,当数据变化的时候,会触发组件的重渲染。至此,第一个 api 就了解完毕了

Hook2 useLocalStore

简单来讲,就是在 Hooks 的环境下封装的一个更加方便的 observable。就是给他一个函数,该函数返回一个需要响应式的对象。可以简单的这样理解

1
2
3
const store = useLocalStore(() => ({key: 'value'}))
// equal
const [store] = useState(() => obserable({key: 'value'}))

然后就没有了,极其简单的一个 api 使用。而后面要讲的一些最佳实践更多的也是围绕这个展开,后文简化使用 local store 代指。

这两个 API 能带来什么?

简单来讲,就是在保留 Hooks 的特性的情况下,解决上面 hooks 所带来的问题。

第一点,由于 local store 的存在,作为一个不变的对象存储数据,我们就可以保证不同时刻对同一个函数的引用保持不变,不同时刻都能引用到同一个对象或者数据。不再需要手动添加相关的 deps。由此可以避免 useCallback 和 useRef 的过度使用,也避免很多 hooks 所面临的的闭包的坑(老手请自动忽略)。依赖传递性和缓存雪崩的问题都可以得到解决

直接上代码,主要关注注释部分

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
// 要实现一个方法,只有当鼠标移动超过多少像素之后,才会触发组件的更新
// props.size 控制移动多少像素才触发回调
function MouseEventListener(props) {
const [pos, setPos] = useState({x: 0, y: 0})
const posRef = useRef()
const propsRef = useRef()
// 这里需要用 Ref 存储最新的值,保证回调里面用到的一定是最新的值
posRef.current = pos
propsRef.current = propsRef

useEffect(() => {
const handler = (e) => {
const newPos = {x: e.xxx, y: e.xxx}
const oldPos = posRef.current
const size = propsRef.current.size
if (
Math.abs(newPos.x - oldPos.x) >= size
|| Math.abs(newPos.y - oldPos.y) >= size
) {
setPos(newPos)
}
}
// 当组件挂载的时候,注册这个事件
document.addEventListener('mousemove', handler)
return () => document.removeEventListener('mousemove', handler)
// 当然这里也可以监听 [pos.x, pos.y],但是性能不好
}, [])

return (
props.children(pos.x, pos.y)
)
}

// 用 mobx 改写之后,这种使用方式远比原生 hooks 更加符合直觉。
// 不会有任何 ref,任何 current 的使用,任何依赖的变化
function MouseEventListenerMobx(props) {
const state = useLocalStore(target => ({
x: 0,
y: 0,
handler(e) {
const nx = e.xxx
const ny = e.xxx
if (
Math.abs(nx - state.x) >= target.size ||
Math.abs(ny - state.y) >= target.size
) {
state.x = nx
state.y = ny
}
}
}), props)

useEffect(() => {
document.addEventListener('mousemove', state.handler)
return () => document.removeEventListener('mousemove', state.handler)
}, [])

return useObserver(() => props.children(state.x, state.y))
}

第二,就是针对异步数据的批量更新问题,mobx 的 action 可以很好的解决这个问题

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
// 组件挂载之后,拉取数据并重新渲染。不考虑报错的情况
function AppWithHooks() {
const [data, setData] = useState({})
const [loading, setLoading] = useState(true)
useEffect(async () => {
const data = await fetchData()
// 由于在异步回调中,无法触发批量更新,所以会导致 setData 更新一次,setLoading 更新一次
setData(data)
setLoading(false)
}, [])
return (/* ui */)
}

function AppWithMobx() {
const store = useLocalStore(() => ({
data: {},
loading: true,
}))
useEffect(async () => {
const data = await fetchData()
runInAction(() => {
// 这里借助 mobx 的 action,可以很好的做到批量更新,此时组件只会更新一次
store.data = data
store.loading = false
})
}, [])
return useObserver(() => (/* ui */))
}

不过也有人会说,这种情况下用 useReducer 不就好了么?确实,针对这个例子是可以的,但是往往业务中会出现很多复杂情况,比如你在异步回调中要更新本地 store 以及全局 store,那么就算是 useReducer 也要分别调用两次 dispatch ,同样会触发两次渲染。而 mobx 的 action 就不会出现这样的问题。// 如果你强行 ReactDOM.unstable_batchedUpdates 我就不说啥了,勇士受我一拜

Quick Tips

知道了上面的两个 api,就可以开始愉快的使用起来了,只不过这里给大家一下小 tips,帮助大家更好的理解、更好的使用这两个 api。(不想用而且也不敢用「最佳实践」这个词,感觉太绝对,这里面有一些我自己也没有打磨好,只能算是 tips 来帮助大家拓展思路了)

no this

对于 store 内的函数要获取 store 的数据,通常我们会使用 this 获取。比如

1
2
3
4
5
6
7
8
9
const store = useLocalStore(() => ({
count: 0,
add() {
this.add++
}
}))

const { add } = store
add() // boom

这种方式一般情况下使用完全没有问题,但是 this 依赖 caller,而且无法很好的使用解构语法,所以这里并不推荐使用 this,而是采用一种 no this 的准则。直接引用自身的变量名

1
2
3
4
5
6
7
8
9
const store = useLocalStore(() => ({
count: 0,
add() {
store.count++
}
}))

const { add } = store
add() // correct,不会导致 this 错误
  • 避免 this 指向的混乱
  • 避免在使用的时候直接解构从而导致 this 丢失
  • 避免使用箭头函数直接定义 store 的 action,一是没有必要,二是可以将职责划分的更加清晰,那些是 state 那些是 action

source

在某些情况下,我们的 local store 可能需要获取 props 上的一些数据,而通过 source 可以很方便的把 props 也转换成 observable 的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
function App(props) {
const store = useLocalStore(source => ({
doSomething() {
// source 这里是响应式的,当外界 props 发生变化的时候,target 也会发生变化
if (source.count) {}
// 如果这里直接用 props,由于闭包的特性,这里的 props 并不会发生任何变化
// 而 props 每次都是不同的对象,而 source 每次都是同一个对象引用
// if (props.count) {}
}
// 通过第二个参数,就可以完成这样的功能
}), props)
// return Node
}

当然,这里不仅仅可以用于转换 props,可以将很多非 observable 的数据转化成 observable 的,最常见的比如 Context、State 之类,比如

1
2
3
4
5
6
7
const context = useContext(SomeContext)
const [count, setCount] = useState(0)
const store = useLocalStore(source => ({
getCount() {
return source.count * source.multi
}
}), {...props, ...context, count})

自定义 observable

有的时候,默认的 observable 的策略可能会有一些性能问题,比如为了不希望针对一些大对象全部响应式。可以通过返回自定义的 observable 来实现。

1
2
3
4
5
6
7
const store = useLocalStore(() => observable({
hugeObject: {},
hugeArray: [],
}, {
hugeObject: observable.ref,
hugeArray: observable.shallow,
}))

甚至你觉得自定义程度不够的话,可以直接返回一个自定义的 store

1
const store = useLocalStore(() => new ComponentStore())

类型推导

默认的使用方式下,最方便高效的类型定义就是通过实例推导,而不是通过泛型。这种方式既能兼顾开发效率也能兼顾代码可读性和可维护性。当然了,你想用泛型也是可以的啦

1
2
3
4
5
6
7
8
9
// 使用这种方式,直接通过对象字面量推导出类型
const store = useLocalStore(() => ({
todos: [] as Todo[],
}))

// 当然你可以通过泛型定义,只要你不觉得烦就行
const store = useLocalStore<{
todos: Todo[]
}>(() => ({todos: []}))

但是这个仅仅建议用作 local store 的时候,也就是相关的数据是在本组件内使用。如果自定义 Hooks 话,建议还是使用预定义类型然后泛型的方式,可以提供更好的灵活性。

memo?

当使用 useObserver api 之后,就意味着失去了 observer 装饰器默认支持的浅比较 props 跳过渲染的能力了,而此时需要我们自己手动配合 memo 来做这部分的优化

另外,memo 的性能远比 observer 的性能要高,因为 memo 并不是一个简单的 hoc

1
2
3
4
5
6
export default memo(function App(){
const xxx = useLocalStore(() => ({}))
return useObserver(() => {
return (<div/>)
})
})

不再建议使用 useCallback/useRef/useMemo 等内置 Hooks

上面的这几个 Hooks 都可以通过 useLocalStore 代替,内置 Hooks 对 Mobx 来说是毫无必要。而且这几个内置 api 的使用也会导致缓存的问题,建议做如下迁移

  • useCallback 有两种做法
    • 如果函数不需要传递给子组件,那么完全没有缓存的必要,直接删除掉 useCallback 即可,或者放到 local store 中也可以
    • 如果函数需要传递给子组件,直接放到 local store 中即可。
  • useMemo 直接放到 local store,通过 getter 来使用

useEffect or reaction?

经常使用 useEffect 知道他有一个功能就是监听依赖变化的能力,换句话说就是可以当做 watcher 使用,而 mobx 也有自己的监听变化的能力,那就是 reaction,那么究竟使用哪种方式更好呢?

这边推荐的是,两个都用,哈哈哈,没想到吧。

1
2
3
useEffect(() =>
reaction(() => store.count, () => console.log('changed'))
, [])

说正经的,针对非响应式的数据使用 useEffect,而响应式数据优先使用 reaction。当然如果你全程抛弃原生 hooks,那么只用 reaction 也可以的。

组合?拆分?

逻辑拆分和组合,是 Hooks 很大的一个优势,在 mobx 加持的时候,这个有点依旧可以保持。甚至在还更加简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function useCustomHooks() {
// 推荐使用全局 Store 的规则来约束自定义 Hooks
const store = useLocalStore(() => ({
count: 0,
setCount(count) {
store.count = count
}
}))
return store
}

function App() {
// 此时这个 store 你可以从两个角度来思考
// 第一,他是一个 local store,也就是每一个都会初始化一个新的
// 第二,他可以作为全局 store 的 local 化,也就是你可以将它按照全局 store 的方式来使用
const store = useCustomHook()
return (
// ui
)
}

App Store

Mobx 本身就提供了作为全局 Store 的能力,这里只说一下和 Hooks 配合的使用姿势

当升级到 mobx-react@6 之后,正式开始支持 hooks,也就是你可以简单的通过这种方式来使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function App() {
return (
<Provider sa={saStore} sb={sbStore}>
<Todo/>
</Provider>
)
}

export function Todo() {
const {sa, sb} = useContext(MobxProviderContext)
return (
<div>{sa.foo} {sb.bar}</div>
)
}

Context 永远是数据共享的方案,而不是数据托管的方案,也就是 Store

这句话怎么理解数据共享和组件通讯呢?举个例子

  • 有一些基础的配置信息需要向下传递,比如说 Theme。而子组件通常只需要读取,然后做对应的渲染。换句话说数据的控制权在上层组件,是上层组件共享数据给下层组件,数据流通常是单向的,或者说主要是单向的。这可以说是数据共享
  • 而有一些情况是组件之间需要通讯,比如 A 组件需要修改 B 组件的东西,这种情况下常见的做法就是将公共的数据向上一层存放,也就是托管给上层,但是使用控制权却在下层组件。其实这就是全局 Store,也就是 Redux 这类库做的事情。可以看出来数据流通常是双向的,这就可以算作数据托管

曾经关注过 Hooks 的发展,发现很多人在 Hooks 诞生的时候开始尝试用 Context + useReducer 来替换掉 Redux,我觉得这是对 Context 的某种曲解。

原因就是 Context 的更新问题,如果作为全局 Store,那么一定要在根组件上挂载,而 Context 检查是否发生变化是通过直接比较引用,那么就会造成任意一个组件发生了变化,都会导致从 Provider 开始的整个组件树发生重新渲染的情况。

1
2
3
4
5
6
7
8
9
10
function App() {
const [state, dispatch] = useReducer(reducer, init)
return (
// 每次当子组件调用 dispatch 之后,会导致 state 发生变化,从而导致 Provider 的 value 变化
// 进而让所有的子组件触发刷新
<GlobalContext.Provider value={{...state, dispatch}}>
{/* child node */}
</GlobalContext.Provider>
)
}

而如果你想避免这些问题,那就要再度封装一层,这和直接使用 Redux 也就没啥区别了。

主要是 Context 的更新是一个性能消耗比较大的操作,当 Provider 检测到变化的时候,会遍历整颗 Fiber 树,比较检查每一个 Consumer 是否要更新。

专业的事情交给专业的来做,使用 Redux Mobx 可以很好的避免这个问题的出现。

如何写好一个 Store

知道 Redux 的应该清楚他是如何定义一个 Store 吧,官方其实已经给出了比较好的最佳实践,但在生产环境中,使用起来依旧很多问题和麻烦的地方。于是就诞生了很多基于 Redux 二次封装的库,基本都自称简化了相关的 API 的使用和概念,但是这些库其实大大增加了复杂性,引入了什么 namespace/modal 啥的,我也记不清了,反正看到这些就自动劝退了,不喜欢在已经很麻烦的东西上为了简化而做的更加麻烦。

而 Mobx 这边,官方也有了一个很好的最佳实践。我觉得是很有道理,而且是非常易懂易理解的。

但还是那个问题,官方在有些地方还是没有进行太多的约束,而在开发中也遇到了类似的问题,所以这里在基于官方的框架下有几点意见和建议:

  • 保证所有修改 store 的操作都只能在 store 内部操作,也就是说你要通过调用 store 上的 action 方法更新 store,坚决不能在外部直接修改 store 的 property 的值。
  • 保证 store 的可序列化,方便 SSR 的使用以及一些 debug 的功能
    • 类构造函数的第一个参数永远是初始化的数据,并且类型保证和 toJSON 的返回值的类型一致
    • 如果 store 不定义 toJSON 方法,那么要保证 store 中的数据不存在不可序列化的类型,比如函数、DOM、Promise 等等类型。因为不定义默认就走 JSON.stringify 的内置逻辑了
  • store 之间的沟通通过构造函数传递实现,比如 ThemeStore 依赖 GlobalStore,那么只需要在 ThemeStore 的构造参数中传入 GlobalStore 的实例即可。不过说到这里,有的人应该会想到,这不就是手动版本的 DI 么。没错,DI 是一个很好的设计模式,但是在前端用的比较轻,就没必要引入库来管理了,手动管理下就好了。也通过这种模式,可以很方便的实现 Redux 那种 namespace 的概念以及子 store
  • 如果你使用 ts 开发,那么建议将实现和定义分开,也就是说分别定义一个 interface 和 class,class 继承 Interface,这样对外也就是组件内只需要暴露 interface 即可。这样可以很方便的隐藏一些你不想对外部暴露的方法,但内部却依旧要使用的方法。还是上面的例子,比如 GlobalStore 有一个属性是 ThemeStore 需要获取的,而不希望组件获取,那么就可以将方法定义到 class 上而非 interface 上,这样既能有良好的类型检查,又可以保证一定的隔离性。

是的,基本上这样就可以写好一个 Store 了,没有什么花里胡哨的概念,也没有什么乱七八糟的工具,约定俗成就足以。我向来推崇没有规则就是最大的规则,没有约束就是最大的约束。很多东西能约定俗成就约定俗成,落到纸面上就足够了。完全没必要做一堆 lint/tools/library 去约束,既增加了前期开发成本,又增加了后期维护成本,就问问你司内部有多少 dead 的工具和库?

俗话说的话,「秦人不暇自哀而后人哀之,后人哀之而不鉴之,亦使后人而复哀后人也」,这就是现状(一巴掌打醒)

不过以上的前提是要求你们的开发团队有足够的开发能力,否则新手很多或者同步约定成本高的话,搞个库去约束到也不是不行(滑稽脸)

缺点?

说了这么多,也不是说是万能的,有这个几个缺点

  • 针对一些就带状态的小组件,性能上还不如原生 hooks。可以根据业务情况酌情针对组件使用原生 hooks 还是 mobx hooks。而且针对小组件,代码量可能相应还是增多。因为每次都要包裹 useObserver 方法。
  • mobx 就目前来看,无法很好在未来使用异步渲染的功能,虽然我觉得这个功能意义不大。某种程度上说就是一个障眼法,不过这个思路是值得一试的。
  • 需要有一定 mobx 的使用基础,如果新手直接上来写,虽然能避免很多 hooks 的坑,但是可能会踩到不少 mobx 坑

总结

Mobx 在我司的项目中已经使用了很久了,但 Hooks 也是刚刚使用没多久,希望这个能给大家帮助。也欢迎大家把遇到的问题一起说出来,大家一起找解决办法。

我始终觉得基于 Mutable 的开发方式永远是易于理解、上手难度最低的方式,而 Immutable 的开发方式是易维护、比较稳定的方式。这两者没必要非此即彼,而 Mobx + React 可以认为很好的将两者整合在一起,在需要性能的地方可以采用 Immutable 的方式,而在不需要性能的地方,可以用 Mutable 的方式快速开发。

当然了,你就算不用 Mobx 也完全没有问题,毕竟原生的 Hooks 的坑踩多了之后,习惯了也没啥问题,一些小项目,我也会只用原生 Hooks 的(防杠声明)。

注释和共享

作为一个喜欢折腾的人,个人搞了很多东西放在自己的服务器上,但是为了方便,能够在世界各地随时随地的打开查看和使用,我将服务器暴露到了公网中,当然了有些在公有云上的本来就暴露出来的。

那么这里就有一个问题,我如何保护我的信息只能我来查看呢?

  • 最简单的方法就是通过 HTTP Basic Auth + HTTPS。记住一定要上 https,否则你的密码也是会泄漏的。为什么说简单呢?因为只需要在 Nginx 或 Traefik 上配置下就可以了。但是这个方案有一个非常麻烦的问题,就是过一段时间之后就要输入用户名和密码。时间短了,到无所谓,时间一长就会觉得很烦。
  • 构建一套 token 验证体系,不管是使用 oauth 也好还是 jwt 也好,都是可以的。安全性也是可以保证的,而且设置好 token 的时间长度,也能保证避免频繁的输入密码。但是这有一个问题就是实现起来太过于复杂,都快赶上公司的一套系统了。而且还要有各种登录页面,想想都烦。
  • 与上面类似,不过验证方式使用 Two Auth,也就是基于时间的 6 位数组。但是依旧比较复杂。

我想了许久,有没有一种不需要输入密码,就可以验证安全的呢?因为是我一个人使用的,所以我根本不需要多用户系统,也就是说验证方式只需要一个密码就可以了。这我突然想起了之前在写 gRPC 的时候有一个双向验证的参数,也可以验证客户端可以不可以。当时觉得只是他们基于 h2 改的协议,结果我一查发现这原来就包含在 https 里面,准确说是 SSL 规范里面。(怪自己当初上计算机网络的时候没好好学这部分,竟然连这个都不知道)

那么至此,思路就很清晰了,给我的所有个人服务都添加 https 客户端校验。只要我的证书够安全,我的页面就是安全的(反正都是我个人的东西,直接拿着 U 盘到处拷贝,手机 Pad 用数据线发送,我就不信这样谁还能盗走我的证书,傲娇脸)

关于 SSL 证书的一些知识

  • 生成证书我们主要采用 openssl 具体的安装教程我就不讲解了,有兴趣的小伙伴自行查阅,主要有下面几个步骤:
    • openssl genrsa:生成 Private Key,用于生成请求文件使用,这里用 .key 后缀。
    • openssl req:依赖上面生成的 Key 去生成 CSR,也就是证书请求文件。使用 .csr 后缀。这期间要填写一些信息,前面的几个大写字母是缩写,后面在命令行使用的时候会用到。
      • C(Country) 国家
      • ST(State/Province) 州或者省
      • L(Locality) 地区,国内写区即可
      • O(Organization) 组织
      • OU(Organization) 组织单位
      • CN(Common Name) 通用名,这个是非常重要的,影响了证书的显示名称和 HTTPS 的域名。
    • openssl x509:根据 x509 规范,利用 CA 的证书和私钥将 CSR 文件加密成真正可以使用到的证书。使用 .crt 后缀
  • SSL 证书必须要采用 sha-2 加密算法。2015 年 12 月 31 日前,CA 机构还会颁发 SHA-1 签名的证书,但是之后只会签发 SHA-2 签名的证书了。Chrome 也会对 SHA-1 签名的证书提示不安全。在 openssl 中用的是 -sha-256 参数。
  • CRTPEM 的关系,大家可以简单的认为 PEM 是将证书 base64 之后的文件,而 CRT 是既能 base64 也能 binary 的一种文件格式。但是通常 openssl 产出的是 base64 的文件,你可以通过 -outform 参数控制产出的类型。

CA 的生成

有了 CA 我们才能去给其他的证书签名,生成 CA 的过程很简单

创建根钥

这个秘钥非常重要,任何获得了这个秘钥的人在知道密码的情况下都可以生成证书。所以请小心保存

openssl genrsa -des3 -out root.key 4096
  • -des3 标明了私钥的加密方式,也就是带有密码。建议添加密码保护,这样即使私钥被窃取了,依旧无法对其他证书签名。你也可以更换其他的加密方式,具体的请自行 help。
  • 4096 表示秘钥的长度。

创建自签名证书

因为是 CA 证书,所以没法让别人去签名,只能自签名。这里可以认为是生成 CSR 和签名两部合成一步走。

openssl req -x509 -sha256 -new -key root.key -sha256 -days 1024 -out root.crt

服务端证书生成

生成证书私钥

openssl genrsa -out your-domain.com.key 2048

和 CA 证书不同,这个私钥一般不需要加密,长度也可以短一些。

生成证书请求文件

openssl req -new -key your-domain.com.key -out your-domain.com.csr

这期间要填入一些信息,注意 CN 的名字一定要是你的域名。

使用 CA 对 CSR 签名

在 Chrome 58 之前,Chrome 会根据 CN 来检查访问的域名是不是和证书的域名一致,但是在 Chrome 58 之后,改为使用 SAN(Subject Alternative Name) 而不是 CN 检查域名的一致性。

而 SAN 属于 x509 扩展里面的内容,所以我们需要通过 -extfile 参数来指定存放扩展内容的文件。

所以我们需要额外创建一个 your-domain.com.ext 文件用来保存 SAN 信息,通过指定多个 DNS 从而可以实现多域名证书。

subjectAltName = @alt_names

[alt_names]
DNS.1 = your-domain.com
DNS.2 = *.your-domain.com
DNS.3 = *.api.your-domain.com

以此类推,如果域名较少,还可以用另外一种简写方案。

subjectAltName = DNS: your-domain.com, DNS: *.your-domain.com

关于语法的更多内容请查看官方文档。在有了 ext 文件之后就直接可以开始签名了。

openssl x509 -req -sha256 -in your-domain.com.csr -CA root.crt -CAkey root.key -CAcreateserial -out your-domain.com.crt -days 365 -extfile your-domain.com.ext

CAcreateserial 这个参数是比较有意思的,意思是如果证书没有 serial number 就创建一个,因为我们是签名,所以肯定会创建一个。序列号在这里的作用就是唯一标识一个证书,当有两个证书的时候,只有给这两个证书签名的 CA 和序列号都一样的情况下,我们才认为这两个证书是一致的。除了自定生成,还可以通过 -set_serial 手动指定一个序列号。

当使用 -CAcreateserial 参数之后,会自动创建一个和 CA 文件名相同的,但是后缀是 .srl 的文件。这里存储了上一次生成的序列号,每次调用的时候都会读取并 +1 。也就是说每一次生成的证书的序列号都比上一次的加了一。

现在,只需要将 your-domain.com.crtyour-domain.com.key 放到服务端就可以使用了。别忘了将 CA 添加系统当中,要不然浏览器访问会出现问题。

客户端证书生成

服务端有了之后,就需要生成客户端的证书,步骤和服务端基本一致,但是不需要 SAN 信息了。

openssl genrsa -out client.key 2048
# 这里也可以采用非交互形式,方便制作成命令行工具
openssl req -new \
    -key client.key \
    -subj "/C=CN/ST=Zhejiang/O=X/CN=*.your-domain.com" \ # 这里的缩写就是文章一开始所说的那些缩写
    -out client.csr
openssl x509 -req -in client.csr -CA root.crt -CAkey root.key -out client.crt -days 365

只不过客户端验证需要的是 PKCS#12 格式,这种格式是将证书和私钥打包在了一起。因为系统需要知道一个证书的私钥和公钥,而证书只包含公钥和签名,不包含私钥,所以需要这种格式的温江将私钥和公钥都包含进来。

openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12

这期间会提示你输入密码,用于安装的时候使用。也就是说不是什么人都可以安装客户端证书的,要有密码才行,这无疑又增加了一定的安全性。当然了,我试过不输入密码,但是好像有点问题,有兴趣的同学可以自己尝试下。

客户端校验证书的使用

这里以 Node.js 举例。使用 https 模块,在创建的时候和普通的创建方式基本一致,但是需要额外指定 requestCertca 参数来开启客户端校验。

https.createServer({
    key: fs.readFileSync('your-domain.com.key'),
    cert: fs.readFileSync('your-domain.com.crt'),
    requestCert: true,
    ca: [fs.readFileSync('root.crt')], // 校验客户端证书的 CA
}, (req, resp) => {
    // blahblah
})

这样只要客户端没有安装合法的证书,那么整个请求就是失败的。而且根本不会进入请求处理的回调函数中,这也意味着显示的错误是浏览器的默认错误。那么这对用户来讲其实不太友好。

那么我们可以通过在参数中添加 rejectUnauthorized: false 来关闭这个功能,也就是说不管客户端证书校验是正确还是失败,都可以进入正常的回调流程。此时我们只需要通过 req.client.authorized 来判断这个请求是否通过了客户端证书的校验,可以给予用户更详尽的错误提示。

另外我们还可以通过 resp.connection.getPeerCertificate() 获取客户端证书的信息,甚至可以根据不同的信息选择给予不同的用户权限。

这里有一个 DEMO: https://www.xgheaven.net.cn:3443,大家打开之后应该会看到一个证书校验失败的提示。这里要说下,我这里的 DEMO 没有使用自签名的服务端证书,只是使用了自签名的 CA 去检查客户端证书。因为用自己签名的服务端证书的话,浏览器会提示不安全,因为用户么有安装自签名的 CA。

可以点击下载客户端证书按钮,安装客户端证书。因为客户端证书是有密码保护的,请输入页面上提示的密码。

再次刷新,如果是 Mac 系统,会提示你要使用哪个客户端证书登录,此时就说明安装成功了。

点击确认,可能还要输入一个系统密码允许 Chrome 访问 Keychain,一劳永与的话在输入密码之后选择 Always Allow,从此就不需要再输入密码了。

按照道理,你就可以看到这个页面了。

结语

有了这个功能,我就可以将我的所有内容全盘私有化而且还能直接暴露在公网中。配合之前毕设搞的微服务化,简直不要美滋滋。如果之前是使用账号密码登录的,也可以接入这个方案。就是将登录页面替换成证书校验就可以了。

Refs

注释和共享

当你看到这个标题的时候,一定很好奇,React 不是很快么?为啥会变慢呢?在写这篇文章之前,我也是这么认为的,但是当我去看了一下 React 有关 Array 的 Diff 之后,我才认识到其实 React 如果你用的不正确,那么是会变慢的。

React Diff 算法

React Diff 算法相信大家不是很陌生吧,这里就不具体展开讲了。不过有一点要补充下,Diff 算法针对的是整个 React 组件树,而不仅仅是 DOM 树,虽然这样性能会比较低一些,但是实现起来却很方便。

而在 Diff 算法中,针对数组的 diff 其实是比较有意思的一个地方。在开始讲解方面,我希望你能对 React 有一定的了解和使用。

试一试有什么区别?

首先我们创建 3 个组件,分别渲染 10000 个 DOM 元素,从 [1...10000] ,渲染成如下。

const e10000 = new Array(10000).fill(0).map((_, i) => i + 1)
element10000.map(i => <div key={`${i}`}>{i}</div>)

每个组件有两个状态,会切换数据的顺序

  • 组件 A 在 [1...10000][2,1,3...10000] 之间切换。
  • 组件 B 在 [1...10000][10000,1...9999] 之间切换
  • 组件 C 在 [1...10000][10000...1] 之间切换,也就是正序和倒序之间切换。

我们简单命名下,默认的初始状态为 S1 而切换之后的状态为 S2 。大家可以思考一下,同一个组件状态切换的时候,所耗费的时间是不是都是一样的?可以直接使用这个 DEMO

可以直接点击上方的 toggle 来切换两者之间的状态,并在控制台中查看渲染的时间。因为每次时间都不是绝对准确的,所以取了多次平均值,直接揭晓答案:

组件 S2 => S1 S1 => S2
A 102ms 103ms
B 129ms 546ms
C 556ms 585ms

有么有觉得很奇怪,为什么同样是 S1 ⇒ S2 ,同样是只改变了一个元素的位置,为什么 A 和 B 的时间差距有这么多的差距。这个具体原理就要从 Diff 算法开始讲起了。

Array Diff 的原理

在讲 React 的实现之前,我们先来抛开 React 的实现独立思考一下。但是如果直接从 React 的组件角度下手会比较麻烦,首先简化一下问题。

存在两个数组 A 和 B,数组中每一个值必须要保证在对应数组内是唯一的,类型可以是字符串或者数字。那么这个问题就转变成了如何从数组 A 通过最少的变换步骤到数组 B。

其实每个元素的值对应的就是 React 当中的 key。如果一个元素没有 key 的话,index 就是那个元素默认的 key。为什么要强调最少?因为我们希望的是能够用最少的步数完成,但是实际上这会造成计算量的加大,而 React 的实现并没有计算出最优解,而是一个较快解。

顺便定义一下操作的类型有:删除元素插入元素移动元素

这里又要引申一个特殊点,React 充分利用了 DOM 的特性,在 DOM 操作中,你是可以不使用 index 来索引数据的。简单来讲,如果用数组表示,删除需要指定删除元素的索引,插入需要指定插入的位置,而移动元素需要指定从哪个索引移动到另一个索引。而利用 DOM,我们就可以简化这些操作,可以直接删除某个元素的实例,在某个元素前插入或者移动到这里(利用 insertBefore API,如果是要在添加或者移动到最后,可以利用 append )。这样最大的好处是我们不需要记录下移动到的位置,只需要记录下那些元素移动了即可,而且这部分操作正好可以由 Fiber 来承担。

举个例子说,从 A=[1,2,3] 变化到 B=[2,3,4,1],那么只需要记录如下操作即可:

有人好奇,不需要记录移动插入到那个元素前面么?其实不需要的,这是因为你有了操作列表和 B 数组之后,就可以知道目标元素在哪里了。而且采用这种方式就根本不需要关心每次操作之后索引的变化。

回到上面的简化后的问题,首先通过对比 A、B 数组,可以得到哪些元素是删除的,哪些元素是添加的,而不管采用什么样子的策略,添加删除元素的操作次数是无法减少的。因为你不能凭空产生或者消失一个元素。那么我们问题就可以再简化一下,把所有的添加删除的元素剔除后分别得到数组 A’ 和 B’,也就是 A’ 中不包含被删除的元素,B’ 中不包含被添加的元素,此时 A’ 和 B’ 的长度一定是一样长的。也就是求解出最少移动次数使得数组 A’ 能够转化成数组 B’。

如果只是简单的求解一下最少移动步数的话,答案很简单,就是最长上升子序列(LIS,Longest Increasing Subsequence)。关于如何证明为什么是最长不下降子序列这个算法,可以通过简单的反证法得到。关于这个算法的内容我就不具体讲解了,有兴趣的可以自行 Google。在这里我们只需要知道这个算法的时间复杂度是 O(n^2)

但是现在我们还无法直接应用这个算法,因为每个元素的类型可能是字符串或者数字,无法比较大小。定义数组 T 为 B’ 内元素在 A’ 的位置。举个例子,如果 A' = ['a', 'b', 'c'] B' = ['b', 'c', 'a'],那么 T = [2, 3, 1]。本文约定位置是从 1 开始,索引从 0 开始。

此时便可以对 T 求解 LIS,可以得到 [2, 3],我们将剩下不在 LIS 中的元素标记为移动元素,在这里就是 1,最后补上被剔除的删除和插入的元素的操作动作。这样 Diff 算法就可以结束了。

上面讲解的是一个个人认为完整的 Array Diff 算法,但是还是可以在保证正确性上继续优化。但是不管优化,这个复杂度对于 React 来讲还是偏高的,而如何平衡效率和最优解成为了最头疼的问题,好在 React 采用了一个混合算法,在牺牲掉一定正确性的前提下,将复杂度降低为 O(n)。下面我们来讲解下。

React 简化之后的 Array Diff

大家有过 React 开发经验的人很清楚,大部分情况下,我们通常是这样使用的:

  • 情形1:一个标签的的直接子子标签数量类型顺序不变,通常用于静态内容或者对子组件的更新

    1
    2
    3
    4
    5
    6
    7
    // 比如每次渲染都是这样的,里面的直接子元素的类型和数量是不变的,在这种情况下,其实是可以省略 key
    <div>
    <div key="header">header</div>
    <div key="content">content</div>
    <div key="footer">footer</div>
    <SubCmp time={Date.now()}/>
    </div>
  • 情形2:一个标签有多个子标签,但是一般只改变其中的少数几个子标签。最常见的场景就是规则编辑器,每次只在最后添加新规则,或者删除其中某个规则。当然了,滚动加载也算是这种。

  • 情形3:交换某几个子标签之间的顺序
  • 情形4:翻页操作,几乎重置了整个子元素

上面只是简单举了几个常见的例子,大家可以发现,大部分情况下子标签变动的其实并不多,React 利用了这个,所以将 LIS 简化成以第一个元素开始,找到最近上升子序列。简单来来讲就是从头开始遍历,只要这个元素不小于前的元素,那么就加入队列。

1
2
3
4
5
Q = [4, 1, 5, 2, 3]
// 标准算法
LIS = [1, 2, 3]
// 简化后的算法,从第一个开始,找到最近的不下降子序列即可。
LIS_React = [4, 5]

我们乍一看,这个算法不对呀,随便就能举出一个例子让这个算法错成狗,但是我们要结合实际情况来看。如果我们套回前面说的几种情况,可以看到对于情况 1,2,3 来讲,几乎和简化前效果是一样。而这样做之后,时间复杂度降低为 O(n) ,空间复杂度降低为 O(1)。我们给简化后的算法叫做 LIS' 方便后面区分。

我们将 LIS 算法简化后,配合上之前一样的流程就可以得出 React 的 Array Diff 算法的核心流程了。(为什么叫核心流程,因为还有很多优化的地方没有讲)

变慢的原因?

当我们在了解了 React 的实现之后,我们再回过来头来看看前面给出的三个例子为啥会有这么大的时间差距?

  • 组件 A 从 [1...10000] 变化到 [2,1,3...10000] 。此时我们先求解一下 LIS' 可以得到 [2,3,4...10000],那么我们只需要移动 1 这个元素就可以了,将移动到元素 3 前面。同理反过来也是如此,也就是说 S1 ⇒ S2 和 S2 ⇒ S1 的所需要移动的次数是一致的,理论上时间上也就是相同的。
  • 组件 B 从 [1...10000] 变化到 [10000,1,2...9999] 。同理,先计算 LIS' 可以得到 [10000],没错,你没看错,就是只有一次元素,那么我需要将剩下的所有元素全都移动到 10000 的后面去,换句话要进行 9999 次移动。这也就是为啥 S1 => S2 的时间会这么慢。但是反过来却不需要这个样子,将状态反过来,并重新计算索引,那么也就是从 [1...10000][2,3....10000,1],在计算一次 LIS' 得到 [2,3...10000] ,此时只需要移动一次即可,S2 ⇒ S1 的时间也就自然恢复的和组件 A 一致。
  • 组件 C 是完全倒序操作,所以只分析其中一个过程即可。首先计算 LIS' 可以得到,[10000] ,也就是说要移动 9999 次,反过来也是要 9999 次,所以时间状态是一致的。

经过这样的分析大家是不是就明白为啥会变慢了吧?

优化细节

降低 Map 的生成操作次数

上面有一点没有讲到,不知道大家有没有思考到,我怎么知道某个元素是该添加函数删除呢?大家第一反应就是构建一个 Set,将数组元素全放进去,然后进行判断就可以了。但是在 React 中,其实用的是 Map,因为要存储对应的 Fiber,具体细节大家可以不用关注,只需要知道这里用 Map 实现了这个功能。

不管怎么样,根据算法,一开始肯定要构建一遍 Map,但是我们来看下上面的 情形1。发现内容是根本不会发生变化的,而且对于 情形2 来讲,有很大的概率前面的大部分是相同的。

于是 React 一开始不构建 Map,而是假设前面的内容都是一致的,对这些元素直接执行普通的更新 Fiber 操作,直到碰到第一个 key 不相同的元素才开始构建 Map 走正常的 Diff 流程。按照这个方式,情形1根本不会创建 Map,而且对于情形2、3来讲也会减少很多 Map 元素的操作(set、get、has)。

降低循环次数

按照上面的算法,我们需要至少 3 遍循环:第一遍构建 Map,第二遍剔除添加删除的元素生成 A’ 和 B’,第三遍计算 LIS 并得到哪些元素需要移动或者删除。而我们发现第二遍和第三遍是可以合并在一起的。也即是说我们在有了 Map 的情况下,不需要剔除元素,当遍历发现这个元素是新增的时候,直接记录下来。

总结

关于 Diff 算法其实还有很多的细节,我这边没有过多讲解,因为比较简单,比较符合直觉。大家有兴趣的可以自己去看下。另外有人应该会注意到,上面的例子中,为什么切换同样的次数,有的时间长,有的时间短了。日后有时间再分析下补充了。

注释和共享

在平时的开发中,我发现大家特别喜欢将很多自己常用或者公司常用的脚本封装成一个 cli,这虽然无可厚非,但是作为一个有强迫症的患者来说,我认为这其实做不好会导致 cli 非常不好用的。所以这里总结了下平时写 cli 的经验希望和大家分享下。

写之前请思考

  1. 我是不是为了 cli 而去写 cli,换句话我觉得这样比较 cool。如果是,那么请放弃这种想法。
  2. 这个 cli 一定会减轻工作量么?
    • 有很多公司喜欢将各种 webpack/eslint/babel 工具封装成一个 cli,那么这真的会降低使用者的工作量么?如果你封装的不好,相反会增加很大的工作量。最神奇的是,封装了这么多工具,配置文件却一个都没少,babelrc/eslintrc/prettierrc,那封装了有何用。
    • cli 的不透明性就会导致使用者永远都是有使用成本的,不论这个 cli 有多简单。他不会知道你是干了什么。所以能避免写一个 cli 就避免写一个 cli。那如果真的要写,有其他方案么?请看第三条
  3. 除了写 node cli 真的没有其他方案了么?
    • 大部分情况下,写 shell 的效率远远高于写 cli,将一个命令拆分成多个 shell,然后配合简单的帮助文档即可。如果用户不知道是做什么的,那么直接就可以去看源码。
    • 而且使用 git 管理 shell 也同样高效,比如将个人的一些脚本放到 private github 上,然后直接 clone 下来就可以用了。这样不需要每次都 npm publish,进而污染 npm。
    • 大部分情况下,去写 Makefile/Rakefile 同样可行。当然,Node 生态也有 Jake,不过不推荐用,因为要装 jake 包。
    • 如果你这个 cli 是作为一个项目的脚手架工具,那么是不是用 yeoman 或者 degit 这类工具更好?除非你的项目非常热门,功能自定义程度高,否则完全不需要自己去写一个脚手架。如果你只是觉得好玩,想写一个脚手架的话,那么请去看第一条,问问自己。
    • 如果是一个团队使用,那么除非有很大的普世性,那么用 git 管理同样比 npm 管理要强的多。
  4. 最后如果决定一定要写 cli 的话,有必要发布到 npm 么?
    • 是不是发布到自己的 scope 下也是一个不错的选择?
    • 是不是直接让别人通过 npm install [github.com/user/package](http://github.com/user/package) 也是一个不错的选择?
    • 是不是上传到公司 or 个人的私有 npm 更好?

开发时请遵循以下几点

  1. 请使用 npm
  2. 不要写死版本号,优先使用 ^
    • 这是因为有可能你的 cli 会被直接当做依赖安装到项目中,如果你写死了版本号,那么可能会装多分依赖。比如项目依赖了 yargs@^13.2.0 ,但是你锁死了 `yargs@13.1.0`,就会导致安装两个 yargs。
  3. 避免引入一些功能很大,而自己只用其中一部分的包
    • 因为没有 webpack 工具,无法进行 tree shaking,所以导致安装了很多比较大的包。比如你只是用了 lodashcloneDepp 却安装了整个包。优先使用 [lodash.xxx](http://lodash.xxx) 子包。
  4. 如果你使用了某些构建工具(babel,webpack,typescript,flow),那么请将构建之后的文件也加入代码仓库。
    • 比如有一个 src 目录存放了 ts 源文件,而 dist 存放了构建之后的 js 文件。那么很多人的选择往往是将这 dist 文件夹加入 gitignore。这样其实是不太好的,原因如下:
    • 第一,方便追踪变化,比如你自己添加了一些 debug 代码,这个时候构建之后发现没有问题。又把 debug 代码删除,当你提交的时候就可以很清楚的看到自己修改了之后没有构建代码
    • 第二,方便通过 unpkg.com 等工具访问
    • 第三,在版本开发依赖升级之后,可以很方便的看到改变的内容。一般使用者会放心升级 cli 之类的开发工具,所以这部分的质量需要我们自己来保证。

交互设计上请遵循以下几点

  1. Throw as possible,将可能的所有报错向上抛出,是一个好系统所必备的能力。但是在错误展示的时候,可以向用户隐藏部分调用栈信息等,只保留关键信息即可。
  2. 尽可能遵循 linux 下的 cli 规范。
  3. 不要让用户产生无畏的等待,通过添加进度条或者输出完成列表等告诉用户你依旧是在工作中的。
  4. 给予用户想查看所有日志的能力。可以通过 -v -vv -vvv 或者 --log-level debug 来控制显示级别
  5. 对所有命令的描述都不要超过一行。不论屏幕宽度如何(一般默认 80),最好不要超过 70。如果需要大量描述,请尝试通过 man 或者单独的页面。
  6. 帮助是最好的文档,写好 cli 的 help 远比去写好一个文档网站要关键

注释和共享

思来想去,我决定还是要写一篇文章分享一下我在网易的经历和生活,我对网易的观点或者想法以及评论也许并不是客观公正的,我只是想从一个校招生的角度来讲述。

2018 年 7 月 01 日,我从杭电毕业,进入了网易,从事大数据前端管理页面开发;
2019 年 3 月 15 日,我主动离开了网易,一共 257 天、6168 小时、370080 分、22204800 秒。

心路历程

进入网易后,我的心态也一点一点的发生着变化,这其中有一些是因为自己的原因,也有一部分是网易的原因,如果你愿意,我愿和你慢慢阐述

意外 —— 校招季拿到了网易 Offer

我原本没想给网易投递 Offer,那个时候我心里只有阿里,至于原因么,有句话说的好,『所有的 Node 大佬不是在阿里,就去在去阿里的路上』,虽然我不是大佬,但是我也有一颗想成为技术大佬的心。那个时候我只投递了阿里的 Offer。

可是事与愿违,18 届的校招可谓说是及其严格,基本都是社招要求,而且听说就招了 300 人(只是听说)。投了两次阿里云,都以失败告终(毕竟我的朴神来阿里云)。这个时候正好有同学在网易实习,我就看了下,有前端岗位,那就去下试试吧。

说实话,那个时候对网易的前端、Node 没有抱有任何希望。网易在前端的开源社区几乎没有任何动静,也几乎从来没参加过任何技术论坛,也从来没听说他们有用任何前端框架,可以说对网易前端没有任何的概念。

面试的时候我也就啥都没准备,纯裸考,只是觉得这公司进不进无所谓吧,甚至 HR 面的时候,稍微顶撞了一下 HR,HR 问了一个问题,我回答了,结果他说我回答的不是他问的问题,我思考了一下,坚定的回了他一句『我回答的就是你的问题,那您不是想问这个?』。结果意外的,我竟然进了,我当时自己都蒙了。直到入职之后才回过神来,原来是他们太缺人了。。。

但是毕竟校招,怎么样还是要试一下么,就安慰自己说可能他们内部用的技术挺好的,只不过按照丁磊的作风,可能不喜欢招摇吧。但后来结果实力打脸,事与愿违~

说实话,当时也找过其他的公司,可是杭州这边,真正既有技术实力、有大佬助阵、面向开源的公司几乎少的可怜,除了阿里几乎找不到几个,于是当时在好朋友的推荐下,投了有赞的前端、七牛的后端。

七牛的后端笔试直接跪了,有赞的拿到了 offer,但是当时脑抽觉得有赞技术不好(另外是我和室友都拿到了有赞的 offer,但是前端的老大跟我的室友说带着他一起搞波大事情,和我却啥都没说,就加了个微信,我觉得受到了某种歧视),而且觉得有赞的平台有点小,于是给推了。

后来真正进入到网易之后,觉得网易还不如有赞呢。或许大家认识有赞是因为 996,都认为有赞 996 不好,可是有赞和普通的互联网企业一样,并没有 996,相反比其他公司还有活力,有各种兴趣组织,甚至还有官方的游戏比赛,Dota/王者,比赛还有专人解说呢。而且钱多福利好。而且杭州除了阿里能数上的有赞也是接入企业滴滴。

反正说了这么多,最终我算是说服自己,现在网易学习 1 年,然后去我喜欢的公司。

期待 —— 嗨,猪行动

期待着进入网易的生活会是什么样子;期待着会不会遇见很多大佬;期待着网易的食堂有多么好吃;期待着能不能和同事很好的相处;期待能不能用到自己喜欢的技术;期待着别人能不能承认自己的能力。

一切的期待,都在拿到校招 Offer 的那一刻开始。

这期间参加了 『嗨,猪行动』,第一次以校招的身份进入网易,我们在 HR 小哥哥小姐姐们的安排下,一起参加了一些有趣有意思的活动。大家分成各个小队,以小队的身份参加比赛,每完成一个比赛,都会有一定的积分奖励,在活动结束之后,可以去换取很多严选的礼物,比如有行李箱、抱枕、牙刷、拖鞋、杯子、笔等等。

我觉得这场活动最成功的便是在最后,来了一场上百人吃鸡游戏,在园区散落着很多盒子,每个盒子里面武器或者,然后找到其他小队成员消灭之。虽然我们的枪只是简单的水枪,子弹也是可消除墨水,命只是一个简单的丝带绑在头上。而且我不得不承认这个游戏 Bug 太多,不过不得不说真的是很好玩的。

很快,虽然我很轻松的活到了前 10 名的样子,但是我是用某些不正常的方法(在某个门后躺着,假装自己已经死掉了,悠闲的等着;而且我们在这个游戏开始前就已经偷偷攒了好多武器和命,就是在玩吃鸡之前的游戏的时候),感觉不是太很公平,于是我就主动退出了。

静静的看着他们战斗,很快场上就只剩下 2 名妹子,一枚来自其他的组,还有一枚便是我们组的。于是最精彩的决赛来了,我们给他们腾出了场地,每人补足武器。

piupiupiu,piupiupiu,真的是太精彩了,我无法描述,但是结果有点遗憾,我们组的妹子惜败第二名。

一天如此开心,感谢 HR 小哥哥小姐姐们的辛勤付出,也感谢队友的努力奋斗,或许在此刻,我对网易的印象有了那么一些改变,感觉让人期待网易的生活会是怎么样子。

满足 —— Mini 项目 『来人吖』

2018 年 7 月 1 日,这一天终于到来了。我真正的不如了网易的大门,发现一切都是如想象的那么美好。

免费的四餐(包含夜宵喽),除了人太多以外味道还是很不错的;各种严选考拉的内购。有各种各样的折扣,而且有些还会直接在食堂门口摆摊。免费的班车简直不要太舒服,就是要每天 7 点半起床赶班车,真的是难受。园区免费停车、新能源免费充电,虽然这个时候我还没有买车,不过我已经在考虑中了。

入职之后,参加了 2 天的素质拓展,终于圆梦了我一直想去尝试的高空断桥,刺激、爽,还想再来一次。哈哈哈。

很快,最令我欣喜激动的事情来了,『Mini 项目』,简单点来讲,就是网易花了 100w 做了 8 个没啥用的产品。当然了,其实不是这样,重新讲一下,就是大家在一个月的时间里面,从开始,做出一个可以上线的产品,从中体验整个产品的开发流程,学习工作中用到的技术栈,培养同学之间的合作意识。这其实是一个非常有意义的事情,从中你可以体会到创业的过程,需求分析、竞品分析、优势分析、未来产品规划、UI 设计、技术架构、团队合作、测试保证、产品推广,每一步都可谓非常有价值。

当然了,这其中最让我满足的便是我在 Mini 项目中担任了技术负责人的身份,终于可以满足我想玩各种技术的心,把各种想用的技术都集成在一起,话不多说,先申请 10 台机器走起,Docker Swarm;Kafka;Node.js;NestJs;小程序;mpvue;Redis;TiDB;ES;CI/CD;Spring Cloud,简直不要太爽。

让我最欣慰满足的是虽然我这个技术负责人当的不是很称职,但是大家不嫌弃,而且一起推动团队的进步,而且对我定下的技术方向没有太多的异议,尤其是后端龙哥定的微服务化的 Spring Cloud 框架简直不要太符合我的胃口。感觉大家都是有技术追求,热爱技术。

另外最爽的就是入职一个月的便天天加班,甚至有几次我都是凌晨 12 点下班,到家都 1 点了,第二天还要 7 点半爬起来赶班车,谢天谢地那个时候还有顺风车。即便如此,我并没有觉得加班有多累,因为我们都有一个共同的目标——产品上线,拿到第一。

哦顺便说一句,那个时候我们就是想做顺风车匹配,只不过我们是做拼滴滴车,简单来讲就是大家都是乘客,匹配到一起了,由其中一个人打滴滴,然后大家一起走。这样做最大的好处便是省钱,比如说我回家快车 80,拼车 60,如果找齐 4 人,便只要 20 快,甚至比顺风车还便宜,而且有快车的服务。但是即便如此,开始产品评审的时候,评委一直无法绕开滴滴顺风车这个大对手,也对我们的产品产生了质疑,而且我们在这期间也质疑过很多次,直到我们做完了之后没多久,2018 年 8 月,顺风车下架了,顺风车下架了,顺风车下架了,重要事情说三遍,我们这才意识到我们的产品是有多么正确,多么正确,多么正确。

向往 —— 就要步入真正的业务线去了

一个月的 Mini 项目结束之后,虽然我们并没有取得第一名,但是我们在这期间收获了更多的东西。

小组里面的每个人就要到对应的业务线去了,有去云信的,有去严选的,有去云音乐的,也有去 AI、安全部门的。虽然这些部门无一例外经历了裁员。

有了 Mini 项目的经历之后,我对接下来步入真正的工作充满了无比的向往,会不会他们也和我们的组员一样有着技术追求,我能不能发挥我自己最大的力量给项目贡献代码,能不能维护以为开源项目,能不能听到很多大佬们的讲座。

以前这些问题我是有所担心的,但是有了 Mini 项目的经历之后,我更相信我所说的。可是事实却是……

愁 —— 为什么和我想象的不一样,甚至说是非常非常非常不一样

可是事实却是事与愿违,进入部门之后,我才发现是我想多了。

有的时候我一直在吐槽说我们项目技术差,比如什么只能用 ES5;没有 webpack;没有测试;没有 CI;虽然是前后端分离,但是代码却和后端放在一个仓库里面;Git 提交没有规范,各种 pull merge;Git Message 也没有规范;……这是一个已经三年的项目了,而且最有意思的是原本开始的时候是孟导定的用 Regular,想兼容 IE。但是我问现在不是只兼容 Chrome 么,这是为啥呢?他们和我说原因就是因为 IE 实在是兼容不了。这个时候我真想说一句,这个理由真的很棒。

这个问题不仅仅体现在前端,后端也是有如此的问题,觉得只要能解决问题就好了,要什么格式,要什么技术追求。这碗面条(代码)我只要保证计算机能吃就行,乱成啥样关我啥事。我只说一句,看了后端 Java 代码,我是第一次见识到 Space 和 Tab 缩进同时使用的,牛啤。

我甚至还开过一个玩笑说,前端越来越追求规范,开始越来越多的使用 TS 强类型的语言,而后端却想着如果绕开 Java 类型限定,内部几乎都是 JSONObject。当然这是一句玩笑,毕竟有些框架的东西你是没法直接用 JSONObject 的,但也能反映一些问题。

愁啊愁啊,当初的期待全部破灭,想推重构,可是我一个刚入职的又如何推的动?

愁啊愁啊,不仅仅是因为技术,而且内部其他的问题也很多,比如说 QA 测试问题、产品乱接需求、文档书写不全。。。夸张点说,我的大学技术社团除了干活没钱、人力不足以外,算是吊打我们部门,至少从技术上是这么讲的。至少社团的服务器环境是全 Docker 化的。都 9102 年了,还不会用 Docker 是来搞笑的么……

更愁的是,网易似乎不接受内部的合作,我曾经给网易云写了一个更好用对象存储 Node.js SDK,nos-node-sdk,想合并到他们的代码库里面,结果他们一直不给我回复。问他们为啥不改进代码,他说人手不够……我 go……我帮你写了,我帮你维护还不乐意啊,再说我就不吐槽你们的文档写的有多差了,错误一片一片的,也就我愿意认认真真的看下来了。

迷茫 —— 完全看不到方向

时间一久,我就陷入了迷茫期,找不到未来发展的方向,就像是我周围的同学都在踏步前进,有研究 Node 内核的,有使用 Egg 开发后台的,有用 React+Antd 的,还有 Typescript 写 IOS 组件的。而我,现在只能看着 Regular,对未来几乎没有任何帮助。换句话说,他们所在的环境至少给他们拓宽了不少眼界,而网易却没给我拓宽任何的眼界。

我也在安慰自己,毕竟学习是自己的事情,公司不给力,只能靠我们自己啦。确实我也在不停的努力,尝试学习其他的东西,可是这种学习毕竟是没有方向性的,我现在缺的是项目经历,而不是基础。基础就像是地基,只要地基够用,如何快速搭建起高楼是最重要的,而不是说连高楼都没造起来的时候想着拓宽地基。

另外,你见过那个专家是可以脱离他们的环境的?工作不用 React,却想成为 React 专家;工作不用 Java,却想成为 Java 专家;工作不接触 Node 内核,怎么成为 Node 专家;工作不接触浏览器内核;怎么成为浏览器专家。一个公司的天花板牵扯着你能早就多高的喽,也能让你知道你现在的基础够不够用,如何更好的提高自己。自我驱动的学习最大的用处是帮你补足基础,而只有真正的项目经历,才能真正让你有质的成长。

但是,这些东西在网易,很难寻得。

也许有人会出来说,网易里面技术栈也是很多的,React/Vue 都是有的。确实,有的确有,可能这又能如何?况且使用这部分技术的人也是少之又少,最关键的还是人!

离开 —— 却没有任何留念

最终我选择了离开网易,去寻找新的家园。

当我真正离开的时候,我才发现原来网易并不是那么的不堪。有的地方还是有许多值得学习借鉴的地方,可惜对于我们这些下层人民来讲,上层优秀的想法无法渗透到我们这里,在我们眼里,网易只是一个完成需求就可以下班的地方,不需要你有太多的想法,只需要按照上面的指示一步一步来做就可以了。

难得说网易的福利好,可惜我并没有沾上多少,食堂 Mini 项目之后我就搬离本部园区,在外面写字楼包了一块办公区,所以食堂基本吃不上了;免费停车就更不用说了,只能停本部,然后走一公里到我的工作地点;内部的技术分享全在园区,根本没时间赶过去,只能在线上看看了;各种摆摊更别说看见了;网易二期别人都搬过去了,为啥我们还不搬。简单点来讲,我培养不了我对网易的感情,对我来我,我就像是给大公司做外包,那我为啥不去一个钱多技术好的公司呢。

离开的时候,最大的愧疚便是对 HR 们,愧对了你们对我们的培养,真的真的真的非常抱歉,路过 HR 办公区的时候,害怕你看到我,虽然你还是看到了我。不过我们是主动离职的,比被裁的要好一点点,可惜就是没钱拿。

但是,这并不能阻止我离开网易。

感谢

  • 感谢陪伴和我一起走过 mini 项目的同学们
  • 感谢导师的帮助
  • 感谢身边的同事们
  • 最最感谢校招 HR 小姐姐小哥哥们的努力,为你们实力打 Call。感谢你们对我们的照顾和指导,让我们更快的融入到了网易的大世界中。谢谢,谢谢,尤其感谢森森和 6 姐。

回首

现在我入职了字节跳动,现在回过头来看了看,对我来说,这真的算是一个非常正确的决定,我也就不吐槽网易跟字节跳动相比的基础设施有多差了,我也不吐槽网易有多扣了。

如果你现在问我推不推荐网易,我觉得还是值得推荐的,但是一定一定一定要看部门,一定一定一定要看 Leader,一定一定一定要看环境。看部门有没有发展前景,看 Leader 愿不愿意培养自己,看环境会不会限制到你的天花板。

另外这里实力打 Call 考拉前端,足够开放,也有大佬,如果真的去可以去这个部门。当然严选的就是有点封闭了,搞了一套很不错的 npm 私有仓库,权限系统也做了,却不尝试在公司内部推广,搞不懂。云音乐和严选差不多,这里又要吐槽一下内部的 nei 是好难用,不仅仅是 ui 丑,而且这么久了,才上线在线 mock 功能,还不支持 swagger 导入,还不如之前我实习的公司大搜车推出的 easy-mock 呢。

过去就是过去,迎接未来,至少字节跳动能给我保证多劳多得,这就足够了!!!

另外说一句,有想要内推字节跳动的学弟学妹么,社招也可以哦,请用简历砸我吧,xutianyang.bradley@bytedance.com,如果想要和我交流的话,可以 Telegram 找我,@xgheaven。

注释和共享

目录

  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 天写文章的水文写完了。以后如果有机会再深入尝试。

注释和共享

目录

  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 等等。

注释和共享

XGHeaven

一个弱弱的码农


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


Weifang Shandong, China