模块化的演变过程
stage1-文件划分方式
将每个功能和状态数据存放在不同的文件中,约定每个文件就是独立的模块,使用时将每个文件在html中同锅script引入
缺点:所有模块都在全局范围工作,污染全局作用域,出现命名冲突,模块成员可以被修改,无法管理模块依赖关系等问题
stage2-命名空间方式
将每个功能和状态数据存放在不同的文件中,约定每个文件只暴露一个全局对象,所有的模块成员都挂载到这个全局对象下
缺点:模块成员依然可以被修改,无法管理模块依赖关系等问题
stage3-IIFE立即执行函数
将每个功能和状态数据存放在不同的文件中,文件中的模块成员都放在一个IIFE函数私有作用域中,需要暴露的成员都挂载到全局对象下
模块化规范
CommonJS规范
以同步的方式加载模块,在启动时加载模块,执行时不会加载模块,在浏览器端使用会出现页面加载效率低下,每次加载都会有大量的加载请求,所以一般用在node环境下
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数载入模块
AMD(Asynchronous Module Definition)规范
第一个参数是模块名称,第二参数是模块依赖项,第三个参数为一个函数,函数的每个参数为依赖项导出的成员
// 定义模块
define('module1', [
'jquery',
'module2'
], function($, module2) {
'use strict'
return {
start: function () {
$('body').text('text')
module2()
}
}
})
// 载入模块
require(['./module1'], function (module1) {
module1.start()
})
- AMD生态较完善,但是使用起来相对复杂
- 模块JS文件请求频繁,页面效率低下
CMD(Common Module Definition)规范
define(function (require, exports, module) {
'use strict'
// 通过require引入依赖
var $ = require('jquery')
// 通过exports或者module.exports对外暴露成员
module.exports = function () {
console.log('module2')
$('body').append('<p>module2</p>')
}
})
ES Modules规范
浏览器中使用的主流模块化规范,其基本特性为
- 自动采用严格模式,忽略’use strict’
- 每个ESM模块都是单独的私有作用域
- ESM是通过CROS去请求外部JS模块的
- ESM的script标签会延迟执行脚本
html中的引入方式
<script type="module">
console.log(this)
</script>
ES Modules 导入导出
- 基本使用方式
// module.js
const foo = 'es modules'
export {
foo
}
// app.js
import { foo } from './module.js'
console.log(foo)
- 重命名导出模块
// module.js
const foo = 'es modules'
export {
foo as bar
}
// app.js
import { bar } from './module.js'
console.log(bar)
- 当导出为default命名时
// module.js
const foo = 'es modules'
export {
foo as default
}
// app.js
import { default as bar } from './module.js'
console.log(bar)
- 默认导出
// module.js
const foo = 'es modules'
export default foo
// app.js
// 默认导出时可以取除开关键词外的名称
import a from './module.js'
console.log(a)
导入导出的注意事项
- export导出的不是对象和对象字面量,而是一种固定的写法
- export default导出的是对象
- import不是对export的对象解构,而是一种固定用法
- export导出的不是新的对象或值,而是导出的对象或值的内存引用地址
- export导出的引用地址是只读地址,是一个常量,在外部不能被修改
ES Modules 导入用法
// 原生的ES Modules不能省略扩展名
// import { name } from './module'
import { name } from './module.js'
import { name } from '/04/module.js'
import { name } from 'http://localhost:3000/04/module.js'
// 原生的ES Modules不能省略index.js
// import { foo } from './utils'
import { foo } from './utils/index.js'
// 原生的ES Modules不能省略根目录路径,否则会被认为是在导入node_modules模块
// import { foo } from 'module.js'
// 只加载执行某个模块,而不提取模块的任何成员
import {} from './module.js'
import './module.js'
// 导入多个成员
import * as mod from './module.js'
console.log(mod)
console.log(mod.foo)
// import不能from一个变量,也不能嵌套在判断里,必须出现在代码最顶层
// const modulePath = './module.js'
// import { name } form modulePath
// if (true) {
// import { name } from './module.js'
// }
// 动态加载模块的正确方式
import('./module.js').then(module => {
console.log(module)
})
// 当同时导出了默认成员和具名成员时的导入
export { name. age }
export default 'default export'
import { name, age, default as title } from './module.js'
import title, { name, age } from './module.js'
ES Modules 直接导出对其他模块的引用
此方法可用在index集中导出中,比如components中集中在index中导出
export { Button } form './button.js'
浏览器环境polyfill兼容不支持ES Modules的浏览器
polyfill的实现原理是利用babel实时的解析ES6代码,来到达低版本浏览器支持ES6,但是此方式只适合开发阶段测试,因为太耗费性能,生产环境不适合此方式,生产环境需要走gulp或webpack等打包
<!-- nomodule属性是为了防止支持es6的浏览器执行两次es6代码,nomodule属性只会在不支持es6的浏览器中运行 -->
<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 type="module">
import { foo } from './module.js'
console.log(foo)
</script>
ES Modules 在Node.js中使用
基本使用
- 文件扩展名需从.js修改成.mjs
- 或在package.json中设置"type": "module"后依然可以使用.js作为扩展名,但是此时CommonJS规范的文件扩展名需修改为.cjs
module.mjs
export const foo = 'hello'
export const bar = 'modules'
work-in-node.mjs
import { foo, bar } from './module.mjs'
console.log(foo, bar)
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
启动node命令的方式
$ node --experimental-modules work-in-node.mjs
// 不支持第三方模块的具名成员导入,第三方的默认成员可以导入,内置模块具名成员可以导入
// import { camelCase } from 'lodash'
import _ from 'lodash'
import { writeFileSync } from 'fs'
也可以使用babel来让ESM运行在CommonJS中
$ yarn add @babel/node @babel/core @bable/preset-env --dev
$ yarn babel-node work-in-node.js --presets-@babel/preset-env
或者在项目中的.babelrc中设置后使用
{
"presets": ["@babel/preset-env"]
}
$ yarn babel-node work-in-node.js
与CommonJS模块交互
- ES Modules中可以导入CommonJS模块
- CommonJS中不能导入ES Modules模块
- CommonJS始终只会导出一个默认成员
- 注意import不是解构导出对象
common.js
module.exports = {
foo: 'commonjs'
}
// exports.foo = 'commonjs'
es-module.mjs
import mod from './common.js'
console.log(mod)
与CommonJS的差异
- ESM中没有CommonJS模块全局成员
- require可以用import实现
- exports可以用export实现
- 其他方法需要模拟,例如模拟__dirname和__filename
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
console.log(__filename)
const __dirname = dirname(__filename)
console.log(__dirname)