JavaScript 常用模块化规范
1.概述
1.1 什么是模块化
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起
- 块的内部数据/实现是私有的,只是向外部暴露一些接口(方法)与其他外部模块通信
1.2 模块化的好处
- 避免命名冲突(减少命名空间污染)
- 更好的分离,按需加载
- 更高复用性
- 高可维护性
2.模块化规范
2.1 命名空间模式
命名空间模式是一种将代码组织为对象的方法,其中每个对象都代表一个模块。这种方法可以使代码更易于组织和维护,但它并没有提供真正的模块化支持。
//main.js
var myModule = {
name: "My Module",
sayHello: function() {
console.log("Hello from " + this.name);
}
};
//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="./main.js"></script>
<script>
myModule.sayHello(); //Hello from My Module
</script>
</body>
</html>
在上面的示例中,我们创建了一个名为myModule的对象,它有一个名为sayHello的方法。这种方法可以将代码组织为对象,使代码更易于组织和维护。
2.2 IIFE
IIFE(Immediately Invoked Function Expression)模式是一种将代码包装在立即调用的函数表达式中的方法。这种方法可以创建私有作用域,使代码更易于维护和重用。
//main.js
var myModule = (function() {
var name = "My Module";
function sayHello() {
console.log("Hello from " + name);
}
return {
sayHello: sayHello
};
})();
//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="./main.js"></script>
<script>
myModule.sayHello(); //Hello from My Module
</script>
</body>
</html>
在上面的示例中,我们创建了一个立即调用的函数表达式,它返回一个包含一个名为sayHello的方法的对象。这种方法可以创建私有作用域,使代码更易于维护和重用。
2.3 CommonJS
CommonJS模块是一种使用require()函数来加载模块的方法。这种方法在Node.js中得到广泛应用,但在浏览器中需要使用工具进行转换。 模块的加载是运行时同步加载的。
2.3.1 服务端实现:Node.js
// module1.js
// module.exports = value 暴露一个对象
module.exports = {
msg: 'module1',
foo () {
console.log(this.msg)
}
}
// module2.js
// 暴露一个函数 module.exports = function(){}
module.exports = function () {
console.log('module2')
}
// module3.js
// exports.xxx = value
exports.foo = function () {
console.log('foo() module3')
}
exports.bar= function () {
console.log('bar() module3')
}
// 将其他模块汇集到主模块
//main.js
let module1 = require("./module1.js");
let module2 = require("./module2.js");
let module3 = require("./module3.js");
module1.foo();
module2();
module3.foo();
module3.bar();
2.3.2 浏览器端实现:Browserify
Browserify
,也称为浏览器端的打包工具(ES6
也使用到)。
- 全局安装
npm install browserify -g
- 项目中安装
npm install browserify --save-dev
。
打包命令:browserify 入口文件 -o 打包文件:
想要运行在浏览器端,要有一个入口的html1
文件。创建index.html
,并引入上述打包生成的build.js
文件 <script src="./dist/build.js"></script>
。
2.4 AMD
专门用于浏览器端,模块的加载是异步的。 这种方法使用define()函数来定义模块,使用require()函数来加载模块。
// module1.js
define(function() {
var name = "My Module";
function sayHello() {
console.log("Hello from " + name);
}
return {
sayHello: sayHello
};
});
// app.js
require(["module1"], function(myModule) {
myModule.sayHello();
});
AMD特点:
- 异步并行加载,不阻塞 DOM 渲染。
- 推崇依赖前置,也就是提前执行(预执行),在模块使用之前就已经执行完毕。
2.5 CMD
专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。 CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖。
具体的使用方法可以上网查询了解了解。定义模块使用全局函数define,接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串。
2.5.1 factory是函数时
factory是函数时,有三个参数,function(require, exports, module):
- require:函数用来获取其他模块提供的接口require(模块标识ID)
- exports:对象用来向外提供模块接口
- module :对象,存储了与当前模块相关联的属性和方法
// 定义 a.js 模块,同时可引入其他依赖模块,及导出本模块
define(function(require, exports, module) {
var $ = require('jquery.js')
exports.price= 200;
});
2.5.2 factory为对象、字符串时
factory为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块。
// 定义 foo.js
define({"foo": "bar"});
// 导入使用
define(function(require, exports, module) {
var obj = require('foo.js')
console.log(obj) // {foo: "bar"}
});
2.6 ES6模块
ES6模块是一种模块化规范,是ECMAScript 6标准的一部分。这种方法使用import和export关键字来导入和导出模块。
// module1.js
var name = "My Module";
function sayHello() {
console.log("Hello from " + name);
}
export { sayHello };
// app.js
import { sayHello } from "./module1";
sayHello();
3.模块化规范总结
3.1 模块化规范总结图
3.2 AMD与CMD的区别
- AMD 是提前执行,CMD 是延迟执行。
- AMD 是依赖前置,CMD 是依赖就近。
// AMD
define(['./a', './b'], function(a, b) { // 在定义模块时 就要声明其依赖的模块
a.doSomething()
// ....
b.doSomething()
// ....
})
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// ...
var b = require('./b') // 可以在用到某个模块时 再去require
b.doSomething()
// ...
})
3.3 CommonJs和ESM的区别
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
现在我们来看第一条,来举个例子:
// a.js
let val = 1;
const setVal = (newVal) => {
val = newVal
}
module.exports = {
val,
setVal
}
// b.js
const { val, setVal } = require('./a.js')
console.log(val);// 1
setVal(101);
console.log(val);// 1
我们可以这样子理解:
const myModule = {
exports: {}
}
let val = 1;
const setVal = (newVal) => {
val = newVal
}
myModule.exports = {
val,
setVal
}
//复制解构是浅拷贝
const { val: useVal, setVal: useSetVal } = myModule.exports
console.log(useVal);
useSetVal(101)
console.log(useVal);
这里我们就可以理解什么叫值的拷贝了。我们的val和模块里的val是不一样的所以使用setVal修改没有效果
在es module中就不是输出对象的拷贝了,而是值的引用
// a.js
import { foo } from './b';
console.log(foo);
setTimeout(() => {
console.log(foo);
import('./b').then(({ foo }) => {
console.log(foo);
});
}, 1000);
// b.js
export let foo = 1;
setTimeout(() => {
foo = 2;
}, 500);
// 执行:babel-node a.js
// 执行结果:
// 1
// 2
// 2
上述场景用CommonJS写:
// a.js
const { foo } = require("./b");
console.log("a:" + foo); //1
setTimeout(() => {
console.log("a:" + foo); //1
}, 1000);
// b.js
let foo = 1;
module.exports = { foo };
setTimeout(() => {
foo = 2;
console.log("b:" + foo);
}, 500);
// 执行:node a.js
// 执行结果:
// a:1
// b:2
// a:1
详细情况见我写的文章https://blog.csdn.net/fageaaa/article/details/145796172第13节。
3.4 关于UMD
UMD是AMD和CommonJS的糅合。UMD的实现很简单:
- 先判断是否支持Node.js模块(exports是否存在),存在则使用Node.js模块模式。
- 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
- 前两个都不存在,则将模块公开到全局(window或global)。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([],factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
return {};
});
严格来说,UMD 并不属于一套模块规范,它主要用来处理 CommonJS、AMD、CMD 的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行。