深入理解JS模块

引言

JavaScript的模块机制其实是借鉴的其他程序设计语言的, 如Java中package的概念, import java.util.ArrayList;; package就是逻辑上相关的代码组织到同一个包内,包内是一个相对独立的作用域,不用担心命名冲突等等, 当需要在外部使用的是否直接import相应的package即可。

由于JavaScript在设计之初的定位原因, 并没有提供类似模块的功能, 随后便出现了各种模拟类似的功能的规范。到今天(2018-5-28)ES6已经十分普及, ES6的模块机制已经大规模使用, 我们完全可以使用ES6提供的模块化规范(机制)。

类模块化

类模块化: 这是我自己理解的一个模块化概念, 指的是像函数封装, 对象, 立即执行函数包装这样的类似模块化的规范。

函数封装

函数就是对实现特定逻辑的一组语句的打包, JS的作用域也是基于函数的, 所以函数可以很自然的作为模块化, 这也是最开始实现模块化的一种方法。

function func1(){
    ...
}
function func2(){
    ...
}

引用模块也即是调用函数, 存在污染全局变量的缺点, 变量冲突等缺点。

对象

var myModule = {
    var1: 1,
    var2: 2,
    func1: function(){
        ...
    },
    func2: function(){
        ...
    }
}

将上面的函数封装在一个对象中, 引用模块即引用相应文件中对象上的属性, 如: myModule.func1(), 通过对象名(模块名)避免了全局变量污染, 但是存在安全问题, 如: 外部可以随意修改模块内部的属性和方法等。

立即执行函数

var myModule = (function(){
    var var1 = 1;
    var var2 = 2;
    function func1(){
        ...
    }
    function func2(){
        ...
    }
    return {
        func1: func1,
        func2: func2
    };
})();

在上面对象的基础之上, 用立即执行函数进行封装, 可以解决全局变量污染, 防止模块内部属性和方法被外部修改, 这是当前主流模块规范的基础。

CommonJS(NodeJS)

CommonJS: 通用模块规范, 主要由NodeJS具体实现; 根据CommonJS规范, 一个单独的文件就是一个模块。每一个模块都是一个单独的作用域, 也就是说, 在该模块内部定义的变量, 无法被其他模块读取, 除非定义为global(浏览器中为window)对象的属性。

CommonJS模块例子:

//模块定义 myModule.js
var name = 'Byron';
function printName(){
    console.log(name);
}
function printFullName(firstName){
    console.log(firstName + name);
}
module.exports = {
    printName: printName,
    printFullName: printFullName
}
//加载模块
var myModule = require('./myModule.js');
myModule.printName();

CommonJS模块存在的问题

require引入模块是同步的, 由于在浏览器环境下, JS都是通过script标签引入, 而这是天生异步的, 因此CommonJS在浏览器环境下无法正常加载(无法处理依赖问题)。NodeJS广泛采用CommonJS的原因主要是NodeJS的require模块都是在本地, 完全不用担心异步过程(即使在服务器上也是如此)。因此, 针对浏览器端异步require模块出现了AMDCMD规范。

AMD(RequireJS)

AMD: Asynchronous Module Definition(异步模块定义), 在浏览器端模块化开发的规范, 不是JavaScript原生支持, RequireJS是AMD规范的具体实现(严格上说是RequireJS的推广中产生的AMD规范)。

RequireJS模块例子:

// 定义模块 myModule.js
define('myModule', ['dependency'], function(){
    var name = 'Byron';
    function printName(){
        console.log(name);
    }
    return {
        printName: printName
    };
});

// 加载模块
require(['myModule'], function (my){
  my.printName();
});

RequireJS定义了一个全局函数define(id?, dependencies?, factory);来创建一个模块。
AMD模块中所有的依赖都前置, 其模块是异步的, 该自定义的模块内用到的模块均等到异步加载完成之后才调用响应模块, 这样浏览器不会失去响应。require指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

例如: 现需要在一个HTML页面中需要使用jQuery-fileupload插件, 并通过script标签的方式引入JS文件, 传统的方式是先引入jquery.min.js再引入jquery.fileupload.js。由于jqery.fileupload.js是基于jQuery的, 必须保证首先引入jQuery, 加载JS时候页面会停止若此时网络较差, 会导致页面失去响应时间较长。

总结

RequireJS主要解决如问题:

  1. 多个JS文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器;
  2. JS加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。

模块机制用途:

  1. CommonJS是同步的, 主要用于服务器
  2. AMDCMD是异步的, 两者的模块定义和加载机制稍有不同, 主要用于浏览器

AMD与CMD的区别

  1. AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块,CMD`推崇就近依赖,只有在用到某个模块的时候再去require
  2. 两个都是定义的全局define函数来定义模块, define接收函数function(require, exports, module)保持一致
  3. CMD是懒加载, 仅在require时才会加载模块; AMD是预加载, 在定义模块时就提前加载好所有依赖
  4. CMD保留了CommonJS风格

CMD(SeaJS)

CMD: Common Module Definition通用模块定义, 由国内发展出来, SeaJS是其典型代表, 即SeaJS是通过浏览器对CMD的具体实现

SeaJS模块例子:

// 定义模块  myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js');
  var foo = require('foo');
  var out = foo.bar();
  $('div').addClass('active');
  module.exports = out;
});

// 加载模块
seajs.use(['myModule.js'], function(my){

});

SeaJS定义了一个全局函数define(id?, deps?, factory)来创建一个模块, define接受一个需要三个参数的函数, 分别为:

  • require: 一个方法, 接受模块标识 作为唯一参数,用来获取其他模块提供的接口:require(id)
  • exports: 一个对象, 用来向外提供模块接口
  • module: 一个对象, 上面存储了与当前模块相关联的一些属性和方法

CMD推崇依赖就近原则(也就是懒加载), 模块内部的依赖在需要引入的时候再引入, 如上例中的var $ = require('jquery.js'), 这一点和通用的CommonJS模块风格保持一致。

UMD

UMD: 是一个既能在seajs(CMD)环境里引入,又能在requirejs(AMD)环境中引入,
当然也能在Node.js(CommonJS)中使用,另外还可以在没有模块化的环境中用script标签全局引入的'模块规范'

UMD模块其实就是在当前JS执行环境中对以上几种模块规范定义的define, module.exports等进行判断, 同一模块根据不同场所返回不同结果。

UMD模块例子:

;(function (global) {
    function factory () {
        var moduleName = {};
        return moduleName;
    }
    if (typeof module !== 'undefined' && typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && (define.cmd || define.amd)) {
        define(factory);
    } else {
        global.moduleName = factory();
    }
})(typeof window !== 'undefined' ? window : global);

UMD模块在不同环境引入:

// Node.js
var myModule = require('moduleName');
// SeaJs
define(function (require, exports, module) {
    var myModule = require('moduleName');
});
// RequireJs
define(['moduleName'], function (moduleName) {

});
// Browse global
<script src="moduleName.js"></script>

ES6模块(import,export)

ES6在语言标准的层面上, 实现了模块功能, 而且实现得相当简单, 完全可以取代CommonJSAMD规范, 是浏览器和服务器通用的模块解决方案。

ES6模块例子:

//模块定义 myModule.js
const name = 'Byron';
function printName(){
    console.log(name);
}
function printFullName(firstName){
    console.log(firstName + name);
}
const myModule = {
    printName: printName,
    printFullName: printFullName
};
export myModule;

//加载模块
import myModule, { printFullName } from './myModule.js';
myModule.printName();
printFullName('Michael');

注意

  1. ES6中的export是ES6对于JS模块的一种新的规范, 不同于CommonJS规范中的module.exportsexports;
  2. CommonJS规范中exports可以理解为指向module.exports的一个指针, 可以exports.newModule = {...}, 但是这样写exports={..}是不行的, 这会将exports这个指针指向新的{...}对象, 不再指向module.exports;
  3. ES6语法一般都经过babel转义为JS, 故可以在ES6中使用CommonJS模块规范, 如: var myModule = require('./myModule.js')

参考文章

  1. js模块化编程之彻底弄懂CommonJS和AMD/CMD
  2. 写一个适应所有环境的js模块
  3. SeaJS
  4. RequireJS
  5. 阮一峰-ES6模块
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341

推荐阅读更多精彩内容