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

阅读数:311 2019 年 10 月 31 日 16:37

QQ音乐商业化Web团队:前端工程化实践总结(二)
  • 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

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

Chrome 调试工具查看 shadow DOM

HTML Template & Slots: 可复用的 HTML 标签,提供了和用户自定义标签相结合的接口,提高组件的灵活性。定义了 template 的标签,类似我们经常用的 < script type=‘tpl’ >,它不会被解析为 dom 树的一部分,template 的内容可以被塞入到 Shadow DOM 中并且反复使用;template 中定义的 style 只对该 template 有效,实现了隔离。

复制代码
<template id="tpl">
<style>
p {
color:red;
}
</style>
<p>hello world</p>
</template>
<script>
var host = document.getElementById('js_host');
var shadow = host.attachShadow({mode: 'open'});
var tpl = document.getElementById("tpl").content.cloneNode(true);
shadow.appendChild(tpl);
</script>

dom 树中的 template 标签,不解析:

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

HTML template-1

最终插入的影子节点效果:

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

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 的位置。

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

slot 的示例
  • HTML Imports
    打包机制,将 HTML 代码以及 Web Componnet 导入到页面中,这个规范目前已经不怎么推动了,在参考了 ES6 module 的机制后,FireFox 团队已经不打算继续支持。
复制代码
<link rel="import" href="/path/to/imports/stuff.html">
  • 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 中配置选择哪一种标准,每一种标准都会包含很多编程规则。各个标准没有绝对的孰优孰劣,选择适用于团队的编程风格和规范就好。

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

husky

如果我们把 Lint 放在了持续集成 CI 阶段,就会遇到这样一个问题:CI 系统在 Lint 时发现了问题导致构建失败,这个时候我们需要根据错误重新修改代码,然后重复这个过程直到 Lint 成功,整个过程可能会浪费掉不少时间。针对这个问题,我们发现只在 CI 阶段做 Lint 是不够的,需要把 Lint 提前到本地来缩短整个修改链路。但是将 Lint 放在本地仅仅依靠开发者的自觉遵守是不够的,我们需要更好的方案,需要依靠流程来保障而不是人的自觉性。

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

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"
}
}

类型检查
JavaScript 是非常灵活的,这得益于它的弱类型语言特点,但也是因为这个原因,我们只有在运行时才知道变量到底是什么类型,无法在编译阶段作出任何类型错误的提示,同时由于函数参数类型的不确定性,编译器的编译结果很可能无法被复用,比如下面的例子中,在执行 add(1,2) 时对 add 函数的编译结果无法直接被下面的 add(‘1’, ‘2’) 复用,第二次调用必须得再重新编译一次,这对性能也是有很大影响。

复制代码
function add(a, b) {
return a + b;
}
add(1, 2);
add('1', '2');

类型检查可以让我们编写出更高质量的代码,减少类型错误的 bug,同时明确了类型也让代码更好维护。

PropTypes
React 在 15.5 的版本后将类型检查 React.PropTypes 移除后使用 prop-types 库代替,它是一种运行时的类型检测机制,包含一整套验证器,可用于确保组件属性接收的数据是正确的类型。
import React, { Component } from ‘react’;
import PropTypes from ‘prop-types’;

复制代码
class App extends Component {
}
App.propTypes = {
title: PropTypes.string.isRequired
}

Flow
和 PropTypes 不同,Flow 是一种静态类型检查器,由 Facebook 开源,赋予 JS 强类型的能力,在编译阶段就可以检测出是否有类型错误,可以被用于任何 JavaScript 项目。

Flow 主要有两个工作方式:

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。
  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。
复制代码
function split(str) {
return str.split(' ')
}
split(11);
function square(n: number): number {
return n * n;
}
square("2");

Flow 风格的代码不能直接在 JS 运行环境中执行,需要使用 babel 进行转换。就目前的发展和生态而言,Flow 离 TypeScript 的差距已经越来越遥远了,Vue 在 2.0 版本开始使用 flow.js,但从 3.0 起已经替换成了 TypeScript。

TypeScript
TypeScript 则是一种 JavaScript 语言的超集,强类型、支持静态类型检查,更像是一门“新语言”。Deno 已经支持直接运行 tcs 了,不需要进行转换。

复制代码
interface Person {
firstName: string;
lastName: string;
}
function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}

2. 文档规范

高质量的项目文档可以提高团队协作效率,便于后期优化维护。维护文档很重要,但是也很繁琐,我们经常会看到代码和文档南辕北辙互相矛盾,下面介绍几种文档构建工具,它们可以很好地帮助我们构建文档,而对于 React、Vue 等组件而言,使用 MDX 可以非常便捷构建 demo,极大减少人工保证代码和文档一致性的工作:

  • JSDoc:根据.js 文件中的注释信息,生成 API 文档。
  • Docz:基于 MDX 的高效、零配置的文档生成工具,目前仅支持 React。
  • Storybook:集组件开发、查看、测试的文档工具,支持 React、RN、Vue、Angular、Polymer 等很多框架,非常强大。
  • react-styleguidist:和 Storybook 类似,生成 React 组件开发环境的文档服务,基于 webpack 支持 HRM。

3. 流程规范

当团队在开发时,通常会使用版本控制系统来管理项目,常用的有 svn 和 git,如何合并代码、如何发布版本都需要相应的流程规范,这可以让我们规避很多问题,比如合并代码后出现代码丢失,又或者将别人未经测试的代码发布出去等等。下面主要介绍几种基于 git 的协作开发模式:

github-flow
以部署为中心的开发模式,持续且高速安全地进行部署,具体流程如下:

1.master 分支一直是可部署的状态,这意味着不要直接在 master 分支上进行 push 操作;
2. 每次开发都从 master 分支创建一个新的特性分支,命名需要有含义;
3. 在远端创建对应的 origin/ 特性分支,定期 push;
4. 开发测试完毕后需要 merge 的时候,创建 Pull Request 进行交流;
5. 其他开发者 review 这次 Pull Request,确认后与 master 分支进行合并。
6. 立刻部署合并后的 master 分支代码,删除该分支。

github-flow 的最大特点就是简单,只有一个 master 长期分支,但是由于要持续部署,当一个部署还未完成的时候,往往下一个 Pull Request 已经完成,这就导致在开发速度越来越快的时候,必须要让部署所需的一系列流程都是自动化的,比如有自动化测试、接入 CI 等。

git-flow
有两个长期分支 master 和 develop,这意味着不要直接在这两个分支上进行 push 操作,所有的开发都在 feature 分支上进行,详见文档。

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

git-flow 工作流

功能开发:首先从 develop 分支创建 feature 分支,然后和上面 github-flow 的流程类似,开发测试完毕后向 develop 分支发起 Pull Request,其他开发者 review 完毕后将此次 PR 合并至 develop 分支。

管理 Release:当 develop 分支可以 release 的时候,首先创建一个 release/ 版本号分支,然后对这个 release 分支打上 tag 后再合并到 develop 和 master 中去。

hotfix:当出现了紧急 bug 的时候,需要开启“hotfix”流程,和 release 不同的是,这个 hotfix 分支是基于 master 创建的,修复 bug 后提交到这个 hotfix 分支,然后又和 release 分支的处理非常类似,改动会被同时合并到 develop 和 master 中去,最后这个 hotfix 分支被删除掉。

gitlab-flow
github-flow 有一个问题,它要求 master 分支和生产环境是完全一致,一旦 PR 通过被合并到了 master 分支,就要立刻部署发布到生成环境,但是往往受限于产品发布时间,master 分支很可能会先于生产环境,这个时候不能依靠 master 分支来追踪线上版本。
git-flow 的流程比较复杂,需要维护两个长期分支 master 和 develop,开发过程要以 develop 分支为准,但是很多开发工具将 master 当做默认分支,就需要频繁切换分支。git-flow 的模式是基于“版本发布”,这对一些持续发布部署的项目不太适用。
gitlab-flow 则是上面两个工作流的综合,推出一个“上游优先”的最大原则,即只存在一个 master 主分支,它是所有分支的上游,只有合并到 master 上的代码才能应用到其他分支,详见文档。

持续发布
对于这种模式的项目,master 分支对应开发环境,然后需要再创建 pre-production 和 production 两个分支,它们的上游链路依次是:master 分支—>pre-production 分支—>production 分支,只有合并进入 master 分支的代码修改才能依次应用合并到”下游”。

版本发布
在这种模式下,首先基于 master 分支创建某个版本的 stable 分支,然后将代码改动合并进 master 分支,当需要发版本的时候,将 master 分支使用 cherry-pick 合并到 stable 分支中去,然后基于 stable 分支进行项目的发布部署。

自动化

自动化是前端工程化的一个重要组成部分,可以减少重复的工作,提高工作效率。

1. 构建

在前端项目开发中我们使用了模块化的方案,有可能还引入了组件化的机制,依赖一些开发框架,这个时候就需要对项目进行构建,构建一般可以包括这几个步骤:

代码转换:允许使用更高级的 JavaScript 语法,比如 ES6、TypeScript 等等,这对代码的开发和可维护性来说是非常有好处的。

模块合并:按模块化开发的代码需要进行打包合并。

文件优化:常见的有代码压缩和 Code Splitting,使用 ES6 module 的模块化机制的还可以考虑构建工具的 Tree Shaking 功能,进一步减少代码体积。

自动刷新:在开发过程中支持 file watch 和 HMR 都是可以很好地提升开发效率。

2. 测试

在软件的生命周期中,不同的测试阶段,针对的测试问题是不一样的:

  • 单元测试:确保每个组件 / 模块正常工作
  • 集成测试:在单元测试的基础上,确保组装成模块、子系统或系统的过程中各部分正常合作
  • 系统测试:在集成测试的基础上,确保整个应用运行正常
  • 验收测试:也称交付测试,是针对用户需求、业务流程进行的正式的测试,以保证达到验收标准

JavaScript 单元测试,我们真的需要吗?答案是需要结合项目看实际情况。如果是基础库或者公共组件这样的项目,单元测试还是很有必要的。而对于那种就上线几天的活动页,写详细的单元测试可能真的会有点入不敷出。引用这篇文章结尾处是思考:

“怎么单元测试写起来这么麻烦”
——说明项目模块之间存在耦合度高,依赖性强的问题。
“怎么要写这么长的测试代码啊”
——这是一劳永逸的,并且每次需求变更后,你都可通过单元测试来验证,逻辑代码是否依旧正确。
“我的模块没问题的,是你的模块出了问题”
——程序中每一项功能我们都用测试来验证的它的正确性,快速定位出现问题的某一环。
“上次修复的 bug 怎么又出现了 ”
——单元测试能够避免代码出现回归,编写完成后,可快速运行测试。

TDD (测试驱动开发 Test-Driven Development)和 BDD (行为驱动开发 Behavior Driven Development)是两种开发模式,并不是单单指如何进行代码测试,它们定义了一种软件开发模式来将用户需求、开发人员和测试人员进行有效的联合,减少三者之间的脱节。TDD 要求开发者先写测试用例,然后根据测试用例的结果再写真正实现功能的代码,接下来继续运行测试用例,再根据结果修复代码,该过程重复多次,直到每个测试用例运行正确。BDD 则是对 TDD 的一种补充,我们无法保证在 TDD 中的测试用例可以完全达到用户的期望,那么 BDD 就以用户期望为依据,从用户的需求出发,强调系统行为。具体区别可以详见文章 The Difference Between TDD and BDD。

评论

发布