时间: 2020-09-03 00:08:26 人气: 2283 评论: 0
本文主要介绍我们在前端工程化的一些探索和实践,结合移动端的基础库重构和UI组件库开发这两个项目详细介绍我们的工程化方案。
随着业务的不断扩展,团队的项目越来越多,面对日益复杂的业务场景和代码逻辑,我们发现在前端工程化方面团队还停留在非常原始的阶段。现有的解决方案已经无法满足各种复杂的场景,我们每天都在疲于应付很多重复的工作,为此我们基于移动端基础库重构和UI组件库的建设这两个项目对团队的项目构建流程进行了详细的分析和梳理,并制定了一套适用于团队的工程化方案。
前端工程化是一个非常广泛的议题,包含的技术和解决方案也是非常丰富的。一个前端工程的生命周期可以大致划分为这四个过程:
任何在这四个过程中应用的系统化、严格约束、可量化的方法都可以称之为工程化。工程化的程度越高,在工作中因人的个体差异性导致的缺陷或者短板就会越少,项目质量可以得到更有效的保障。对上面四个过程的工程化并不是完全分隔的,而是相辅相成,比如开发阶段的优化也会对测试、部署和维护产生很大的影响。
下面从模块化、组件化、规范化和自动化这四个方面进行具体介绍。
模块化可以对复杂逻辑进行有效分割,每个模块更关注自身的功能,模块内部的数据和实现是私有的,通过向外部暴露一些接口来实现各模块间的通信。开发阶段前端需要关注JS、CSS和HTML,下面我们将分别对JS、CSS、HTML的模块化进行简单介绍。
JS模块化是一个逐渐演变的过程,开始的namespace概念实现了简单对象封装,约定私有属性使用_开头,到后来的IIFE模式,利用匿名函数闭包的原理解决模块的隔离与引用,下面介绍现在比较流行的几种模块化标准。
Nodejs中的模块化方案,就是基于CommonJS规范实现的。一个文件就是一个模块,有自己的作用域,没有export的变量和方法都是私有的,不会污染全局作用域,模块的加载是运行时同步加载的。CommonJS可以细分为CommonJS1和CommonJS2,二者的模块导出方式不同,CommonJS2兼容CommonJS1,增加了module.exports的导出方式,现在一般所指的都是CommonJS2。
require同步加载依赖的其他模块,通过module.exports导出需要暴露的接口;require的同一模块只会在第一次加载时运行,并将运行结果缓存,后续直接读取缓存结果,如果需要重新执行,需要先清理缓存;// sum.js // CommonJS2 module.exports.add = function (a, b) { return a + b; } // CommonJS1 exports.add = function (a, b) { return a + b; } // main.js const sum = require('sum'); sum.add(1, 2);
浏览器加载js文件需要进行网络请求,而网络请求的耗时是不可预期的,这使得CommonJS同步加载模块的机制在浏览器端并不适用,我们不能因为要加载某个模块js而一直阻塞浏览器继续执行下面的代码。AMD规范则采用异步的方式加载模块,允许指定回调函数,这非常适合用于浏览器端的模块化场景。
define定义一个模块,使用require加载模块;// 定义一个模块 define(id ? , dependencies ? , factory); // 引用一个模块 require([module], callback)
类似于AMD规范,是应用在浏览器端的JS模块化方案,由sea.js提出,详见 https://www.zhihu.com/question/20351507 。
UMD规范兼容AMD和CommonJS,在浏览器和Nodejs中均可以运行。
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['jquery', 'underscore'], factory); } else if (typeof exports === 'object') { // Node, CommonJS之类的 module.exports = factory(require('jquery'), require('underscore')); } else { // 浏览器器全局变量量(root 即 window) root.returnExports = factory(root.jQuery, root._); } }(this, function ($, _) { // 方法 function a() {}; // 私有方法,因为它没被返回 (见下面) function b() {}; // 公共方法,因为被返回了 function c() {}; // 公共方法,因为被返回了 // 暴露公共⽅方法 return { b: b, c: c } }));
ES6从语言标准的层面上实现了模块化,是ECMA提出的模块化标准,后续浏览器和Nodejs都宣布会原生支持,越来越受开发者青睐。
import引入模块,export导出模块;require的时候就会执行代码;在浏览器中可以通过下面的方式引入es6规范的模块js:
<script type="module" src="foo.mjs"></script> <!-- 等同于 --> <script type="module" src="foo.mjs" defer></script>
defer和async不同,它会阻塞DomContentLoaded事件,每个模块js会根据引入的顺序依次执行。
随着更多浏览器对ES6的支持,现在有一些方案开始提出直接使用ES2015+的代码在浏览器中直接执行来提高运行效果,这篇文章Deploying ES2015+ Code in Production Today中有详细的介绍,可以结合这份性能测试报告综合评估ES6在node以及各种浏览器环境下的执行效率对比。
CSS 自诞生以来,基本语法和核心机制一直没有本质上的变化,它的发展几乎全是表现力层面上的提升。不同于JS,CSS本身不具有高级编程属性,无法使用变量、运算、函数等,无法管理依赖,全局作用域使得在编写CSS样式的时候需要更多人工去处理优先级的问题,样式名还有压缩极限的问题,为此,出现了很多“编译工具”和“开发方案”为CSS赋予“编程能力”。
随着页面越来越复杂,为了便于开发和维护,我们常常会将CSS文件进行切分,然后再将需要的文件进行合并。诸如LESS、SASS、Stylus等预处理器为CSS带来了编程能力,我们可以使用变量、运算、函数,@import指令可以轻松合并文件。但各种预处理器并不能完全解决全局作用域的问题,需要结合namespace的思想去命名。
OOCSS和SMACSS都是有关css的方法论。OOCSS(Object Oriented CSS)即面向对象的CSS,旨在编写高可复用、低耦合和高扩展的CSS代码,有两个主要原则,它们都是用来规定应该把什么属性定义在什么样式类中。
SMACSS(Scalable and Modular Architecture for CSS)是可扩展模块化的CSS,它的核心就是结构化CSS代码,则有三个主要规则:
layout-这样的前缀。/* 依赖html结构,不提倡 */ .sidebar ul h3 { } /* 建议直接定义 */ .sub-title { }
BEM是一种CSS命名规范,旨在解决样式名的全局冲突问题。BEM是块(block)、元素(element)、修饰符(modifier)的简写,我们常用这三个实体开发组件。
.block的后代,和块一起形成一个完整的实体。在选择器中,BEM要求只使用类名,不允许使用id,由以下三种符号来表示扩展的关系:
.type-block__element_modifier {}
从上面BEM的命名要求可以看到,类名都很长,这就导致在对CSS文件进行压缩的时候,我们无法得到更大的优化空间。而且BEM仅仅是一种规范,需要团队中的开发者自行遵守,在可靠性上无法得到有效保障,而且还可能和第三方库的命名冲突。
CSS in JS是一种比较激进的方案,彻底抛弃了CSS,完全使用JS来编写CSS,又用起了行内样式(inline style),它的发展得益于React的出现,具体的原因可以参见组件化这部分内容。
下面以styled-components为例:
import React from 'react'; import styled from 'styled-components'; const Container = styled.div` text-align: center; `; const App = () => ( <Container> It is a test! </Container> ); render(<App />, document.getElementById('content'));
构建后的结果如下,我们发现不会再有.css文件,一个.js文件包含了组件相关的全部代码:
var _templateObject = _taggedTemplateLiteral(['\n text-align: center;\n'], ['\n text-align: center;\n']); function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } var Container = _styledComponents2.default.div(_templateObject); var App = function App() { return _react2.default.createElement( Container, null, 'It is a test!' ); };
CSS module则最大化地结合了现有CSS生态和JS模块化的能力,以前用于CSS的技术都可以继续使用。CSS module最终会构建出两个文件:一个.css文件和一个.js。
:global声明全局样式;以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没有被转换,具有全局作用域。
const styles = { "color": "style__color--rUMvq", "className": "style__className--3n_7c", "subClass": "style__subClass--1lYnt" }
说明:React对样式如何定义并没有明确态度,无论是BEM规范,还是CSS in JS或者CSS module都是支持的,选择何种方案是开发者自行决定的。
最初,网页开发一般都会遵循一个原则”关注点分离”,各个技术只负责自己的领域,不能混合在一起,形成耦合。HTML只负责结构,CSS负责样式,JS负责逻辑和交互,三者完全隔离,不提倡写行内样式(inline style)和行内脚本(inline script)。React的出现打破了这种原则,它的考虑维度变成了一个组件,要求把组件相关的HTML、CSS和JS写在一起,这种思想可以很好地解决隔离的问题,每个组件相关的代码都在一起,便于维护和管理。
我们回想一下原有引用组件的步骤:
这种引入方式很繁琐,一个组件的代码分布在多个文件里面,而且作用域暴露在全局,缺乏内聚性容易产生冲突。
组件化就是将页面进行模块拆分,将某一部分独立出来,多个组件可以自由组合形成一个更复杂的组件。组件将数据、视图和逻辑封装起来,仅仅暴露出需要的接口和属性,第三方可以完全黑盒调用,不需要去关注组件内部的实现,很大程度上降低了系统各个功能的耦合性,并且提高了功能内部的聚合性。
React、Vue、Angular等框架的流行推动了Web组件化的进程。它们都是数据驱动型,不同于DOM操作是碎片的命令式,它允许将两个组件通过声明式编程建立内在联系。
<!-- 数据驱动的声明式Declarative--> <pagination current={current} total={maxCount/20} on-nav={this.nav(1)}> </pagination> <!-- DOM操作的命令式Imprective --> <pagination id='pagination'></pagination> <script> // 获取元素 var pagination = document.querySelector('#pagination'); // 绑定事件 pagination.addEventListener('pagination-nav', function(event){ ... }) // 设置属性 $.ajax('/blogs').then(function( json ){ pagination.setAttribute('current', 0) pagination.setAttribute('total', json.length / 20) }) </script>
从上面的例子可以看到,声明式编程让组件更简单了,我们不需要去记住各种DOM相关的API,这些全部交给框架来实现,开发者仅仅需要声明每个组件“想要画成什么样子”。
React使用JSX,非常灵活,与JS的作用域一致。Vue、Angular采用模板DSL,可编程性受到限制,作用域和JS是隔离的,但也是这个缺点使得我们可以在构建期间对模板做更多的事情,比如静态分析、更好地代码检查、性能优化等等。二者都没有浏览器原生支持,需要经过Transform才能运行。
Web Component是W3C专门为组件化创建的标准,一些Shadow DOM等特性将彻底的、从浏览器的层面解决掉一些作用域的问题,而且写法一致,它有几个概念:
<x-foo>Custom Element</x-foo>
/* 定义新元素 */ var XFooProto = Object.create(HTMLElement.prototype) 技术沙龙 教程文章 热点综合