前端模块化的理解(AMD,CMD,CommonJs,ES6)

前言

初期的web端交互还是很简单,不需要太多的js就能实现。随着时代的的发展,用户对Web浏览器的性能也提出了越来越高的要求,浏览器也越来越多的承担了更多的交互,不再是寥寥数语的js就能解决的,那么就造成了前端代码的日益膨胀,js之间的相互依赖也会越来越多,此时就需要使用一定的规范来管理js之间的依赖。
本文主要是什么是模块化,为什么需要模块化以及现下流行的模块化规范:AMD,CMD,CommonJs,ES6。

什么是模块化

要想理解模块化是什么可以先理解模块是什么?
模块:能够独立命名并且能够独立完成一定功能的集合。
因此在js中就可以理解为模块就是能够实现特定功能独立的一个个js文件。
模块化:就可以简单的理解为将原来繁重复杂的整个js文件按功能或者按模块拆成一个个单独的js文件,然后将每一个js文件中的某些方法抛出去,给别的js文件去引用和依赖。

为什么需要模块化

一、模块化的进程

1、全局function模式:将不同的功能封装为不同的函数
缺点:污染全局命名空间,容易引起命名冲突,看不出模块间的依赖
2、namespace模式:封装为对象模式
作用:减少全局变量,解决命名冲突
缺点:数据不安全(外部函数可以修改模块内的数据),看不出模块之间的依赖

const module = {
    data:1,
  getData(){console.log(this.data)}
}
module.data = 2; //这样会直接修改模块内部的数据

3、IIFE模式:匿名函数自调用(闭包)
作用:解决了数据安全,数据是私有的,外部只能调用暴露的信息
缺点:需要绑定到一个全局变量上例如window向外暴露,这样也会有命名冲突的问题
4、IIFE增强模式:引入依赖
作用:解决了模块直接的依赖问题
缺点:引入js的时候需要注意引入的顺序,并且当依赖很多的时候也会有弊端

// IIFE模式:匿名函数自调用(闭包)
(function(window){
    let data = '这是IIFE模式';
  getData(){
    console.log(data);
  }
  window.module = { getData }
})(window)
// IIFE增强模式
(function(window,$){
    let data = '这是IIFE模式';
  getData(){
    console.log(data);
    $('body').css('background', 'red')
  }
  window.module = { getData }
})(window,jQuery);
//index.html
//需要注意引入的顺序
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>
//当<script>过多的时候的缺点:
// 1. 请求过多
// 2. 依赖模糊:不清楚依赖直接是什么关系,很容易因为引入的顺序导致出错
// 3. 难以维护

从以上的发展历程来看虽然模块化还不是那么的完善,但是也不难能发现模块化的优点:

二、模块化的优点:

  • 避免命名冲突
  • 更好的分离模块,按需加载
  • 高复用性
  • 高维护性

模块的规范

AMD

AMD(Asynchronous Module Definition):异步模块定义。采用异步方式加载模块,模块的加载不影响后续语句的执行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。浏览器环境要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
RequireJs是AMD规范的最佳实践,所以我们用AMD规范的时候要引入requirejs。

AMD的语法:

define用来定义模块;
require用来加载模块,通常AMD框架会以require方法作为入口,进行依赖关系分析并依次有序地进行加载。
AMD是依赖前置,就是说,在define或require中方法里传入的模块数组会在一开始就下载并执行

//define定义模块
//第一个参数:依赖的模块,没有可以不写
define(['module1','module2'],function(m1,m2){
  //如果引入了module,但是没有使用,模块的内容也会执行
    return 模块;
})
//引入模块,相当于主函数的引用
require(['module1','module2'],function(m1,m2){
  //使用m1,m2
  //如果引入了module,但是没有使用,模块的内容也会执行
})
AMD具体使用流程
  1. 引入requirejs
    a. requirejs官网下载:requirejs
    b. github:requirejs
    c. 或者直接用require的js链接放在index.html中引用(只建议在学习时候用): https://requirejs.org/docs/release/2.3.6/minified/require.js
  2. 定义模块
//module1.js
define(function(){
    const msg='module1';
  return {
    msg
  }
})
//module2.js
define(function(){
    const msg="module2";
  return {
    msg
  }
})
//module3.js
define(['module2'],function(m2) {
    const msg="module3";
    const msg2 = m2.msg
    return {
        msg,
        msg2
    };
});
//main.js,入口文件不需要module2,因此就不需要引入
require.config({
    paths:{
        jquery:'jquery' //用于引入第三方库,此处填写的是路径,文件的路径;当然也可以在index.html用script标签引入
    }
})
require(['module1','module3','jquery'],function(m1,m3,$){
    console.log(m1,'module1');
    console.log(m3,'module3');
    console.log($,'第三方库');
});
console.log('模块加载'); //会优先打印
  1. index.html的引入
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <!--data-main:文件入口-->
    <script data-main="main" src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script>
</body>
</html>
  1. 输出结果


    image

CMD

CMD(Common Module Definition):通用模块定义。用于浏览器端,是除AMD以外的另一种模块组织规范。结合了AMD与CommonJs(后面会讲到)的特点。也是异步加载模块。
与AMD不同的是:AMD推崇的是依赖前置,而CMD是依赖就近,延迟执行。
依赖前置&&依赖就近,延迟执行

//依赖前置:AMD
require(['module1','module2'],function(m1,m2){
    //依赖的模块首先加载,无论后续是否会用到
})
//依赖就近,延迟执行
define(funciton(require){
    const module1 = require('./module1'); //用到的时候再申明,不需要就不用申明,也就不会加载进来       
})
CMD具体使用步骤
  1. 引入sea.js
    a. 官网:https://seajs.github.io/seajs/docs/#downloads
    b. github:https://github.com/seajs/seajs
  2. 模块定义
//module1.js
define(function(require,exports) {
    const msg='这是模块1';
    // const module2 = require('./module2');
    exports.msg = msg; // 注意这里是用的exports
    // exports.module2 = module2; // 如果是多个就得这么写,所以如果暴露多个接口不建议用exports
});
//module2.js
define(function(require,exports,module) {
    const msg="这是模块2";
    module.exports = { //这里用的是module.exports,跟exports作用一样
        msg
    }
});
//module3.js
define(function(require,exports,module) {
    const msg="这是模块3";
    const module2 = require('./module2');
    module.exports = { //这里用的是module.exports,跟exports作用一样
        msg,
        module2
    }
});
//main.js
define(function(require, exports,module) {
    const module1 = require('./module1');
    const module3 = require('./module3');
    console.log(module1);
    console.log(module3);
});
console.log('模块加载');
  1. index.html引入
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="sea.js"></script>
    <script>
        seajs.use('main');
    </script>
</body>
</html>
  1. 输出结果
    image

CommonJS

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
CommonJs有4个毕竟重要的变量:modulerequireexportsglobal

CommonJs的特点:
  1. 所有代码都运行在模块作用域,不会污染全局作用域;如果想要多个模块共享一个变量,需要给global添加属性(不建议这么用)
//module.js
global.data = '共享变量'; //添加到global的属性是多个文件可以共享的
let x = {
    a:5
}
let b=0;
const add = function(val){
    x.a+=x.a;
  b=++b;
}
module.exports.x=x; //只有对外暴露了变量,在外部引用的时候才能获取到,否则x,b就是模块内部的私有变量
module.exports.add = add;
  1. 模块可以多次加载,但是只有再第一次加载的时候才会执行,后续的加载都是使用的缓存结果;如果想要再次加载需要清除缓存,或者对外暴露一个函数,加载之后执行暴露的函数
// test1.js
let msg="测试缓存";
console.log(msg);
exports.msg=msg;
// test2.js
const msg = require('./test1');
const msg2 = require('./test1');
// 上面的输出结果是:测试缓存
// 会发现只打印了一次结果,以此可以证明只有第一次加载的时候才会执行,第二次加载的时候使用的是缓存的结果
// 删除指定模块的缓存
delete require.cache[moduleName] // moduleName必须是绝对路径
// 删除所有模块缓存
Object.keys(require.cache).forEach(function(key){
    delete require.cache[key]
})
  1. 模块加载的顺序是按照再代码中书写的顺序加载的,同步加载模块。
  2. 引入的值其实是输出值的拷贝(浅拷贝)。也就是说一旦值输出之后,模块内的改变就不会影响这个值的改变(引用类型除外)
// example.js
let x= {
    a:5
};
let b=0;
const add = function(val){
    x.a+=x.a;
    b=++b;
    // return x.a++;
}
module.exports.x = x;
module.exports.b=b;
module.exports.add = add;
// main.js
const example = require('./example');
const add = require('./example').add;
example.add();
console.log(example.x) // {a:10}
console.log(example.b) // b:0 
CommonJs的语法
//对外暴露接口
module.exports
exports
//引入模块
require(模块的路径) //模块的路径有多种写法,后续补充

module.exports&&exports
module.exports: 每个模块内部,module代表了当前这个模块,module是一个对象,对外暴露的就是exports这个属性,加载某个模块其实就相当于加载的module.exports这个属性
exports: 其实就是module.exports的引用。为了使用方便,node为每个模块创建了一个exports变量,这个变量就指向了module.exports。因此以下两种做法是错误的。

//错误一
exports='msg'; // 此时相当于改变了exports的指向,失去了与module.exports的联系,也就失去了对外暴露接口的能力
//错误二
exports.msg='msg';
module.exports = 'Hello world'; // 上面的msg是无法对外暴露的,因为module.exports被重新赋值了;此时对外暴露的就是『Hello world』

ES6

ES modules(ESM)是 JavaScript 官方的标准化模块系统。ES6模块设计的思想是尽量的静态化,使得编译时就能知道模块的依赖关系,以及输入和输出的变量。有两个主要的命令:export和import。export用于对外暴露接口,import用于引入其他模块。

ES6模块的特点:
  • 严格模式:ES6 的模块自动采用严格模式
  • import read-only特性: import的属性是只读的,不能赋值,类似于const的特性
  • export/import提升: import/export必须位于模块顶级,不能位于作用域内;其次对于模块内的import/export会提升到模块顶部,这是在编译阶段完成的
  • 兼容在node环境下运行
  • ES modules 输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
//ES6模块值的引用 .mjs 主要是为了能用node环境运行:node --experimental-modules
// example.mjs
let x= {
    a:5
};
let b=0;
const add = function(val){
    x.a+=x.a;
    b=++b;
}
export {x,b,add}
// main.mjs
import { x,b, add} from './example.mjs';
add();
console.log(x,b); // {a:10} 1 

export&&import用法
export:用于向外暴露接口
import:用于引入外部接口

// 方法一:
//export单个向外暴露接口
export const x = 1;
export const y = {a:1}
export const add = function(){console.log(123)}
//export一起向外暴露接口
const x=1;
const y={a:1};
const add = function(){console.log(123)};
export {x,y,add}
//import引入外部接口
//针对以上两种方式import可以写成如下两种情况
import {x,y,add} from './exmaple'; //使用哪个就引入哪个
import * as moduleName from './exmaple'; // 全部引入,使用的时候使用moduleName.x
// 方法二:
//除了使用export 向外暴露接口外还可以使用export default向外暴露接口:同一个模块中export可以有多个,但是export default只能有一个
const x=1;
const y={a:1};
export default { x,y}
// 对应的import
import moduleName from './exmaple'; //全部引入,使用方式moduleName.x

总结

  • AMD:异步加载模块,允许指定回调函数。AMD规范是依赖前置的。一般浏览器端会采用AMD规范。但是开发成本高,代码阅读和书写比较困难。
  • CMD:异步加载模块。CMD规范是依赖就近,延迟加载。一般也是用于浏览器端。
  • CommonJs:同步加载模块,一般用于服务器端。对外暴露的接口是值的拷贝
  • ES6:实现简单。对外暴露的接口是值的引用。可以用于浏览器端和服务端。

后记

模块化的一次有一次的变更,让系统模块化变得也越来越好,但是响应的也引起了一些问题。例如使用模块的时候,发现有些模块引入了但是并没有真正的使用到,这样就造成了代码的冗余,多了一些不必要的代码。这些模块有时通过检查是很难发现的,因此就要想如何能够快速的去除这些无用的模块呢,此时Tree Shaking就出现了;又比如如何能够在开发代码的时候比较便捷,然后在生产中又有高强度的兼容性呢,此时就出现了babel;又比如如何预处理模块,此时出现了webpack……
出现了解决问题的办法,就得继续学习啦……

参考文章

前端模块化的十年征程
彻底理清 AMD,CommonJS,CMD,UMD,ES6 modules
前端模块化—CommonJS、CMD、AMD、UMD和ESM
前端模块化详解(完整版)

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

推荐阅读更多精彩内容

  • ~出來包括,嗯,每一項每一項工作,然後需要做什麼?在什麼節點做,啊,怎麼做?把這些全部都羅列出來,羅列出來之後我們...
    戈壁女神阅读 469评论 0 0
  • 诗配画是儿童画的一种,只不过在儿童画的基础上配以古诗(现代诗)文字内容,以这种独特的方式来简简单单表现中国文...
    前郭225王冬芹阅读 185评论 0 0
  • 就算是自己的日记本吧,毕竟还是太懒根本就不可能坚持下来 ,天哪,好像就是这个样子的。 继续坚持自己一贯的作风,起了...
    给一柄剑卍许一座城阅读 252评论 0 1
  • 欣入梅岭鉴清雪 移履不为春瓶折 贪赏失寒犹恋恋 省却对窗叹早谢 昆南于己亥端月十八
    昆南阅读 351评论 0 5
  • 最近在使用Masonry出现了一个问题,以前居然没有遇到过,也是神奇了,就是我在一个View上面添加一个label...
    coder_feng阅读 354评论 0 0