前端工程化实践总结 | QQ音乐商业化Web团队

蓝字关注,回复“加群”加入前端技术群 与大家一起成长

| 导语   本文主要介绍在前端工程化的一些探索和实践,结合移动端的基础库重构和UI组件库开发这两个项目详细介绍工程化方案 。

随着业务的不断扩展,团队的项目越来越多,面对日益复杂的业务场景和代码逻辑,我们发现在前端工程化方面团队还有很多需要优化的地方。现有的解决方案已经无法满足各种复杂的场景,我们每天都在疲于应付很多重复的工作,为此我们基于移动端基础库重构和UI组件库的建设这两个项目对团队的项目构建流程进行了详细的分析和梳理,并制定了一套适用于团队的工程化方案。

浅谈前端工程化

前端工程化是一个非常广泛的议题,包含的技术和解决方案也是非常丰富的。一个前端工程的生命周期可以大致划分为这四个过程:

640?wx_fmt=png

前端工程的生命周期

任何在这四个过程中应用的系统化、严格约束、可量化的方法都可以称之为工程化。工程化的程度越高,在工作中因人的个体差异性导致的缺陷或者短板就会越少,项目质量可以得到更有效的保障。对上面四个过程的工程化并不是完全分隔的,而是相辅相成,比如开发阶段的优化也会对测试、部署和维护产生很大的影响。

下面从模块化、组件化、规范化和自动化这四个方面进行具体介绍。


模块化

模块化可以对复杂逻辑进行有效分割,每个模块更关注自身的功能,模块内部的数据和实现是私有的,通过向外部暴露一些接口来实现各模块间的通信。开发阶段前端需要关注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 = `

It is a test title

`;

构建运行后生成的dom结构如下:

It is a test title

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写在一起,这种思想可以很好地解决隔离的问题,每个组件相关的代码都在一起,便于维护和管理。

我们回想一下原有引用组件的步骤:

  1. 引入这个组件的JS;

  2. 引入这个组件的样式CSS(如果有);

  3. 在页面中引入这个组件的;

  4. 最后是编写初始化组件的代码。

这种引入方式很繁琐,一个组件的代码分布在多个文件里面,而且作用域暴露在全局,缺乏内聚性容易产生冲突。

组件化就是将页面进行模块拆分,将某一部分独立出来,多个组件可以自由组合形成一个更复杂的组件。组件将数据、视图和逻辑封装起来,仅仅暴露出需要的接口和属性,第三方可以完全黑盒调用,不需要去关注组件内部的实现,很大程度上降低了系统各个功能的耦合性,并且提高了功能内部的聚合性。

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语义;

Custom Element
/* 定义新元素 */	
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 = '

Hello World

';

Chrome调试工具:DevTool > Settings > Preferences> Show user agent shadow DOM

640?wx_fmt=png

Chrome调试工具查看shadow DOM 

HTML Template & Slots: 可复用的 HTML 标签,提供了和用户自定义标签相结合的接口,提高组件的灵活性。定义了template的标签,类似我们经常用的

你可能感兴趣的:(前端工程化实践总结 | QQ音乐商业化Web团队)