每个团队都有一个框架梦,我们也不例外,只要遇到的足够复杂的问题,对团队的整体效率与稳定不断造成冲击的时候,往往就是去做问题归纳和推出框架的时候,这次我们聊一聊关于 PC 网站的网页前端开发方案,我们遇到的问题和对于框架的初步尝试,为保持给大家轻松的阅读体验和更宽的视角,本文不会深入代码细节,至于源码将来我们某天开源后大家便可一睹芳容。

启动框架的初心

一切技术决策皆有特定业务背景,我们在日常的开发中,主要面临如下三类场景:

  • 中后台的单页 PC ERP/CRM 等系统,上百种权限的十几个角色在后台各自的工作界面,每个角色都有十几个到几十个独立页面的业务流程,比如物流专员的发车调度、销售助理的加价推新等等,对于这些系统有可能是一个独立入口的 SPA,也有可能是多个独立入口的多 SPA 集合,在同一个仓库内维护
  • 前台的 PC 网站,几十个关联性不强的页面,没有太复杂的逻辑,主要做展示,比如公司官网
  • 在手机浏览器上的 H5 网站,几十上百个有强业务逻辑的页面,也有角色的区分,比如有交易业务的移动版商城

在小菜前期三四年的创业历程中,我们扎根在销地做撮合型的 toB 交易,所有的协同流程都移动化,交易也依托于独立的 APP,彼时也正是移动互联网崛起的时候,我们对于 PC 系统的建设和沉淀比较简单粗暴,公司中台只有一个囊括了几乎所有角色的 ERP(含 CRM 部分功能)后台,上百个页面,jQuery 与 React 共存, 通过 webpack 设计了多入口的打包方案,每一个独立的子系统(面向某类角色的业务流程页面)都独立打包,但随着我们的业务规模攀升和生鲜供应链的上下游打通,业务场景越来越多,特定层面的业务越来越垂直细分,包括中台和工具层面的建设,整个大中台开始有了雏形,十几个后台 PC 系统像雨后春笋一样快速启动,仓促上线,同时数量上也丝毫没有没看到减速的态势,我们必须有一套标准方案在做到快速的复制来降低重复开发与维护的成本,不为炫技只为解决问题就是我们的初心。

遇到了那些挑战

在这样的业务背景下,我们遇到了巨大的挑战,主要是如下几方面:

生态丰富却失去约束

我们知道  React 自身单薄,但是周边生态异常丰富,每有一个新特性出来就会多出非常多的轮子,在眼花缭乱的周边中,我们早期也迷失其中:

  • 在跨组价通信和状态管理上,我们用过 context、event-emiiter、redux、mobx 还有基于 redux 包装的 rematch等,比如 16 年起 redux 异步解决方案就有非常多的轮子:thunk、saga、redux-observable 等,而在中后台项目,尤其是 CMS 类型项目,几乎不会涉及竞态的问题,有些方案实现的有点牛刀杀鸡
  • 在 css in js 方案上,有原始的 sass/less scope 方案、css modules 、 style-components 、jss 等同样让人眼花缭乱,很容易选择新的和舍弃旧的
  • 在路由上,有 react-router 、reach-router、react-navigation 等。我们虽然沿用了 react-router, 但也不舒适,在路由权限这里我们自己完全去定制开发

整个过程趟过来,我们会发现 react 周边工具都能衍生出自己的生态,这样的裂变组合,导致我们前期的前端工程千奇百怪,交叉开发时,进入的新同学理解成本很大。

基础环境升级困难

当我们想要享受到基础工具升级带来的好处,比如早前 webpack2 的 tree shaking,webpack3 的 common chunks 策略更改,那么每个项目都需要升级一遍,就会陷入重复开发的泥淖中,而这些对于业务的同学最好是弱感知的,可以获得更好的开发体验,效率和开发幸福感自然会提升。

代码无分层,测试难

早期的代码是一个巨大的 class,数据请求对象也揉在 class 内,这个 class 会变得非常大,不可维护。按照关注点分离的原则,UI 层和数据层应当分离,只有这样,UI 测试和数据层的测试才能独立,多人开发才能成为可能。

工具函数散落 没有版本管理

前端和后端基于网关通信,我们会封装调用网关的请求工具,而各个项目都有自己的实现,一旦出问题,不仅排查起来没有规律,也无法集中解决,维护性很差。

开发环境差异化

当出现很多内部子系统时,每个系统启动命令和打包命令都不一致,让我们的心智成本非常的高,这个在我们开发了前端发布系统后暴露的尤其严重,我们取消了本地发包发布,都转移给运维发布平台去做执行,但在迁入发布平台时,每个项目的发布命令不同,就需要写出不同的运维脚本,完全是差异化的规则,并且一旦遇到紧急线上重启项目,还需要逐个分清楚它的命令参数,效率很低。

新启项目慢 响应业务的速度慢

那么多内部系统,每新启动一个 PC 系统,从登录到本地用户态与网关的保持,与权限系统的对接(限制页面的路由访问层级),整个系统的工程目录重新梳理等等各方面重新来一遍,整个配置过程弄下来一天就过去了,偶尔还碰到 webpack 和 babel 的bug,让新项目的搭建非常的耗时。

问题多多,全部总结下来就是随着系统的变多,没有框架约束的项目会面临三个问题:维护性差、效率低、扩展性差。

如何给框架定位

针对上述问题,我们希望在复用性、规范性、可维护性上产出一套方案,重点不是做出一个什么框架,而是系统解决掉前端工程上的这些问题,所以我们并不纠结这个框架的自研程度或者概念上的正确性,它一定是逐年逐季度迭代的,也一定会随着业务不断受到巨大挑战甚至某一天会夭折,但在特定的连续时间段内,能解放我们的成产效率,为业务带来更好的支持,它的目的就达到了。

因此结合我们业务多变,系统繁多,中后台居多的特征,我们把这个框架定义成:轻量级框架 + 自助餐搭配,具体的意思如下:

它可以兼容到我们的中后台项目以及 H5 工程,同时能搭建轻量级的非 SPA 工程,不能囊括进来的项目就不用该框架搭建,同时它对新人同学足够的友好。
模板市场会承载一部分无法纳入框架管理的定制需求,满足解决不同场景的需要,例如 H5 和 PC 的模板不同,最典型的便是兼容性要求不一样。

具体程序载入的策略和执行的策略则交给框架去约束,简单说就是业务代码按照框架的约定(实现框架的接口),就能让你的业务代码程序跑起来,不按我的约定,就不能跑起来,更别提上线了。

我们想要达到的效果是:业务开发不用关心基础环境的任何问题,基础环境有 bug,只要提出 issue,解决后直接升级基础框架即可。提高开发效率,减少解决重复 bug 的情况,最终提高业务响应速度。

如何设计工具包

上面的工具包载体就是 cli (command line tool), cli 的功能块如下:

这里简单地介绍下 cli 相关的命令,cli 的交互式命令行依赖了 2 个核心的三方包,分别是:

  • commander: 用于处理命令行内的参数
  • inquier:  处理交互式命令行的问答

init

在 init 阶段选择需要初始化的模板(模板来自模板市场,现在可以简单认为模板市场就是一个代码仓库)根据选择的类型去仓库内拉取相应的代码,然后安装 node_modules 包,整个工程就搭建起来了。

dev

dev 则是启动本地开发环境,有时我们需要本地启动不同的环境查看效果。在命令行设计时,我们可以针对常见的三个环境做不同的启动命令。如启动测试环境 dev:test, 就会依赖项目中针对不同环境配置的参数做整体替换。

build

build 则是打包本地的项目,也同样可以根据不同环境做不同的打包策略。

deploy

可能大多数公司也会从这个阶段过渡过来,所以提一下。在早前没有接入发布平台前,我们是本地发布的,html scp 到应用服务,静态资源走CDN,此时的 deploy 也需要针对不同环境做不同的打包策略。接入发布平台后,这个命令就应当被废弃了。本地打包带来的问题非常多,服务器权限、lock问题、协作效率等等。

test

依托 jest 对项目进行关键点的测试。测试固然非常重要,想做到100%覆盖率则会耗费非常多的时间,我们不强求100%覆盖,只要求对工具函数和业务组件做到覆盖。

对 webpack 的思考

前面我们简单介绍了几个常用命令,dev、build、deploy 都和 webpack 强相关。我们常常会将 webpack 相关的命令都放在 package.json 内,webpack 也同样存在于项目内。但这样就会出现之前所说两个问题,分别是基础环境重复配置暴露基础环境配置容易被修改,最终业务项目各自为政,难以维护。

所以,我们需要对业务开发者屏蔽 webpack 配置,将 webpack 的配置放入 cli 的工程内,当在业务项目中调用全局的命令时则根据响应的对应的子命令参数,获取对应的 webpack 配置。

将 webpack 的配置收敛在全局则会出现无法扩展的问题,为了满足不同业务项目的需求,我们约定在业务项目的根目录内存放一份配置文件,在调用命令时会先读取响应的配置合并到内存中,这里会涉及 webpack 配置合并的问题, 可使用 webpack-merge 或者 webpack-chain 这样的工具进行合并。

工具链辅助

框架周边一定是围绕着各种工具链,并且它的重要性不亚于框架本身:

关于效率

  • 数据 mock      mocks.js 定制
  • 简化路由配置  glob 读取文件夹作为路由
  • 所改即所见     webpack-dev-server 的 hotreload(H5 营销活动系统)与保持状态的 react-hot-reload(长业务流程需要保留state)
  • 代码生成器         定制生成  pageFile、service、model 文件的命令

关于质量

  • code lint 基于 eslint 定制,配合 husky 做到开发时、提交时、上线时的校验
  • 提交规范 commit-lint
  • 指定代码规约做 code review

发布

  • 缓存策略

    • 动态资源 html 放应用服务器,走协商缓存
    • 静态资源 hash化,放入 CDN,走强缓存
  • 发布策略

    • html scp 上传到应用服务器,这件事前期是项目负责人本地发布,后期是发布平台操作
    • cdn 资源 hash 化,永久缓存

如何做到可升级

  • 基础配置收敛进全局
  • 通用组件、工具函数用 npm 包管理

cli 遵循语义化,升级与否由开发者自己决定。为了及时通知到开发者,我们可以在 npm run dev 脚本里加入一段脚本用于 check 本地 cli 的版本和最新的cli版本是否一致,如果不一致则吐出 CHANGELOG 的链接,然后由业务开发同学阅读后选择是否升级。

光是解决基础环境的升级还不够,在业务系统中我们将通用的请求函数、异常处理、通用的组件也统统抽成 npm 包,各个业务团队利用 npm 语义特性可以进行无痛的升级。所以,鼓励将业务中搜集到的问题和好的解决方案抽出并发布 npm 包。

关于模板市场

利用 yeoman 定制相关的 templates,而各个模板需要各条线的业务同学贡献过来,目前使用最多的是中后台的项目,因此质量最好的中后台相关的。

React 的工程化

前面所讲的是工具包相关的,这部分讲讲业务代码部分的工程化,工具是开发效率的利器,虽然开发环境问题已经被 cli 解决,工程化规范是保证业务项目牢靠的关键,需要一套规约。

自助餐清单

  • css in js:  jss 搭配 less/sass
  • 状态管理:  rematch
  • 路由:react-router
  • ui 库: ant-design
  • 测试框架:jest
  • mock 工具:mock.js

简版工程结构

.
├── assets // 全局的静态资源
│   ├── icon
│   └── index.js
├── components // 全局的components
│   ├── index.js
│   ├── menu
│   │   ├── index.js
│   │   └── index.less
│   └── slider
│       ├── index.js
│       └── index.less
├── pages // 页面,约定每一个页面都是一个路由
│   ├── 404
│   │   └── index.js
│   └── user
│   |  ├── service.js // 存放本页面需要的数据请求对象和业务逻辑
|   |  ├── models
|        |     |     └── index.js //存放本页面自己的 rematch model 文件
|        |     └── index.js
|        └── login
│        └── index.js
├── routes // 自动路由配置
│   └── index.js
├── services // 存放数据请求对象
│   ├── index.js
│   └── user
│       └── index.js
├── store // 状态管理
│   ├── index.js
│   └── models
│       └── user.js
└── utils // 工具函数

工程结构按照功能结构划分,没有将所有的分层文件提升到最顶层,减少来回切换的心智成本,并且明确了服务层,既解耦界面和逻辑,又方便测试。

路由权限

路由权限上,我们会从后端拉取当前用户拥有的路由表,在 react-router 加入 component 处加入路由级权限校验的逻辑。

状态管理

关于状态管理,我们选择了 rematch,简单易用,也刚好够用,适合的就是最好的。状态管理可以让数据和逻辑抽离 UI 层,可以做到更高的测试也降低了巨型 class 组件的维护成本。

服务层

服务层存放数据请求对象,即请求接口。除此以外,一些较重的业务逻辑也会放入 service 内管理,我们希望状态管理竟可能轻量,将其中较重的逻辑收敛入 service 中管理。这么做的目的:

  • 复用一些 comb 的接口
  • 态管理层的心智负担,例如一些字段的default
  • 方便测试一些关键业务点

与后端交互

RESTFul 部分抽出公用的网关工具函数并放入私有的 npm 包进行版本管理,GraphQL 部分抽出 pc 版的 gpm 包对接 GQServer。

单页和多页

从 webpack 配置上看,除了入口配置有点不同,其他部分几乎是一样的。单页通常适用于业务之间联系较为紧密的整体应用,例如CRM系统。多页则适用于页面非常多,并且彼此之间没有太多关系的应用,例如营销 H5 系统。

中后台系统中我们应该按照业务域划分出各自的子系统,不应该将全部的页面都放入同一个工程。起初内部的 ERP 系统从 Java VM 模板中迁移出来,不得以用多页形式进行过渡。现在切出业务子系统,各个小系统由各个小团队自己维护,解耦且拥有单页良好的体验,越来越多的业务子系统的启动,也是我们做框架的必要性之一。

总结

实现框架的全链路建设是一个长周期的事情,需要持续不断的投人力进去,虽然听上去很高大上,但执行起来还是相对琐碎,尤其是框架推出阶段和升级阶段,基本上框架的实现者就是在当客服,当然相对优质的升级文档或者版本间的兼容程度会降低这种工作量,无论怎样,对于团队来说,有大量业务场景支持的框架会迸发它更大的价值,对于个人来说,参与框架的建设是一个很考核全方位能力的过程,而对于所有使用框架的同学来说,则多了一个顺手的工具同时也多了一个可以学习的封装范例,也正因为如此我们对于这件事情持 open 的态度,目前我们只是实现了它的从 0 到 1,还有监控集成、单元测试覆盖、发布系统接入、差异化打包方案、系统模板平台等等很多有意思的方向还没有更多的同学加入去共创去建设,还有相当长一段路要走。

:::info
Scott 近两年无论是面试还是线下线上的技术分享,遇到许许多多前端同学,由于团队原因,个人原因,职业成长,技术方向,甚至家庭等等原因,在理想国与现实之间,在放弃与坚守之间,摇摆不停,心酸硬扛,大家可以找我聊聊南聊聊北,对工程师的宿命有更多的了解,有更多的看见与听见,Scott 微信: codingdream,也可以来关注 Scott 语雀跟进最新动态,本文未经许可不许转载,获得许可请联系 Scott,否则在公众号上直接转载,尤其是裁剪内容后转载,我都会直接进行投诉处理。
:::


03-05 16:55