蚂蚁雄兵的年代,人人皆可为王。

如今早已是全民联网年代,可以发现自己的父母大姑大姨等亲戚甚至爷爷奶奶,只要他们在使用千元智能机了,无论是看头条还是微信还是抖音,都在参与整个互联网的流量和内容的产生,他/她们通过手机消费了自己的时间,同时为所有的产品提供了更多人物画像的行为数据,无论是上翻下翻还是留言点赞,每一个 APP 都是一个独立的平台,而在这两年,冉冉升起的区别于 APP 的新应用平台,非小程序莫属:

框架设计:小程序框架与组件化如何选型定制 - 图1

上图是从阿拉丁公布的数据报告中截取引用过来的,整个 2018 年,基于小程序生态的融资规模是 80 亿,是 2017 年的整整 8 倍,小程序几乎扎根了所有领域的所有层面,而且除了微信小程序,其他小程序小应用也都在布局:

框架设计:小程序框架与组件化如何选型定制 - 图2

在整个泛小程序生态崛起的过程中,许多创业公司都主动登船,小菜也不例外,我们是在 2018 年 3 月份开始技术预研和拥抱小程序生态,至此 1 年我们业务上收获颇丰,而技术这里,也略有积淀,就跟大家分享小程序这块我们的思考和沉淀。

用原生还是用框架

小菜前端的个别同学,包括我都有过一些开发小程序的经验,没使用任何框架就用小程序原生语法来实现,但小程序的功能都比较简单,所以也自然没遇到太大的工程挑战,而我们 2018 年要启动的小程序产品具有两个特征:第一个是功能和交互足够的复杂,第二是迭代速度要非常快,两周就要首版上线,之后每周至少发一到两个版本,基于这样的业务背景,我们也就毅然的选择使用框架而非原生,但这个决策带来的后期风险也的确超出了我们的预期。

如果我们再回到一年前做选型,估计还是会选择框架,但至少不会那么乐观的 All in 其中,而是择时机尽早切回原生(虽然我们目前已是半原生)。所以给大家的建议是:如果产品功能相对简单,一二十个页面也没有太奇怪的交互和太大的列表数据量,用框架用原生都可以,开发周期短的话就用框架,实际上市面上看到的绝大多数小程序都不属于是复杂应用,用框架都能很好很快的 hold 住,但反过来假如你要实现的产品交互和前端列表数据量较大,图片图表视频多媒体复杂搜索应有尽有,那么能用原生就用原生。

如何选择框架

框架的核心价值就是效率,一旦我们决定使用框架,那么就要把当下及未来市面上可用的小程序框架,做一个必要的比较和选择,这个过程通常会比较纠结。因为截止到 2019 年,还并没有一个小程序框架足够足够的好,大家都在六七十分上下,那我们可以从哪些方面来考虑呢?

小菜前端最初选型框架主要考虑如下几个方面(都很必要但优先级程度自上向下依次降低):

  • 框架的成熟程度与开发效率 - 是否满足产品迭代需求
  • 多端(H5/小程序)的兼容程度 - 是否满足产品的端覆盖需求
  • 配置成本/易用性/拓展性 - 是否满足工程师的工程架构需求
  • 整体性能的表现 - 是否能顶得住复杂交互和大数据列表
  • 团队成员的学习成本 - 是否有必要的人员和技术栈储备
  • 框架背后的团队实力 - 是否有足够好的工程实践和开发支持
  • 框架在社区的沉淀和生态 - 是否有群众基础和社区方案
  • 框架是否是 KPI 产品 - 是否会慢慢 Bug 不修弃坑跑路

总体做选型的路子是优先保产品,再看工程质量及合理性,最后看社区及开发者生态健康程度。

很可惜,即便我们考虑了这么多,限于当时(2018 年 3~5 月)的小程序框架生态太过早期也太单薄,我们最终选择了美团的 MPVue(实际上没得选),但上面的参考项今天看来对大家依然是通用的,当时的比对过程我再给大家呈现下,帮大家加深下这个选型运用的过程:

首先针对能否优先保产品,也就是框架成熟程度和开发效率,多端兼容程度这块,我们实际上只有 wepy,MPVue,Taro 可选,针对他们三个,我们是这样比对的:

  • 社区生态是否足够活跃
  • 跨端迁移成本
  • 框架核心团队是否背靠大厂,是否是 KPI 产物
  • 开发效率问题,更多是踩坑时间问题

其次,针对工程质量与合理性,也就是框架的配置成本/易用性/拓展性,整体性能这块,我们是这样评估的:

  • 框架上手成本
  • 底层工程架构基建是否支持到位
  • 性能问题: 先上再说,过得去就好,剩下的就慢慢优化

最后,针对社区/开发者生态,也就是团队成员既有技术栈、社区反馈和生态这块,我们是这样评估的:

  • 团队成员中是否有人具备相应的技术栈能力
  • 框架是否支持已有的库快速接入
  • 社区对于该框架的反应如何,是否还属于半成品或者存在 Bug 很多的问题

如何做组件化

小菜最初的产品端载体主要是 APP 和 PC Web,尤其是 APP,有 7 个之多,所以 RN 的组件化在 2018 年是有过两个大版本的累计了好几十个,但这个对于小程序来讲远水解不了近渴,而小程序的页面数目越来越多,组件复用变成了刚需,所以我们开始了小程序的组件建设之旅。

基于 MPVue 在小程序里写组件,也是比较神伤。因为小程序端代码是静态的(即提前编译好的模版), 所以像 HOC 就没办法用了(还有很多其他一些 Vue 语法糖编译不了), 我们这里用的都是 mixins,基础的 mixins 有很多:

  • 函数调用相关(去抖/节流)
  • 列表加载相关
  • 用户基础信息/微信信息
  • 数据初始化相关
  • 分享调用相关
  • 页面下拉/滚动相关
  • 收集 Form Id
  • 授权处理相关

基于 mixins 就可以来设计组件了,一旦决定要抽象出一个组件,我们主要考虑如下因素:

  • UI/功能的耦合程度
  • 内外部调用的耦合程度
  • 组合组件(组件间的通信层)
  • 组件性质(基础/业务)
  • 兼容问题
  • 后期扩展能力

我们已经沉淀的组件有:

  • 上传组件(Upload)
  • Tabbar
  • Spinner
  • Radio/Radio Group
  • Video
  • Cell
  • Input/Textarea
  • SearchBar
  • 授权/登录弹窗 组件
  • 保存/预览图片 组件
  • 图片懒加载/占位 组件
  • 语音录制/播放 组件(Record-bubble/Record-button)
  • 页面异常状态组件 (102/404)
  • 其他业务组件……

这些组件里面:

比如 Input/Textarea 组件,它需要重点考虑 UI/功能的耦合,要如何设计?

  • 组件复杂度高的话,可以切分数据服务层与 UI 层,采用 Mixin 混合抽象方法到 UI 层里。

比如授权/登录弹窗组件,它需要重点考虑内外部调用的耦合,要如何设计?

  • 组件需要具备单一职责,不能图方便把很多东西都耦合进去
  • 如何和其他组件配合使用

比如 Upload Group 组件,它属于是组合组件,要如何设计?

  • 上传发生异常的处理与上抛。
  • 组合层之间调用的 Hook。
  • 注意 Props 的透传与值的双向绑定。

比如语音录制/播放组件,它需要组件性质,要如何设计?

  • 业务级别的组件,在复杂度高的情况下,也可以考虑切分 UI/数据服务层。
  • 如果通信层较复杂的话,可以考虑类似 redux 的设计,下层组件 dispatch action 到上层,上层统一管理 action 并 分发数据/事件 给下层组件。

除此以外,组件设计的时候还要兼顾小程序端上与 Vue 的差异性。比如生命周期(组件与页面),在 MPVue 编译完后, Vue 组件生命周期并不会编译成小程序组件的生命周期,说白了就是需要你熟悉两套生命周期: Vue 组件生命周期与小程序组件/页面生命周期。

  1. MPVue 编译完的 Vue 组件生命周期并不会和 小程序组件生命周期的调用一致,比如第二次点开同一个页面,非页面级别的组件 mounted 永远只会调用一次(除非你在组件或者组件上层挂了 v-if ),而实际上这个组件在小程序里被调用了两次 attached 小程序组件生命周期。
  2. 更不用说 beforeMount/onUnload 的问题了。
  3. 页面数据缓存问题,小程序本身会缓存打开过的页面数据,常见场景就是商品详情页面,第二次打开的时候,在进行页面数据更新之前,页面并不是空状态,而是上一次详情的数据。处理的话就有很多种方法了,可以在 beforeMount/onUnload/其他生命周期 里初始化页面数据及置空所有状态,具体就看你怎么设计和抽象了,最主要的目的还是让开发者尽可能少的感知/手动调用。

除了要考虑以上这些问题之外,我们还需要去解决端上的兼容问题以及框架带来的问题。

微信端的兼容问题简直是层出不穷,不仅和机型、系统版本有关,甚至还和微信版本有关。所以当遇到这一类问题的时候,我们只能求助于 微信开发者社区,期待有相应的解决方案。

框架带来的问题有时候就有点恶心了。比如说 MPVue 并不支持动态的传入 inputtype 属性,这就导致如果我们需要文本键盘、数字键盘、带小数点数字键盘等等这一类的 input 时,我们需要每种类型都写一个 input 组件,平白增加了代码量,并且定位到问题也不方便。

逃不过的原生能力

以上我们探讨了框架选型和组件化设计,最后再回归到我们认为非常核心的一个能力,就是小程序的原生开发和优化能力,MPVue 编译完的代码运行时的性能在很多场景里并不达到你的要求,比如输入组件的双向绑定之光标闪烁/内容回退,,大数据量之操作延迟等等…… (没遇到过的同学自行搜索)。总的来说就是 MPVue 底层对于数据的操作实现的很稀烂,这个问题也只有当我们将项目做大了才遇到。

当遇到类似上面这种性能问题的时候,就避免不了去寻找解决方案。但所谓的解决方案很有限,要么换框架,这时候已经上车,全量换框架工程太过浩大不敢想。要么写原生,写原生不仅工程量也浩大,而且原生代码的复用成本也难度不小,这就是框架选型带来的后续维护成本和风险了。

但问题是躲不过去的,终究要解决。除了上面这几点需要去衡量的之外, 还要衡量一些非技术因素:

  • 产品发展形态/可预见的趋势:随着业务的发展以及小程序的权限收缩,为了继续扩大产品的可触达范围,势必会新增更多的端。
  • 用户群体:需要去收集用户的地理信息、手机型号、网络环境等等,综合这些数据去优化相应的点,做到有的放矢。

我们最终还是决定当部分组件替换为原生组件,比如:

  • MPVue 写的 input/textarea 在输入的时候就会出现光标闪烁/内容回滚的异常(原因就不赘述了),所以使用小程序原生语法重写了 input/textarea 组件,主要目的就是让组件的输入能够脱离 MPVue 的更新,直接组件内部走小程序的 setData ,因为当时 MPVue 还没有对数据更新做 diff 操作,目前据说做了一层 diff (实际效果大家自己去测评吧)
  • 产品中的文章详情需要 Markdown 渲染,因为文章内容相对来说都是数据量比较大的,使用 MPVue 去对这部分数据进行解析的话性能会相对来说较差。因此我们也使用了原生去实现了一个 Markdown 渲染的组件。

其实除了将组件替换为原生组件之外,我们还可以去优化原生的 setData 方法,具体内容可以阅读 westore 这个库,进一步提升性能。

最终,我们的架构慢慢就会变成了这样:Vue 会慢慢成为一个数据注入/数据分析/事件分发三者集合的中心枢纽一样的存在,底层是由各种高性能的原生组件,上层是由 Vue 来分发事件给不同组件,注入数据到不同组件里,收集埋点行为/事件。

这样一个现状依然有它新的问题,首先是多端复用成本低,现在还无法一套代码在 H5/小程序里全部通用,其次是小程序原生学习成本还是比框架要高,这对于不熟悉原生的同学都是一个不小的技术学习成本。

当下的困境

当下来看,我们之前选择的 MPVue 已经不能很好地支撑业务需求了。其一是因为 Vue 与我们的主体技术栈的分割,二来 MPVue 并不能支持跨多个平台开发。

跨多平台开发已经是当下的一个迫切需求。假如一个需求需要维护多端的代码,那么势必会需要更多的前端资源投入,这个成本是难以接受的。因此我们也开始考虑到底是使用市面上类似 Taro 这样的框架还是选择自研,两者的选择都有各自的好处。选择 Taro 这样的框架能够让我们迅速进入开发,但是长远来看免不了可能会出现 MPVue 类似的维护问题;选择自研的话虽然不会存在维护上的问题,但是会短期内需要投入更多的前端资源。

以上这些问题就是我们当下遇到的一个困境,选择任何一个方案都可能对将来造成巨大的问题,就比如当初我们选择 MPVue 那样。

语雀内容

送个稻谷,支持我写下去👇