模块化演进
阶段一:文件即模块,使用<script>标签引入
- 污染全局作用域
- 命名冲突
- 无法管理模块依赖关系
阶段二:命名空间方式,文件暴露一个全局对象,所有方法挂载到全局对象上形成命名空间
- 缓解命名冲突,但无法避免
- 没有私有属性,从而无法避免出现私有属性值被不小心更改的情况
- 依赖关系仍然无法管理
阶段三:IIFE立即执行函数表达式,可以给全局对象上挂载属性来暴露接口,也可以返回一个对象来暴露接口。
- 私有成员得到保证
- 通过IIFE传递参数,可以在一定意义上管理依赖关系。如
;(function($) {
// 通过在调用时传入jQuery,表示该模块需要依赖jQuery,则jQuery就需要在该模块执行前就存在了。
})(jQuery)
模块规范化
- 通过代码来加载模块,即动态去加载模块,而不是使用静态的<script>标签
模块标准化和模块加载器
CommonJS规范
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数导入模块
- 同步模式加载模块,node在启动的时候加载模块,执行的过程中使用模块
AMD规范(异步模块定义规范)
Require.js库
- 实现AMD规范
- 同时也是模块加载器
- 定义模块:使用define()函数
- 加载模块:使用require()函数,动态创建<script>标签,加载模块代码并执行
- AMD使用相对复杂
- 模块JS文件的请求频繁
模块化标准规范
目前基本统一为以下两种规范:
- 浏览器环境:ES Modules
- node环境:CommonJS
ES Module规范
基本特性
- 自动采用严格模式
- 每个模块都有私有作用域
- ES module通过CORS的方式进行跨域请求模块文件,因此如果使用ES module请求跨域资源,需要服务器支持CORS
- ES module的<script>标签会延迟执行脚本,相当于添加了defer,从而避免阻塞浏览器渲染。
导入导出规范
- 静态导入导出
导出:export关键字
- 成员声明并初始化的时候导出
export const name = 'xx';
- 统一导出成员
export { member1, member2 }
- 包含默认导出的成员
export default memberDefault
,default关键字后面的是值。 - 导出时可以重新命名
export { member1 as memberAlpha, member2 as memberBeta }
- 导出的成员是一个只读的引用,模块外部不允许修改导出的成员
注意: - 统一导出的语法
export {}
并不是导出一个字面量对象,而是固定的语法,同理,对应的import {}
也不是对象解构语法。 - 如果使用
export default {}
这种语法,则导出的是一个对象,因为default后面跟的是值。
导入:import关键字
- import from后的路径必须是完整的,不能省略.js扩展名(CommonJS中是可以省略.js扩展名的),也不能省略默认入口文件index.js。这些在使用打包工具的时候才能省略。
- from 相对路径中的
./
不能省略(一般情况下,相对路径的./
可以省略),因为省略的话,认为是在导入安装了的第三方模块 - from 可以使用绝对路径或者url来导入模块
- 如果不提取成员,只需要使用
import 'path'
- 导入所有成员并提供命名空间
import * as name from 'path'
- from 后不能用变量来提供路径
- 动态导入函数
import('path')
,返回的是一个Promise
import('./module.js').then(function(module) {
// module是导出成员
})
同时导入模块的默认成员与具名成员
// 导出
export { name, age }
export default 'default value'
// 导入, 将默认成员放在统一导入里,但需要用as关键字来命名
import { name, age, default as value } from './module.js'
// 或者,默认成员的导入与统一导入分开,这样就不需要用as来命名
import value, { name, age } from './module.js'
导出直接导入的成员
- 这种语法通常用于在入口文件,对资源进行统一的导出管理时用到。
// ./components/index.js
// 直接将module.js中的成员导出,这种语法就是把import替换为export
export { name } from './module1.js'
export { age } from './module2.js'
// app.js
import { name, age } from './components/index.js'
浏览器中ES Module的Polyfill
当浏览器对ES Module不支持时,需要使用Polyfill或者Babel等编译工具来支持(生产阶段不这样使用,因为这是运行时编译,导致执行速度大大降低)。
- browser-es-module-loader
- 使用polyfill时,可能会造成支持ES Module的浏览器会对module文件执行两次。这个问题可以通过给<sciprt>标签添加一个nomodule属性来解决。
- nomodule属性表示如果浏览器不支持ES Module,则执行该脚本;如果浏览器支持ES Module,则不执行该脚本。
在node.js中的ES Module
node从8.5以上开始逐渐实验性支持ES Module。
- ES Module的文件使用.mjs的后缀
- 使用
node --experimental-modules
来启动node - node内置模块一般都有默认导出和具名导出,而第三方模块一般都是默认导出,没有具名导出。
在node中使用ES Module导入CommonJS模块
CommonJS始终只会导出一个默认成员(对象、函数、变量等),而且import {} from ''
不是一个对象解构的语法
// node环境
// index.js
import mod from './commonjs.js'
console.log(mod.foo); // 'commonjs exports foo'
// commonjs.js
module.exports = {
foo: 'commonjs exports foo'
}
不能在CommonJS模块中通过require()导入ES Module模块
// node环境
// index.js
export const foo = 'es module foo';
// commonjs.js
const foo = require('./index.js'); // 会报错
ES Module与CommonJS在node中的差异
node中提供的与模块相关的全局变量和全局函数只能在CommonJS模块中引用,ES Module模块拿不到。实际上,CommonJS模块代码加载时会最终加载到一个函数中,这个函数提供了下列的全局变量。
- require()
- module
- exports
- __filename:在ES Module中,可以通过以下方式来访问到
- 导入url模块的fileURLToPath方法:
import { fileURLToPath } from 'url'
- 传入import.meta.url当前文件的文件URL将其转换为文件路径
const __filename = fileURLToPath(import.meta.url)
- __dirname:在ES Module中,也可以通过上述方式访问
- 导入path模块的dirname方法:
import { dirname } from 'path'
- 传入上述得到的__filename:
const __dirname = dirname(__filename)
node新版本对ES Module的进一步支持
- package.json中type字段设置为"module",表示该目录下的JS文件都是ES Module,因此,也不需要后缀名.mjs,直接.js即可。
- 如果需要在目录下使用CommonJS的模块,则将后缀名改为.cjs,即可在文件中使用CommonJS模块规范(require, module, exports, __filename, __dirname)。
使用Babel兼容早期Node版本对ES Module的支持
- 下载开发依赖包
- @babel/node
- @babel/core
- @babel/preset-env
- 使用
npx babel-node
启动 - 使用
babel-node index.js --presets=@babel/preset-env
来运行index.js文件。 - 或者使用配置文件.babelrc,添加presets的配置。