前端模块化
对于开发大型的、复杂的项目,模块化显得十分重要。
ES6出现之前,社区主要有CommonJS和AMD两种,前者用于服务器,后者用于浏览器。ES6模块化则可以完全替代它们。模块化的一个特点就是:不会污染全局作用域
ES6模块与CommonJS和AMD的主要区别
ES6模块依赖是在编译时就能确定(方便静态优化,需要处于模块顶层),而后两者只能在运行时才能确定。
- CommonJS 模块输出的是一个值的拷贝(如果是引用类型,拷贝指针),ES6 模块输出的是值的动态引用(singleton模式)(如果我想让一个ES6模块重复执行怎么做?)。
- CommonJS 模块存在缓存,需要时要手动清除缓存
- CommonJS 和 AMD 模块是运行时加载,ES6 模块是编译时输出接口(引用)(import()方法除外)。
- ES6顶层this指向undefined,CommonJS模块的顶层this指向一个空对象({})
ES6模块化使用
在ES6代码中,文件就是模块,其内部所有变量,外部都无法获取,this指针值为undefined而非global对象
1、export命令
export var firstName = 'Michael';
export function print(){
console.log("print");
}
export class Person{
}
等价于<=>
var firstName = 'Michael';
var print = function(){
console.log("print")
}
class Person{
}
export { firstName, print , Person};
对于export输出的变量可以使用as关键字重命名
function v1(){...}
export {
v1 as newName
}
使用export导出时,需要提供一个接口(变量定义或者对象直接量形式),这个接口是其模块的一个 引用,可以动态更新。
// 报错
var m = 1;
export m; //相当于export 1
2、import命令
//另一个js文件
import { firstName, lastName, year } from './profile.js';
- 导入的接口是只读的
- import不能使用表达式和变量
- import语句是 Singleton 模式。
//import命令的提升效果
foo();
import { foo } from 'my_module'; //这一行会提升到文件首部,因为编译阶段就执行了
- 可以整体加载模块
//整体加载
import * as circle from './circle';
- 默认导出(可以和常规导出一起存在‘,’隔开)
// export-default.js导出
export default function () { //相当于将这个匿名函数赋值给default变量
console.log('foo');
}
// import-default.js导入
import customName from './export-default';
- 模块的转发
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
//还可以改接口名
export { foo as baz, bar } from 'my_module';
- 模块的继承
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
// main.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
- 运行时加载
import()函数可以替代require的动态加载,返回一个promise,参数为模块对象,可以解构出接口
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
3、ES6模块循环引用
因为ES6模块是采用动态引用方式,所以接口是在编译时确定好的,运行时是否有效需要开发人员自己保证。
CommonJS模块化使用
Node就是采用CommonJS规范。module变量代表当前模块,加载该模块就是加载该模块的module.exports属性。
CommonJS的特点:
- 运行结果会缓存,需要时需要手动清除缓存
- 模块加载顺序与代码中引用顺序一致
module对象属性
- module.id模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,带有绝对路径。
- module.loaded 返回一个布尔值,表示模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块。
- module.exports 表示模块对外输出的值。
module.exports 和 exports
exports就指向module.exports,是个引用
注意:
- 不要给exports赋值
- 如果module.exports被赋值为primitive value,则exports无法使用
AMD规范与CommonJS规范
CommonJS规范加载模块是同步的,AMD规范是非同步的
require()方法
- require一个目录
如果require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。 - require.resolve查询模块完整绝对路径(不会加载模块)
const mod = require.resolve('./moduleB')
console.log(mod)
//console结果
E:\Code-Files\Front-End-Files\Study-Files\temp\moduleB.js
模块缓存
第一次执行一个模块,Node会缓存module.exports。输出的是一个值的拷贝,所以之后模块内部的变化不会影响输出,可以使用引用来解决这个问题。
//main.js
const mB = require('./moduleB')
setTimeout(() => {
console.log(mB.label)
}, 1000)
//moduleB.js
let a={
label:"hello"
}
setTimeout(()=>{
a.label+=" world"
},500)
module.exports=a;
//输出结果为"hello world"
// 删除指定模块的缓存
delete require.cache[moduleName];
CommonJS模块的循环加载
如果模块A加载B,B又加载A,则B将加载A的不完整版本(A中require(B)的前面已经执行的部分部分),等到B执行完毕接着回来执行A剩余的部分,最后返回。
模块加载的实现
1、浏览器加载
//指定type=“module”
<script type="module" src="./foo.js"></script>
2、ES6模块加载CommonJS模块
Node的import命令会自动将module.exports当做模块默认输出。共有三种方式引入
// 写法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }
//由于CommonJS是运行时确定接口,所以禁止一下写法
import { readFile } from 'fs';
3、CommonJS 模块加载 ES6 模块
采用 import() 函数,而不能使用require命令