4种模块规范
模块化的好处
- 将系统分离成独立功能的模块,不仅可复用,还可按需加载
- 提高了代码的可复用性和维护性
- 隔离作用域,避免变量冲突
发展过程
IIFE自执行函数 => CommonJS(服务端) => AMD(浏览器) => CMD(浏览器) => ES6 Module
以前ES5没有模块化规范,所以产生了CommonJS、AMD、CMD三种规范,另外ES6又新增了一种
如今webpack自己实现了一套模块机制,无论是CommonJS的require语法还是ES6的import语法,都能被解析成指定环境的可运行代码,因此ES6广泛应用(也就是说,不一定要浏览器原生支持)
IIFE自执行函数
-
思路
- 在一个单独的函数作用域中执行代码并返回
-
使用
(function(){ return { data:[] } })()
CommonJS
-
思路
- 每个js文件都可以作为一个模块
- 在服务器端
- 模块的加载运行是同步阻塞的,会等待引用完毕再执行下面
- 在浏览器端
- 模块需要提前编译处理打包,不然浏览器不能识别
-
使用
//定义 module.exports = obj 或者 exports.xxx = value//引入 var yyy = require(模块路径); -
用途
- 后端的Node.js和Webpack采用此种模块化规范
-
注意
- 在前端浏览器中并不支持,因为缺少四个Node.js环境的变量:
module/exports/require/global - 当引用加载同一个模块时,不会再执行该模块,而是取到缓存中的值,也就是说,都只会在第一次加载时运行一次,除非手动清除系统缓存
- 在前端浏览器中并不支持,因为缺少四个Node.js环境的变量:
AMD规范
-
思路
- 由于CommonJS的同步机制只适用于服务器端,搬到浏览器端就容易陷入等待的假死状态,因此需要改进为异步机制
- 异步机制的实现:模块在定义的时候,就必须先知道其需要依赖的模块,然后把本模块代码写在回调函数中执行,因此衍生出AMD规范
-
使用
-
没有依赖的module1模块(一个参数)
//module1.js define(function(require,exports,module){ exports.name = "jack" //暴露 }) -
依赖module1的module2模块(两个参数)
//module2.js define(['module1','module2'], function(m1,m2){ }) -
在main.js中引入模块
require(['module1','module2'], function(m1,m2){ //通过m1和m2去使用这两个模块 console.log(m1.name) })
-
-
具体实现:RequireJS
-
顺序:模块化加载 => 全部模块预执行 => 主逻辑中调用模块
-
核心原理:创建script元素,通过指定script元素的src属性来实现动态加载模块的
-
内部机制:模块的定义是一个function,这个function实际是一个 factory(工厂模式),加载执行过一次就不重复加载了,直接返回模块实例
-
缺点:饱受诟病的是,定义模块的时候需要把所有依赖模块都罗列一遍(推崇前置依赖),而且在使用时还需要在 factory 中作为形参传进去
- 即:
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... });
-
CMD规范
-
思路
- 由于AMD的具体实现RequireJS不优雅,“前置依赖”的模块管理能力差,因此要改进
- 所以出现了新的具体实现:SeaJS,基于新的CMD规范,推崇“就近依赖”
- 顺序:模块预加载 => 主逻辑调用模块时才执行模块中的代码
-
用法
- 基本和AMD相同,都是异步的
- 不过还融合了CommonJS的写法,模块引入有同步和异步方式
-
二者区别在于:
- AMD当加载了依赖模块之后立即执行依赖模块,依赖模块的执行顺序和我们函数内书写的顺序不一定一致;
- 而CMD加载完依赖模块之后,并不会立即执行,等所有的依赖模块都加载好之后,进入回到函数逻辑,遇到require语句的时候,才执行对应的模块,这样模块的执行顺序就和我们书写的时候一致
-
使用
- 定义模块
//module1.js define(function (require, exports, module) { exports.name="i am module1" })- 引入模块
//main.js define(function (require, exports, module) { //引入依赖模块(同步) var module2 = require('./module2'); console.log(module2.name) //引入依赖模块(异步1) require.async('./module3', function (m3) { //这里m3对应module3 }) //引入依赖模块(异步2) var module4=require.async('./module4'); console.log(module4.name) })
ES6的Module
使用:
- 定义:通过export暴露API
- 引入:通过import用结构语法{ }引入,除非是针对default导出
注意:ES6模块只能在模块顶层定义和引入,属于编译时加载,不是对象,在代码块中会报错,也就是说无法像CommonJS那样生成一个对象后在运行时动态加载,对于此缺点,有一个新提案建议引入import()方法,返回一个Promise对象,实现动态引入
注意:在某些浏览器是不支持的,因此需要Babel先转译,转成ES5语法才能兼容,涉及到的包有babel-cli,babel-preset-es2015,browserify,还要定义.babelrc文件
-
三种方式
-
分别暴露
//module1.js export let m = 1 export let arr = [1,2,4] export function fun(){ console.log('i am a fun'); } //引入与使用(结构引入) import { m as name, arr, fun } from './module1' console.log(name); fun() -
统一暴露
//module2.js let m=1; let arr=[1,2,4] function fun() { console.log('i am a fun'); } export { m, arr, fun } //引入与使用(结构引入) import { m, arr, fun } from '/module2.js' console.log(arr); fun() -
默认暴露
//错误,因为是导出一个叫default的变量,其后不能跟声明语句 export default var a = 1; //正确,导出一个对象,对象即default export default { m:1, fun(){ console.log('i am a fun from export defalut') } } //引入和使用(module为自定义任意名字) import module from './module3.js' module.fun()
-
-
对比CommonJS的案例
//CommonJS模块 let { basename, dirname, parse } = require('path'); //ES6模块 import { basename, dirname, parse } from 'path';- 区别:
- 当 require path 时,CommonJS会将path模块运行一遍,并返回一个对象,这个对象包含path模块的所有API,只是通过ES6的对象操作符,取出其中几个变量
- 当 import path 时,ES6只会讲列出的几个变量从path模块中提取出来,预编译放入,从而实现tree-shaking
- 区别: