在JS最早出现的时候,是为了实现一些简单的功能,但随着浏览器的不断发展,对于JS的要求也越来越高,需要实现一些较为复杂的功能。这个时候开发者为了维护方便,会把不同功能的模块抽离出来写入单独的 js 文件里,但是当项目更为复杂的时候,html 可能会引入很多个js文件,而这个时候就会出现命名冲突,污染全局作用域,代码库混乱不堪等一系列问题,这个时候模块化的概念及实现方法应运而生。
目录
模块化
什么是模块化规范
模块化规范就是为 JavaScript 提供一种模块编写、模块依赖和模块运行的方案。降低代码复杂度,提高解耦性。
一个模块就是实现某个特定功能的文件,我们可以很方便的使用别人的代码,想要什么模块,就引入那个模块。但是模块开发要遵循一定的规范,后面就出现了我们所熟悉的AMD和CMD规范。
为什么要使用模块化
模块化可以使我们的代码低耦合,功能模块直接不相互影响。模块化主要有以下几点好处:
- 可维护性:每个模块都是独立的。良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。维护一个独立的模块比起一团凌乱的代码来说要轻松很多。
- 命名空间:在 JavaScript 中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,我们就会遇到“命名空间污染”的问题【这样的问题在我们开发过程中是要极力避免的】。
- 可复用性:现实来讲,在日常工作中我们经常会复制自己之前写过的代码到新项目中。
AMD(Asynchronous Module Definition)
采用 异步方式 加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。推崇依赖前置。
在AMD规范中,我们使用 define
定义模块,使用 require
加载模块。
定义模块(define) 及 规范模块加载(require)
模块必须采用 define()
函数来定义。
define(id?, dependencies?, factory);
/*
* id是定义的模块名,这个参数是可选的,如果没有定义该参数,模块名字应该默认为模块加载器请求的指定脚本的名字,如果有该参数,模块名必须是顶级的绝对的。
* dependencies是定义的模块中所依赖的模块数组,依赖模块优先级执行,并且执行结果按照数组中的排序依次以参数的形式传入factory。
* factory是模块初始化要执行的函数或对象,只被执行依次,如果是对象,则为该模块的输出值。
*/
AMD 采用 require
语句加载模块,它要求两个参数。
require([module], callback);
/*
* require 要传入两个参数
* 第一个是[module],是一个数组,就是要加载的模块
* 第二个 callback 是加载成功之后的回调函数
*/
使用方法
-
若一个模块不依赖其他模块。可以直接定义在
define()
函数中。// math.js define(function (){ var add = function (x,y){ return x+y; }; return { add: add }; });
-
若这个模块还依赖其他模块,那么
define()
函数的第一个参数,必须是一个数组,指明该模块的依赖性。当require()
函数加载 test 模块时,就会先加载math.js
模块。// dataService.js define(['math'], function (math) { function doSomething() { let result = math.add(2, 9); console.log(result); } return { doSomething }; });
-
设置一个主模块,统一调度当前项目中所有依赖模块。
// main.js (function () { require.config ({ // baseUrl:'', paths:{ dataService:'./dataService', math:'./math' } }) require(['dataService'], function (dataService) { dataService.doSomething() }); })();
-
在 index.html 中引入
require.js
,并设置data-main
入口主模块。// index.html <script data-main="./js/main.js" src="./js/require.js"></script>
-
本案例中所有源码,目录结构及每个模块的作用,如下图所示(源码同上1234步骤)
非规范模块加载
理论上 require.js
加载的模块,必须是按照 AMD 规范用 define()
函数定义的模块。但实际上,虽然已经有一部分流行的函数库(比如 jQuery )符合 AMD 规范,但更多的库并不符合。那么require.js
如何能够加载非规范的模块呢?
这样的模块在用 require()
加载之前,要先用 require.config()
方法,定义它们的一些特征。
/*
* require.config() 接受一个配置对象,这个对象有一个 shim 属性,专门用来配置不兼容的模块。每个模块要定义:
* exports:输出的变量名,表示这个模块外部调用时的名称;
* deps:数组,表示该模块的依赖性。
*/
// 如jQuery 的插件还可以这样定义:
require.config({
shim: {
'jquery.scroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
}
}
})
例如,underscore
和 backbone
这两个库,都没有采用 AMD 规范编写。如果要加载的话,必须先定义它们的特征。
require.config({
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
特点
- AMD 允许输出的模块兼容 CommonJS;
- 异步并行加载,不阻塞 DOM 渲染;
- 推崇依赖前置,也就是提前执行(预执行),在模块使用之前就已经执行完毕。
CMD(Common Module Definition)
CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖。
定义模块(define) 及 模块加载(require)
定义模块使用全局函数 define
,接收一个 factory
参数,可以是一个函数,也可以是一个对象或字符串;
// factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块。
define(factory);
// 如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数 require、exports 和 module。
define(function(require, exports, module) {
//...
});
/*
* factory 是函数时有三个参数,function(require, exports, module):
* require:函数用来获取其他模块提供的接口require(模块标识ID)
* exports: 对象用来向外提供模块接口
* module :对象,存储了与当前模块相关联的属性和方法
*/
使用方法:
-
factory
是函数时:// foo.js define(function(require, exports, module) { const name = "bajie"; const age = 18; function sum(num1, num2) { return num1 + num2 } // exports.name = name; // exports.age = age module.exports = { name, age } }) // main.js define(function(require, exports, module) { const foo = require("./foo"); console.log("main:", foo); })
-
factory
为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块:// 定义 foo.js define({"foo": "bar"}); // 导入使用 define(function(require, exports, module) { var obj = require('foo.js') console.log(obj) // {foo: "bar"} });
UMD(Universal Module Definition)
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 {};
});
CommonJS(同步加载模块)
CommonJS 规范主要应用于Node,每个文件就是一个模块,有自己的作用域,即在一个文件中定义的变量、函数、类都是私有的,对其他文件不可见。
CommonJS 规范包括了模块(
modules
)、包(packages
)、系统(system
)、二进制(binary
)、控制台(console
)、编码(encodings
)、文件系统(filesystems
)、套接字(sockets
)、单元测试(unit testing
)等部分。
这个模块中包括CommonJS规范的核心变量:exports
、module.exports
、require
;
我们可以使用这些变量来方便的进行模块化开发;
CommonJs规范规定,每个模块内部有两个变量可以使用:require
和 module
。
require
函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
module
代表的是当前模块,是一个对象,存储着当前模块的相关联的属性和方法。exports
是module
上的一个属性。该属性表示当前模块对外输出的接口,其它文件加载该模块,实际上就是读取module.exports
变量。
导出模块(module.exports)
Node.js 为每个模块提供一个 exports
变量,指向 module.exports
。相对于在每个模块头部,有一行这样的命令:var exports = module.exports
;
使用方法
function myModule() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayName = function() {
console.log('Hello ' + name);
};
}
module.exports = myModule;
加载模块(require)
使用 require
函数 加载模块(即被依赖模块的 module.exports
对象)。
- 按路径加载模块;
- 通过查找
node_modules
目录加载模块; - 加载缓存:Node.js 是根据实际文件名缓存,而不是
require()
提供的参数缓存的,如require('express')
和require('./node_modules/express')
加载两次,也不会重复加载,尽管两次参数不同,解析到的文件却是同一个。 - 核心模块拥有最高的加载优先级,换言之如果有模块与其命名冲突,Node.js 总是会加载核心模块。
使用方法
var myModule = require('./myModule');
var myModuleInstance = new myModule();
myModuleInstance..setName('BaJie');
myModuleInstance..sayName(); // 'Hello BaJie!'
特点
- 同步加载方式,适用于服务端,因为模块都放在服务器端,对于服务端来说模块加载较快,不适合在浏览器环境中使用,因为同步意味着阻塞加载。
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。
- 模块加载的顺序,按照其在代码中出现的顺序。
require文件查找规则
一、导入格式:
require(X)
二、查找规则:
-
情况一:X是一个Node核心模块,比如 path、http
- 直接返回核心模块,并且停止查找
-
情况二:X是以
./
或…/
或/
(根目录)等开头的-
第一步:将 X 当做一个文件在对应的目录下查找;
-
如果有后缀名,按照后缀名的格式查找对应的文件
-
如果没有后缀名,会按照如下顺序:
1. 直接查找文件X 2. 查找X.js文件 3. 查找X.json文件 4. 查找X.node文件
-
-
第二步:没有找到对应的文件,将 X 作为一个目录,查找目录下面的 index 文件
1. 查找X/index.js文件 2. 查找X/index.json文件 3. 查找X/index.node文件
-
第三步:如果没有找到,那么报错:
not found
-
模块的加载过程
-
模块在被第一次引入时,模块中的js代码会被运行一次;
-
模块被多次引入时,会缓存,最终只加载(运行)一次;
Q:为什么只会加载运行一次呢?
A:这是因为每个模块对象 module 都有一个属性:loaded
,为false
表示还没有加载,为true
表示已经加载; -
如果有循环引入,那么加载顺序是什么?
Q:如果出现上图模块的引用关系,那么加载顺序是什么呢?
A: 这个其实是一种数据结构:图结构;
图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
Node采用的是深度优先算法:所以加载顺序为:main -> aaa -> ccc -> ddd -> eee -> bbb
将 CommonJS 应用于浏览器?
1. CommonJS 加载模块是同步的:
同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
这个在服务器不会有什么问题,因为服务器加载的 js 文件都是本地文件,加载速度非常快;
2. 如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行;
那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
所以在浏览器中,我们通常不使用CommonJS规范:
当然在 webpack 中使用 CommonJS 是另外一回事;
因为它会将我们的代码转成浏览器可以直接执行的代码;
ES Module
在 ES6 没出来之前,模块加载方案主要使用 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。ES6 的模块功能汲取了 CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。
ES6 模块的设计思想,是尽量的静态化,使得 编译时 就能确定模块的依赖关系,以及输入和输出的变量。
ES6 中,import
引用模块,使用 export
导出模块。默认情况下,Node.js默认是不支持 import
语法的,通过 babel
项目将 ES6 模块 编译为 ES5 的 CommonJS。因此 Babel
实际上是将 import/export
翻译成 Node.js 支持的 require/exports
。
导出模块(export)
一般来说,一个模块对应的就是一个文件,该文件内部的变量外部无法获取,如果你希望外部能够读取到某个变量,就需要使用 export
关键字输出该变量。
-
方式一:在语句声明的前面直接加上 export 关键字
export const name = "八戒"; export const age = 18;
-
方式二:将所有需要导出的标识符,放到 export 后面的 {} 中
// user.js let name = "八戒"; let age = 18; function sayHello() { console.log("Hello 八戒!"); } export { name, age, sayHello }
-
方式三:导出时给标识符起一个别名
// user.js const name = "八戒"; const age = 18; function sayHello() { console.log("Hello 八戒!"); } export { name as fName, age as fAge, sayHello as fSay } //此时导入时要用别名导入 import {fName, fAge, fSay} from "./user.js"
加载模块(import)
上面已经使用 export 命令定义了模块对外的接口后,其它的JS文件就可以通过 import
命令加载这个模块。
-
方式一:普通导入
import {name, age, sayHello} from "./user.js"
-
方式二:导入时给标识符起别名
import {name as fName, age as fAge, sayHello as fSay} from "./user.js"
-
方式三:通过
*
将模块功能放到一个模块功能对象(a module object
)上// 将导出的所有内容放到一个标识符中 import * as user from "./user.js" // 在使用时 console.log(user.name); console.log(user.age); console.log(user.sayHello());
-
注意:导入是实时只读的,不允许运行时改变
// ⚠️ 下面的写法是不允许的 import * as user from "./user.js" user.height = 180; user.setOld = function () {};
export 和 import 结合使用
在当前 js 文件中导入 math.js 和 format.js,然后统一将导入的模块以当前文件的形式导出
import {add, sub} from './math.js'
import {timeFormat, priceFormat} from './format.js'
export {
add,
sub,
timeFormat,
priceFormat
}
default 用法
在一个模块中,只能有一个默认导出(default export)
- 默认导出
export
时可以不需要指定名字; - 在导入时不需要使用
{}
,并且可以自己来指定名字;
// user.js
const name = "BaJie";
export default name;
// main.js
import bj from "./user.js"
console.log(bj); // BaJie
总结
模块化规范大总结
AMD 与 CMD 的区别?
-
模块定义时对依赖的处理不同
- AMD推崇依赖前置,在定义模块时就要声明其依赖的模块;
difine(['module1', 'module2'],function(m1, m2){ })
- CMD推崇就近依赖,只有在用到某个模块时再使用 require 导入;
define(function(require, exports, module){ const module1 = require('./module1'); })
- AMD推崇依赖前置,在定义模块时就要声明其依赖的模块;
-
对依赖模块的处理机制不同
- 相同:AMD 和 CMD 对模块的加载方式都是异步的;
- AMD 当加载了依赖模块之后立即执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致;
- CMD 加载完依赖模块之后,并不会立即执行,等所有的依赖模块都加载好之后,进入回到函数逻辑,遇到 require 语句的时候,才执行对应的模块,这样模块的执行顺序就和我们书写的时候一致了。
ES Module 与 CommonJS 模块加载的区别?
-
CommonJS 是 运行时加载
-
ComminJS 加载是先加载整个模块,生成一个对象(这个对象包含了这个模块的所有API),然后再从这个对象上面读取方法。
-
只能在运行时确定模块的依赖关系,以及输入和输出的变量,一个模块就是一个对象,输入时必须查找对象属性。
let { stat, exists, readfile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;
-
-
ES Module 是 编译时加载
-
ES6 Module 不是对象,它的对外接口只是一种静态定义,在代码静态定义阶段就会生成。
-
可以在编译时就完成模块加载,引用时只加载需要的方法,其他方法不加载。效率要比 CommonJS 模块的加载方式高。
import { stat, exists, readFile } from 'fs';
-
require 与 import 的区别?
由于内容较多,这里不展开来讲,欢迎前往 require 与 import 两种引入模块方式到底有什么区别? 进行查看。
本项目部分代码主要参考 https://www.cnblogs.com/echoyya/p/14577243.html、https://blog.csdn.net/weixin_52834435/article/details/123896045,感谢大佬分享,如有侵权请联系删除。