什么是模块?
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
为什么要使用模块化
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
实现模块化方式
IIFE
立即调用的函数表达式
- 在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
(function(globalVariable){
globalVariable.test = function() {}
// ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
AMD
- 异步加载模块
- 是 RequireJS 在推广过程中对模块定义的规范化产出
- 用于浏览器端。
- 优点
- 在浏览器环境中异步加载模块;并行加载多个模块;
- 缺点
- 开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;
// AMD
define(['./a', './b'], function(a, b) {
// 加载模块完毕可以使用
a.do()
b.do()
})
CMD(目前很少使用)
- 异步加载模块,模块使用时才会加载执行(玉伯开发,已经没有维护了)
- 是 SeaJS 在推广过程中对模块定义的规范化产出
- 用于浏览器端。
- 优点
- 依赖就近,延迟执行 可以很容易在 Node.js 中运行;
- 缺点
- 依赖 SPM 打包,模块的加载逻辑偏重;
// CMD
define(function(require, exports, module) {
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require('./a')
a.doSomething()
})
CommonJS
- 同步加载模块
- 用于服务端(Node.js)。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
ES Module
- ES Module 是原生实现的模块化方案
- ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// 单个导出
export function init() {}
export const myName = 'hjw'
// 导出一组变量
function init() {}
const myName = ''
export { init, myName };
// 引入
import { init, myName } from './export.js'
// as 关键字重命名
import { myName as newName } from './a.js'
注意
- 原生浏览器不支持 require/exports,可使用支持 CommonJS 模块规范的 webpack 等打包工具,它们会将 require/exports 转换成能在浏览器使用的代码。
- import/export 在浏览器中无法直接使用,我们需要在引入模块的
总结
- CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
- ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块与 CommonJS 模块 差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
- CommonJS 模块的require命令是同步加载模块,ES6 模块的import命令是异步加载,有独立的模块依赖的解析阶段。
Node.js 的模块加载方法
概述
- JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
- CommonJS 模块是 Node.js 专用的,从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
- Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
- Node.js 要求 CommonJS 模块采用.cjs后缀文件名。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。
- 总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。
package.json 的 main 字段
- package.json文件有两个字段可以指定模块的入口文件:main和exports。比较简单的模块,可以只使用main字段,指定模块加载的入口文件。
{
"type": "module",
"main": "./src/index.js"
}
package.json 的 exports 字段
exports字段的优先级高于main字段。它有多种用法。
- 子目录别名
- main 的别名
- exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。
{
"exports": {
".": "./main.js"
}
}
// 等同于
{
"exports": "./main.js"
}
- 由于exports字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。
{
"main": "./main-legacy.cjs",
"exports": {
".": "./main-modern.cjs"
}
}
- 上面代码中,老版本的 Node.js (不支持 ES6 模块)的入口文件是main-legacy.cjs,新版本的 Node.js 的入口文件是main-modern.cjs。
- 条件加载
利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。
{
"type": "module",
"exports": {
".": {
"require": "./main.cjs",
"default": "./main.js"
}
}
}
拓展-框架输出产物
来源:Vuejs设计与实现
- 直接在HTML页面中通过script标签引入
- 需要输出 IIFE 格式的资源
- 或者输出 UMD 格式的资源(输出内容包含 amd,cjs 和 iife ,现在用的比较少)
- 兼容性好,但是包体积特别大
- 现在慢慢用的比较少
- 通过原生 ESM引入,即通过 script type="module"标签引入
- 需要输出 esm 格式的资源
- 在 Node.js 中引入,场景:服务端渲染
- 需要输出 cjs 格式的资源
const config = {
input: 'input.js',
output: {
file: 'output.js',
format: 'iife' // 指定模式 or esm/cjs
}
}