js在没有模块化之前,通过script一个一个文件导入:
- 文件与文件中的依赖关系不好管理,要手动维护js的加载顺序
- 每个script都要向服务器请求一次静态资源,建立成本很高,过多请求严重拖慢网页的渲染速度
- script顶局作用域即全局作用域,没有特别处理变量或函数,会造成全局作用域污染
模块化解决以上问题:
- 通过导入导出,清晰知道模块间依赖关系
- 可以通过打包工具进行打包,在页面中只需要加载合并后的资源文件,减少网络开销
- 多模块间的作用域隔离,彼此命名不会有冲突
从09年开始,Js社区对模块化进行不断尝试,出现了:
Commonjs(值拷贝和动态声明)
-
commonjs是基于服务端而设计的,规定一个文件就是一个模块,每个模块都有自身的作用域,所有的变量和函数都只有自己能访问,每个模块内部都有一个module对象,代表当前模块。(这是区别原生script文件的最大区别,原生script在顶层作用域声明变量或函数,会导致污染全局环境)。
- module对象的属性:
- id: 模块的标识符,通常是带有绝对路径的模块文件名
- filename: 模块的文件名,带有绝对路径
- loaded:布尔值,表示模块是否已经被加载完成成
- parent:一个对象,表示调用该模块的模块
- children:一个数组,表示该模块要用到的其它模块
- exports:一个对象,表示模块对外输出的值
- module对象的属性:
-
它使用module.exports导出一个模块,通过require导入模块。
-
使用require加载的模块会被缓存,只会被加载一次,module对象会存放模块的信息,其中有一个属性loaded来判断是否被加载过,如果为true则是被加载过
-
使用动态加载模块,require支持使用表达式导入模块,导入和导出在代码的运行阶段,也就是可以自由地在表达式语句中引用模块。
-
commonjs导出的是值拷贝,可以对导入值进行修改,但因为是值拷贝,所以不会影响原模块
// CommonJS
// a.js
let a = 1;
let b = 2;
module.exports = {
x: a,
y: b
}
// or
exports.x = a;
exports.y = b;
// b.js
const a = require('./a')
console.log(a); // { a: 1, b: 2 }
ESM
直到2015,es6定义了js模块标准(ESM),使之有了模块的概念。
- esm是静态声明的:
- 必须在模块首部声明
- 不可以使用表达式或变量
- 不允许被嵌套到其它语句中使用
- 因为是静态加载的,在es6代码的编译阶段,就可以分析模块间的依赖关系,可以进行编译过程的优化
- es6 module导出的是值的映射(或者说是值的引用),导入值是只读的,不能进行修改,因为会影响到原模块
// a.js
// 这种是静态导入
import {a, b} from './b.js'
console.log(a) // 1
console.log(b); // 2
// b.js
let a = 1;
let b = 2;
export { a, b }
<!-- 静态导入的html -->
<body>
<!-- 不能通过打开本地文件的形式打开html,会提示跨域,要通过服务器的方式打开 -->
<!-- 可以安装插件:Live Server,然后右击以“live server方式”打开文件 -->
<script src="./a.js" type="module"></script>
</body>
ESM对比commonjs的优势:
- 死代码检测和排除:可以使用静态分析工具判断哪些模块不会执行,在打包时去掉这部分无用模块
- 模块变量类型检查:js是动态语言,不会在代码执行前检查类型错误。es6 module静态模块结构有助于确保模块间传递的值或接口类型是正确的
- 编译器优化。commonjs导入的是一个对象,而es6支持直接导入变量,减少引用层级,效率更高。
es6目前已经得到大多现代浏览器支持,但在应用中还需要等待一段时间,原因:
- 无法使用code splitting
- 大多Npm包还是commonjs的形式,浏览器不支持此语法,因此这些包无法直接使用
- 仍要考虑个别浏览器及平台兼容问题
所以就诞生了模块打包工具(module bundle):
- 解决模块间的依赖
- 使其打包后能在浏览器上正常运行
比较出名的:
- webpack
- rollup
- parcel等
AMD
在ES6模块出现之前,AMD(异步模块定义)是一种很热门地浏览器模块化方案。
- AMD的定义和引用
AMD规范只定义了一个全局函数define,通过它可以定义和引用模块,它有3个参数:- 第1个为id,模块的名称;
- 第2个为数组,它定义了所依赖的模块,依赖的模块必须根据模块工厂函数优先级执行,并且执行的结果应该按照数组中的位置顺序以参数的形式传入工厂函数中
- 第3个factory为模块初始化要执行的函数或对象。如果是函数,则是单例模式,只会被执行一次;如果是对象,则此对象为模块的输出值。
define(id?, dependencies?, factory)
// 比如:
// 1. 定义了一个叫alpha的模块
// 2. 它依赖了require,exports,beta三个模块
// 3. 它导出了verb函数
define("alpha", ["require", "exports", "beta"], function(require, exports, beta) {
exports.verb = function() {
return beta.verb();
}
})
- AMD特性:异步加载,即同步并发加载所依赖的模块,当所有依赖模块都加载完后,再执行当前模块的回调函数。
CMD
CMD(common module definition,通用模块定义),它是基于浏览器环境制定的模块规范。
- 定义和引用
它通过一个全局函数define来实现的,但只有一个参数,该参数可以是函数,也可以是对象。如果是对象,那么模块导出的就是这个对象;如果是函数,这个函数会被传入3个参数:require, exports和 module
define(fatory)
// 如果参数是函数
// 第1个参数为require,用来引用其它模块,也可以调用require.async函数来异步调用模块
// 第2个参数为exports,是个对象,当定义模块时,需要通过向参数exports添加属性来导出模块API
// 第3个参数module是一个对象,它包含3个属性:uri模块完整的路径;dependencies,模块的依赖;exports,模块需要被导出的API,作用同第二个参数
define(function(require, exports, module) {
// 导入的模块
var add = require('math').add;
// 定义导出的对象
exports.increment = function(val) {
return add(val, 1)
}
// 定义当前模块的信息,比如模块id名称
module.id = "increment"
})
- CMD特点:懒加载
它不需要在定义模块时声明模块,可以在模块执行时动态加载依赖。而且,它同时支持同步和异步加载模块。
UMD
universal module definition 统一模块标准,它不是模块管理规范,而是带有前后端同构思想的模块封装工具。通过UMD可以在不同环境选择对应的模块规范。比如nodejs使用commonjs,在浏览器下支持AMD的,采用AMD模块,否则导出为全局函数。
它的实现原理:
- 判断是否支持AMD(即define是否存在),存在则使用AMD方式加载模块
- 判断是否支持nodejs模块格式(即exports是否存在),存在则使用commonjs加载模块
- 如果前两个都不存在,则将模块公开到全局,比如window或global
function test() {
let a = 1;
let b = 2;
return { a, b}
}
// AMD规范环境
if(typeof define === 'function' && define.amd) {
define(test)
// 如果commonjs环境
}else if(typeof module === 'object' && typeof module.exports === "object") {
module.exports = test()
// 不使用任何模块系统,直接挂载到全局window上
}else {
window.test = test;
}
// 以上可以优化成:
(function(root, callback) {
// AMD规范环境
if(typeof define === 'function' && define.amd) {
define(callback)
// 如果commonjs环境
}else if(typeof module === 'object' && typeof module.exports === "object") {
module.exports = callback()
// 不使用任何模块系统,直接挂载到全局this上,因为全局变量不一定就是window,也有可能是global,所以用this指向当前全局环境
}else {
root.test = callback;
}
})(this, function() {
let a = 1;
let b = 2;
return { a, b}
})
网友评论