早期的 JS 编码并没有模块化这个概念,写出来的代码就像“挂面”一样,有时候单个页面的 JS 代码就很长,维护起来十分麻烦,而且还存在“变量污染”、“命名冲突”和“缺少文件依赖系统”等问题。随着Node.js的出现和前后端分离开发模式的愈益流行,JS模块化技术也越来越成熟,其主要思想是利用“闭包”和“异步加载JS”来解决以上的问题。其实JS模块化的发展历程也是比较曲折的,这里给出一个其描述比较详细的链接:https://www.cnblogs.com/lvdabao/p/js-modules-develop.html。
JS模块化的方式大致分为这么几种:CommonJS,AMD,CMD,ESM,UMD。
CommonJS 是 Node.js 的模块化规范,在 Node.js 中每个 JS 文件就是一个模块。每个 JS 文件都有一个 module 对象,它又有一个 exprots 属性,顾名思义它就是JS文件默认导出的对象。
而且每个 JS 文件都有自己的私有作用域,其中的变量、常量、函数和类等对其他的 JS 文件隐藏,如果其他JS文件想要访问这个 JS 文件中的变量、常量、函数和类等,需要我们把变量、常量、函数和类等添加到 module.exports 对象上,然后通过 require 方法来引入该文件(实际上就是引入了该JS文件的 module.exports 对象)进行访问。
有一点需要注意的是:直接给 exports 赋值(exports = xxx)是起不到任何作用的,你可以通过 module.exports = xxx 来改变引用。
// ./test.js
// 这种方式起不到任何作用,它也不会被挂载到 global 上..
exports = {a:1};
console.log(module.exports) // {}
exports.a = 2;
console.log(module.exports) // { a: 2 }
module.exports = {a:3};
console.log(module.exports); // { a: 2 }
b = '我是所有模块共享的变量'
console.log(global.b) // 我是所有模块共享的变量
exports = {a:1}; // 它不会被挂载到 global 上
console.log(global.exports); // undefined
// ./main.js
var o = require('./test');
console.log(o); // { a: 3 }
AMD (Asynchronous Module Definition),即异步模块定义,是客户端实现 JS 模块化的一种方式,由 Require.js 实现。
它借鉴了 CommonJS 的思想,使用了 CommonJS 里 require、module 和 exports 特性,其主要方法有 config、define 和 require 。
在使用 Require.js 的时候,需要在标签属性上添加 data-main 属性,当 Require.js 加载完后会去加载 data-main 里面指定的 JS。
Require.js
// ./main.js
/**
* 配置模块加载位置,webpakc 需要配置别名。
* Require.js 根据路径去加载对应的 JS,
* 一般是引入第三方 JS 时使用
*/
requirejs.config({
paths: {
test: './test'
}
});
/**
* 这种方式 webpack 可以识别,需要当前目录的 test.js 文件存在
*
* 第1种方式:根据 JS 文件路径去加载JS,
* 等 test 对应的 JS 加载并执行完后就执行 callback
*/
require(['./test'], function(test) {
console.log('test2', test);
});
/**
* 如果不是以别名或 '/', './' 开头,
* webpack 会使用 nodejs 的 require 方式
* 从 node_modules 一层层的网上找,找不到
* 则报错。所以 wepack 和 require.js 的
* 处理方式不一致,webpack 会因为找不到对
* 应的模块而报错。
*/
/**
* 第2种方式:根据 id 去加载JS,
* 等 test 对应的 JS 加载并执行完后就执行 callback
*/
require(['test'],function(test){
console.log('test1', test);
});
/**
* 等定义的 uitl1 模块执行完后,调用 callback
*/
require(['util1'],function(util){
console.log('util1', util);
});
/**
* webpack 编译后的代码只能导出最后一个,
* 它默认一个文件只能有一个 define ,即
* 一个文件一个模块。
*/
/**
* 同一文件中定义一个 util1 模块,
* 如果用 webpack 来编译,还是按照
* 一个文件一个模块来编译,所以这里的
* util1 是不起作用的,编译后被消除。
*/
define('util1', function() {
return {desc: 'util1'};
});
// ./test.js
// 定义一个模块,实际上也可以依赖其他模块的,这里不做研究
// define({ a: 1, b: 2, c: 3 });
// 还可以这么写
define(function(require, exports, module) {
// 可以这么写
// return {a: 1, b: 2, c: 3};
// 也可以这么些
// exports.a = 1;
// exports.b = 2;
// exports.c = 3;
module.exports = {a: 1, b: 2, c: 3};
});
CMD(Common Module Definition),即普通模块定义,它看上去像是 AMD 和 CommonJS 的结合体,由 Sea.js 实现。在 CMD 的规范里,一个文件表示一个模块。
它同样使用了 CommonJS 里 require、module 和 exports 特性,常用方法有 config、define 和 require 这三个,这些方法和 AMD 中对应的方法类似,这里就不再赘述了。
另外它还有一个 use 方法,这个是用于加载入口模块的,和 data-mian 的作用类似,用来执行入口 JS 文件。和 AMD 不同的是,CMD 支持使用 require.async(id) 实现懒加载。
Sea.js
// ./main.js
// webpack 需要配置别名
// seajs 的全局配置
seajs.config({
// 配置加载 JS 的基础路径
base: '.',
// 别名
alias: {
'test': 'test'
},
charset: 'utf-8',
timeout: 20000,
debug: false
});
// 定义个模块,模块标识由文件名决定
define(function(require, exports, module) {
/**
* 引入 test 模块,如果使用 var a = require('test'),
* sea.js 支持这种写法, 但是 webpack 不支持,它会因
* 为找不到模块而报错,其原因和用 webpack 编译 AMD 模
* 块化代码一样,需要换为路径才能解析成功。
*/
var a = require('./test');
// 也可以写成
// exports.desc = 'entry module';
// exports.data = a;
// 也可以写成
// module.exports = {desc: 'entry module', data: a};
return {
desc: 'entry module',
data: a
}
});
// ./test.js
// 定义一个模块
define((require, exports, module) => {
// 也可以写成
// exports.a = 1;
// exports.b = 2;
// exports.c = 3;
// 也可以写成
// return {a: 1, b: 2, c: 3}
module.exports = {
a:1,
b:2,
C:3
}
});
ESM(ECMA Script Modules),即 ES6 的模块化规范。
但是因为目前有些低版本的浏览器还是不支持 ESM ,所以一般需要将 ESM 转成其他的模块化方式,这个过程一般由 Webpack 等自动化构建工具去转换。
// ./main.js
// 星号别名导出,导出的是整个的模块( Module 的实例)
import * as obj from './test.js';
console.log(obj);
// 解构导入,这里从 Module 实例中解构 a
import {a, b} from './test.js'
console.log(a); // {a: 1}
console.log(b); // {b: 2}
// 导入默认,xxx 不是关键字就行了
// 前提是被导入模块必须有 default 默认导出
import xxx from './test.js';
console.log(xxx); // {desc: "我是默认导出"}
// ./test1.js
// 将 a 、b 和 default 变为 Module 实例的属性,并导出 Module 实例
export var a = { a: 1 }
export var b = { b: 2 };
export default { desc: '我是默认导出' };
// 以上导出相当于下面这一句, 不过这里 default 是关键字,会有错误,
// 所以这里的花括号并不是对象!它只是一种表示形式,类似于解构的反向操作。
// export { a, b , default }
UMD(Universal Module Definition),即通用模块定义,是 CommonJS 和 AMD 模块化标准的集合,它还兼容 “Root” 模式,支持从全局对象上获取模块。
// ./main.js
// UMD
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
console.log('main', 'AMD');
// AMD,定义一个以文件名为模块名的模块,并导入 vue 对应的依赖
define(['./test'], factory);
} else if (typeof exports === 'object') {
console.log('main', 'CommonJS');
// CommonJS
module.exports = factory(require('./test'));
} else {
console.log('main', 'Root');
// 浏览器全局变量(root 即 window)
root.yyy= factory(root.test);
}
}(this, function(a) {
console.log(a);
}));
// ./test.js
// UMD
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
console.log('test', 'AMD');
// AMD,定义一个以文件名为模块名的模块,并导入 vue 对应的依赖
define(factory);
} else if (typeof exports === 'object') {
console.log('test', 'CommonJS');
// CommonJS
module.exports = factory();
} else {
console.log('test', 'Root');
// 浏览器全局变量(root 即 window)
root.test= factory();
}
}(this, function() {
console.log('test');
return {
desc: 'test'
}
}));
可以看出,UMD 没有自己的实现,而是 AMD 、CommonJS 和 Root 模式的集合。
webpack 支持编译以上所有模块化方式的代码,并且可以兼容它们的混合模式,因为它们最终都会被编译为 webpack 自己的模块化代码。
https://github.com/zzp-dog/reception-learn/tree/master/webpack_review/module