阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

QQ 音乐商业化 Web 团队:前端工程化实践总结(三)

  • 2019-10-31
  • 本文字数:7352 字

    阅读完需:约 24 分钟

QQ音乐商业化Web团队:前端工程化实践总结(三)

前端如何做单元测试?

测试环境


和后端不同,前端有运行环境的差异性,需要考虑兼容性,如何模拟浏览器环境,如何支持到 BOM API 的调用,这些都是需要考虑的。可以考虑以下几种测试环境的解决方案:



测试工具


测试框架就是运行测试用例的工具,常见的有 Macha、Jasmine、Jest、AVA 等等。


断言库主要提供语义化方法,用于对参与测试的值做各种各样的判断。这些语义化方法会返回测试的结果,要么成功、要么失败。Node 内置断言库 assert,常见的断言库还有有 chai.js、should.js。断言库可以支持不同的开发模式,比如 chai.js 就是一个 BDD/TDD 模式的断言库。


测试覆盖率工具是用于统计测试用例对代码的测试情况,生成相应的报表,如 Istanbul(Jest 内置集成)。


Karma 是一个测试平台,可以在多种真实浏览器(e.g Chrome Firefox Safari IE 等等)中运行 JavaScript 代码,可以和很多测试框架集成,比如 Mocha、Jasmine 等等,还可以使用 Istanbul 自动生成覆盖率报告。


CI/CD


首先先看一张图片,来理解 Agile(敏捷开发)、CI(持续集成),CD(持续交付/部署)和 DevOps(开发运维一体化)涵盖的生命周期范围。CI/CD 并不等同于 DevOps,它们只是 DevOps 的部分流程中的一种解决方案。


DevOps 是 Development 和 Operations 的组合,是一种方法论,是一组过程、方法与系统的统称,用于促进应用开发、应用运维和质量保障(QA)部门之间的沟通、协作与整合。以期打破传统开发和运营之间的壁垒和鸿沟。



各个术语涵盖的生命周期范围


持续集成(Continuous Integration)中开发人员需要频繁地向主干提交代码,这些新提交的代码在最终合并到主干前,需要经过编译和自动化测试(通常是单元测试)进行验证。


CI 的好处在于可以防止分支偏离主干太久,这种持续集成可以实现产品快速迭代,但是由于要频繁集成,所以需要支持自动化构建、代码检查和测试,实现这些自动化流程是 CI 的核心。



持续集成


持续交付(Continuous Delivery)指的是,频繁地将软件的新版本,交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。


CD 是 CI 的下一步,它的目标是拥有一个可随时部署到生产环境的代码库。



持续交付


持续部署是持续交付的延伸,实现自动将应用发布到生产环境。



持续部署


公司内部常用的解决方案有:蓝盾 DevOps 平台 、orange-ci、QCI,各花入各眼,详情可以阅读这篇文章 CI 工具哪家强。


这些 CI 平台是怎样将 git 仓库中的代码变动和自动化构建流程相关联起来的呢?答案就是 Webhook,它与异步编程中“订阅-发布模型”非常类似,一端触发事件,一端监听执行。


在 web 开发过程中的 Webhook,是一种通过通常的 callback,去增加或者改变 web page 或者 web app 行为的方法。这些 callback 可以由第三方用户和开发者维持当前,修改,管理,而这些使用者与网站或者应用的原始开发没有关联。Webhook 这个词是由 Jeff Lindsay 在 2007 年在计算机科学 hook 项目第一次提出的。


  • Webhooks 是”user-defined HTTP 回调”。它们通常由一些事件触发,这里可以查看 GitHub 上面支持的 Event 类型,比如 git push、fork 等等,也就是说这些代码托管平台首先要支持 Webhook 的功能。

  • 当事件发生时,源网站可以发起一个 HTTP 请求到 Webhook 配置的 URL。通常这里配置的 URL 指向某个 CI 系统,这意味着当 git 仓库中“订阅”的事件发生时,CI 系统可以收到通知。

  • CI 系统在收到通知后就可以触发 build 等流程。


CI 自动化构建只是应用 Webhook 的一个案例,Webhook 的应用远不止这些,由于 webhook 使用 HTTP 协议,因此可以直接被集成到 web service,有时会被用来构建消息队列服务,例如一些 RESTful 的例子:IronMQ 和 RestMS。

我们的项目构建现状

介绍完了前端工程化的一些概念和技术后,下面结合我们团队中的具体项目具体分析。

1.现状分析

这是目前团队移动端基础库的项目结构:主要有 9 个模块,其中 3 个 UI 组件依赖框架。



基础库项目结构


  • 模块化


我们团队在移动端基础库的开发中,最初采用的是 IIFE 模式。从严格意义上来说,这并不是一种标准的模块化方式,只是通过闭包实现了私有数据,将数据和行为封装到一个函数内部, 通过给全局对象 window.M 添加属性来向外暴露接口,我们无法确认每个模块间的依赖关系,模块合并时还要关注依赖顺序。在新的方案中,我们引入了 ES6 的模块化标准来解决这个问题。


  • 重复开发,复制粘贴


由于业务特点,对于一些快速上线的活动页使用 Zepto 库,而对常驻页面进行了技术升级,社交团队使用了 Preact 框架,这导致基础库的开发有了两个版本,分别在不同的代码仓库维护,但实际上二者 90%+的代码都是一样的,仅仅是三个 UI 组件不同。在基于 TSW 的同构直出项目中,有些基础库方法又要在 node 端执行,这个时候也是复制粘贴了一份 m.js 放到了该项目目录中。在新的方案中,我们使用差异化的构建在一份代码仓库中分别构建出多个版本。


  • 组件 css 的问题


对于组件的样式,我们是有专门的重构组进行开发维护的,他们遵循 BEM 规范,开发组件的时候当字符串引入:


var css ='.qui_dialog__mask{position:fixed;top:0;left:0;bottom:0;right:0;}...';appendToHead(css);
复制代码


这种模式对 CSS 的开发维护很不友好,虽然我们不需要关注样式的细节,但还是每次要把重构发给我们的.css 文件中的样式 copy 出来。新方案中,我们引入 CSS module 的方案。

2.技术选型

主流构建工具


构建工具的选择,主要对比了 Webpack4、Rollupjs 和 Parcel,因为基础库的构建文件只有 js,而且从构建体积来说,rollupjs 是有绝对优势的,所以选择了 rollupjs。



主流构建工具对比


CSS 模块化


由于 CSS in JS 需要引入额外的依赖,在对比了 CSS Module 和 CSS in JS 后,我们选择 CSS Module 的方案。



CSS 模块化方案对比


单元测试框架


单元测试框架我们选择了 Jest,主要是因为开箱即用,不需要再引入断言库,生态也很好,较多用于 React 项目,而且组内的 UI 自动化测试系统是支持 Jest 的,这篇文章 Migrating from Mocha to Jest 中介绍了 Airbnb 的尝试。



单元测试框架对比


Lint 方案


由于接入了 CI 系统进行 lint 自动化检查,为了减少“无效”的 commit,我们选择了 husky+lint-staged 来进行本地代码提交前的 lint。



Lint 方案


工作流和 CI?


各种工作流中,首先需要在各自的开发分支进行开发测试,然后将代码合并到追踪生成环境的长期分支进行持续地发布部署,这意味着对这个长期分支要有完善的自动化测试能力,因为谁也不能保证 merge 的代码就一定不会有问题,目前新的方案引入了单元测试,对 UI 组件引入了基于 puppeteer 的截图测试,但一些功能缺乏在更多设备、更多平台上的自动化验证,因此我们认为在自动化测试方面的建设还不是非常完善,所以新方案接入了 CI,但是对发布外链基础库 music.js 这种会直接影响到全量业务的并没有接入,还是使用 ARS 发布,除非紧急 bug,其他的代码更改会在测试环境验证一段时间(一般 2-3 天)后才会发布外网。

我们的工程化实践

1.构建方案

新旧方案对比


首先可以看一下新旧构建方案的对比,在新方案中推广使用 ES6,增加了对代码质量的控制:代码检查+单元测试,并接入了 CI 系统。



新旧方案对比


打包方案


这是我们整体的打包方案,核心是一份源码开发维护,通过构建工具的差异化配置实现多种版本的构建。



打包方案


开发流程


这是整体的开发流程,本地开发使用 package.json 管理项目依赖,规范代码格式,接入单元测试;提交之前 git hook 设置保证代码检查和测试通过后才能提交成功;使用 QCI 自动进行项目的构建、检查和测试,通过后将 JSDOC 文档推送到文档服务器,并发布 npm 包,外链 js 还是使用 ars 发布。



开发流程

2.UI 组件开发和文档

我们选择 react-styleguide 作为 UI 组件开发调试工具以及文档生成器,这是一个组件的 MD 文件示例:

组件式引入

  • 可以提前插入 dom 结构,如果浮层中有图片的话会先加载;

  • 属性中的 visible 控制组件是否可见。


import Button from '../../basic/Button/Button'import QMDialog from './QMDialog';
class QMDialogExample extends React.Component { constructor(props) { super(props); this.state = {visible1: false} }

render() { const {visible1} = this.state; return (
<div> <Button onClick={() => { this.setState({ visible1: true }) }}>基本使用</Button> <Button onClick={() => { this.setState({ visible2: true }) }}>带头图的浮层</Button> <Button onClick={() => { this.setState({ visible3: true }) }}>传入一个react节点</Button>
<QMDialog visible={visible1} title="QQ音乐" message="这是一段描述" btn={'我知道了'} handleTap={index => { if(index === -1) { this.setState({ visible1: false }) } else { console.log('我知道了按钮被点击,index=', index) } }} /> </div>
) }}<QMDialogExample />
复制代码


react-styleguide 会根据组件的源码和这个 md 文件生成文档和 demo,开发调试阶段支持 webpack 配置 HMR,非常方便。



demo 文档截图

3.Jest 单元测试

Jest 可以设置全局的 Setup,会在所有 test 执行之前运行,也可以设置全局 Teardown,会在所有 test 执行完毕之后运行,比如这里就可以设置一些测试需要的 Global 对象、运行环境等等。describe 可以将测试用例进行分组,beforeEach、afterEach、beforeAll、afterAll 这些方法可以定义在测试用例之前或者之后运行的方法。


测试方案


根据上面介绍的打包方案和业务特点,基础库需要分别运行在 node 端和浏览器端,因此需要考虑到不同运行环境下的测试结果。


浏览器端


  • npm 命令

  • jest --coverage --config ./config/jest/music.jest.config.js

  • 设置–coverage 生成测试覆盖率。

  • 配置文件(music.jest.config.js):

  • 基于 jsdom 设置全局环境:jest-environment-jsdom-fourteen,提供浏览器端 BOM 对象。

  • 设置 cookie 操作权限的 domain:testURL: “https://y.qq.com/m/demo.html”,仅可以操作此域名下的 cookie。


module.exports = {  clearMocks: true,  coverageDirectory: "jest-coverage/coverage-music-node",  preset: null,  rootDir: '../../',  testEnvironment: "jest-environment-jsdom-fourteen",  testMatch: [    "**/tests/music-node/**/*.test.[jt]s?(x)",  ],  testURL: "https://y.qq.com/m/demo.html",  transformIgnorePatterns: []};
复制代码


Node 端


node 端和浏览器端的不同在于运行环境 testEnvironment 不同,jest 提供 jest-environment-node,我们为 node 端单独配置了 music-node.jest.config.js。


UI 组件


Jest 支持对 React App 的测试,可以采用截图测试(Snapshot Testing)、模拟 DOM 操作(DOM Testing)等方法详见文档。在组件文档和 demo 这一章节中我们已经有了组件示例,并构建了文档页,可以直接接入团队的自动化测试系统,结合使用 puppeteer 进行截图对比。


下面是对 QMDialog 组件的测试用例,首先准备一张基准图片,然后写测试流程:打开页面——点击按钮触发组件——截图对比。screeshotDiff 方法的实现参考了这篇 KM 文件通过 puppeteer 实现页面监控,图片 diff 核心算法由 pixelmatch 库实现。


const iPhone = devices['iPhone 6'];await page.emulate(iPhone);
await log("进入页面");await page.goto('http://[host]/reactui/index.html#/QMDialog', { waitUntil: 'load'});
await timeout(3000);let dom = await page.$('#QMPreload-container .rsg--preview-35 .button');
await dom.click();
await timeout(200)let diff = await screenshotDiff({ img: 'https://y.gtimg.cn/music/common/upload/t_cm3_photo_publish/1677163.png'});
if (diff > 10) { fail(); return;}
success();
复制代码


这是一次测试运行结果,从左到右依次是:基准图、测试截图、diff 结果图,screeshotDiff 根据第三张图片返回差异点的占比,由于 QMPreload 组件的特点,加载进度受网络影响,设置阈值为 10%,即只要差异率在 10%以内就可以认为是正常的。



QMPreload 测试结果


和上面 QMPreload 不同,对 QMDialog 组件的判断则是需要差异值为 0,如下面第三张图所示,没有差异点。



QMDialog 测试结果


mock


这是我们参照官网的文档接入的 mock 示例,这里需要注意__mock__的目录结构,详见文档。


.├── config├── src│   ├── music│   │   ├── utils│   │   │   ├── __mock__│   │   │      └── loadUrl.js│   │   └── loadUrl.js├── node_modules├── ...└── tests
复制代码


loadURL 方法用来动态加载 js,使用 jest.fn().mockImplementation 对 loadUrl 进行 mock,并 mock 了 window.pgvMain 和 window.pgvSendClick。


export const loadUrl = jest.fn().mockImplementation((url, callback) => {    if (/ping.js/.test(url)) {        let pvCount = 0;        window.pgvMain = jest.fn().mockImplementation( (p1, p2) => {            expect(p1).toBe('');            expect(p2.virtualDomain).toBe('y.qq.com');            if (pvCount === 1) {                expect(p2.ADTAG).toBe('all');            }            pvCount++;        })        window.pgvSendClick = jest.fn().mockImplementation( (p) => {            expect(p.hottag).toEqual(expect.stringContaining('.android'));        });    }    callback();});
export default loadUrl;
复制代码


因为使用了 ES module 的 import,需要 jest.mock 对整个模块进行 mock。对于 mock 的函数才能调用 toHaveBeenCalledTimes 的断言。


import tj from '../../src/music/tj';import loadUrl from '../../src/music/utils/loadUrl'
jest.mock('../../src/music/utils/loadUrl');
describe('【tj.js】点击上报', () => { test('tj.pv tj.sendClick', () => { expect(typeof window.pgvMain).toBe('undefined'); expect(loadUrl).toHaveBeenCalledTimes(0); tj.pv(); expect(loadUrl).toHaveBeenCalledTimes(1); expect(typeof window.pgvMain).toBe('function'); expect(window.pgvMain).toHaveBeenCalledTimes(1); tj.sendClick(); tj.sendClick('tjtag.click'); window.tj_param = { ADTAG: 'all' } tj.pv(); expect(loadUrl).toHaveBeenCalledTimes(1); expect(window.pgvSendClick).toHaveBeenCalledTimes(1); });})
复制代码


测试覆盖率


这是某一次的测试报告,上面有每个模块详细的测试覆盖率。为了便于对各个模块灵活处理,我们将每个函数细分拆成一个文件,如下面的 src/music/type 目录下的各个文件。



测试覆盖率-1



测试覆盖率-2



测试覆盖率-3


通过单元测试发现的代码 bug


这些都是我们通过单元测试发现的之前一些函数的 bug,仅举例一部分:


4.一些 Tips

声明 pkg.module


声明 pkg.module 可以让构建工具利用到 ES Moudle 的很多特性来提高打包性能,比如利用 Tree Shaking 的机制减少文件体积,这篇文章 package.json 中的 Module 字段是干嘛的有详细介绍。


sideEffects


Tree Shaking 可以在构建的时候去除冗余代码,减少打包体积,但这是一个非常危险的行为,在 webpack4 中,可以在 package.json 中明确声明该包/模块是否包含 sideEffects(副作用),从而指导 webpack4 作出正确的行为。如果在 package.json 中设置了 sideEffects: false,webpack4 会将 import {a} from 'moduleName’转换为 import a from ‘moduleName/a’,从而自动修剪掉不必要的 import,作用机制同 babel-plugin-import。这个功能亲测是很有效的


对于 rollupjs 来说,有时候 Tree Shaking 并不有效,这是官网的一段解释,大意就是静态代码分析很难,为了安全 rollupjs 可能会无法应用 Tree Shaking,这个时候建议最好还是明确 import 的 PATH,这里可以结合适应上面的 babel-plugin-import 插件。


Tree-Shaking Doesn't Seem to Be Working


插件


  • @babel/plugin-transform-runtime


这个插件可以避免每一个 js 文件分别引入胶水代码,而是整个构建文件引入一份胶水代码,减少代码体积。


  • eslint-friendly-formatter


对 eslint 的错误输出进行格式化,方便查看和定位问题。


  • babel-plugin-transform-react-remove-prop-types


由于运行时的性能原因,RN 已经在 production 模式下移除了 PropTypes,我们引入这个 babel 插件在生产模式中移除组件属性的类型校验相关的代码。


—noConflict


在将外链 js 用 rollupjs 构建成 umd 规范的时候,我们设置了–noConflict,可以解决全局变量 M 冲突的问题,类似于 jQuery.noConflict()。


(function (global, factory) {    typeof exports === 'object' && typeof module !== 'undefined' ?        factory(exports) :        typeof define === 'function' && define.amd ?            define(['exports'], factory) :            (global = global || self, (function () {                var current = global.M;                var exports = global.M = {};                factory(exports);                exports.noConflict = function () {                    global.M = current;                    return exports;                };            }()));
复制代码


本文转载自公众号云加社区(ID:QcloudCommunity)。


原文链接:


https://mp.weixin.qq.com/s/INlxjk4DnBFZynmbUkYGJA


2019-10-31 16:372000

评论

发布
暂无评论
发现更多内容

“似水无形” - 小程序化

FN0

小程序 容器化 轻应用

Spark Driver CPU 占用异常问题排查

观远数据

spark bug修复 #开源 8月月更

[教你做小游戏] H5小游戏技术选型分析,低代码?小游戏框架?canvas或SVG?还能用React?

HullQin

CSS JavaScript html 前端 8月月更

分布式系统接口用例自动回归实践

转转技术团队

接口测试

【真送礼物】1 分钟 Serverless 极速部署盲盒平台,自己部署自己抽!

阿里巴巴中间件

阿里云 Serverless 云原生

TDesign 设计资源大更新,产品经理和设计师都可以省心啦~

TDesign

设计 设计师

已有小程序应用转App的一种技术

Speedoooo

小程序 小程序容器 小程序转app

圆壹智慧创始人兼CEO 潘麓蓉:AI制药工业落地的痛点与前进方向

阿里云弹性计算

HPC 高性能计算 AI制药

中小微企业如何快速开发信息化系统

力软低代码开发平台

无影云电脑

六月的雨在InfoQ

无影云电脑 云电脑 8月月更

全链路灰度新功能:MSE 上线配置标签推送

阿里巴巴中间件

阿里云 微服务 云原生

多核驱动时代的降维打击 英特尔异构混合架构破局之路

科技之家

什么数据库这么猛?5.6 版本刚开源一个半月,8.0 版本竟然就要启动了?| StoneDB 社区答疑第二期

StoneDB

MySQL 数据库 开源 StoneDB 8月月更

周一见!距离阿里巴巴开源开放周还有3天

阿里巴巴中间件

阿里云 开源

华为云CDN&云视频通信专场:828低价购,CDN0.05元/GB起,短信0.006元/条起

sofiya

直播预告 | 流程挖掘如何助力头部制造业实现千万级增长?

望繁信科技

Beetle编译/部署自动化

转转技术团队

CI/CD

“软件定义汽车”时代,车载生态安全运转需小程序化技术

Speedoooo

小程序 车联网 小程序容器 车载安全

SpringBoot 整合 MyBatis

mybatis springboot 8月月更

高项-第一章 信息化和信息系统(1)

索隆

项目管理 软考 笔记分享

“九章云极DataCanvas AI平台赋能厦门航空”荣获AI平台应用标杆案例

九章云极DataCanvas

人工智能

参与 TDesign 收获了什么?听听社区贡献者怎么说

TDesign

设计 产品经理 设计师

在 WSL2 上部署 PyTorch

DisonTangor

WSL2 Windows 10 PyTorch

九章云极DataCanvas YLearn因果学习开源项目荣获“可信AI实践优秀案例”奖

九章云极DataCanvas

付费会员之我见-02(44/100)

hackstoic

商业模式 付费会员

实力上榜|海泰方圆跻身2022企业网络安全服务Top15

电子信息发烧客

他只是试图运用自己的能力,给这个领域带来改变

图灵教育

通信

诚邀|8月31日,【因果学习和决策优化挑战赛TOP10队伍作品秀】邀您共享因果学习智慧盛宴

九章云极DataCanvas

人工智能

开源一夏 | 一个裸机工程转FreeRTOS的实例

矜辰所致

开源 stm32 STM32CubeMX 8月月更 FreeRTOS

开源一夏 | 23张图,4500字从入门到精通解释Redis

wljslmz

redis 开源 8月月更

前端食堂技术周刊第 49 期:Deno即将迎来重大变革、Blitz 2.0 Beta、Chrome删除HTTP/2服务端推送

童欧巴

JavaScript typescript deno

QQ音乐商业化Web团队:前端工程化实践总结(三)_行业深度_sara_InfoQ精选文章