模块化的演进过程
- 文件划分:一个文件就是一个模块,通过
script标签引入。但是可以在外部直接访问到文件中的变量,没有一个独立的私有空间,导致外部可以直接修改模块内部成员,污染全局作用域。模块多了之后,带来命名冲突问题,也无法管理模块依赖关系。 - 命名空间方式:约定每个模块暴露一个全局对象,所有的模块成员都挂载到这个全局对象下。
var module1 = {
name: 'module1',
method1: function(){
alert(11)
},
method2: function(){
alert(22)
},
}
这样可以减少命名冲突的问题,但是仍然没有私有空间,外部仍可以访问并修改模块内部成员,模块间的依赖关系也没有得到解决。
- IIFE:通过立即执行函数为模块提供私有空间。
;(function(){
var name = 'module2';
function method1(){
alert(33)
};
function method2(){
alert(44)
};
window.module2 = {
method1: method1,
method2: method2,
}
})()
这样在外部只能访问到暴露出来的method1和method2,name等私有成员不能访问。
模块化规范的出现
以上都是通过约定的方式来实现模块化的代码组织,在不同的开发者实施时会有细微的差别。为了统一不用的开发者和代码之间的差异,需要一系列的标准去规范。
-
CommonJS规范:在nodejs中提出的规范,以同步模式加载模块- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过
module.exports导出成员 - 通过
require函数载入模块
-
ES Modules规范:在浏览器环境规范即
ECMAScript 2015(ES6)
ES Modules 特性
目前浏览器已大部分支持该特性。通过给script标签添加type=module的属性,就可以以ES Modules的标准执行其中的JS代码。
<script type='module'>
alert('this is module')
</script>
特性:
-
自动采用严格模式,忽略
'use strict'例如,打印
this,在module属性下,this为undefined,否则this指向全局对象。 -
每个ESM模块都是运行在单独的私有作用域中
<script type='module'>
var foo = 'foo';
console.log(foo); // foo
</script>
<script type='module'>
console.log(foo); // error: foo is not defined
</script>
-
EMS通过
CORS的方式请求外部JS模块如果请求的
src地址服务端不支持CORS,则会提示跨域的错误。 -
EMS的
script标签会自动延迟执行脚本会等待网页的执行渲染完成之后,再执行脚本。类似在
script标签上添加来defer属性。
ES Modules 的导入和导出
// module.js
var foo = 'es module'
export { foo };
// app.js
import { foo } from './app.js';
console.log(foo); // es module
可以导出一个对象,里面是要暴露出的成员,可以通过as对导出的变量重命名:
// module.js
export {
foo as fooname,
fn as foofn,
}
// app.js
import { fooname, foofn } from './module.js'
可以默认导出一个成员,导入时可以直接引入并修改变量名:
// module.js
export default name;
//app.js
import name from './module.js';
// or
import fooname from './module.js';
注意:
export {}和import {}是固定语法,并不是导出的一个对象或对象的解构;导入的变量是对导出变量的址引用,模块中值的改变会影响引入模块的改变;导入模块不能对导出变量的重新赋值。
ES Modules 导入导出
直接在文件中将导入的成员导出: export { foo } from './modules.js'
使用场景:需要将所有的模块整合到一个模块中统一导出时。
比如:components 下的Button 和 Avatar 模块
// index.js
import { Button } from './Button.js';
import { Avatar } from './Avatar.js';
export { Button, Avatar };
// 直接导出方式,在当前模块是无法使用的。此时index文件是作为一个整合桥梁。
export { Button } from './Button.js';
export { Avatar } from './Avatar.js';
如果直接导出的成员是默认成员,则需要对default重命名
export { default as Button } from './Button.js';
ES Modules in Browers 中 polyfill兼容方案
浏览器如果不识别ES6,可以通过babel将代码转为浏览器可识别的ES5代码。
ES Modules in NodeJS
与CommonJS 模块交互
- 不能在CommonJS模块中通过require载入ES Modules
- ES Modules模块中可以导入CommonJS模块
- CommonJS始终只会导出一个默认成员,import不是解构导出对象
modules.exports = {
name: '张三',
}
// or
exports.name = '李四'
与CommonJS 模块的差异
// commonjs
console.log(require);
console.log(module);
consoloe.log(__filename);
console.log(__dirname);
// esm 中没有CommonJS中的那些模块全局成员
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url)
consoloe.log(__filename);
const __dirname = dirname(__filename)
console.log(__dirname);
模块文件名,新版本node支持ESM
module.cjs为commonJS,module.mjs为ESM
Babel兼容
使用Babel的预设新特性可以帮助在低版本的node环境下实现ESM兼容
- 安装依赖
yarn add @babel/node @babel/core @babel/preset-env preset-env是插件的集合
- 运行
yarn babel-node index.js --presets=@babel/preset-env
- 配置文件.babelrc
{
"presets": ["@babel/preset-env"]
}
运行:yarn babel-node index.js
- 使用插件完成转换
yarn add @babel/plugin-transform-modules-commonjs
- 配置文件.babelrc
{
"plugins": "@babel/plugin-transform-modules-commonjs"
}