前端模块化方式: IIFE(立即执行函数)、CommonJS、AMD、CMD、ES6、UMD
为什么会有前端模块化?
JavaScript一开始是为了实现简单地页面交互逻辑,然后随之浏览器性能逐渐提升,加上Ajax和NodeJS的出现,大量的前端库出现,前端的代码逐渐膨胀,因此就需要一个规范去管理JS代码,因此就出现了模块化的概念。
- 内部,外部的代码模块的管理和组织
- 模块源代码到目标代码的编译和转换
什么是模块?
- 将一个复杂的程序,依据一定的规范分装成几个文件或者代码块,并组合在一起
- 模块内部的数据和实现是私有的,只是向外部暴露出一些方法
前端模块化的方式
1. 定义全局函数Function
- 实现: 将不同功能的代码封装成不同的全局函数
- 缺点: 会污染全局的命名空间,容易出现命名冲突或者数据不安全的情况,而且模块成员之间看不出直接的关系
function m1(a, b) {}
function m2(c, d, e) {}
2. 命名空间namespace模式
- 实现: 将对象简单封装
- 作用: 减少了全局变量,避免了命名冲突问题
- 问题: 所有的模块成员都被暴露,数据不安全,外部可以直接修改对象的数据
const module = {
msg: "Hello World!",
say() {
console.log(this.msg);
}
}
module.msg = "Hi Javascript!";
module.say(); // 输出 Hi Javascript!
3. IIFE模式 -- 现代模块实现的基⽯
- 实现: 匿名函数自调用——闭包
- 作用: 数据是私有的,外部只能通过暴露的方法进行操作
// module.js 文件下
(function(window) {
let msg = "Hello world";
function say() {
console.log(msg);
}
function sing() {
console.log("music ~~")
}
window.module = { say, sing }
})(window)
// index.html
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
module.say() // Hello world
console.log(module.msg) // undefined
module.msg = "hi"
module.say() // Hello world msg没有更改
module.sing() // music ~~ 调用暴露的另一个函数
</script>
提问:如果IIFE需要引入JQuery模块呢?
// module.js 文件下
(function(window, $) {
let msg = "Hello world";
function say() {
console.log(msg);
}
function redBg() {
$('body').css('background', 'red')
}
window.module = { say, redBg }
})(window, $)
// index.html
// 注意js文件引入需要有一点的顺序
<script type="text/javascript" src="jquery-1.4.min.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
module.say() // Hello world
console.log(module.msg) // undefined
module.msg = "hi"
module.say() // Hello world msg没有更改
module.redBg() // 背景就被改成红色了
</script>
4. 模块化的好处
- 避免命名冲突
- 按需加载,更好的分离逻辑
- 更高的复用性
- 高可维护性
5. 多个script标签存在的问题
- 请求过多:依赖多个模块,则需要发送多个请求
- 依赖模糊:
- 引入需要分先后顺序,不清楚模块间具体依赖关系容易出错
- 牵一发而动全身,一个出错容易导致项目出现问题
因此需要一个规范来解决这些问题,从而就出现了后续的CommonJS,AMD, ES6,CMD的规范。
模块化规范
1. CommonJS
Node应用是由模块组成的,采用CommonJS模块规范。每个文件就是一个模块,有自己的作用域,内部变量、函数和类都是私有的,对其他文件不可见。
- 在服务端,模块加载时运行时同步加载的
- 在浏览器端,模块需要提前编译打包处理
1.1 特点
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但运行一次后会被缓存,后续加载直接读缓存结果;想再次运行需要清除缓存
- 模块加载的顺序,按照其在代码中出现的顺序**(同步加载)**
1.2 基本语法
-
暴露模块
const value = 123 module.exports = { val } // 或者 exports.val = value问:CommonJS暴露的模块到底是什么?
答: CommonJS规范规定,每个模块内部, module变量代表当前模块。这个变量是⼀个对象,它的exports属性(即module.exports)是对外的接 ⼝。加载某个模块,其实是加载该模块的module.exports属性。
-
引入模块
const module = require('xxx') // 1. 如果是第三方模块, xxx为模块名 // 2. 如果是自定义模块, xxx为文件路径示例
// module.js var val = 123; var add = function (a) { val = 1 return a + val; }; module.exports.x = val; module.exports.add = add;const module = require('./module.js') console.log(module.x) // 123 module.add(1) console.log(module.x) // 123运行一次后会被缓存,后续加载直接读缓存结果;想再次运行需要清除缓存
CommonJS模块的加载机制是,输⼊的是被输出的值的拷⻉。也就是说,⼀旦输出⼀个值,模块内部的 变化就影响不到这个值。
1.3 服务端实现
- 安装node环境
- npm init
- 下载第三方包
- 即可以开始使用commonJs
1.4 浏览器端实现
Browserify: 使用该插件对代码进行解析,整理出代码中的模块依赖关系形成一个普通JS代码文件。因为浏览器中不支持require的模块化语法
-
下载browserify
- 全局: npm install browserify -g
- 局部: npm install browserify --save-dev
-
打包处理js
browserify js/src/app.js -o js/dist/bundle.js -
页面引入打包后的文件
2. AMD(Asynchronous Module Definition)
CommonJS是同步的,加载完成才能执行后面的操作。AMD则是非同步加载模块,允许指定回调函数。
由于服务端一般都已经存在模板文件,所以加载较快,无需考虑非同步加载的方式;浏览器需要请求,所以更适合用AMD异步加载的方式。
AMD规范比CommonJS在浏览器实现更早。
2.1 基本语法
-
暴露模块
define(function () { return 模块 }) // 定义有依赖模块的模块 define(['module1', 'module2'], function(m1, m2) { return 模块 }) -
引入模块
require(['module1', 'module2'], function(m1, m2) { // 使用m1 / m2 }) -
main.js 声明模块配置
(function() { require.config({ baseUrl: '', // 基本路径 paths: { // 映射—— 模块标识名:路径 module: 'src' // 注意不能写成xxx.js,会报错 } }) require(['module'], function(module) { // 使用module }) })()
2.2 特点
-
AMD模块定义的⽅法⾮常清晰,不会污染全局环境,能够清楚地显示依赖关系。
-
AMD模式可以⽤于浏览器环境,并且允许⾮同步加载模块,也可以根据需要动态加载模块。
3. CMD(Common Module Definition)
CMD规范专⻔⽤于浏览器端,模块的加载是异步的,模块使⽤时才会加载执⾏。
CMD规范整合了 CommonJS和AMD规范的特点——异步加载,同步执行。
3.1 基本语法
和AMD差别:
- 使用了CommonJS的模板导出,而不是return 返回模块
- 不用提前声明模块的依赖
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引⼊依赖模块(同步)
var module2 = require('./module2')
//引⼊依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
3.2 CMD实现
在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
- 官⽹: seajs.org/
- github : github.com/seajs/seajs
ES6模块化
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输⼊和输出的变量。
CommonJS 和 AMD 模块,都只能在运⾏时确定这些东⻄
1. 基本使用
-
定义模块
注意:ES6输出的是值的引用
var basicNum = 0; var add = function (a, b) { return a + b; }; export { basicNum, add };export var basicNum = 0; export function add (a, b) { return a + b; };export default function () { console.log('foo'); } -
引用模块
// 解构导入 需要知道所要加载的变量名或函数名, import { basicNum, add } from "xxx" // 指定名称 需要模板中有export default import foo from "xxx"
ES6 模块与 CommonJS 模块的差异
- CommonJS 模块输出的是⼀个值的拷⻉,ES6 模块输出的是值的引⽤
- CommonJS 模块是运⾏时加载,ES6 模块是编译时输出接⼝
2. ES6的实现
使⽤Babel将ES6编译为ES5代码,使⽤Browserify编译打包js。
-
安装babel-cli, babel-preset-es2015和browserify
npm install babel-cli browserify -g npm install babel-preset-es2015 --save-dev -
定义.babelrc⽂件
{ "presets": ["es2015"] } -
定义模板代码
-
编译代码
babel js/src -d js/lib browserify js/lib/app.js -o js/lib/bundle.js
UMD(Universal Module Definition)
javascript通⽤模块定义规范,让你的模块能在javascript所有运⾏环境中发挥作⽤。
意味着要同时满⾜CommonJS, AMD, CMD的标准
兼容所有的模块规范
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object')
{
console.log('是commonjs模块规范,nodejs环境')
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
console.log('是AMD模块规范,如require.js')
define(factory)
} else if (typeof define === 'function' && define.cmd) {
console.log('是CMD模块规范,如sea.js')
define(function(require, exports, module) {
module.exports = factory()
})
} else {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory();
}
}(this, function() {
return {
name: '我是⼀个umd模块'
}
}))
总结
- CommonJS加载模块是同步的,主要用于服务端(运行时,导出值的拷贝)
- AMD异步加载模块,可以多模块并行加载,更适合浏览器环境;但是AMD规范开发成本较大
- CMD规范与AMD相似,但是不用声明模块,依赖就近可以异步加载同步执行
- ES6使用export导出,开发简单,完全可以取代CommonJS和AMD(编译时,导出引用)
- UMD是兼容CommonJS、AMD、CMD规范的实现