早期JS中用不到模块化,JS多用来表单验证,没必要放到多个文件中来写;
后来JS需求变得复杂:ajax前后端分离、SPA、前端路由和Node等,使得对模块化的需求愈加强烈。
为此社区中产生了AMD、CMD、CommonJS等模块化规范,随着官方ES6中ES Module的提出,社区规范可以落幕了
什么是模块化
- 将程序划分为一个个模块,在模块内部可以编写属于自己作用域的代码,不会影响其他模块
- 模块可以将自己希望暴露的东西暴露给别的模块使用,也可以导入其他模块暴露的变量、函数、对象等
- 一个独立的js文件就是一个模块
没有模块化带来的问题
在html文件中,<script>标签导入的文件没有独立作用域,会出现变量命名冲突,产生难以察觉的bug,可以采用立即执行函数来包裹每个js文件,将需要暴露的变量作为函数返回值
//a.js:暴露变量a
var moduleA = (function () {
var a = "aaaaa";
return {
a: a,
};
})();
// index.js:引用a.js中变量a
(function () {
// 现在有全局变量moduleA
var a = moduleA.a;
console.log("why", a);
})();
采用立即执行函数虽然解决了作用域的问题,但是依然存在模块命名冲突的问题,同时还需要记住名字,增加了维护成本
几种模块化的方案
1 CommonJS规范
Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发
- 导出
const name = "xs";
function sum(n1, n2) {
return n1 + n2;
}
// module.exports指向要导出的对象
module.exports = {
name,
sum,
age: 18,
}
module.exports和exports之间建议使用前者,那么node中要exports何用,主要是为了符合CommonJS规范
- 导入
const {name, age, sum} = require('文件路径.js')
缺点:
导入文件是同步的,在服务端导入的是本地文件,速度较快;但是在浏览器端,去服务器上请求文件会阻塞之后JS代码的运行,所以CommonJS一般用于浏览器端
2 AMD
Asynchronous Module Definition 异步模块定义
- 异步加载模块
- 用于浏览器端
- 实现的库:requirejs
3 CMD
Common Module Definition 通用模块定义
- 异步加载模块,吸收了CommonJS关键字
- 用于浏览器端
- 实现的库:SeaJS
4 ES Module⭐
4.1 用法
如果js文件中使用了ES Module的模块化规范,在html导入js文件时,必须设置type属性为module
<script src="./main.js" typr="module"></script>
同时,该html文件应以本地服务器的方式从浏览器获取,否则会出现CORS跨域问题。(可以安装VScode插件Live Server)
4.2 export & import
- 导出:
export ***
// 1 分别导出
export const name = "xs";
export function sum(n1, n2) {
return n1 + n2;
}
// 2 一起导出
const name = "xs";
function sum(n1, n2) { return n1 + n2; }
export{
name,
age,
// name: 'xs 错误,{}不是对象的意思,不能写键值对
}
// 3 默认导出 不能写为export default const foo = 18;
const age = 18;
export default age;
- 导入:
import *** from ***
// 1 直接导入
import {name, sum} from "./file.js";
// 2 导入时起别名
import {name as fName, sum} from "./file.js";
// 3 全部导入
import * as foo from "./file.js"
foo.name // 见名知意:知道是foo导入的name,区分于当前文件中定义的name
foo.sum
// 4 默认导入 在导入时不需要使用 {},并且可以自己来指定名字
import age from "./file.js"
- 导入+导入
export *** from ***
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;
这样方便指定统一的接口规范,也方便阅读;这个时候,我们就可以使用export和import结合使用
场景:utils文件夹下有文件:math.js、format.js等,每个文件都提供了一些通用的工具方法
通常在utils文件夹下创建一个index.js文件,导入math.js、format.js中暴露的函数,然后再依次暴露出去。
// index.js
// 导出方式一
import { add, sub } from "./math.js";
import { priceFormat, typeFormat } from "./format.js";
export { add, sub, priceFormat, typeFormat };
// 导出方式二
export { add, sub } from "./math.js";
export { priceFormat, typeFormat } from "./format.js";
// 导出方式三
export * from "./math.js";
export * from "./format.js";
4.3 静态+动态导入
静态:写在文件开头,在代码执行前JS引擎先进行解析
动态:写进逻辑里,在代码执行时动态导入,使用import函数,返回值是一个promise
let isFlag = true;
if (isFlag){
import("./module/aaa.js").then((aaa)=>{
aaa.aaa();
})
}else{
import("./module/bbb.js").then((aaa)=>{
bbb.bbb();
})
}
4.4 原理
识别到模块化之后,解析有三个阶段
-
构建阶段
fetch:获取下载文件
parse:解析为模块记录
其中使用module map映射表来管理各依赖模块的信息
-
实例化阶段+ 求值阶段
实例化阶段:对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址
求值阶段:运行代码,计算值,并且将值填充到内存地址中
只有导出模块可以更改变量的值,导入模块无法更改变量的值
总结
AMD和CMD现在很少用了,成为时代的眼泪。目前虽然CommonJS还在用,但是ES Module作为JS语言本身提出的规范,势必会一统天下。在当前的webpack开发中,由于webpack内部做了支持,可以让CommonJS和ES Module混用,譬如CJS语法导出,ESModule语法导入,但最好不要混用。
一句话:认准并学好ES Module!