从2017年年初接手网易来钱项目,到现在不到一年时间。这期间,来钱前端技术栈经历了渐进式地持续改进。优化没有终点,追求没有止境,但是对于过去的2017年,我想可以先做一个阶段性的总结。
接下来,我会把自己在改造过程中面临的主要问题和做出的抉择一一道来。这其中很多问题具有特定场景,并非通用问题。有些抉择事后看来也并不是最优方案,当然还有一些问题有更好的解决方案,只是能力所限,我不知道或应用不了。
将这些分享出来,或许能为各位同事在遇到类似问题时做个参考,如果有同事能给出更好的建议就更好了。水平有限,希望大家不吝赐教。
网易来钱是一款内嵌于网易支付 APP 的贷款产品,2017年初,来钱的前端技术栈大致为下面的样子:
这个技术栈基本是在网易金融其他系统既有模式下稍加改造得到的,主要有以下特点:
整个技术栈的问题包括:
此外,还存在以下问题:
这些问题很多是老系统的共用问题,但由于产品处于初期阶段,功能迭代较快,并不能停下来进行一步到位的改造,重写或大规模重构是不现实的。
在保障正常业务迭代需求的情况下,我们对来钱前端技术栈进行了渐进式地改进。每次实现一个“小目标”,前后经历大概半年时间,达到一个初步稳定的状态。主要改造历程如下:
1 引入 Webpack
引入 Webpack 的改动还是比较大的。
1.1 构建
首先,需要将原本 Grunt 构建过程中的处理都在 Webpack 中实现。还好,Webpack 有丰富的插件,像 CSS 预编译、文件 hash 处理、代码压缩,这些都很好配置。
比较难处理的是 FTL 文件。Webpack 并没有给后端渲染模板的构建提供很好的支持,通常使用的 html-webpack-plugin 插件,也是用于处理前端模板,而且输出结果为 HTML,并不适用于这种输出结果为非 HTML 结构的情况。
为此,几经尝试,最终选择将 FTL 作为普通文本文件进行处理,通过编写 Webpack 插件方式,将构建后的资源地址插入 FTL 文件中引用。这个插件开源了:asset-inject-html-webpack-plugin。
通过在 FTL 中以注释方式声明要插入的资源类型、名称,在 Webpack 构建过程中对注释进行替换,例如:
<head>
...
<!-- css_inject_point chunk_common -->
</head>
<body>
...
<!-- js_inject_point asset_jquery -->
<!-- js_inject_point chunk_index -->
</body>
经过构建,输出:
<head>
...
<link rel="stylesheet" href="https://cdn.example.com/common.a50e7157.css">
</head>
<body>
...
<script src="https://cdn.example.com/jquery.min.js"></script>
<script src="https://cdn.example.com/index.f63474ef.js"></script>
</body>
FTL 文件带来的另一个问题是本地开发的页面渲染。如果是 HTML 文件,直接使用 Webpack 的 dev server/middleware 就好,但是对应 FTL 文件,需要额外的服务端渲染工具。
这意味着,在本地开发时,对于 FTL 文件,还是需要输出为本地文件,然后再通过启动类似 ftl-server 的工具来实现 FTL 渲染的。为此,选用了 write-file-webpack-plugin 插件,单独将 FTL 文件输出。
然后 ftl-server 需要启动,我希望能够只启动一个 dev server,本地开发时会简单很多。为此,通过研究 ftl-server,将其核心部分抽离,然后包装为 express middleware:freemarker-middleware。
这样,本地开发时,只需要启动一个 dev server,加载 webpack-dev-middleware 和 freemarker-middleware。
1.2 模块化
模块化的改造是指,将基于 Sea.js 的 CMD 模块,改造为 CommonJS 模块,然后搞定原来针对 Sea.js 所做的特殊配置,将其迁移、配置到 Webpack 中。
对于类似 jQuery 这样的外部库,考虑到并不会频繁更新,所以没有纳入构建过程,而是直接在页面进行引用,然后将 jQuery 这样的全局变量包装为模块使用,例如:
// webpack.config.js
module.exports = {
// ...
externals: {
jquery: 'jQuery'
}
}
// index.js
var $ = require('jquery');
$(function () {
// ...
});
以上改造,并没有对原有的 FTL 的渲染逻辑、JS 中原有业务逻辑进行改动,这样就降低了上线风险。相关技术方案经过验证成熟后,在一个发布迭代(一周时间)中引入工程,进行一次替换后,即可上线。
2 初步制定代码规范
通过引入 eslint、stylelint,将对代码的规定落实下来,在开发中辅助进行检查,确保代码风格一致,没有明显的问题。
不过,并没有通过插件将 eslint、stylelint 的检查纳入构建过程中。一方面是由于对于代码的改造不是短时间可以完成的,还有大量的业务需要需要支持;另一方面,部门层面没有统一制定规则,只能在项目层面进行约束,但只能是一种“自觉”,而不能“强制”。
3 引入 Vue.js
FTL 后端渲染 + jQuery 的方式,一个很大的弊端是开发效率低。有的情况下,FTL 渲染出的内容,还会由于用户交互,要 jQuery 再进行渲染更新。
例如来钱首页,由于用户借款、还款是异步处理的,需要及时获取最新状态并更新页面显示。页面首次加载时服务器端通过 FTL 模板渲染得到内容进行展示,浏览器端判断处于借款中、还款中状态时,需要频繁查询后台获取最新状态,一旦状态变更,则在前端进行更新渲染。
在产品不断完善的过程中,来钱首页的展示逻辑越来越复杂,例如添加了账户冻结、逾期、还款卡解绑等等新的逻辑,这使得传统的页面渲染技术越发捉襟见肘,开发效率非常低,而且由于渲染逻辑分散,很容易出问题。
在这种情况下,我们选择引入更好的技术方案。如项目技术栈的现状而言,将服务端渲染改为前端渲染是首先要考虑的问题。而前端渲染方面,使用前端模板还是现代化的前端框架,也是要进行抉择的。
最终,在经过慎重考虑之后,我们选用了 Vue.js 进行前端渲染的方案。一方面,Vue.js 上手简单,对于构建过程要求低,初步上线时可以不引入任何特别的处理,方便在业务迭代情况下低风险使用。
为此,我们首先在压力最大的来钱首页进行了实践。后端配合改造,将原本提供给 FTL 的业务数据进行 JSON 序列化处理,随 FTL 渲染后“数据直出”,这样前端在页面加载后可以立即进行渲染,体验上不会太差。
初期上线时,我们只在来钱首页进行改造,其他页面仍采用后端渲染 + jQuery 模式,构建过程没有改动。
4 mock 改造
本地开发时,有关数据 mock 有两个主要的问题:
对于第一点,由于 mock 数据有两个使用场景,FTL 模板渲染和 Ajax 请求,都需要兼顾。Ajax 请求比较简单,可以对 dev server 进行改造。而 FTL 方面,则需要对 freemarker-middleware 进行改造,使其能够接收传入的 mock 数据。
freemarker-middleware 的改造还好,底层 jar 本身就提供了传入数据的接口,只要在请求到达 freemarker-middleware 之前将数据准备好,然后约定一个方式提供给 freemarker-middleware 从而进一步用于 FTL 渲染就行了。
结合具体项目情况,我们开发了 mock-middleware,基本可以满足本地开发需求。
再来看不同场景下的数据模拟。以来钱首页为例,即账户状态方面,就有冻结、无订单、无可用额度、逾期、借款中、还款中等状态,并且还有其他如可用额度低于 500 元等情况,这些在页面展现、交互上都要分别处理。在开发特定功能,如新增首页银行卡解绑状态提示,需要将首页的 mock 数据置为特定状态。传统上,是通过手动修改对应 mock 数据的方式进行处理,这一方面不够灵活,另一方面历史上不同场景下的 mock 数据没有积累下来,其他同事开发时还需要自己修改 mock 数据。
为此,我们设计了在 mock 数据中预置多种场景下的数据,在本地开发时进行动态选取使用的方案。
简单来说,对于一个 FTL 数据或 Ajax 数据模块,针对不同场景提供不同数据:
// entry.js
module.exports = function (req) {
switch (req.$config.Entry) {
case 'InActive':
return {
result: 'success',
data: {
status: 'inActive',
hasOrder: false,
// ...
}
}
case 'ActiveHasOrder':
return {
esult: 'success',
data: {
status: 'active',
hasOrder: true,
// ...
}
}
// ...
}
}
注意上面代码中的 req.$config.PageA
,这就是配置工具可以动态修改的了。
这一功能内置到 mock-middleware 插件中,通过访问本地开发服务器的 /mock-config/
页面,即可动态修改场景配置,从而在下一次请求到达,并进入对应的 mock 数据模块时,带入最新的场景配置,进而返回不同的数据。
采用这种机制后,鼓励开发同学将接口在不同场景下的 mock 数据都新增处理,而非覆盖原有数据,这样其他同事也能复用这些 mock 数据。当然,对应地,开发环境中要有一个配置文件,将接口和对应的各种场景列举出来,以便于在 mock-config 页面选择:
// /mock/config.js
module.exports = {
Entry: {
desc: '入口页面',
options: [
{value: 'Normal', desc: '激活'},
{value: 'InActive', desc: '未激活'},
{value: 'ActiveHasOrder', desc: '激活有订单'},
{value: 'ActiveNone', desc: '激活无额度'},
{value: 'Overdue', desc: '逾期'},
{value: 'OverdueNoAvailable', desc: '逾期且借光了'},
{value: 'Freeze', desc: '已冻结'},
]
},
// ...
}
将上述方案加入项目后,可以逐步替换原有的页面和接口 mock 数据,一段时间后,即可全部删除所有用于 mock 的 FTL 页面。
5 Vue.js 完善
为便于 Vue.js 的使用,我们将对 *.vue 文件的支持加入(vue-loader),也同时将 ES6、PostCSS 等支持引入,由于 Vue 社区中已经有了很多的实践,这些新增的内容并没有花费我们太多时间。
对于已有的代码,我们逐步进行重构,使用 ES6 特性简化原有 JS 代码,通过 autoprefixer 的引入,省略原有代码中的 CSS 兼容处理等。
另外一方面,是去除 jQuery 相关代码。比较重要的是对于 Ajax 实现的替换,由 $.ajax 改为使用基于 fetch 封装的类似接口,减少迁移成本。但随着 Promise 的引入,逐步由回调模式改为 Promise 模式。
经过一段时间的持续改进,新增功能时的开发体验有了明显提升。
6 单元测试
测试对代码质量的重要性不用多说,但是前端项目的自动化测试方面,我们的经验并不多。经过调研和思考,我们决定先引入单元测试,覆盖重要的公共模块。最终选用的框架是 karma + karma-webpack + jasmine 的模式。
不过在覆盖率方面,我们没有一个目标,甚至以后也没有将覆盖率提升到一个较高水平的打算。我的想法是,前端代码,特别是在引入了 Vue 后的页面,很多与 UI 相关的模块不必进行测试,并且也很难进行测试。与其追求覆盖率,不如对功能、模块进行更好地划分,让关键的代码更具有“可测试性”,UI 相关部分的代码质量由框架进行保障。例如,进行数据修改后,不会去检查页面上某个组件是否正常更新,而是检查 Vue 组件实例上与 UI 相关的数据状态是否正常更新,数据与 UI 的同步由 Vue 保障。
7 单页化改造
原有的多页模式,在涉及页面跳转时,有很多不能控制的情况。例如,在借款流程中,当用户借款成功跳转到“借款成功”页面后,不能再返回到上一页面,只能跳转到来钱首页。
为了解决这些问题,并且优化流程上前后页面切换的用户体验,我们决定对关键业务流程逐个进行单页化改造。
首先,需要后台开发同学配合的是将原来同步的 FTL 页面改造为异步 Ajax 接口提供给前端。
由于已经积累了 Vue 改造的成果,主要页面已经 Vue 化,在接口数据结构不变的情况下,前端的修改工作量并不大,基本上只是将对应的 Vue 组件配置添加到 VueRouter 的 routes 中,然后修改跳转到该页面的逻辑。
并且,借助 vue-router 提供的路由钩子机制,在页面路由变更时,可以进行有效控制,例如,签名提到的借款成功页面只能返回来钱首页的路径,可以在接口成功页面对应的组件中加入:
module.export = {
// ...
// 借款成功页面只能返回首页
beforeRouteLeave(to, from, next) {
if (to.name === 'index') {
next();
} else {
next(false);
this.$router.push({name: 'index'});
}
}
}
不过到目前为止,还是有一些页面并没有进行单页化改造,而且也不打算进行改造。这些页面并非核心业务,改动并不频繁,单纯为了单页化进行改造只是增加前后端以及测试的工作量而已。
经过上述改造,目前来钱的前端技术栈大致为:
总结网易来钱 2017 年的前端技术栈改进历程,我自己的评价是两个字“务实”。
整个渐进式的改造历程中,我们不会暂停业务迭代,甚至有时候恰恰是为了提升业务才适当引入一些新技术。我们不追求“新技术”,也不追求“技术的纯粹性”,在进行一些技术改造时,我们会评估并控制其影响范围,考虑方案兼容性,不追求“一步到位”。
例如,在近两三个月的过程中,线上许多页面是 Vue 与 jQuery 并存的。某个页面可能整体是采用 Vue 开发,但是其调用的 toast、dialog,可能还是 jQuery 实现的。不过现在 jQuery 早已完全下线。
从这个漫长的改造中,我们收获的经验是:
本文来自网易实践者社区,经作者汤康兴授权发布。