阅读 6014

2020 年,我是如何做到技术再次突飞猛进的 | 掘金年度征文

我是一名大前端架构师,今年过完,我就是一个在互联网行业沉浮八年的老兵了。

我的成长历程,几乎贯穿了前端行业的一半历程。

我亲身体会过IE6 彻底衰败

html5 快速崛起

jQuery 淡出视线

React 一路高歌

Angular 高开低走

Vue 后来居上

我见证了前端工程化从 0 到 1 的兴盛。

很幸运的是,经历了那么多,我依然还没有停止前进的脚步。

2020 这一年,我取得了很大的进步。技术能力用突飞猛进来形容也不为过。

const aspirant = 'persistence'
复制代码

一、在大型项目中,对封装与解耦有了更深刻的体会

只有在大型项目中,我们才会有意识的把可维护性的重视程度提高到最高级别。

可维护性:在需求调整时,尽量不改底层代码,即使修改也能让风险在可预判的范围之内。而这里的底层代码,通常就是指封装好的代码,不会轻易变动。

而当我们把可维护性作为代码质量的判断标准时,我们自己肯定会有一个长足的进步。

大型项目代码庞大复杂,任何一处改动,它对项目造成的影响,很有可能超出我们的认知范围。

可怕的是,改动一处逻辑,会引发新的问题,而新的问题,我们自己极有可能意识不到。

因此每一处改动的风险很大。因为风险大,验证成本也会特别高。

当变动过大,甚至会造成项目重构。

我们整个团队,遭受了大量的重构毒打。

为了在重构中降低开发成本,减少 bug 率,我在如何封装与解耦上做了大量的思考与总结。

封装是一个基础概念。在初学的时候,就知道应该把逻辑封装起来,以方便使用。

而要在各种场景下都找到合适的封装方式,却绝非易事。

特别是 JavaScript 非常灵活,解决同样的问题,有许多不同的方案可供选择。但并不是所有的方案,就具备高可维护性。

在遵循可维护性的思维指导下,我发现了很多以前的解决方案跟我现在的认知是恰好相反的。

这也让我对许多设计模式的运用有了更深刻的认知。

设计模式,说到底,就是为了解决可维护性的问题。

例如我们的小程序非常复杂。特别是入口场景非常复杂。

不同的场景目的不一样,进入小程序时传入的参数也会不一样,对应的逻辑也会不同。虽然从大的方向上来说,都是登陆逻辑。但是登陆之后,要干的事情却差异越来越大。

在常规的思维中,登陆逻辑是所有入口场景共同的逻辑,那么登陆之后需要处理的事情虽然有一些差异,但是封装在一起,使用条件判断区分开,影响不大,也不需要在每个页面去单独思考这个问题。

// 场景一
if (params.router) {

}

// 场景二
if (params.shopId) {

}
复制代码

但是后来慢慢发现,在需求扩展时,条件判断用于区分场景的方式非常容易影响已经存在的场景的逻辑。

例如场景三种,也能传入 shopId,但是它与场景二还是有一些差别,那此时风险就来了。我必须在场景二中去处理场景三。

// 场景一
if (params.router) {

}

// 场景二
if (params.shopId) {
    // toto
    if (params.sence == '1212') {}
}
复制代码

此时尽管场景二与场景三大多数逻辑都类似,但是有一点小差别。但是我代码的扩展已经影响到了场景二,我代码的修改到底会不会导致场景二出问题,我不确定。

因此,常规的统一处理逻辑,在这里就变成了一种风险很大的方案。

将不同的场景拆分重组,才是良策。即使他们相似程度很高。

虽然会导致代码量增加,但是风险几乎没有,不对已有的场景造成任何影响,才是最终目的。

二、在大型项目中,探索出来了完全摒弃 redux、mobx 的方案

自从 19 年初见识了 React hooks 的魅力之后,我一直在研究如何在项目中,完全不使用 redux、mobx 这类的全局状态管理器。

全局状态管理并非最好的方案。

特别是在大型项目中,我们将所有的页面状态,都放在全局的 store 进行管理的时候。项目占用的内存会在项目使用过程中持续升高。因为全局 store 中的数据并不会释放。

而且 redux 的代码复杂度问题,在 hooks 出来之后,就显得尤为突出。

因此,探索更简洁的代码实现,是我这两年一直在做的事情。我希望能够完全摒弃全局状态管理工具,或者只是轻量的使用。仅仅只用于解决一些全局的需求,例如皮肤修改,缓存登陆信息等。

以其中一个后台管理系统为例。

最开始的版本是使用 antd pro 推荐的方案,dva 来做状态管理。

分析之后发现,真正需要全局共享的状态 在 antd pro 中几乎没有。

具体的页面中,也只是部分页面会共享状态。大多数都是,列表页与详情页这种关系,第一步第二步这样的关系。

于是我意识到,完全可以舍弃 dva。

并且我在去年基于 context 搞出了一个极大减少代码量的状态管理工具 mozz 用于解决局部状态共享的问题。

与 context 相比,它最大的优势是理解更简单,并具备完整的 ts 类型推导。

const App = () => {
  const { counter, decrease, increase } = useStore();
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <a className="App-link" href="/" rel="noopener noreferrer">
          Learn moz
        </a>

        <div>
          <p>{counter}</p>
          <button onClick={increase}>+</button>
          <button onClick={decrease}>-</button>
        </div>
        <Comp />
      </header>
    </div>
  );
};
复制代码

能够具备完整的类型推导是它最具吸引力的地方,这样我们就可以不用专门针对自定义的 hooks 写一堆类型声明,感觉就像是没有使用 ts,但是依然具备了 ts 的特性。

后来我在公众号,写了一系列文章来专门介绍这个事情。

当然,它还有一部分遗留问题没有解决。比如不同的路由页面之间的状态共享问题。

于是针对这些问题我做了很多尝试。

例如,轻度依赖通过路由改变页面。页面的变化自己控制状态来实现。

例如,通过 localStorage 解决一部分共享的问题。

例如,借助发布订阅模式,监听部分页面的状态。

例如,将 hooks 与发布订阅模式结合起来封装自定义 hooks,让监听在使用时处于无感状态。

// 一个简化版封装
export default function useLoginEffect(cb: () => any, options: LoginOptions = {}) {
  const [refresh, setReresh] = useState(true)
  useEffect(() => {
    if (!refresh) {
      return
    }

    let isLaunch = false;
    const curPageStack = Taro.getCurrentPages()
    const launchPageInfo = Taro.getLaunchOptionsSync()
    if (curPageStack.length === 1 && launchPageInfo.path == curPageStack[0].route) {
      isLaunch = true
    }
    if (!isLaunch) {
      setReresh(false)
      cb()
      return
    }
    if (isLaunch) {
      const info = Taro.getStorageSync('userinfo')
      if (!info || !info._id) {
        // 登陆返回之后,执行一次回调,因此在这里监听,在登陆成功之后需要出发该监听
        Taro.eventCenter.on(`${launchPageInfo.path}/login`, cb)
        Taro.navigateTo({url: '/pages/login/index'})
        return
      }
      setReresh(false)
      cb()
    }
  }, [refresh])

  return {refresh, setReresh}
}
复制代码

他们都非常有效果。

三、学会了原子操作概念,并运用于项目实践

原子操作是在群里跟大家讨论时学到的新概念。

意思为:不可分割的一系列操作。

要么都做,要么都不做。不存在操作一部分的情况。

虽然平时在实践中,我们都在大量的解决类似的问题。但是一直没有将这样的现象提炼为一个概念。

当然从产品逻辑的角度来说,原子操作有非常多可以扩展的案例。我们面临的工业软件,有大量的这种需求的场景,一旦中断就会造成处理成本成倍的提高。

如果仅前端的角度来说,通常就是复杂表单的处理。

当表单非常复杂的时候,有的人就希望能够存一个草稿,用于下一次继续操作。

甚至有的时候还希望每选择一项,都能够往后端存储一项。

还希望能够复制上一次的填写结果,能够节省大量的编辑时间。

在原子操作的思维指导下,要满足这些需求,前端缓存是工作量最小的解决方案。能够极大的减少数据库设计的复杂度。

最关键的是,有专业术语做支撑,在方案选择的沟通会议中,具备极强的说服力。

这是我通过原子操作get到的最重要的收获。

四、尝试将一些前端概念,运用于数据库设计

可以通过一个案例跟大家分享。

我们在做数据报表统计时,有很多难题。

例如,我要统计上个月公司有多少人是销售顾问。并且还查看明细。

在常规的数据库设计中,我们要查询当前公司有多少名销售顾问,很简单。

但是要知道一个小时以前有多少名销售顾问,就很难。知道一个月之前的,就更难,误差更大。

原因就是因为实际情况的人事变动会导致结果出现变化。

因此数据报表的常规方案就是抽取数据。每一天留一个备份,或者每一个月留一个备份。

备份结果很简单,但是要备份明细,消耗就很大。

因此我灵机一动,结合前端的一个重要概念,不可变数据,来尝试解决这个难题。

不可变数据是什么意思呢。

例如我有一个数据如下

var state = {
  list: [],
  loading: true
}
复制代码

我想要修改该数据中的 loading 为 false。常规的思路很简单

state.loading = false
复制代码

但是不可变数据的思路是怎么做的呢

// 第一步,基于原有对象,创建一个新对象
var newState = {
  list: state.list,
  loading: false
}

// 第二步,让 state 等于新对象
state = newState
复制代码

所以,不可变数据的指导思想是什么呢,就是原有的数据对象,不能被改变,如果想要得到新的结果,就必须创建一份新的数据。

这是在前端应用很广泛的一个概念。

而这个思路,刚好可以解决我们上面提到的问题。

人事变动时,我不修改之前的数据,而是生成一条新的数据。

人员职位状态创建时间改变时间
张三销售顾问false2018.10.11 10:00:002019.10.11 10:00:00
张三销售组长false2019.10.11 10:00:002020.10.11 10:00:00
张三店长true2018.10.11 10:00:00
李四销售顾问true2018.10.11 10:00:00

在新的数据生成时,只需要把之前的数据状态,调整成 false,这样,我们就可以知道张三当前的职位是什么。

同时,也能够知道张三的历史职位有哪些。

于是,当我们想要统计过去某个时间里,公司有多少销售顾问时,就变得非常简单。这样的方案,比抽取数据更优秀,适应性更强。

当然,这只是一个思路指导,实践场景的数据远没有这么简单。灵活运用可以简化大量工作。

五、解决问题的能力得到更大的提升

通过上面几个总结,大家也能感受到,这一年里我只是一个探索的角色。

我一直在试图寻找更简单的方案解决实际问题。

这也符合是我对架构师的定义:做好基础服务,让其他同事的开发成本更低,开发体验更好,出错的概率更低。

在这个过程中,我也面临了更多新的问题。

而促使我解决问题的能力得到提升的一个关键原因,是因为我已经熟练的掌握了产品经理的思维去看待问题。

这一年里,我深度参与到产品的设计与讨论中,大量的时间都在思考产品的必要性与合理性。

也深刻的理解了工业软件的复杂度。

对线上与线下的大趋势,有了更清晰的认知。

这也促使我看待问题的角度发生了一些变化。

当我拉通UI、前端、后端、数据库设计、产品思维贯通全局来思考自己的项目时,往往会产生新的理解。

解决问题的能力也有了很大的提高。

六、帮助其他很多人取得了进步

帮助他人成长,是自我成长的重要手段。

因为公众号的原因,我维护了两个技术群。

我会不定期的组织免费的公开课。

卡颂卡总的视频课「自顶向下学习 React 源码」就是以我们的一次免费公开课为契机推出的高质量源码课程。

在群里跟大家聊天,帮助大家解决问题。往往会让我对同样的问题产生新的理解。

对于同样的问题,这会促使我去思考如何表达才更加通俗易懂。

在这个思考过程中,很容易让自己对某一个知识点的理解,变得更深刻。

例如,我现在已经能够非常简单的让不懂闭包的同学懂得闭包是怎么回事。很少有人能做到这一点。

当然,我也有机会接触到别人的思考过程,例如原子操作,都是在群里的沟通里学习到的。这也能让我得到极大的提高。

七、输出了一套质量过硬的 JavaScript 进阶课程的文章

几年前我曾经出过一本书,「JavaScript 核心开发解密」。

已经卖完了,也没准备再印刷,大家不用去关注

因为对很多知识和概念,有了更深刻的理解,因此决定在此基础上,结合自己这几年的沉淀,重构一版。

重构的过程也是一个不断思考的过程。

思考如何帮助阅读者能够更加容易的掌握我想要传递的知识。

思考如何在结合书中的知识传递一种学习方法。

思考如何帮助阅读者明白知识体系的重要性,以及如何构建前端知识体系。

在这样一个反复的思考过程中,我自己又有了新的体会和提高。

输出:也是成长的重要手段。

所以很多大厂的招聘信息中,都会写明一条:有写博客习惯的优先考虑。

如果不出意外,未来的某一天这一套文章会以小册的形式与大家见面。

计划一共有72篇文章,目前已经完成了50篇。

八、2021 年目标

1. 学习 Vue 3.0

虽然没有深入学习过,但是通过简单的了解,我觉得 Vue 3.0 应该也是一套颠覆性比较强的解决方案。加上工作中的技术栈更多的是对 React 的研究。而对于 Vue 的掌握还比较片面。

因此希望明年能够从 0 开始重新学习 Vue 3.0。扩充自己的技术栈。

2. 输出一套最好的 React 学习教程

很多人想要学习如何使用 React,但是往往只能学到皮毛,无法将 React 深入运用到项目中。我之前也写了很多关于 React 的文章,但是都零散不成体系。对于基础薄弱的同学帮助有限。

因此明年会写一套 React 学习教程,也算是对自己学习的一个总结。

除了基础理论,还包括有一个完整的实践项目。目前项目已经准备好,就是基于 Taro + React + typescript 开发的小程序「码易」,大家可以搜索简单体验。

与此同时,这套书已经与清华大学出版社签了合同。最迟明年八月可以完成。

初步计划的简单目录

3. 锻炼身体,重视健康,重视睡眠

懂的都懂。爱惜身体健康才是王道。


最后,天赋异禀的人不可怕,一直在持续进步的人才可怕。

愿我们都能成为持续进步的人。与大家共勉。

我的公众号:不知非攻,期待您的关注。

掘金年度征文 | 2020 与我的技术之路 征文活动正在进行中......