前端模块化
什么是模块化
模块化是一种编程技术,它将一个复杂的应用程序拆解成一个一个模块,模块化具有以下特点:
- 每个模块有自己的作用域,模块内部的变量和函数在模块外部是不可见的。
- 模块自己的接口只暴露必要的接口,外部只能通过接口与其他模块进行交互。
模块化的发展
1. 全局Function
模式
javascript
// 模块一
function module1() {
// ...
}
// 模块二
function module2() {
// ...
}
- 特点:将不同的功能模块封装到一个个全局函数中,通过全局函数的调用来实现模块间的通信。
- 问题:污染全局命名空间,并且随着项目越来越大会出现命名冲突的问题。
2.简单对象封装模式
javascript
// 模块一
let module = {
data: 'foo',
foo(){
// ...
}
bar(){
// ...
}
};
module.data = 'bar';
module.foo();
- 特点:将模块封装成一个个简单对象,通过对象属性的访问来实现模块间的通信。
- 问题:内部属性可以直接修改,数据不安全。
3.IIFE
模式(闭包 + 立即执行函数)
普通模块:
javascript
// 创建模块
(function (window) {
// 内部私有数据
let data = 'foo';
function foo() {
console.log(data);
}
function bar() {
privateFunc();
}
function privateFunc() {
console.log('bar');
}
// 向外暴露接口
window.module = {
foo,
bar,
};
})(window);
html
<!-- 引入模块 -->
<script src="module.js"></script>
<script>
module.foo(); // 'foo'
module.bar(); // 'bar'
console.log(module.data); // undefined
console.log(module.privateFunc); // undefined
</script>
引入其他依赖:
javascript
// 引入JQuery模块
(function (window, $) {
function foo() {
$('body').css('background', 'red');
}
// 向外暴露接口
window.module = {
foo,
};
})(window, JQuery);
html
<!-- 必须先引入JQuery -->
<script src="jquery.js"></script>
<script src="module.js"></script>
<script>
module.foo();
</script>
- 特点:将模块的内部数据和方法封装到一个立即执行的函数中,通过参数传入外部环境和依赖,利用闭包的特性,通过暴露内部接口,来访问函数内部私有数据,并且内部私有数据不可更改。
- 问题:模块引入顺序有要求,在不清楚模块之间依赖关系的时候,可能会出现模块引入顺序错误的情况。
上述模块化方式除以上问题外,还存在很多问题,比如在引入多个 script 标签时,会发送很多请求,模块之间的依赖比较模糊,模块难以维护等等,因此,针对以上问题,社区和官方提出了很多模块化规范。
模块化规范
1.CommonJs
CommonJs
是node
环境中的一种模块规范。每个文件就是一个模块,有自己的作用域,文件内部的变量和函数都是私有的,需要向外暴露才可见。 注意:CommonJs
的模块加载机制是同步的,要想在浏览器端运行,需要进行打包编译。
特点:
- 所有的代码都运行在模块的作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,以后再加载就直接返回第一次运行的结果,不会再次运行。
- 模块按照代码出现顺序依次加载。
基本语法:
javascript
// module1.js
module.exports = {
data: 'foo',
bar() {
console.log('bar');
},
};
// module2.js
const module1 = require('./module1');
console.log(module1.data); // 'foo'
module1.bar(); // 'bar'
这里有两点需要注意:
-
CommonJs
可以通过module.exports
和exports
两种方式进行模块导出,本质上module.exports
和exports
以及this
都是指向同一个对象。而最终导出的对象是module.exports
指向的对象 ,所以通过给module.exports
或者exports
添加属性,导出的对象上都会被添加上相应的属性,而给module.exports
重新赋值,则会覆盖原有导出的对象。 -
CommonJs
通过require
函数来加载模块,它是同步加载,也就是说,只有当前模块的依赖加载完成,才能执行当前模块的脚本 。此外,require
可以接受一个参数,如果参数是一个相对或者绝对路径,则会从这个路径加载模块,否则,则会从node_modules
目录下加载,如果没有发现指定模块,则会抛出错误。
模块加载机制:
CommonJs
模块导入的值是可以理解成导出的值深拷贝后的值,因此一旦一个值被导入之后,那么这个值就不会被修改了,即使在原模块对其进行了修改,也不会影响到导入的值。下面是具体的例子:
javascript
// module1.js
let a = 1;
setTimeout(() => {
a = 2;
}, 1000);
module.exports = { a };
// module2.js
const module1 = require('./module1');
console.log(module1.a); // 1
setTimeout(() => {
console.log(module1.a); // 1
}, 2000);
在 module2.js
中,我们通过 require
加载 module1.js
,然后打印 module1.a
的值,此时 module1.a
的值为 1
,然后我们在 module1.js
中设置了一个定时器,在 1 秒后将 a
的值改为 2
,然后我们 1.5 秒后再次打印 module1.a
的值,此时 module1.a
的值仍为 1
,说明 module1.a
的值是不会被修改的。
2.AMD
与 CommonJs
不同的是,AMD
是非同步加载模块,允许指定回调函数。因此 CommonJs
适用于服务端加载模块,而 AMD
适用于浏览器端。
AMD
模块需要使用 require.js
这个库,通过 define
函数来定义模块,define
函数接受两个参数,第一个参数是模块的依赖列表(如果没有依赖的模块可不传),第二个参数是模块的执行函数。通过 require
函数来加载模块,require
函数接受一个数组参数,数组的每个元素是一个依赖模块的名字。通过 require.js
加载的模块必须符合 AMD
规范,如果不符合,需要先用require.config()
方法,定义它们的一些特征 。这里不过多阐述。
定义模块:
js
// module1.js
// 定义没有依赖的模块
define(function () {
let bar = 1;
function foo() {
console.log(bar);
}
return {
foo,
};
});
// module2.js
// 定义有依赖的模块
define(['jquery'], function ($) {
function bar() {
$('body').css('background', 'green');
}
return {
bar,
};
});
加载模块:
js
// 加载 module1.js
require(['./module1'], function (module1) {
module1.foo(); // 1
});
引入 require.js
和 模块, 并制定 data-main
属性值作为入口文件:
html
<!-- 引入 require.js -->
<script
data-main="./index.js"
src="https://requirejs.org/docs/release/2.3.7/minified/require.js"
></script>
<script src="./module1.js"></script>
3.CMD
CMD
是 Sea.js
的模块定义规范。它整合了CommonJs
和AMD
的特点, 并且和 AMD
类似,也是为了解决浏览器端模块化问题而提出的,CMD
的模块加载是异步的,在模块使用时才会加载执行。
基本语法:
javascript
// 定义模块
define(function (require, exports, module) {
module.exports.bar = function () {
console.log('bar');
};
});
// 加载模块
define(function (require) {
let m1 = require('./module1');
m1.bar(); // 'bar'
});
使用 sea.js
同时指定入口文件。
html
<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.1.1/sea.js"></script>
<script>
seajs.use('./index.js');
</script>
ESModule
ESModule
是 ECMAScript
的一个提案,旨在解决模块化问题。 CommonJs
和 AMD
模块都是在运行时确定模块关系,而 ESModule
模块化的思想是在编译时就确定莫亏啊的依赖关系。
在 HTML
文件中的基本使用:
html
<!-- 通过给 `<script>` 标签添加 `type="module"` 属性,就可以启用 `ESModule` 模块: -->
<script type="module">
console.log('这是一个模块');
</script>
特性:
ESM
自动采用严格模式 相当于开启了use strict
- 每个
ESModule
都有自己的单独作用域 ESModule
通过CORS
的方式请求外部JS
脚本ESModule
的script
标签会延迟执行脚本 相当于defer
html
<!-- 1. ESM自动采用严格模式,相当于开启了 `use strict` -->
<script type="module">
console.log(this); // undefined 严格模式下,全局的this为undefined
</script>
<!-- 2. 每个ESModule 都有自己的单独作用域 -->
<script type="module">
let x = 1;
console.log(x); // 1
</script>
<script type="module">
console.log(x); // undefined 不同模块的作用域互不干扰
</script>
<!-- 3. ESModule 通过 CORS 的方式请求外部 JS 脚本 -->
<script type="module" src="不支持跨域访问的js脚本"></script>
// 报跨域错误
<script type="module" crossorigin src="支持跨域访问的js脚本"></script>
// 允许跨域访问
<!-- 4. ESModule 的 script 标签会延迟执行脚本 -->
<script type="module" src="demo1.js"></script>
// 脚本的执行不会阻塞后续的渲染,相当于 defer 属性
<p>需要显示的内容</p>
导入导出语法:
ESModule
使用 import
和 export
关键字来导入和导出模块。
javascript
// module1.js
let foo = 1;
export { foo }; // 导出
// module2.js
import { foo } from './module1'; // 导入
console.log(foo); // 1
导入导出可以通过 as
进行重命名:
javascript
// module1.js
let foo = 1;
let bar = 2;
export { foo as myFoo, bar }; // 重命名导出
// module2.js
import { myFoo, bar as myBar } from './module1'; // 导入重命名
console.log(myFoo); // 1
console.log(myBar); // 2
默认导出:
默认导出有两种方式:
- 使用
export default
关键字导出默认值 - 将变量重命名为
default
:export { foo as default }
针对这种方式,在导入是需要将default
重命名为其他名字。
javascript
// 方式1
// module1.js
let foo = 1;
export { foo as default };
// module2.js
import {default as foo} from './module1'
// -----------------
// 方式2
// module1.js
let foo = 1;
export default foo;
// module2.js
import foo from './module1';
console.log(foo); // 1
混合导出:
ESModule
可以同时默认导出和命名导出。
javascript
// module1.js
let foo = 1;
let bar = 2;
export { foo, bar };
let a = 2;
export default a;
// module2.js
// 两种导入都可以
// import { foo, bar, default as a } from './module1';
import a, { foo, bar } from './module1';
console.log(foo); // 1
console.log(bar); // 2
console.log(a); // 2
需要注意的是, ESModule
与 CommonJs
不同, ESModule
导入的值并不是拷贝关系,而是值的引用。 这里用个例子演示一下:
javascript
// module1.js
let a = 1;
setTimeout(() => {
a = 2;
}, 1000);
export { a };
// module2.js
import { a } from './module1';
console.log(a); // 1
setTimeout(() => {
console.log(a); // 2
}, 1500);
在 module2.js
中,我们通过 import
加载 module1.js
,然后打印 module1.a
的值,此时 module1.a
的值为 1
,然后我们在 module1.js
中设置了一个定时器,在 1 秒后将 a
的值改为 2
,然后我们 1.5 秒后再次打印 module1.a
的值,此时 module1.a
的值变为 2
,说明 module1.a
的值是会被修改的, 因为 ESModule
导入的值是值的引用。 因此,强烈建议在导出变量时,要用 const
声明,避免出现意外的修改。
动态导入:
ESModule
提供了 import()
函数来动态导入模块。
javascript
// module1.js
let a = 1;
export { a };
// module2.js
import('./module1').then((m) => {
console.log(m.a); // 2
});