模块化演变过程
第一阶段:
约定每个JS文件就是一个独立模块,通过script标签引入。在这种模式中,所有模块都在全局空间去工作,没有自己的私有空间。这有很多缺陷:
- 污染全局作用域,所有模块都能在外部被任意访问或修改。
- 容易产生命名冲突问题。
- 无法很好管理模块之间依赖关系。
第二阶段:
通过命名空间方式,将每个模块定义在一个独立的全局对象上,可以减少命名冲突的可能,但它仍然没有私有空间。
第三阶段:
IIFE,使用立即执行函数方式为模块提供私有空间,将需要暴露给外部的函数挂载到window对象上。
第四阶段:
各个模块化标准开始出现,最开始出现的是运行于Node环境的Common.JS,但它因为自身问题无法在浏览器推展开来:
- Common.JS是同步的。浏览器要在JS文件,需要从远程服务器获取,网路传输效率远远低于Node环境读取本地文件效率。采用Common.JS规范则会阻塞代码的执行,极大降低运行性能。
- Common.JS属于社区标准,非官方标准,而它读取了JS需要放入到一个运行环境中去执行,需要浏览器厂商的支持,可是各个浏览器厂商不愿意去更改底层代码去支持,所以也难以推行。
因为Common.JS的种种问题原因,所以后来出现了AMD规范、CMD规范,以及最后官方推出了统一浏览器端的ES Modules规范。
AMD的实现原理核心是通过创建script标签定义data-main的方式去拉取js文件,当文件下载完成后,执行定义的回调函数。这个可以去具体看看源码。
<script data-main="./js/index.js" src="./js/require.js"></script>
所以它存在一些缺陷:
- AMD使用起来相对复杂,需要define和require。
- 模块JS文件请求频繁。
而ES Modules规范是随着WebPack等一系列打包工具的流行,才开始普及。
ES Modules
基本特性
ESM自动采用严格模式,忽略'use strict'。- 每个
ES Module都是运行在单独的私有作用域中。 ESM是通过CORS的方式请求外部JS模块的。ESM的Script标签会延迟执行脚本。
要注意ES6 模块导入导出语法并不是解构,而是一个固定语法。
与 CommonJS 相比,CommonJS 输出的是一个值的拷贝,所以在 CommonJS 中:
- 对于基本数据类型导入属于复制,即会被模块缓存。同时在另一个模块可以对该模块输出的变量重新赋值。
- 对于复杂数据类型导入属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对一个模块的值做修改时会影响另一个模块。
- 当使用
require命令加载某个模块时,就会运行整个模块的代码。 - CommonJS 导入模块时,可以自动补全文件路径的扩展名。而ES modules不可以。
而 ES6 模块输出的是值的引用 ,ES6 模块中的导入属于【动态只读引用】:
- 对于只读来说,即不允许修改导入变量的值,
import的变量是只读的,当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。不像CommonJS可以修改读取的变量。 - 对于动态来说,由于
ES6输出的是引用,当该引用的原始值发生变化时,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型 。
其它小点:
- 引用模块时的点斜线是不能省略的,这两者都会认为是加载第三方模块,例如:
import A from "Module.js"
import可以直接引用网络资源模块。 例如:
import { name } from 'http://localhost:3000/04-import/module.js'
- 可以仅仅导入模块,而不引用。对于导入子功能模块很有用:
import {} from './module.js'
import './module.js'
- 通过星号导入模块所有文件。例如:
import * as mod from './module.js'
- 通过全局的
import方法,实现和CommonJS一样的动态导入机制。它返回的是个Promise。
- 具名成员和默认成员可以同时导入,例如:
import abc, { name, age } from './module.js'
- export import的书写方式,将零散的模块组织到一起进行输出:
export { foo, bar } from './module.js'
export { default as Button } from './button.js' // 对于默认导出情况
export { Avatar } from './avatar.js' // 对于具名导出情况
ES Modules的Polyfill
通过以下cdn来兼容不支持ES Modules的模块,原理就是将babel和es module loader下载下来,loader将浏览器不识别的ES Modules交给babel去转换。对于需要import进来的文件通过ajax的方式去请求,再使用babel进行转换。从而支持ES Modules。
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
同时,我们给script标签添加了 nomodule 属性,那么它只会在不支持ES Modules的浏览器下去工作。
ES Modules in Node.js
支持情况
在 node v8.5 将 ES Modules作为实验特性引入了,使用方法如下所示:
- 将文件的扩展名由 .js 改为 .mjs;
- 启动时需要额外添加
`--experimental-modules`参数;
// 此时我们也可以通过 esm 加载内置模块了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
// 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
// 对于第三方的 NPM 模块也可以通过 esm 加载
import _ from 'lodash'
_.camelCase('ES Module')
// 不支持,因为第三方模块都是导出默认成员
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))
在新的 Node v12 之后的版本中,可以通过 package.json 中添加 type 字段为 module,将默认模块系统修改为 ES Module,此时就不需要修改文件扩展名为 .mjs 了。
{
"type": "module"
}
如果需要在 type=module 的情况下继续使用 CommonJS,需要将文件扩展名修改为 .cjs。
对于早期的 Node.js 版本,可以使用 Babel 实现 ES Module 的兼容。babel工作原理图示:
{
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/node": "^7.6.1",
"@babel/plugin-transform-modules-commonjs": "^7.6.0"
}
}
babel的核心模块并不会去转换代码,具体的转换需要我们去配置插件,它是基于插件机制的,每个特性的转换都有对应的插件。而presetenv是一个插件的集合,包含了最新JS标准的所有新特性:
// .babelrc
{
"presets": ["@babel/preset-env"],
// 具体将其它模块标准转换为commonjs的插件
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
与CommonJS交互
在nodejs环境中:
- 在ES Module 文件中可以导入 CommonJS 模块,因为CommonJS 模块始终只会导出一个默认成员,所以不能通过import语法去直接提取成员。
- 不能在 CommonJS 模块中通过 require 载入 ES Module,不支持ES Module模块。
- 注意 import 不是解构导出对象。
与CommonJS差异
在 ESM 中没有模块全局成员,例如这些变量和方法都无法使用了:
// 加载模块函数
console.log(require)
// 模块对象
console.log(module)
// 导出对象别名
console.log(exports)
// 当前文件的绝对路径
console.log(__filename)
// 当前文件所在目录
console.log(__dirname)
解决方法如下:
// require, module, exports 自然是通过 import 和 export 代替
// __filename 和 __dirname 通过 import 对象的 meta 属性获取
// const currentUrl = import.meta.url
// console.log(currentUrl)
// 通过 url 模块的 fileURLToPath 方法转换为路径
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)
在node源码中,它也是将模块代码进行了简单的字符串包装: