JavaScript模块化演化史

852 阅读7分钟

前言

本文可能不涉及一些基础概念,比如什么是模块,对每种方案也没有都做详细的解释(如果解释的话,每个都值得一篇文章),准备和落实这篇文章主要是有两方面原因。

  1. 平时遇到的一些名词有些摸不着头脑,总是大概知道是个什么意思,但细想又不知道其原理和联系,比如平时遇到CommonJS,seaJS,CMD,AMD,UMD,Browserify,RequireJS,知道他们和js模块有关,但具体是什么关系,js模块化应该用哪个的却是不知道。
  2. 使用webpack做模块打包,不知道过程中做了什么(除了plugins和loader),是从什么样的模块打包成什么样的模块,最后又变成了什么。

综上两点,就是对JS的模块不熟悉导致的。

本文目标

对JS模块化有一个直面的了解,并能捋清楚各个规范之间的之间的联系,再遇到这些名词不会再有它认识我,我不认识它的感觉

演变过程

首先来说为什么需要模块化,这个大家心里应该都有一两个答案,比如避免命名冲突啊,全局变量过多啊,依赖不好管理啊等等等等。总得来说模块化大方向上有几个好处。

  1. 易于维护
  2. 命名空间(解决命名冲突)
  3. 复用性
  4. ...

总体上模块化基本上是一个百利无一害的实现

原始时代

开始JS没有模块的概念,因此导致了很多问题,比如命名冲突,比如:

var origin = 100;
....其他代码
var origin = 1232;
....其他代码
console.log(origin)//目标是让它输出100,但实际会输出1232,因为后来的命名覆盖

还有依赖问题,比如a.js依赖了b.js,那么下载时必须是先下载b,在下载a,顺序一定不能错。

IIFE

为了解决这些没有模块带来的问题,前辈们提出了使用IIFE来模仿模块,如下:

(function(){
	var name = 'wingtao;
	var sayHello = function (){
		console.log('hello '+name);
	}
	sayHello(); // hello wingtao
})()

上面利用立即执行函数模拟了一个模块,该模块中的变量外界无法访问,避免了命名冲突的问题,jQuery就用了这种方式,

(function(global){
	global.jquery = ...
})(window);

但还是没有完全实现模块化,如对模块依赖的管理、如何将api暴露出来而不污染全局环境

CommonJS && nodejs

CommonJS社区首先提出了模块化的规范CommonJS,所以CommonJS是一个规范!在node上只需要简单的require和exports就可以实现模块的导入和导出,如下:

a.js
exports.add = function(a,b){
	return a+b;
}

b.js 
var add = require('a.js').add;
console.log(add(1,2))//3

看起来非常棒!而且nodeJS模块实现了这种规范,意味着在node中可以直接使用这种方式。

CommonJS && Browserify

既然服务端能实现了这种模块化的规范,浏览器上对此也是非常迫切的,自然也是想要实现这块,但是直接拿来用是有一些问题,

  1. node中require是同步加载的,因为直接从内存或硬盘里读就可以了,而在浏览器上不能同步加载,因为浏览器上每一个文件都是需要下载下来的,都是需要时间的,而且浏览器上下载js都是通过script来加载的,不能同步执行,所以也就没办法同步加载模块了。
  2. 没有立即执行函数的包裹,加载的模块变量又暴露在全局上了。

因此如果想在浏览器上使用CommonJS是需要改造的,对此人们分成了几派,一派是认为还是按照CommonJS规范来,只是加上函数包裹和异步加载,在浏览器上能执行就行了;一派认为CommonJS不适合浏览器端,需要一个新的规范;第三方是个“和稀泥”的,认为CommonJS和重新改革都有可取之处,所以各取所长。

其中第一派坚持使用CommonJS的做出了浏览器端的实现Browserify,名字也很形象,Browserify可以将node端模块文件转换为浏览器可识别的模块文件。所以Browserify是CommonJS在浏览器端的实现

AMD && RequireJS

AMD(规范)其实就是上面说的第二派,就是抛弃CommonJS,提出新的可异步加载的模块规范。AMD最大的特点便是可以异步加载模块,它的实现是RequireJS,编写时像这样:

define(['myModule', 'myOtherModule'],function(myModule, myOtherModule) {
	console.log(myModule.hello());
});

过程是先加载依赖myModule,myOtherModule(后台不阻塞的方式加载),加载完成后执行回调函数,其中回调函数的参数便是已经加载完成的模块。其实AMD还是有很多问题的,比如define的时候所有依赖要挨个写一遍,比如不管现在用不用的到都会把依赖先下载下来,不过这些问题AMD都有优化,这里不提。

UMD

UMD全称是Universal Module Definition,目的兼容CommonJS和AMD,所以它会做一层判断,判断当前环境是浏览器还是node,如果是浏览器则使用AMD,node环境使用CommonJS方式,UMD实现了两种环境的兼容,但同时也导致了十分臃肿,肉眼观察实在有点费劲。

CMD && seaJS

提起CMD,不怕被笑话,我之前还以为和CommonJS是同一个东东呢😂。seaJS是阿里前端工程师玉伯做出来的,并提出了CMD,CMD吸取了AMD和CommonJS两者的优点,融合了百家之长(但好像只在国内有影响,国外影响有限),所以CMD是规范而seajs是它的实现。

ES6 Module

以上说的种种方式都是因为ECMA缺乏官方的模块规范才出来的,既然对模块化的需求这么旺盛,官方在ES2015(ES6)里也就提出了官方的模块化方案,主要使用import和export,用法非常简单,而且它和之前的方案的区别除了它是官方提出并且写法简单之外,还有重要的一点就是它是静态解析的,什么是静态解析呢?另开一篇文章再讲,不过这个特性可以带来很多优化,如tree-shaking。ES6模块机制理论上是浏览器原生支持的,但实际上现在支持度还不够,这个大家应该也能理解,不过在未来应该可能就能在浏览器中直接加载导出模块了,

webpack && gulp && rollup

这些工具其实和上面讲的模块规范已经不是一个维度的事情了,上面说的是模块化,这些是一些可以打包的工具,打包什么呢?模块!这就是他们之间的联系了。就不展开讲了,这是三个东西太多了。

总结

简单总结一下上面讲的东西,其实就是讲了下js模块的几个方案,每个方案都有自己的规范,然而只有规范还不行,需要有实现来支持它,所以总得来说就是:

环境 规范 实现
node CommonJS nodejs 模块
浏览器 CommonJS Browserify
浏览器 AMD RequireJS
浏览器 UMD 👆两者
浏览器 CMD SeaJS

写下来这些就是希望自己能对js模块化有一个较为全面的了解,面对这些名词不再默认,并了解他们之间的关系。

文章仓促,有不足之处希望多多指出。

如果你看完对你有些许的帮助,那是意外之喜😊。

参考文章

JavaScript模块:指南

JavaScript模块:模块打包