模块化二—CommonJS、AMD、CMD、ES6模块
一 CommonJS规范:是Node.js使用的模块化规范方式
1 暴露定义的模块:
// m1.js // 1. 正确 module.exports = { name: 'lindaidai', sex: 'boy' } // 2. 正确 exports.name = 'lindaidai'; exports.sex = 'boy' // 3. 正确 module.exports.name = 'lindaidai'; module.exports.sex = 'boy' // 4. 无效 exports = { name: 'lindaidai', sex: 'boy' }
2 引入模块:require()全局方法是 node
中的方法哈,它不是 window
下面的,所以如果你没做任何处理,想直接在 html
里用肯定就是不行的了,只能在node环境中使用该方法。
3 模块标识符:指在引入模块时调用 require()
函数的参数,如:
// 直接导入 const path = require('path'); // 相对路径 const m1 = require('./m1.js'); // 直接导入 const lodash = require('lodash');
path
这种它是Node.js
就自带的模块,m1
是路径模块,lodash
是我们使用npm i lodash
下载到node_modules
里的模块。模块引入方式分为三类:
- 核心模块(
Node.js
自带的模块):查找方式:直接跳过路径分析和文件定位 - 路径模块(相对或绝对定位开始的模块):查找方式:直接得出相对路径就好了
- 自定义模块(
node_modules
里的模块):查找方式(路径分析):先在当前目录的node_modules
里找这个模块,如果没有,它会往上一级目录查找,查找上一级的node_modules
,依次往上,直到根目录下都没有, 就抛出错误。
3.1文件定位:
在NodeJS路径分析中, 省略了扩展名的文件, 会依次补充上.js, .node, .json来尝试, 如果传入的是一个目录, 那么NodeJS会把它当成一个包来看待, 会采用以下方式确定文件名
第一步, 找出目录下的package.json, 用JSON.parse()解析出main字段
第二步, 如果main字段指定的文件还是省略了扩展, 那么会依次补充.js, .node, .json尝试.
第三步, 如果main字段制定的文件不存在, 或者根本就不存在package.json, 那么会默认加载这个目录下的index.js, index.node, index.json文件.
以上就是文件定位的过程, 再搭配上路径分析的过程, 进行排列组合, 这得有多少种可能呀. 所以说, 自定义模块的引入, 是最费性能的.
4 CommonJS规范特点:
- 所有代码都运行在模块作用域,不会污染全局作用域;
- 模块是同步加载的,即只有加载完成,才能执行后面的操作;
- 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;
- CommonJS输出是值的拷贝(即,
require
返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。
二 AMD(Asynchronous Module Definition)异步模块定义,是一种前端模块组织规范:AMD的实现主要有两个: requireJS和curl.js
AMD的理念可以用如下两个API概括: define和require
define方法用于定义一个模块,它接收两个参数:
- 第一个参数是一个数组,表示这个模块所依赖的其他模块
- 第二个参数是一个方法,这个方法通过入参的方式将所依赖模块的输出依次取出,并在方法内使用,同时将返回值传递给依赖它的其他模块使用。
三 CMD(Common Module Definition)通用模块定义:sea.js是遵循CMD规范实现的模块加载器
和AMD不同的是,CMD没有提供前置的依赖数组,而是接收一个factory函数,这个factory函数包括3个参数
- require: 一个方法标识符,调用它可以动态的获取一个依赖模块的输出
- exports: 一个对象,用于对其他模块提供输出接口,例如:exports.name = "xxx"
- module: 一个对象,存储了当前模块相关的一些属性和方法,其中module.exports属性等同于上面的exports
如下所示
// CMD define(function (requie, exports, module) { //依赖就近书写 var module1 = require('Module1'); var result1 = module1.exec(); module.exports = { result1: result1, } }); // AMD define(['Module1'], function (module1) { var result1 = module1.exec(); return { result1: result1, } });
AMD和CMD区别:
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块。
// 在 AMD中 math.js define(['m1'], function (m1) { console.log('我是math, 我被加载了...') var add = function (a, b) { return a + b; } var print = function () { console.log(m1.name) } return { add: add, print: print } }) // 在CMD中math.js define(function (require, exports, module) { console.log('我是math, 我被加载了...') var m1 = require('m1'); var add = function (a, b) { return a + b; } var print = function () { console.log(m1.name) } module.exports = { add: add, print: print } })
假如此时m1.js
中有一个语句是在m1
模块被加载的时候打印出"我是m1, 我被加载了..."
。
执行结果区别:
AMD
,会先加载m1
,"我是m1"
会先执行CMD
,我是"我是math"
会先执行,因为本题中console.log('我是math, 我被加载了...')
是放在require('m1')
前面的。
即 两者之间,最明显的区别就是在模块定义时对依赖的处理不同:
1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块2、CMD推崇就近依赖,只有在用到某个模块的时候再去require
四 ES6 Modules规范:ES6
标准出来后,ES6 Modules
规范基本成为了前端的主流
1 export有两种模块导出方式:
- 命名式导出(名称导出)
- 默认导出(自定义导出)
命名式导出:
// 以下两种为错误 // 1. export 1; // 2. const a = 1; export a; // 以下为正确 // 3. const a = 1; export { a }; // 4. 接口名与模块内部变量之间,建立了一一对应的关系 export const a = 1, b = 2; // 5. 接口名与模块内部变量之间,建立了一一对应的关系 export const a = 1; export const b = 2; // 或者用 as 来命名 const a = 1; export { a as outA }; const a = 1; const b = 2; export { a as outA, b as outB };
容易混淆的可能是2
和4
两种写法了,看着很像,但是2
却不行。2
直接导出一个值为1
的变量是和情况一一样,没有什么意义,因为你在后面要用的时候并不能完成解构。
但是4
中,接口名与模块内部变量之间,建立了一一对应的关系,所以可以。
默认导出,默认导出会在export
后面加上一个default
:
// 1. const a = 1; export default a; // 2. const a = 1; export default { a }; // 3. export default function() {}; // 可以导出一个函数 export default class(){}; // 也可以出一个类
2 import导入:import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。
// 某个模块的导出 moudule.js export const a = 1; // 模块导入 // 1. 这里的a得和被加载的模块输出的接口名对应 import { a } from './module' // 2. 使用 as 换名 import { a as myA } from './module' // 3. 若是只想要运行被加载的模块可以这样写,但是即使加载2次也只是运行一次 import './module' // 4. 整体加载 import * as module from './module' // 5. default接口和具名接口 import module, { a } from './module'
第四种写法会获取到module
中所有导出的东西,并且赋值到module
这个变量下,这样我们就可以用module.a
这种方式来引用a
了。
3 Babel下的ES6模块转换:如果你有使用过一些ES6的Babel的话,你会发现当使用export/import
的时候,Babel也会把它转换为exports/require
的形式。
五 CommonJS与ES6 Module规范的区别:
- CommonJS模块是运行时加载,ES6 Modules是编译时输出接口
- CommonJS输出是值的浅拷贝;ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变
- CJS是
同步加载
,ESM是异步加载;(
由于CJS是用于服务器端的模块体系,需要加载的模块都在本地,所以采用同步加载也不会出问题,但是ESM用于浏览器端时,可能涉及到一些异步请求,所以需要采用异步加载。)
- CommonJs导入的模块路径可以是一个表达式,因为它使用的是
require()
方法;而ES6 Modules只能是字符串 - CommonJS
this
指向当前模块,ES6 Modulesthis
指向undefined
- 且ES6 Modules中没有这些顶层变量:
arguments
、require
、module
、exports
、__filename
、__dirname
相关阅读: