1.为什么需要模块化
随着前端应用的日益复杂,我们的项目代码已经逐渐膨胀到了不得不花大量时间去管理的程度了。而模块化就是一种最主流的代码组织方式,它通过把复杂的代码按照功能的不同划分为不同的模块单独维护,从而提高开发效率、降低维护成本。模块化可以使你能够更容易地重用代码。你可以创建一个模块来完成一个特定的功能,然后在多个地方重用这个模块,而不是复制和粘贴代码。
2.没有工具和规范时模块化的演进历史
2.1文件划分
最早期的模块化是通过文件划分的方式,将不同的文件划分为不同的模块,一个文件就对应一个模块,如下图就有2个模块a和b。
想要使用模块的时候就用script标签引入该模块
<script src="module-a.js"></script>
<script src="module-b.js"></script>
这种方式存在的问题
- 难以管理模块之间的依赖关系
- 多个模块的变量名会出现冲突
- 外部可以修改模块的内容
2.2命名空间
为了解决以上出现的问题,又有了一种新的模块化方式,便是命名空间,通过将每个模块包裹成一个全局对象来实现,这样的确解决了命名冲突问题,但是仍然存在外部可以修改模块内部内容的问题
使用模块
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
//模块成员可以被修改
moduleA.name = 'foo'
</script>
2.3立即执行函数
用立即执行函数实现了私有成员的方式,外部无法修改内部的变量,通过挂载到window对象上来完成模块化的暴露
3.模块化规范
前面所提到的几种早期模块化方式都有一个问题,就是必须通过script脚本标签来使用模块,但是如果随着项目规模的增大,忘记加入script标签或者引入了已经删除的模块,就会出现一些问题。也就是说,最好要把引入模块化这个工作放到js代码中去完成,而不只是在html中引入
3.1 CommonJS
NodeJS里的CommonJS规范是一个很好的模块化方式,CommonJS包含以下几个特征
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过 module.exports 导出成员
- 通过 require 函数载入模块
特征中的第4个,require是同步的加载,在Node中只会在启动的时候加载,执行的时候只是去使用,而到了浏览器端,每一次刷新页面都会导致大量的同步模式请求出现,这就无法使用了。
3.2 AMD(Asynchronous Module Definition)
AMD(Asynchronous Module Definition)是 RequireJS 在推广过程中对模块定义的规范化产出,。由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是require.js。
AMD这个规范约定每一个模块都必须通过 define 这个函数定义,默认可以接收两个参数,也可以传递三个参数:
- 第一个参数是模块的名字;
- 第二个参数是一个数组,用于声明模块依赖项;
- 第三个参数是一个函数,函数的参数与前面的依赖项一一对应,每一项分别为依赖项这个模块导出的成员,这个函数的作用可以以理解为为当前的这个模块提供一个私有的空间。如果需要在这个模块当中向外部导出一些成员,可以通过 return 实现
AMD也可以通过require方法来加载对应的模块,require与define的区别是,require只是用来加载,而define是定义一个模块
案例
src
├── index.html
├── index.js
├── lib
│ └── require.js // 使用require.js 库
└── modules
├── dataServe.js
└── example.js
- dataServe
// 导入example
define(['example'], function (example) {
let msg = "data"
function showMsg () {
console.log(msg, example.getName());
}
return { showMsg }
})
- example.js
define(function () {
let name = "w"
function getName () { return name }
return { getName }
})
- index.js
(function () {
requirejs.config({
paths: {
example: './modules/example',
dataServe: './modules/dataServe'
}
})
requirejs(['dataServe'], function (d) {
d.showMsg()
})
})()
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script data-main="./index.js" src="lib/require.js"></script>
</body>
</html>
问题:
- 使代码复杂度提高
- 如果模块划分的过于细致,同一个页面的请求会过多,页面效率低下
3.3 CMD+Sea.js
与CommonJS基本保持一致,但是后来也被require.js兼容了
3.4 ES Module
ES Module是现在最常用的模块化解决方案,仍然采用了与CommonJS相似的import和export来完成模块的导入和导出
在html中,只需要在script标签里加入type="module"就可以导入模块
<script type="module"> console.log('this is es module') </script>
与普通script标签不同的地方是:
- es模块会自动开启严格模式,忽略掉use strict。
- es模块都有单独的作用域
- es模块通过CORS方式请求,如果请求的资源不支持CORS会报跨域错误
- es模块等于在脚本上加入defer属性,让脚本等同步内容加载完后异步按顺序执行
<script type="module">
//es模块会自动开启严格模式
console.log(this); //undefined
</script>
<script type="module">
//es模块都有单独的作用域
let a = 1
console.log(a); //1
</script>
<script type="module">
//es模块都有单独的作用域
let a = 2
console.log(a); //2
</script>
导出:使用export关键词来完成导出
普通导出:
方式一 用{}包裹需要导出的变量,函数或者类,如果想要改名,可以在导出时用as来改
const name = "why";
const age = 18;
function sum(a, b) {
return a + b;
}
class Person {
constructor(name) {
this.name = name;
}
}
//3.统一导出时使用as关键字给变量起别名
export { name as bName, age, sum as bSum, Person };
方式二 export直接放在变量,函数,类声明之前
export const name = "why";
export const age = 18;
export function sum(a, b) {
return a + b;
}
export class Person {
constructor(name) {
this.name = name;
}
}
默认导出
方式一:不使用{}包裹变量,函数,类
const height = 1.88;
export default height;
方式二:使用{}包裹变量,函数,类,但必须通过as改变名字为default
const height = 1.88;
export {
height as default
};
导入:使用import关键词来完成导入
方式一:分别导入,可以通过as来起别名
import {
name as barName,
age,
sum,
Person as BarPerson,
} from "./bar.js";
方式二:整体导入,通过as来起别名,然后分别使用
import * as baz from "./baz.js";
console.log(baz.name, baz.age);
baz.sum(1, 2);
const person2 = new baz.Person("lily");
console.log(person2);
方式三:导入默认导出的变量,不加{}包裹
import height from "./demo.js";
console.log(height);
导入导出注意点:
- ES Module导出的变量并非变量的值本身,而是一个引用,所以导入的变量的值会受原模块的影响
- 导入的变量是只读的,不能进行赋值更改
- import异步实现,会有一个独立的模块依赖的解析阶段
- 不能与CommonJS相似地在导入路径中省略.js(可通过打包配置改善)
举例:导入的变量的值会受原模块的影响
在导入中使用导出:把import from改成export from
常用于集中导出,方便后续导入资源
与CommonJS的互动
在node环境下,虽说一般都是CommonJS规范的模块化,但是node也做了兼容可以让ES Module正常使用,只要把原来的.js文件改为.mjs就可以正常使用import语法了。import导入的时候还可以导入CommonJS的模块,只是所有CommonJS模块都会被当作默认导出的方式来导入。但是在CommonJS里面,无法使用require去导入ES Module导出的内容,也就是在下面的b.js里面会报错
- a.mjs
import b from './b.js'
console.log(b.name); // 1234
export let a = 4
- b.js
const a = require('./a.mjs') // 报错
module.exports = {
name: '1234'
}