前端工程化实践总结 | QQ音乐商业化Web团队
蓝字关注,回复“加群”加入前端技术群 与大家一起成长
随着业务的不断扩展,团队的项目越来越多,面对日益复杂的业务场景和代码逻辑,我们发现在前端工程化方面团队还有很多需要优化的地方。现有的解决方案已经无法满足各种复杂的场景,我们每天都在疲于应付很多重复的工作,为此我们基于移动端基础库重构和UI组件库的建设这两个项目对团队的项目构建流程进行了详细的分析和梳理,并制定了一套适用于团队的工程化方案。
浅谈前端工程化
前端工程化是一个非常广泛的议题,包含的技术和解决方案也是非常丰富的。一个前端工程的生命周期可以大致划分为这四个过程:
前端工程的生命周期
任何在这四个过程中应用的系统化、严格约束、可量化的方法都可以称之为工程化。工程化的程度越高,在工作中因人的个体差异性导致的缺陷或者短板就会越少,项目质量可以得到更有效的保障。对上面四个过程的工程化并不是完全分隔的,而是相辅相成,比如开发阶段的优化也会对测试、部署和维护产生很大的影响。
下面从模块化、组件化、规范化和自动化这四个方面进行具体介绍。
模块化
模块化可以对复杂逻辑进行有效分割,每个模块更关注自身的功能,模块内部的数据和实现是私有的,通过向外部暴露一些接口来实现各模块间的通信。开发阶段前端需要关注JS、CSS和HTML,下面我们将分别对JS、CSS、HTML的模块化进行简单介绍。
1. JS模块化
JS模块化是一个逐渐演变的过程,开始的namespace概念实现了简单对象封装,约定私有属性使用_开头,到后来的IIFE模式,利用匿名函数闭包的原理解决模块的隔离与引用,下面介绍现在比较流行的几种模块化标准。
2. CommonJS
Nodejs中的模块化方案,就是基于CommonJS规范实现的。一个文件就是一个模块,有自己的作用域,没有export的变量和方法都是私有的,不会污染全局作用域,模块的加载是运行时同步加载的。CommonJS可以细分为CommonJS1和CommonJS2,二者的模块导出方式不同,CommonJS2兼容CommonJS1,增加了module.exports的导出方式,现在一般所指的都是CommonJS2。
每个文件一个模块,有自己的作用域,不会污染全局;
使用require同步加载依赖的其他模块,通过module.exports导出需要暴露的接口;
多次require的同一模块只会在第一次加载时运行,并将运行结果缓存,后续直接读取缓存结果,如果需要重新执行,需要先清理缓存;
Nodejs环境下可以直接运行,各个模块按引入顺序依次执行。
AMD
浏览器加载js文件需要进行网络请求,而网络请求的耗时是不可预期的,这使得CommonJS同步加载模块的机制在浏览器端并不适用,我们不能因为要加载某个模块js而一直阻塞浏览器继续执行下面的代码。AMD规范则采用异步的方式加载模块,允许指定回调函数,这非常适合用于浏览器端的模块化场景。
使用define定义一个模块,使用require加载模块;
异步加载,可以并行请求依赖模块;
原生JavaScript运行环境无法直接执行AMD规范的模块代码,需要引入第三方库支持,如requirejs等;
CMD
类似于AMD规范,是应用在浏览器端的JS模块化方案,由sea.js提出,详见 https://www.zhihu.com/question/20351507 。
UMD
UMD规范兼容AMD和CommonJS,在浏览器和Nodejs中均可以运行。
ES6 Module
ES6从语言标准的层面上实现了模块化,是ECMA提出的模块化标准,后续浏览器和Nodejs都宣布会原生支持,越来越受开发者青睐。
使用import引入模块,export导出模块;
与CommonJS的执行时机不同,只是个只读引用,只会在真正调用的地方开始执行,而不是像CommonJS那样,在require的时候就会执行代码;
支持度暂不完善,需要进行代码转换成上面介绍的某一种模块化规范。
在浏览器中可以通过下面的方式引入es6规范的模块js:
defer和async不同,它会阻塞DomContentLoaded事件,每个模块js会根据引入的顺序依次执行。
随着更多浏览器对ES6的支持,现在有一些方案开始提出直接使用ES2015+的代码在浏览器中直接执行来提高运行效果,这篇文章《Deploying ES2015+ Code in Production Today》中有详细的介绍,可以结合这份性能测试报告综合评估ES6在node以及各种浏览器环境下的执行效率对比。
3. CSS模块化
CSS 自诞生以来,基本语法和核心机制一直没有本质上的变化,它的发展几乎全是表现力层面上的提升。不同于JS,CSS本身不具有高级编程属性,无法使用变量、运算、函数等,无法管理依赖,全局作用域使得在编写CSS样式的时候需要更多人工去处理优先级的问题,样式名还有压缩极限的问题,为此,出现了很多“编译工具”和“开发方案”为CSS赋予“编程能力”。
预处理器
随着页面越来越复杂,为了便于开发和维护,我们常常会将CSS文件进行切分,然后再将需要的文件进行合并。诸如LESS、SASS、Stylus等预处理器为CSS带来了编程能力,我们可以使用变量、运算、函数,@import指令可以轻松合并文件。但各种预处理器并不能完全解决全局作用域的问题,需要结合namespace的思想去命名。
OOCSS & SMACSS
OOCSS和SMACSS都是有关css的方法论。OOCSS(Object Oriented CSS)即面向对象的CSS,旨在编写高可复用、低耦合和高扩展的CSS代码,有两个主要原则,它们都是用来规定应该把什么属性定义在什么样式类中。
Separate structure and skin(分离结构和主题)
Separate container and content(分离容器和内容)
SMACSS(Scalable and Modular Architecture for CSS)是可扩展模块化的CSS,它的核心就是结构化CSS代码,则有三个主要规则:
Categorizing CSS Rules (CSS分类规则):将CSS分成Base、Layout、Module、State、Theme这5类。
Naming Rules(命名规则):考虑用命名体现样式对应的类别,如layout-这样的前缀。
Minimizing the Depth of Applicability(最小化适配深度):降低对特定html结构的依赖。
BEM
BEM是一种CSS命名规范,旨在解决样式名的全局冲突问题。BEM是块(block)、元素(element)、修饰符(modifier)的简写,我们常用这三个实体开发组件。
块(block):一种布局或者设计上的抽象,每一个块拥有一个命名空间(前缀)。
元素(element):是.block的后代,和块一起形成一个完整的实体。
修饰符(modifier):代表一个块的状态,表示它持有的一个特定属性。
在选择器中,BEM要求只使用类名,不允许使用id,由以下三种符号来表示扩展的关系:
中划线( - ) :仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。
双下划线( __ ):双下划线用来连接块和块的子元素。
单下划线( _ ):单下划线用来描述一个块或者块的子元素的一种状态。
从上面BEM的命名要求可以看到,类名都很长,这就导致在对CSS文件进行压缩的时候,我们无法得到更大的优化空间。而且BEM仅仅是一种规范,需要团队中的开发者自行遵守,在可靠性上无法得到有效保障,而且还可能和第三方库的命名冲突。
CSS in JS
CSS in JS是一种比较激进的方案,彻底抛弃了CSS,完全使用JS来编写CSS,又用起了行内样式(inline style),它的发展得益于React的出现,具体的原因可以参见组件化这部分内容。
解决全局命名污染的问题;
更贴近Web组件化的思想;
可以在一些无法解析CSS的运行环境下执行,比如React Native等;
JS赋予CSS更多的编程能力,实现了CSS和JS间的变量共享;
支持CSS单元测试,提高CSS的安全性;
原生JS编写CSS无法支持到很多特性,比如伪类、media query等,需要引入额外的第三方库来支持,各种库的对比详见css-in-js;
有运行时损耗,性能比直接class要差一些;
不容易debug;
下面以styled-components为例:
构建后的结果如下,我们发现不会再有.css文件,一个.js文件包含了组件相关的全部代码:
CSS module
CSS module则最大化地结合了现有CSS生态和JS模块化的能力,以前用于CSS的技术都可以继续使用。CSS module最终会构建出两个文件:一个.css文件和一个.js。
解决全局命名污染的问题;
默认是局部的,可以用:global声明全局样式;
受CSS的限制,只能一层嵌套,和JS无法共享变量;
能支持现在所有的CSS技术。
以webpack为例,使用css-loader就可以实现CSS module:
module.exports = { ... module: { rules: [ ... { loader: 'css-loader', options: { importLoaders: 1, modules: { localIdentName: "[name]__[local]--[hash:base64:5]" }, } } ... ] } ...
}
/* style.css */
.color { color: green;
} :local .className .subClass :global(.global-class-name) { color: blue;
}
/* component.js */
import styles from './style.css';
elem.outerHTML = `<h1 class=${styles.color}>It is a test title</h1>`;
构建运行后生成的dom结构如下:
<h1 class="style__color--rUMvq">It is a test title</h1>
component.js中styles变量的值如下,我们看到声明成:global的类名.global-class-name没有被转换,具有全局作用域。
说明:React对样式如何定义并没有明确态度,无论是BEM规范,还是CSS in JS或者CSS module都是支持的,选择何种方案是开发者自行决定的。
组件化
最初,网页开发一般都会遵循一个原则”关注点分离”,各个技术只负责自己的领域,不能混合在一起,形成耦合。HTML只负责结构,CSS负责样式,JS负责逻辑和交互,三者完全隔离,不提倡写行内样式(inline style)和行内脚本(inline script)。React的出现打破了这种原则,它的考虑维度变成了一个组件,要求把组件相关的HTML、CSS和JS写在一起,这种思想可以很好地解决隔离的问题,每个组件相关的代码都在一起,便于维护和管理。
我们回想一下原有引用组件的步骤:
引入这个组件的JS;
引入这个组件的样式CSS(如果有);
在页面中引入这个组件的;
最后是编写初始化组件的代码。
这种引入方式很繁琐,一个组件的代码分布在多个文件里面,而且作用域暴露在全局,缺乏内聚性容易产生冲突。
组件化就是将页面进行模块拆分,将某一部分独立出来,多个组件可以自由组合形成一个更复杂的组件。组件将数据、视图和逻辑封装起来,仅仅暴露出需要的接口和属性,第三方可以完全黑盒调用,不需要去关注组件内部的实现,很大程度上降低了系统各个功能的耦合性,并且提高了功能内部的聚合性。
1.React、Vue、Angular…
React、Vue、Angular等框架的流行推动了Web组件化的进程。它们都是数据驱动型,不同于DOM操作是碎片的命令式,它允许将两个组件通过声明式编程建立内在联系。
从上面的例子可以看到,声明式编程让组件更简单了,我们不需要去记住各种DOM相关的API,这些全部交给框架来实现,开发者仅仅需要声明每个组件“想要画成什么样子”。
JSX vs 模板DSL
React使用JSX,非常灵活,与JS的作用域一致。Vue、Angular采用模板DSL,可编程性受到限制,作用域和JS是隔离的,但也是这个缺点使得我们可以在构建期间对模板做更多的事情,比如静态分析、更好地代码检查、性能优化等等。二者都没有浏览器原生支持,需要经过Transform才能运行。
2.Web Component
Web Component是W3C专门为组件化创建的标准,一些Shadow DOM等特性将彻底的、从浏览器的层面解决掉一些作用域的问题,而且写法一致,它有几个概念:
Custom Element: 带有特定行为且用户自命名的 HTML 元素,扩展HTML语义;
<x-foo>Custom Element</x-foo>
/* 定义新元素 */
var XFooProto = Object.create(HTMLElement.prototype); // 生命周期相关
XFooProto.readyCallback = function() { this.textContent = "I'm an x-foo!";
}; // 设置 JS 方法
XFooProto.foo = function() { alert('foo() called'); }; var XFoo = document.register('x-foo', { prototype: XFooProto }); // 创建元素
var xFoo = document.createElement('x-foo');
Shadow DOM:对标签和样式的一层 DOM 封装,可以实现局部作用域;当设置{mode: closed}后,只有其宿主才可定义其表现,外部的api是无法获取到Shadow DOM中的任何内容,宿主的内容会被Shadow DOM掩盖。
var host = document.getElementById('js_host');
var shadow = host.attachShadow({mode: 'closed'});
shadow.innerHTML = '<p>Hello World</p>';
Chrome调试工具:DevTool > Settings > Preferences> Show user agent shadow DOM
Chrome调试工具查看shadow DOM
HTML Template & Slots: 可复用的 HTML 标签,提供了和用户自定义标签相结合的接口,提高组件的灵活性。定义了template的标签,类似我们经常用的<script type='tpl'>,它不会被解析为dom树的一部分,template的内容可以被塞入到Shadow DOM中并且反复使用;template中定义的style只对该template有效,实现了隔离。
dom树中的template标签,不解析:
HTML template-1
最终插入的影子节点效果:
HTML template-2
由于Shadow DOM中宿主元素的内容会被影子节点掩盖,如果想将宿主中某些内容显示出来的话就需要借助slot,它是定义在宿主和template中的一个插槽,用来“占位”。
<div id="host"> <span>Test1</span> <span slot="s1">slot1</span> <span slot="s2">slot2</span> <span>Test2</span>
</div>
<template id="tpl"> <span>tpl1</span> <slot name="s1"></slot> <slot name="s2"></slot> <span>tpl2</span>
</template>
宿主元素中设置了slot属性的节点被“保留”了下来,并且插入到了template中定义的slot的位置。
slot的示例
HTML Imports: 打包机制,将HTML代码以及Web Componnet导入到页面中,这个规范目前已经不怎么推动了,在参考了ES6 module的机制后,FireFox团队已经不打算继续支持。
Polymer
Polymer是基于Web Componet的一种数据驱动型开发框架,可以使用ES6 class来定义一个Web Component,由于现在浏览器对Web Component的支持度还不是很好,需要引入一些polyfill才能使用。
React和Web Component并不是对立的,它们解决组件化的角度是不同,二者可以相互补充。与Web Component不同的是React中的HTML标签运行在Virtual DOM中,在非标准的浏览器环境,React的这种机制可以更好地实现跨平台,Web Component则更有可能实现浏览器大统一,是浏览器端更彻底的一种解决方案。
规范化
规范化是保障项目质量的一个重要环节,可以很好地降低团队中个体的差异性。
1.代码规范
代码规范是一个老生常谈的话题,我们需要制定一些原则来统一代码风格,虽然不遵守规范的代码也是可以运行的,但是这会对代码的维护带来很多麻烦。
Lint
根据维基百科的介绍,首先看一下lint的定义:
lint最初是一个特定程序的名称,它在C语言源代码中标记了一些可疑的和不可移植的构造(可能是bug)。这个术语(lint或者linter)现在一般用于称呼那些可以标记任何计算机语言编写的软件中可疑用法的工具,这些工具通常执行源代码的静态分析。
一般代码的Linter工具提供下面两大类的规则:
格式化规则:比如 max-len, no-mixed-spaces-and-tabs等等,这些规则只是用来统一书写格式的。
代码质量规则:比如 no-unused-vars, no-extra-bind, no-implicit-globals等等,这些规则可以帮助提升代码质量,减少bug。
在实际的项目中可以引入lint的机制来提升代码质量,可以参考GitHub 官方出品的 Lint 工具列表 ,下面简单介绍几个常用工具。
Prettier
Prettier是一个代码格式化工具,可以统一团队中的书写风格,比下面Eslint这类工具的功能要弱,因为只是对格式上的约束,无法对代码质量进行检测。
ESlint
ESLint是一款非常常用的JS编程规范库,当然还有很多其他的lint工具。下面的表格里简单介绍了3种常用的规范标准,可以在ESLint中配置选择哪一种标准,每一种标准都会包含很多编程规则。各个标准没有绝对的孰优孰劣,选择适用于团队的编程风格和规范就好。
标准 | 简介 |
---|---|
Airbnb JavaScript Style Guide | 目前最受欢迎的JS编程规范之一,对很多JS框架都有支持,比如React等。 |
Google JavaScript Style Guide | Google Style的JS编程规范。 |
JavaScript Standard Style Guide | 很强大,自带linter和自动代码纠正,无需配置,自动格式化代码。很多知名公司所采用,比如 Nodejs、npm、express、GitHub、mongoDB 等。 |
husky
如果我们把Lint放在了持续集成CI阶段,就会遇到这样一个问题:CI系统在Lint时发现了问题导致构建失败,这个时候我们需要根据错误重新修改代码,然后重复这个过程直到Lint成功,整个过程可能会浪费掉不少时间。针对这个问题,我们发现只在CI阶段做Lint是不够的,需要把Lint提前到本地来缩短整个修改链路。但是将Lint放在本地仅仅依靠开发者的自觉遵守是不够的,我们需要更好的方案,需要依靠流程来保障而不是人的自觉性。
Lint的问题
husky可以注册git hooks,拦截一些错误的提交,比如我们就可以在pre-commit这个hook中增加Lint的校验,这里可以查看支持的git hooks。
lint-staged
通过husky注册的git hook会对仓库中的全部文件都执行设置的npm命令,但我们仅仅需要对提交到staged区的文件进行处理来减少校验时间,lint-staged可以结合husky实现这个功能,在package.json中的示例:
{ "husky": { "hooks": { "pre-commit": "lint-staged", } }, "lint-staged": { "src/**/*.js": "eslint" }
}
类型检查
function add(a, b) { return a + b;
}
add(1, 2);
add('1', '2');
import React, { Component } from 'react';
import PropTypes from 'prop-types'; class App extends Component { } App.propTypes = { title: PropTypes.string.isRequired
}
- 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。
- 类型注释:事先注释好我们期待的类型,Flow会基于这些注释来判断。
function split(str) { return str.split(' ')
}
split(11); function square(n: number): number { return n * n;
}
square("2");
interface Person { firstName: string; lastName: string;
} function greeter(person: Person) { return "Hello, " + person.firstName + " " + person.lastName;
}
2.文档规范
- JSDoc:根据.js文件中的注释信息,生成API文档。
- Docz:基于MDX的高效、零配置的文档生成工具,目前仅支持React。
- Storybook:集组件开发、查看、测试的文档工具,支持React、RN、Vue、Angular、Polymer等很多框架,非常强大。
- react-styleguidist:和Storybook类似,生成React组件开发环境的文档服务,基于webpack支持HRM。
3.流程规范
github-flow
- master分支一直是可部署的状态,这意味着不要直接在master分支上进行push操作;
- 每次开发都从master分支创建一个新的特性分支,命名需要有含义;
- 在远端创建对应的origin/特性分支,定期push;
- 开发测试完毕后需要merge的时候,创建Pull Request进行交流;
- 其他开发者review这次Pull Request,确认后与master分支进行合并。
- 立刻部署合并后的master分支代码,删除该分支。
git-flow
gitlab-flow
自动化
1.构建
2.测试
- 单元测试:确保每个组件/模块正常工作
- 集成测试:在单元测试的基础上,确保组装成模块、子系统或系统的过程中各部分正常合作
- 系统测试:在集成测试的基础上,确保整个应用运行正常
- 验收测试:也称交付测试,是针对用户需求、业务流程进行的正式的测试,以保证达到验收标准
“怎么单元测试写起来这么麻烦”
——说明项目模块之间存在耦合度高,依赖性强的问题。
“怎么要写这么长的测试代码啊”
——这是一劳永逸的,并且每次需求变更后,你都可通过单元测试来验证,逻辑代码是否依旧正确。
“我的模块没问题的,是你的模块出了问题”
——程序中每一项功能我们都用测试来验证的它的正确性,快速定位出现问题的某一环。
“上次修复的 bug 怎么又出现了 ”
——单元测试能够避免代码出现回归,编写完成后,可快速运行测试。
- 测试环境
运行环境 | 特点 |
jsdom
|
node端直接运行,伪浏览器环境,速度快,内置BOM对象,目前也有了对sessionStorage、localStorage和cookie的支持。
|
puppeteer
|
在真实的浏览器中运行测试,很方便,但是运行速度会慢一点。
|
phantomjs
|
无头浏览器,在puppeteer发布后,作者已经宣布不维护了。
|
- 测试工具
CI/CD
DevOps是Development和Operations的组合,是一种方法论,是一组过程、方法与系统的统称,用于促进应用开发、应用运维和质量保障(QA)部门之间的沟通、协作与整合。以期打破传统开发和运营之间的壁垒和鸿沟。
持续集成(Continuous Integration)中开发人员需要频繁地向主干提交代码,这些新提交的代码在最终合并到主干前,需要经过编译和自动化测试(通常是单元测试)进行验证。
持续交付(Continuous Delivery)指的是,频繁地将软件的新版本,交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。
在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等流程。
我们的项目构建现状
1.现状分析
- 模块化
- 重复开发,复制粘贴
- 组件css的问题
var css ='.qui_dialog__mask{position:fixed;top:0;left:0;bottom:0;right:0;}...';
appendToHead(css);
2.技术选型
主流构建工具
CSS模块化
单元测试框架
Lint方案
工作流和CI?
我们的工程化实践
1.构建方案
新旧方案对比
打包方案
开发流程
2.UI组件开发和文档
### 组件式引入
- 可以提前插入dom结构,如果浮层中有图片的话会先加载;
- 属性中的 `visible` 控制组件是否可见。
```jsx
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 />
```
3.Jest单元测试
测试方案
浏览器端
- 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端
UI组件
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();
mock
.
├── config
├── src
│ ├── music
│ │ ├── utils
│ │ │ ├── __mock__
│ │ │ └── loadUrl.js
│ │ └── loadUrl.js
├── node_modules
├── ...
└── tests
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;
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); });
})
测试覆盖率
通过单元测试发现的代码bug
测试用例 | 错误输出 | 正确输出 |
M.type(undefined)
|
"nan"
|
"undefined"
|
M.isPlainObject(Object.creact({}))
|
false
|
true
|
Mozilla/5.0 (Linux; U; en-us; KFTT Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.21 Safari/535.19 Silk-Accelerated=true<br />M.os.tablet
|
false
|
true
|
M.param({a: 1, b: {c: 1}})
|
"a=1&b=c%3D1"
|
"a=1&b%5Bc%5D=1"
|
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。这个功能亲测是很有效的
插件
- @babel/plugin-transform-runtime
- eslint-friendly-formatter
- babel-plugin-transform-react-remove-prop-types
—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; }; }())
);
我们是怎么进行前端工程化的
我对前端工程化的理解
浅谈 CSS 预处理器(一):为什么要使用预处理器?
CSS BEM 书写规范
CSS Modules使用详解
深入探讨前端组件化开发
CSS-in-JS,向Web组件化再迈一大步
jsx与模板dsl的优劣思考
从年会看声明式编程(Declarative Programming)
React & Web Components
The state of Web Components
用 husky 和 lint-staged 构建超溜的代码检查工作流
JEST
值得参考的css理论:OOCSS、SMACSS与BEM
浅谈 shadow dom 中的 template 和 slot
GitHub Flow & Git Flow 基于Git 的两种协作开发模式
git-flow的工作流程
Git工作流程
Introduction to GitLab Flow
软件测试的四个阶段,单元测试、集成测试、系统测试、验收测试
The Product Managers’ Guide to Continuous Delivery and DevOps
一文收录16张DevOps ”拍照神图”
DevOps漫谈之一:DevOps、CI、CD都是什么鬼?
Webhook到底是个啥?
Webpack中的sideEffects到底该怎么用?
https://cloud.tencent.com/developer/article/1500013
如果你对QQ音乐商业化Web团队感兴趣,想加入我们,请前往:
https://www.lagou.com/jobs/2944745.html
“ 公众号后台回复“加群”,可加入「前端技术群」共同学习交流”
前端工程化实践总结 | QQ音乐商业化Web团队相关推荐
- 前端工程化实践:从开发到构建测试部署——由此及彼
前端工程化实践 前端工程化实践 什么叫前端工程化 从实践方面来说 从理论角度来讲 前端工程化的意义: 1.规范化(从源头处约束到优雅-聚沙成塔) 1. 制定各项开发规范,让工作有章可循 2. 针对于版 ...
- 前端工程化实践 - 代码规范 提交规范 构建流程 Monorepo(附 React Native 案例)
前端工程化实践 - 代码规范 & 提交规范 & 构建流程 & Monorepo 前言 仓库策略 Multirepo 什么是 Multirepo? Multirepo 的优点 M ...
- QQ音乐客户端Web页面通用性能优化实践
导语 | QQ音乐 Android 客户端的 Web 页面日均 PV 达到千万量级,然而页面的打开耗时与 Native 页面相距甚远,需要系统性优化.本文将介绍 QQ 音乐 Android 客户端在进 ...
- 前端类名优秀命名例子_这是一篇需要花费你15分钟阅读的干货!浅谈前端工程化...
01前端工程化的背景 随着业务的扩展.业务需求更加复杂.项目团队的壮大以及项目的增多等.制定一套适用于团队的前端工程化方案很有必要. 02前端工程化是什么 前端工程化是一个很广泛的话题.涉及的技术与解 ...
- 分享狼叔关于《大前端工程化的实践与思考》
前言 本文来自极客前端训练营的主题公开课,非原创. 作者简介 桑世龙(狼叔),阿里巴巴前端技术专家,nodejs<狼书>作者. 快速发展的大背景 前端发展太快了,在2004年之前,大概只要 ...
- QQ音乐PB级ClickHouse实时数据平台架构演进之路
导语 | OLAP(On-Line Analytical Processing),是数据仓库系统的主要应用形式,帮助分析人员多角度分析数据,挖掘数据价值.本文基于QQ音乐海量大数据实时分析场景,通过Q ...
- 前端架构,前端工程化
前端架构: 1.前端工程化 web应用复杂度的增加,特别是单页面应用的风靡.组件化,工程化,自动化成了前端发展的趋势.或者说一线的互联网公司就是这么做的. 每个前端团队都在打造自己的前端开发体系,这通 ...
- [API ]新浪微博腾讯QQ音乐网易云音乐小米云钉钉笔记百度高德地图淘宝阿里云 API
常用&免费 微博: 需要先注册登录 https://open.weibo.com/wiki/API bilibili b站(part1@社会易姐QwQ) https://github.com/ ...
- QQ音乐Android客户端Web页面通用性能优化实践
QQ音乐 Android 客户端的 Web 页面日均 PV 达到千万量级,然而页面的打开耗时与 Native 页面相距甚远,需要系统性优化.本文将介绍 QQ 音乐 Android 客户端在进行 Web ...
最新文章
- 生活娱乐 在上海怎么租房
- GCD介绍(一): 基本概念和Dispatch Queue
- 冻结拆分_冻结首行与尾行?还有能这种操作
- java、python什么意思_Python为什么叫Python,Java又如何而来?
- 经典的 div + css 鼠标 hover 下拉菜单
- 《大型数据库技术》MySQL的进阶开发技巧
- 理解伪元素:before和:after
- oracle数据库服务器c盘满,Oracle数据库服务器磁盘满导致数据库无法登陆,通过清理归档文件解决...
- Flink 执行引擎:流批一体的融合之路
- 百度手机输入法中的五笔9键盘有问题?
- Git(10):删除远端仓库中多余文件(**.iml/target文件)
- Java-实现图书管理系统
- jboss 服务器配置文件,Apache jboss 集群详细配置
- 计算机中文无敌版,与电脑下象棋无敌版
- 计算机机房搬迁预算,信息中心机房整体搬迁方案.doc
- 手机日历怎么备注农历生日提醒
- ERROR: operator does not exist: integer = character varying
- VS2019++QT5.12.10+PCL1.11.1+VTK8.2.0+opencv(camke3.18.0)环境搭配及演示实例
- 物理引擎学习06-碰撞反馈
- 《漫画傅里叶解析》笔记(6)
热门文章
- 中国人工智能算力城市排行榜:京、深、杭、沪、渝位列前五
- Win10 蓝屏CRITICAL_PROCESS_DIED值为 0x000000EF
- 高考作文题“幸存者偏差”难哭了?这有份标准答案
- 大话电脑(转自知乎Jeffersli)
- js添加到桌面快捷方式(实现功能)
- 技术分享电商 API 获取商品详情返回值说明(可测试)
- 架构道术-从心流视角看费曼学习法
- 加密硬件货币钱包-市场现状及未来发展趋势
- windows记事本特别注意
- 班级计算机应用论坛研讨,以“心”育人,处处花开——我校教师参加第四届中小学班主任高峰论坛暨班级管理策略专题研讨会...