1. 模块化开发
1.1 模块化背景
模块化是JavaScript一个非常迫切的需求:
- 但是JavaScript本身,直到ES6(2015)才推出了自己的模块化方案;
- 在此之前,为了让JavaScript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、CommonJS等;
1.2 没有模块化的问题
小明和小丽同时在开发一个项目,并且会将自己的JavaScript代码放在一个单独的js文件中
// 小明开发的aaa.js
var flag = true;
if (flag) {
console.log("aaa的flag为true")
}
// 小丽开发的bbb.js
var flag = false;
if (!flag) {
console.log("bbb使用了flag为false");
}
很明显出现了一个问题:
大家都喜欢使用flag来存储一个boolean类型的值; 但是一个人赋值了true,一个人赋值了false; 如果之后都不再使用,那么也没有关系; 但是,小明又开发了ccc.js文件:
// 小明开发了ccc.js
if (flag) {
console.log("使用了aaa的flag");
}
问题来了:小明发现ccc中的flag值不对
- 对于聪明的你,当然一眼就看出来,是小丽将flag赋值为了false;
- 但是如果每个文件都有上千甚至更多的代码,而且有上百个文件,你可以一眼看出来flag在哪个地方被修改了吗?
备注:引用路径如下:
<script src="./aaa.js"></script>
<script src="./bbb.js"></script>
<script src="./ccc.js"></script>
所以,没有模块化对于一个大型项目来说是灾难性的。
当然,我们有办法可以解决上面的问题:立即函数调用表达式(IIFE)
// aaa.js
const moduleA = (function () {
var flag = true;
if (flag) {
console.log("aaa的flag为true")
}
return {
flag: flag
}
})();
// bbb.js
const moduleB = (function () {
var flag = false;
if (!flag) {
console.log("bbb使用了flag为false");
}
})();
// ccc.js
const moduleC = (function() {
const flag = moduleA.flag;
if (flag) {
console.log("使用了aaa的flag");
}
})();
命名冲突的问题,有没有解决呢?解决了。但是,我们其实带来了新的问题:
- 第一,我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
- 第二,代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写;
- 第三,在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;
所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的。
- 我们需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码;
- 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性;
JavaScript社区为了解决上面的问题,涌现出一系列好用的规范,接下来我们就学习具有代表性的一些规范。
2. CommonJS规范
3. ES Module规范
es的导入导出的属性是
只读的,其他模块导入时不能修改。但是可以修改对象中的属性,不能修改对象。导出的变量也不能修改
3.1 export导出
// 方式一:在语句声明的前面直接加上export关键字
export const name = 'coderwhy';
export const age = 18;
export let message = "my name is why";
export function sayHello(name) {
console.log("Hello " + name);
}
// 方式二:将所有需要导出的标识符,放到export后面的 {}中
// -注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;
// -所以: export {name: name},是错误的写法;
const name = 'coderwhy';
const age = 18;
let message = "my name is why";
function sayHello(name) {
console.log("Hello " + name);
}
export {
name,
age,
message,
sayHello
}
// 方式三:导出时给标识符起一个别名
export {
name as fName,
age as fAge,
message as fMessage,
sayHello as fSayHello
}
3.2 default用法
注意:在一个模块中,只能有一个默认导出(default export);
前面我们学习的导出功能都是有名字的导出(named exports):
- 在导出export时指定了名字;
- 在导入import时需要知道具体的名字;
还有一种导出叫做默认导出(default export)
- 默认导出export时可以不需要指定名字;
- 在导入时不需要使用 {},并且可以自己来指定名字;
export default function sub(num1, num2) {
return num1 - num2;
}
import sub from './modules/foo.js';
console.log(sub(20, 30));
3.3 import关键字
import关键字负责从另外一个模块中导入内容,导入内容的方式也有多种:
import {标识符列表} from '模块';
注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
// 方式一:
import { name, age, message, sayHello } from './modules/foo.js';
// 方式二:导入时给标识符起别名
import { name as wName, age as wAge, message as wMessage, sayHello as wSayHello } from './modules/foo.js';
// 方式三:将模块功能放到一个模块功能对象(a module object)上
import * as foo from './modules/foo.js';
console.log(foo.name);
console.log(foo.message);
console.log(foo.age);
foo.sayHello("Kobe");
3.4 import()
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
if (true) {
import sub from './modules/foo.js';
}
为什么会出现这个情况呢?
- 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
- 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
- 甚至下面的这种写法也是错误的:因为我们必须到运行时能确定path的值;
const path = './modules/foo.js';
import sub from path;
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
如果根据不懂的条件,动态来选择加载模块的路径;
这个时候我们需要使用import()函数来动态加载;
// aaa.js模块:
export function aaa() {
console.log("aaa被打印");
}
export let mm = 2
//bbb.js模块:
export function bbb() {
console.log("bbb被执行");
}
//main.js模块:
let flag = true;
if (flag) {
import('./modules/aaa.js').then(a => {
a.aaa();
})
} else {
import('./modules/bbb.js').then(b => {
b.bbb();
})
}
解析:import('./modules/aaa.js')动态加载完aaa.js后import()返回的是promise对象,then的参数a是aaa.js中导出的导出对象,格式如下
a: {
aaa: function(){...}
mm: 2
}
b: {
bbb: function(){...}
}
3.5 node中支持esmodule
需要将.js后缀修改为.mjs