-
测试同学,测试的内容范围,多数情况下,取决于前端同学给定的测试范围。而很多时候代码的改动,前端也不确定到底哪些页面会受影响。所以要么导致测试同学测试不完整,要么导致测试同学需要全量回归,这可是非常巨大的测试成本。
-
当其他前端开发人员,参与到项目中时,面临这种复杂的项目也是头大,需要花费很大的成本梳理清楚业务与代码的关联。导致合作或者交接项目时,困难。
我们需要通过发现的这些问题,来寻找合适的解决方案。
1. 解决代码层面的问题
=============
代码层面的问题,主要来源于 flag
变量过多,及 if/else
的嵌套及大量分支,导致难以修改和扩展,任何改动和变化都是致命的。其实这类问题,在设计模式中是有合适的方案——状态模式。
1.1. 状态模式
状态模式主要解决的是,当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,减少相互间的依赖,可以把复杂的判断逻辑简化。
「状态模式是一种行为模式,在不同的状态下有不同的行为,它将状态和行为解耦。」
从类图中可以看到,状态模式是多态特性和面向接口的完美体现,State
是一个接口,表示状态的抽象,ConcreteStateA
和 ConcreteStateB
是具体的状态实现类,表示两种状态的行为,Context
的 request()
方法将会根据状态的变更从而调用不同 State
接口实现类的具体行为方法。
状态模式的好处是,「将与特定状态相关的行为局部化,并且将不同状态的行为分割开来」。这样这些对象就可以不依赖于其他对象而独立变化了,未来增加或修改状态流程,就不是困难的事了。
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。
1.2. 状态机
状态机,全称有限状态机(finite-state machine,缩写:FSM),又称有限状态自动机(finite-state automaton,缩写:FSA),是现实事物运行规则抽象而成的一个数学模型,并不是指一台实际机器。状态机是图灵机的一个子集。它是一种认知论。从某种角度来说,我们的现实世界就是一个有限状态机。
有限状态自动机在很多不同领域中是重要的,包括电子工程、语言学、计算机科学、哲学、生物学、数学和逻辑学。有限状态机是在自动机理论和计算理论中研究的一类自动机。在计算机科学中,有限状态机被广泛用于建模应用行为、硬件电路系统设计、软件工程,编译器、网络协议、和计算与语言的研究。它是非常成熟的一套方法论。
有限状态机包含五个重要部分:
-
初始状态值 (initial state)
-
有限的一组状态 (states)
-
有限的一组事件 (events)
-
由事件驱动的一组状态转移关系 (transitions)
-
有限的一组最终状态 (final states)
更简洁的总结,就三个部分:
-
状态 State
-
事件 Event
-
转换 Transition
同一时刻,只可能存在一个状态。例如,人有 “睡着” 和 “醒着” 两个状态,同一时刻,要么 “睡着” 要么 “醒着”,不可能存在 “半睡半醒” 的状态。
逻辑学中说,现实生活中描述的事物都可以抽象为命题。命题本质上就是状态机的 State,Event 就是命题的条件,通过命题和条件推导过程。而 Transition 就是命题推导完成的结论。
所以当我们拿到需求的时候,首先要分离出哪些是已知的命题(State),哪些是条件(Event),哪些是结论(Transition)。而我们要通过这些已知命题和条件,推导出结论的过程。
1.2.1. 拿我们经常用到的 Fetch API 来举例子
fetch(url).then().catch()
有限的一组状态:
初始状态:
有限的一组最终状态:
有限的一组事件:
-
Idle
状态只处理FETCH
事件 -
Pending
状态只处理RESOLVE
和REJECT
事件
由事件驱动的一组状态转移关系:
1.3. 状态机 VS 传统编码 示例
下面采用一个小需求来对比一下区别。
1.3.1. 需求描述
根据输入的关键字进行搜索,并将搜索结果显示出来。如下图所示:
1.3.2. 基于传统编码
根据关键字拿到请求结果,再将结果塞回去就行了,代码如下:
function onSearch(keyword) {
fetch(SEARCH_URL + “?keyword=” + keyword).then((data) => {
this.setState({ data });
});
}
看似几行代码就把这个需求搞定了,但其实还有一些其他问题要处理。如果接口响应比较慢,则需要给一个用户预期的交互,如 Loading 效果:
function onSearch(keyword) {
this.setState({
isLoading: true,
});
fetch(SEARCH_URL + “?keyword=” + keyword).then((data) => {
this.setState({ data, isLoading: false });
});
}
还会发生出请求出错的情况:
function onSearch(keyword) {
this.setState({
isLoading: true,
});
fetch(SEARCH_URL + “?keyword=” + keyword)
.then((data) => {
this.setState({ data, isLoading: false });
})
.catch((e) => {
this.setState({
isError: true,
});
});
}
当然,不能忘记把 Loading 关掉:
function onSearch(keyword) {
this.setState({
isLoading: true,
});
fetch(SEARCH_URL + “?keyword=” + keyword)
.then((data) => {
this.setState({ data, isLoading: false });
})
.catch((e) => {
this.setState({
isError: true,
isLoading: false,
});
});
}
我们每次搜索时,还需要把错误清除:
function onSearch(keyword) {
this.setState({
isLoading: true,
isError: false,
});
fetch(SEARCH_URL + “?keyword=” + keyword)
.then((data) => {
this.setState({ data, isLoading: false });
})
.catch((e) => {
this.setState({
isError: true,
isLoading: false,
});
});
}
这就结束了么,是不是我们把所有的 Bug 都考虑进去了?并没有。当用户在等待搜素请求的时候,不应该再去搜索,所以搜索结果返回前,禁止再次发送请求:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.setState({
isLoading: true,
isError: false,
});
fetch(SEARCH_URL + “?keyword=” + keyword)
.then((data) => {
this.setState({ data, isLoading: false });
})
.catch((e) => {
this.setState({
isError: true,
isLoading: false,
});
});
}
可以看到,应用的复杂度在不断变大,可能你经历的场景比这个小示例还要复杂的多的多。如果因为搜索接口特别慢,用户希望有一个中断搜索的功能,那么新的需求又来了:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.fetchAbort = new AbortController();
this.setState({
isLoading: true,
isError: false,
});
fetch(SEARCH_URL + “?keyword=” + keyword, {
signal: this.fetchAbort.signal,
})
.then((data) => {
this.setState({ data, isLoading: false });
})
.catch((e) => {
this.setState({
isError: true,
isLoading: false,
});
});
}
function onCancel() {
this.fetchAbort.abort();
}
不能落下对 catch 的特殊处理,因为中断请求会触发 catch:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.fetchAbort = new AbortController();
this.setState({
isLoading: true,
isError: false,
});
fetch(SEARCH_URL + “?keyword=” + keyword, {
signal: this.fetchAbort.signal,
})
.then((data) => {
this.setState({ data, isLoading: false });
})
.catch((e) => {
if (e.name == “AbortError”) {
this.setState({
isLoading: false,
});
} else {
this.setState({
isError: true,
isLoading: false,
});
}
});
}
function onCancel() {
this.fetchAbort.abort();
}
最后还要处理没有值的情况:
function onSearch(keyword) {
if (this.state.isLoading) {
return;
}
this.fetchAbort = new AbortController();
this.setState({
isLoading: true,
isError: false,
});
fetch(SEARCH_URL + “?keyword=” + keyword, {
signal: this.fetchAbort.signal,
})
.then((data) => {
this.setState({ data, isLoading: false });
})
.catch((e) => {
if (
e &&
e.name == “AbortError”
) {
this.setState({
isLoading: false,
});
} else {
this.setState({
isError: true,
isLoading: false,
});
}
});
}
function onCancel() {
if (
this.fetchAbort.abort &&
typeof this.fetchAbort.abort == “function”
) {
this.fetchAbort.abort();
}
}
仅仅这么简单的一个小需求,从开始几行代码就可以完成,到最终判断各种边界完成的代码,对比一下,如下图所示:
可以看到,这种包含各种 flag
变量和嵌套着各种 if/else
的代码,会越来越难维护,所有的逻辑只存在于你的脑子里。当你写测试的时候必须从头再梳理一遍代码逻辑,才能写出来。
由于业务的高频变化,很多业务开发人员是不写单元测试的,因为成本太高太高,这也导致了交接代码时,别人去理解你的代码是一件很困难的事。写久了,你自己都可能读不懂代码里面的逻辑了。
这样会导致:
-
难以测试
-
难以阅读
-
可能含有隐藏的 Bug
-
难以扩展
-
新功能增加时还会使逻辑进一步混乱
1.3.3. 基于状态机
看一下我们用状态机的做法。记住流程:梳理出有哪些状态,每个状态有哪些事件,经历了这些事件又会转换到什么状态。
下面是用 XState 状态机工具的 JSON 描述:
{
“initial”: “空闲”,
“states”: {
“空闲”: {
“on”: {
“搜索”: “搜索中”
}
},
“搜索中”: {
“on”: {
“搜索成功”: “成功”,
“搜索失败”: “失败”,
“取消”: “空闲”
}
},
“成功”: {
“on”: {
“搜索”: “搜索中”
}
},
“失败”: {
“on”: {
“搜索”: “搜索中”
}
}
}
}
没错,就这几行代码就描述清楚所有的关系了。并且,可以把它可视化出来,如下图所示:
可以看到状态之间表达的非常清晰,结合到 View 中,也不需要再去编写复杂的 flag
及 if/else
了,View 中只需要知道当前是什么状态,已及将事件发送到状态机就可以了,其他什么都不需要做。在新增或者修改需求的情况下,只需要对状态进行新增或者编排就可以了。
而且可视化后,有以下变化:
-
清晰的看到有哪些状态
-
清晰的看到每个状态可以接受哪些事件
-
清晰的看到接受到事件后会转移到什么状态
-
清晰的看到到达某个状态的路径是怎么样的
2. 解决协作的问题
===========
另一个很大的问题是解决协作问题,主要包括:
-
与测试开人员的协作沟通
-
与 PD 人员的协作沟通
-
与其他前端开发人员的协作沟通
-
与用户的协作沟通
这里就需要引用一个可视化的概念了。「可视化,是利用人眼的感知能力对数据进行交互的可视表达以增强认知的技术」 。
所以很大程度上,可视化可以解决一大部分协作问题。当然,必须要确定把什么进行可视化才是有意义的。
要想可视化,状态工具就需要具备可序列化的能力。这也是 Redux 之类的状态管理工具缺乏的,主要有以下几方面问题:
跳槽是每个人的职业生涯中都要经历的过程,不论你是搜索到的这篇文章还是无意中浏览到的这篇文章,希望你没有白白浪费停留在这里的时间,能给你接下来或者以后的笔试面试带来一些帮助。
也许是互联网未来10年中最好的一年。WINTER IS COMING。但是如果你不真正的自己去尝试尝试,你永远不知道市面上的行情如何。这次找工作下来,我自身感觉市场并没有那么可怕,也拿到了几个大厂的offer。在此进行一个总结,给自己,也希望能帮助到需要的同学。
面试准备
面试准备根据每个人掌握的知识不同,准备的时间也不一样。现在对于前端岗位,以前也许不是很重视算法这块,但是现在很多公司也都会考。建议大家平时有空的时候多刷刷leetcode。算法的准备时间比较长,是一个长期的过程。需要在掌握了大部分前端基础知识的情况下,再有针对性的去复习算法。面试的时候算法能做出来肯定加分,但做不出来也不会一票否决,面试官也会给你提供一些思路。