一、背景
如果你和我一样,之前对于前端打包工具的发展一无所知,甚至于不知道这些工具出现的必要性。你可以浏览此部分的内容,如果你不想知道这些或者对这些并不感兴趣,可以直接跳过此部分。
互联网程序现状
随着移动互联的来袭,当前越来越多的网站已经从单纯的网页模式,开始升级为webapp模式。它们运行在现代的浏览器中,使用HTML5、CSS3、ES6等技术开发,已经从单一的浏览功能转变为一个基于浏览器的富客户端。并且webapp通常是一个SPA(Single Page Application 单页面应用)。每个页面(View)通过异步的方式加载,有着良好的用户体验。但是这样做的结果是导致程序初始化和使用的过程中需要更多、更复杂的JavaScript代码来实现,这就对前端程序的开发带来巨大的挑战!
模块化系统的演变
随着程序的复杂性的增加,项目结构的庞大。把单一js文件按职责进行模块化划分。
我们在写页面的时候会这样写:
<script src="base.js"></script> <script src="utils.js"></script> <script src="vipPush.js"></script>
这是最基础的JavaScript加载方式,每个JS的所有方法和属性都是暴露在window对象中的(就像把所有代码都放在一个命名空间或者同一个包下),借助全局对象,我们就能使用这些属性和方法。如果更为复杂的程序会使用命名空间的概念来组织这些模块的接口,比如:YUI
这种开发方式带来的弊端:
-
全局的作用域下容易造成变量的相互冲突(这是一个很常见的问题)
-
文件只能按照<script>的书写顺序进行加载
-
开发者要解决各个模块和代码库之间的依赖
-
如果按照此模式进行开发,长期下去整个项目(前端)代码必定会混乱不堪
因为有了模块的概念,让我们的开发变得比较方便。让我们可以很方便的使用别人的代码,想要什么功能就加载什么模块。这样下去模块的规范就变的更重要。目前:通用的JavaScript模块主要有:
-
CommonJs
-
AMD、CMD
-
ES6的模块
CommonJS: 同步加载解决方案
著名的node.js模块系统就是参照CommonJS规范来实现的。其核心思想就是通过require来进行同步加载其它模块,然后通过exports 或 module.exports来导出需要暴露的接口。
require("module"); require("./file.js"); exports.doStuff = function() {}; module.exports = someValue;
优点:
-
服务器端模块便于重用
-
在NPM里有很多功能模块
-
简单易用
缺点:
-
同步加载的方式注定不能用于客户端(clients),同步的加载意味着阻塞加载,浏览器的加载方式是异步的
-
不能非阻塞的并行加载多个模块
代表:
-
服务端 node.js
-
Browserify,浏览器端的 CommonJS 实现,可以使用 NPM 的模块,但是编译打包后的文件体积可能很大
AMD: 异步加载解决方案
AMD(asynchronous Module Definition)意思就是"异步模块定义",其规范主要是一个接口define(id?, dependencies?, factory),它采用的是异步加载的方式加载模块,模块的加载不影响它后面请语句的运行。所有执行语句都是在模块加载完成之后的回调函数中执行的。
define("module", ["dep1", "dep2"], function(d1, d2) { return someExportedValue; }); require(["module", "../file"], function(module, file) { /* ... */ });
优点:
-
适合在浏览器环境中进行加载模块
-
可以并行多个模块
缺点:
-
提高了并发的成功,代码的阅读和书写比较困难
-
不符合通用模块化的思维方式,是一种妥协的实现
实现:
-
RequireJS
CMD: 另一种异步加载解决方案
CMD(Common Module Definition)规范与AMD很相似,尽量保持简单,并与CommonJs和Node.js的Module规范保持了很大的兼容性
define(function(require, exports, module) { var $ = require('jquery'); var Spinning = require('./spinning'); exports.doSomething = ... module.exports = ... })
优点:
-
依赖就近,延迟执行
-
可以很容易在 Node.js 中运行
缺点:
-
依赖 SPM 打包,模块的加载逻辑偏重
实现:
-
Sea.js
ES6 模块
在ECMAScript2015(es6)中,增加了JavaScript语言层面上的模块体系定义,其设计思想是:尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出变量。
import "jquery"; export function doStuff() {} module "localModule" {}
优点:
-
容易进行静态分析
-
面向未来的 EcmaScript 标准
缺点:
-
原生浏览器端还没有实现该标准
-
全新的命令字,新版的 Node.js才支持
实现:
-
Babel
把程序所有的文件进行模块化之后,我们还要处理一个问题那就是传输问题。模块的化分让我们可以让程序变得可以组件化进行开发,组件虽然被客户端执行,但是依然要由服务器传送给客户端。
关于组件的传送有两个极端:
-
每个组件,一个HTTP请求
-
优点:仅仅传送依赖项
-
缺点:请求多,负载高,更慢的启动延迟
-
优点: 更快,更低的延迟
-
传送了没有必要传送的东西
让我在这两种情况之间做一个妥协:分块传输,按需进行懒加载,在实际用某些模块的时候进行增量的更新,才是比较合理的加载方案。
要实现这个功能,需要在编译打包时进行静态的分析、模块进行分批次的打包。那么这个分批次谁来做呢?