weixin_39994627
weixin_39994627
2021-01-02 15:11

枸杞的由来和技术栈

由来

在去年 8 月份的时候,我就想做一个项目并把它开源出去,作为一名粉丝向一位长者的高寿诞辰表示祝贺。

去年前端的圈子风起云涌,各种新技术层出不穷,而我当时的工作主要负责的是 NW.js 和 Node.js ,再加上写一点 Vue。于是我就想做一个使用前端新技术的项目让自己不要落伍了。使用这些新技术不应该是浅尝辄止,而应该尽量向工业级和 best pratice 靠拢。由于我有一个失败的开源项目的维护经验(vue-strap),再加上工作也见到了很多随着一些项目越来越复杂,维护成本越来高的情况。我就下定决心以后做开源项目一定要有强制的 Linting,一定要有 Code Review,一定要有高测试覆盖率作为维护的保证。经过一段时间的考研,我选择了如下技术栈:

  • React Native。因为我想写一个 App ,但又不熟悉原生开发,Weex 和 NativeScript 生态远没有成熟,Ionic 又和写前端没太多区别,所以只能选 React Native。
  • TypeScript。当时在学习 Rust ,感受过静态+强类型语言编译成功基本就能跑的好处,对动态语言所谓「灵活」的好处也越来越嗤之以鼻,所以选择了 TypeScript。
  • Redux。跟一些 demo 写了几个 redux 例子不一样,枸杞在一开始的时候就使用了全局 store,除了少数内部自治状态和可能独立开源出去的组件之外,所有组件都没有自己的状态。
  • Redux-saga。用来处理所有的 I/O 和 effects,这样一来组件没有状态也没有逻辑,为实现 100% 测试覆盖率打下了很好的基础。

由于这些技术或者技术的使用方法都是我在日常工作中暂时用不到或者不能用的(必须考虑到团队的接受程度和开发效率),因此我写枸杞除了贺寿之外,实际上就是为了得到些经验踩一些坑,而枸杞选择技术栈和实现的的时候也是以能不能测试/好不好测试为目标。因此这实际上是一个看文章送测试,跑测试送 App 的项目。

React Native

简单来说,我认为 React Native 没有取代原生语言的能力,单靠 React Native 本身想构建一个稍大型的 App 是不现实的。

如果没有原生语言开发者,对于枸杞和系统资源有交互这样的中小型项目,React Native 原生插件社区能提供的帮助也不多。在去年的时候由于当时 React Native 没有操控 Media Center 的插件,我就一直等到了今年才能继续写下去。我现在也还记得之前 react-native-blur 这个插件没法支持 animation,当时半夜两点钟邮件收到有人发了这个 feature 的 PR,我就赶忙爬起来给 commiter 点赞拍马屁,希望 owner 赶紧 review 之后 release。可之后社区又因为 iOS 8 的兼容问题扯皮了两个月,到现在这个功能也没能真正可用。而像音乐播放这个基础功能,目前也没有任何库能够提供 cache。而这些功能如果不熟悉原生开发是完全没办法解决的。

对于 React Native 视图层的社区也并没有好到哪里去。目前带有测试并且仍然在良好维护的库屈指可数。你甚至仍然能找到一个 1k star 但是把临时变量也放在 this.state 里并且直接给 state 赋值的库。好不容易找到一个看起来靠谱的,你又发现它的文档根本没描述清楚它的所有功能(包括 React Native 本身的核心视图库)。所以为了达到你想要的功能你还得先去看看源码追根溯源一番。如果有时间的话,我建议视图库能自己写就自己写,如果实在没时间,ant-designreact-native-community 以及 wix 三个组织产出的代码都靠谱,而且多数仓库都有人在维护。

但我对 React Native 的未来还是看好的。在去年的八月份,单靠 GitHub 和开源的力量写一个枸杞这样简单的 App 都不容易。而在今天,尤其是今年 3 月的 React Conf 之后,整个社区都在蓬勃地发展:现在你可以用 create-react-native-app 省心地创建新项目,可以用 react-navigation 优雅地处理路由,可以用 react-native-interactablelottie-react-native 高效地实现复杂的动画,广受诟病的 ListView 也有了 <VirtualizedList>SectionListFlatList 三个替代品。如果你是在一年前对 React Native 浅尝辄止,那我建议不妨今天再去看看。

最遗憾的事情还是由于音乐版权的原因枸杞并不能上架,所以我也没有经验谈谈 React Native bundler 和 codepush 这两个功能。

TypeScript

我现在是 TypeScript 的脑残粉。

别误会,我还是很喜欢 ES6 和 ESLint 的。尤其是 ESLint,感觉这才是尼古拉斯·赵凯最好的发明,YUI 和 《JavaScript 高级程序设计》 都是次要的。但即便 ES6 + ESLint 的组合能够规避很多 ECMAScript 自身的缺陷,TypeScript 仍然有着不可比拟的优势。

静态类型

假设我们有这么一段代码:

javascript
const sum = (str) => str.match(/\d+/g).reduce((n, acc) => n + Number(acc), 0)

很容易看出程序期望输入一个字符串,然后返回这个字符串所有数字的和。稍有经验的开发者则能看出这个程序至少有两个错误:

  1. 传入的字符串并没有包含任何数字,match 方法会返回 null,而 null 并没有数组的方法,进而报错;
  2. 当调用 sum(str) 方法,传入的参数不是一个字符串也会报错。

对于第一种情况,在 TypeScript 中第一时间写下代码就会直接报错,因为编译器能检查到 match 返回的类型不能都使用 reduce 方法。而对于第二种情况,只要你在代码的任何第一个地方可能传入 string 以外的类型,也会报错。而完成这些检测根本不需要运行代码。

有些同学就会说,切,这有什么了不起的,老子根本不会写这种代码 in the first place 好伐?

可兄弟啊,人是一定会犯错的,而且迟早会犯错。前面的那段代码来自于一个 下载量 20万 的 NPM 包,作者在 GitHub 的 Follower 超过四位数,他没能避免写出这样的代码,你也迟早有一天也会出现这样的纰漏。而对于这种情况,工具和机器能更好解决的问题,就不应该交给人类来解决。

Language as Sever

TypeScript 还极具创新地引入了 Language as Sever 的特性(Rust 最近也把这个功能抄了过去)。什么意思呢?就是只要 tsserver 这个模块启动,就可以享受到来自于 TypeScript 本身提供的智能代码补全和代码重构功能。而这样代码补全和重构是基于类型、定义、模块的精准补全和重构,而不是不知道从哪下载到 snippets。

而这样功能并不依赖于某个特定的平台或 IDE(有人以为这个功能 VS Code 提供的),事实上不管你在任一平台 VS Code 或 Atom,还是在命令行上跑 Spacemacs 或 Vim,都可以得到这样近似于 Java 在 IntelliJ IDEA 的开发体验。可以负责任地说这个功能完全可以把写类型和定义的时间全都给挣回来。

良好的社区和开发计划

只要稍微关注一下 TypeScript 的 issue,released note 就不难发现,由 Anders Hejlsberg 领衔的 TypeScript 是一个相当靠谱的团队。(不得不说微软在工程管理方面真的不知道比某些互联网公司高到哪里去了,no offense)

TypeScript 在最近几个版本更新的 feature 都相当有诚意,例如 Generic defaults,keyof ,更好的 React / React Native 支持,更好的 literal types,同时也支持了更多 ES 新特性,例如 async/await 支持编译到 ES3/5,async iterationObject rest/spread

由于 Contextual Type 的出现,Vue 这样使用 Object literal 的 API 也能享有更好的类型推导和开发体验。听说 TypeScript 还准备实现类似于 template typing 的功能,那 Vue 和 Angular 也可能会得到 JSX 那样良好的类型推导。这样一来三大前端框架在使用 TypeScript 之后都能有效降低维护难度,不得不说实在是喜大普奔的好消息。

最后我还想讲一个很多人没有提到的优势:TypeScript 编译出来的代码(相比 babel)可读性很高,而且性能普遍比 babel 强 1,大部分语法和原生 ES6 代码性能接近,部分语法(例如 for..of rest, spread)比原生 ES6 在没有部署 Turbofan 和 Ignition 的 V8 (Node.js 8.0 / Chrome 60 以下)还快 2。另外 TypeScript 实际上可以用来做 ES6 编译器,只要你不写某些还在 stage-0/1 的语法就行。

不足

有些人会提到,ES 的原型链灵活的一逼,使用 TypeScript 限制了我的发挥。这样的说法也不对,TypeScript 是 JavaScript 的超集,你可以用 class 也可以用原型链,如果两个都熟悉混着用也没有任何关系。

我觉得最大的问题还是目前社区提供的 typings 的数量和质量都不太尽如人意。

如果引用的库没有 typings ,或者 typings 错误地映射了源码,那既得不到强类型语言的严谨,也得不到弱类型语言的灵活。写 TypeSciript 的体验就像是写残疾的 ES6。就拿 React Native 来说,FlatList 功能已经更新了两个月,typings 也还一直有问题。我本来以为对于这样一个热门的项目社区会很快有人 PR ,没想到等了那么久还是得自己动手 PR。

对于广大开发者来说,如果你发现某个库的 typings 不对或者没有,大可给 DefinitelyTyped 发 PR。自己可以看看源码学习学习,别人也能从中获益,这等利人利己的好事不妨多做。

Redux

Redux 应该是前端争议最大的一个项目之一。很多批评者认为 boilerplate 代码要写太多,概念也太多,搞来搞去也没发现能提供什么好处。我觉得 Redux 的作者 Dan Abramov 说过一句话很适合回答这类言论:

Redux 就像雨伞,当你需要的时候你就知道为什么要使用它。

而有一些项目本身交互就简单,数据流动一眼也就能望得到边。其实很可能连 React 都不需要,如果再加一个 Redux,这就有点像穿着一身蓑衣撑着一把巨伞在 CBD 的商场里逛街。——除了让人以为你在 cosplay 武林高手之外没有任何好处。

而当你真正需要 Redux 的时候,有些教程会告诉 Redux 有共享组件状态、组件间通信,统一记录/管理 action,time travel/undo/redo 等等功能。不过那些都是次要的,Redux 最重要的优点是可预测性,对于既定输入一定会产生既定输出。

可预测性有什么优点呢?最直观的优点就是易于测试,如果没有 I/O 当然最好,如果有的话需要加一层 middleware 处理写 mock 也不难;其次是数据的流动会非常清晰,这点搞一个 redux-devtool 就可以看出来;第三点是你可以先写数据结构和业务逻辑再写 UI,这听起来有点不可思议,没有 UI 和设计图怎么搞前端?

有兴趣的朋友可以看一下枸杞的 commit 记录,所有页面全都是先写数据结构和逻辑再写 UI。例如做一个播放器,点击了「下一首」这个按钮一定会发生这些事情:一定会更改播放状态;一定会请求服务器真实地址;一定会 push 一个播放记录等等... 这些事情跟把「下一首」的按钮放在哪里长什么样没有任何关系。

试想一下这个场景:你的产品经理和设计师就一个功能是放在 modal 好还是页面好,是点击触发好还是滑动触发好展开了激烈的撕逼,这个时候你已经把逻辑和测试都写好了。你可以大大方方地和他们说「你们先聊,我先走了」,等你把 PS4 都打累了,他们的撕逼也有了结果,这个时候你只需要写 dumb component,把数据和方法 connect 到 React 中就完事了。

Redux-saga

Redux 本身并没有提供处理异步的解决方案,所以需要引入一个 middleware。流行的解决方案有 redux-thunkredux-promise ,除此之外还有一些不知名的解决方案,我推荐国产的 redux-action-tools ,还可以省下一个 redux-actions 的依赖。不过这些实现都有问题:

  1. 会导致把 action 写得五花八门什么都有,破坏了 action 的语义;
  2. 不好测试,首先 I/O 必须写 mock,而对于 React Native I/O 不止是 Ajax,有些 mock 不好写,其次逻辑一旦复杂一个 action 要开好几个测试不同的 branch。

而 Redux-saga 则很好地解决了这些问题,它采用了 generator 启动了一个 long-lived transaction,然后把所有 action(不管异步还是同步)都隔离到各个 worker 上去。这意味着什么?

  • 逻辑都放在 worker 里,我们所有 action 都会是一个 Flux Standard Action ,简洁、清晰、明了;
  • 每一个 worker 是 generator ,所以我们可以像 async/await 那样写异步代码(准确来说是更像早期的 co)。但是比 async/awaitco 都更棒的是 redux-saga 为我们提供了很多专门用来处理 effect 的函数,让我们可以用声明式的语法来处理各种交互。
  • 由于 generator 其实就是迭代器的生成器,也就是说我们可以一步一步地测试 worker,校验每一个迭代,也可以测试完了这两步迭代,再倒回三步,重新测试另外的 branch。

例如我们有一个简单的登录 worker:

javascript
function* loginFlow() {
    const { username, password } = yield take(ATTEMPT_LOGIN)
  const { code, profile } = yield call(fetchLogin(username, password))
  if (code === 200) {
    yield put(updateProfile(profile))
    yield fork(Router.toHome)
  } else {
    yield put(toast('error', '登录失败'))
  }
}

我们可以这样写这样单元测试:

javascript
test('login flow', () => {
  const LOGIN_SUCCEED = { code: 200 }
  const LOGIN_FAILED = { code: 400 }
  const WALLACE = { username: 'Wallace', password: 'veryTall' }
  testSaga(loginFlow).next()
    .take(ATTEMPT_LOGIN)
    .next(WALLACE)
    .call(fetchLogin, 'Wallace', 'veryTall')
    .next(LOGIN_SUCCEED)
    .put(updateProfile(undefined))
    .next()
    .fork(Router.toHome)
    .next()
    .back(3) // 测试完成功登录之后,我们退 3 步测试登录失败的 branch
    .call(fetchLogin, 'Wallace', 'veryTall')
    .next(LOGIN_FAILED)
    .put(toast('error', '登录失败'))
    .next()
    .isDone()
})

就这样 statements 和 branch 的测试覆盖率都达到了 100%,简直不要太无脑。

最后我想谈一下 redux-saga 的作者 Yassine Elouafi:一个 40岁的中年大叔,房地产公司老板(你没看错,搞房地产的),自学编程之后为我们贡献了 redux-saga ,在这之后大叔也并没有停止奋斗。他还在多个 repo (包括 MobX ,Rxjs)的 issue 下都有鞭辟入里的发言 45,把 Redux 、Rxjs、MobX 的优劣势、适用场景说得很清楚。大叔的本可以去 Medium 发几篇文章吹吹牛逼,但他却选择了在各个 issue 下耐心帮人答疑解惑,而且不装逼不打广告不争名不夺利,实在佩服。

测试

在这篇文章里我谈过很多次测试的重要性,那测试能带来什么好处呢?

  1. 首先写测试的时候必须重新 review 源码,必要时可能还会重构源码,这就让源码变得更可靠、更健壮;
  2. 重构代码时能得到更多保障;
  3. 多人协作时能避免同事把你的代码改没用;
  4. 定位 bug 时更迅速,更自动化。再说一次,人是一定会犯错,一定会偷懒的,机器和工具能做得更好的事情就不要交给人类去做;
  5. 不写测试的公司或团队通常加班更凶。

而对于前端(或者 Node.js) 测试框架而言,现在也是百花齐放,我简单说几个:

Jest

Jest 是枸杞使用的测试框架,也是目前最热门的,它提供了很多好处:

  • Facebook 官方维护;
  • 和 React 搭配合作完美,还提供 snapshot 测试功能;
  • 几乎不用写配置文件就能支持 ES6, mock, async test等功能;
  • 跑得快;

打开 Twitter 和 Medium 也能发现到处都是安利 Jest 的声音,看起来 Jest 几乎是完美的,在我最初使用的时候也是这么个想法,直到我发现 Jest 会创建 VM 跑测试,而在 VM A 中一个数组,并不是 VM B 中 global.Array 的 instance。也就是说如下代码会返回 false : vm.runInNewContext("a instanceof Array", {a:[]}) 6。这样的情况也不能说是 bug,但会导致很多库在使用 instanceOf Array 作为判断数组方法时失效,这样就大大地增加了排查 bug 的难度。

Ava

Ava 是在 Jest 之前最受推崇的测试框架。之前枸杞用来测试网易云音乐的 API。它的优点其实和 Jest 有点像,快,不怎么用写配置文件或者加插件。但它的快是通过并发来实现的,这就导致测试流程的时候可能会把 beforeafter 写得到处都是,另外如果同时测试的文件过多,而机器本身的性能不够,也可能会影响到被测试程序本身。我觉得 Ava 还是比较适合测试一些没有什么时序或者规模较小的项目。

简单用了一下两个新晋热门测试框架之后,结论还是 mocha 最稳定 7 8。不过在测试 React 相关的项目的之后我建议还是选择 Jest。

总结

感觉就说了好多废话,最后再感慨一下。

这几年前端社区真的有了翻天覆地的变化,我现在都有点想不起来被 jQuery 支配的那些日子了。多亏了PL/编译器社区的 Anders Hejlsberg,函数式编程社区的 Evan Czaplicki,还有房地产社区😄的 Yassine Elouafi ,我们前端终于有了新玩法。而对于这么多新技术,我们应该如何应对呢?

一位长者早已告诉过我答案:

这既是我的工作,也是我个人兴趣所在。现代科学发展得这么快,我们必须加强学习以跟上形势。 ——《他改变了中国》,p210,罗伯特·库恩。

References:

[1] ES6 polyfill vs. feature performance tests

[2] V8, Advanced JavaScript, & the Next Performance Frontier (Google I/O '17)

[3]入坑React前没有人会告诉你的事

[4] how to throttle and then watchLatest, can we compose high-level helpers? · Issue #105 · redux-saga/redux-saga · GitHub

[5] Understanding MobX and when to use it. · Issue #199 · mobxjs/mobx · GitHub

[6] Jest globals differ from Node globals · Issue #2549 · facebook/jest · GitHub

[7] Switch to a new testing system · Issue #703 · koajs/koa · GitHub

[8] 单元测试 :Egg.js - 为企业级框架和应用而生

该提问来源于开源项目:yuche/gouqi

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

24条回答

  • weixin_39729272 weixin_39729272 3月前

    这总结写得好

    点赞 评论 复制链接分享
  • weixin_39586649 weixin_39586649 3月前

    总结赞👍

    点赞 评论 复制链接分享
  • weixin_39842955 weixin_39842955 3月前

    苟.......全性命于乱世

    点赞 评论 复制链接分享
  • weixin_39695701 weixin_39695701 3月前

    🤓受安利了,准备换TS,先写状态和逻辑再写视图不要太爽啊,可惜我这没条件,连测试都没法做。

    点赞 评论 复制链接分享
  • weixin_39989949 weixin_39989949 3月前

    写的非常好, 目前项目在使用类似redux-action-tools的异步框架, 一直很想迁移到redux-saga上面来,但是generator有两个非常头疼的问题不知道你是怎么解决的。 1. ts对generator的支持不太好 https://github.com/Microsoft/TypeScript/issues/2983 ,很多情况下redux-saga都是返回的any类型,导致ts强类型的优势完全体现不出来。尽管redux-saga很赞,但是如果这个问题解决不好,切换到redux-saga会非常痛苦。

    1. generator很难调试,source-map经常错位,调试起来也很痛苦。

    这两点导致我一直对使用generator的框架保持观望的态度。如果这些问题解决不了(特别是第一点),可能也不会用redux-saga了吧。

    点赞 评论 复制链接分享
  • weixin_39994627 weixin_39994627 3月前

    这两个问题我也没找到没什么很好的办法。我之前在使用 redux-saga 的时候它的 typings 还是有问题的,有时候甚至得用 as 强行指定类型。如果你很在意 TS 的类型系统,倒是可以切换到 Rx.js + redux-observable,Rx 本身就是 TS 写的,另外它的异步处理应该是目前 JS 技术栈最强大的。

    点赞 评论 复制链接分享
  • weixin_39579468 weixin_39579468 3月前

    作者主动提issue,提的最好的,没有之一了吧

    star下来慢慢学习

    点赞 评论 复制链接分享
  • weixin_39949297 weixin_39949297 3月前

    666

    点赞 评论 复制链接分享
  • weixin_39961636 weixin_39961636 3月前

    🐶🌰🇨🇳🏠!

    点赞 评论 复制链接分享
  • weixin_39789979 weixin_39789979 3月前

    楼猪注意身体,你需要的不是枸杞是玛卡

    点赞 评论 复制链接分享
  • weixin_39609752 weixin_39609752 3月前

    而有一些项目本身交互就简单,数据流动一眼也就能望得到边。其实很可能连 React 都不需要,如果再加一个 Redux,这就有点像穿着一身蓑衣撑着一把巨伞在 CBD 的商场里逛街。——除了让人以为你在 cosplay 武林高手之外没有任何好处。

    哈哈哈哈太有画面感了

    点赞 评论 复制链接分享
  • weixin_39983223 weixin_39983223 3月前

    澈哥厉害啊

    点赞 评论 复制链接分享
  • weixin_39800112 weixin_39800112 3月前

    尼古拉斯·赵凯

    image

    点赞 评论 复制链接分享
  • weixin_39864261 weixin_39864261 3月前

    厉害了!

    点赞 评论 复制链接分享
  • weixin_39630771 weixin_39630771 3月前

    必须点赞,哈哈,看了标题进来的,想问问,单元测试是放在较稳定版本阶段,还是实时跟进呢?

    点赞 评论 复制链接分享
  • weixin_39557402 weixin_39557402 3月前

    :joy: 厉害了,想问一下API做了多久?

    点赞 评论 复制链接分享
  • weixin_39994627 weixin_39994627 3月前

    如果是 UI 的话我觉得应该稳定后写(用 Jest 的 snapshot 写现在还挺方便)或者不写(假设你用 Vue 的话写 UI 测试就很难,可以等些日子官方在做一个 test utils)。如果是逻辑和数据部分已经能确定的代码写完马上跟进测试比较好,甚至先写测试再写代码。

    这部分我是先写测试再写代码。当时因为 react native 没有 Node.js 的 crypto 包我弄了蛮久。(你可以看到 package.json,tough-cookie 部分也是我自己修改的版本)具体时间我也不记得了,你可以查一下最早的 commit 记录。

    点赞 评论 复制链接分享
  • weixin_39650139 weixin_39650139 3月前

    不过有些 UI 的内容可能在多个页面之间相互转换,先写逻辑也不能写太多

    点赞 评论 复制链接分享
  • weixin_39834745 weixin_39834745 3月前

    学习了

    点赞 评论 复制链接分享
  • weixin_39885383 weixin_39885383 3月前

    点赞 评论 复制链接分享
  • weixin_39950812 weixin_39950812 3月前

    文章写得很好

    点赞 评论 复制链接分享
  • weixin_39553753 weixin_39553753 3月前

    厉害了 佩服

    点赞 评论 复制链接分享
  • weixin_39562554 weixin_39562554 3月前

    👍

    点赞 评论 复制链接分享
  • weixin_39957951 weixin_39957951 3月前

    写的挺好的,都想学TypeScript了,项目一大没有类型的约束还是很难定位问题。

    点赞 评论 复制链接分享