前情
很久以前,前端页面功能简单,浏览器运行单一js,没有模块的概念。后面复杂起来后,出现了很多模块处理的机制
CommonJs
// 通过require 引入module
require('xxx');
// exports 当前module导出的功能
exports.xxx = xxx;
同步的形式加载模块,主要是node使用
- AMD -
Requirejs
// 定义模块一个
define(['a'], (a) => {
// 导出的功能
return {
d: 'xxx'
};
});
// 引入模块
require(['b', 'c'], (b, c) => {});
异步的形式加载模块,浏览器加载js一般都是异步的,所有AMD一般在浏览器端使用。
- CMD -
sea.js
CMD是AMD的一种优化。Requirejs在申明依赖的时候就会加载并运行依赖。
// AMD
define(['a', 'b', 'c'], (a, b, c) => {
if (false) {
// 即便没用到某个模块 c,但 c 还是提前执行了
a();
}
});
// CMD
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
AMD推崇依赖前置、提前执行
CMD推崇依赖就近、延迟执行
- UMD
UMD是commonjs、AMD的兼容写法,这样打包出来的功能,可以在两种模块机制下运行。
((root, factory) => {
if (typeof define === 'function' && define.amd) {
//AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
//CommonJS
var $ = requie('jquery');
module.exports = factory($);
} else {
//都不是,浏览器全局定义
root.testModule = factory(root.jQuery);
}
})(this, ($) => {
//do something... 这里是真正的函数体
});
- ES6 module
import xxx from 'xxx';
export {}
export default xxx
es6 module出现后,上面的都变成时代的眼泪了。
目前使用es module还需要依赖打包工具,打包成单一的js。打包工具内部也实现了一套 模块加载机制。
Script module
浏览器原生支持的模块加载
基本用法
// 加载资源 xxx.js,xxx.js 内部可以用 import 和 export 加载到导出模块
<script type="module" src="./xxx.js"></script>
// 直接在script标签内,import module
<script type="module">
import xxx from './xxx.js'
// do something
</script>
import a from './a.js'; // 可以在浏览器看到 网络请求了 a.js 资源
特点:
- 用法和
es6 module相同 import的时候需要带后缀。type="module"的script标签自带defer属性,会在dom运行结束才运行。import的模块只会被加载和执行一次。import内部定义的变量只在内部可以被使用,export出去的只有import的时候才能获取,window上是没有的。
动态加载
// 动态加载
import('xxx').then((xxx) => {
// do something
})
执行到的时候才会加载对应的module,加载成功后Promise.resolve
Top Level await
// color.js
// fetch request
const colors = fetch("../data/colors.json").then((response) => response.json());
export default await colors;
// main.js
import colors from "./modules/getColors.js";
// do something
循环引用
循环引用的时候不是总是有问题的。
// -- a.js --
import { b } from "./b.js";
setTimeout(() => {
console.log(b); // 1
}, 10);
export const a = 2;
// -- b.js --
import { a } from "./a.js";
setTimeout(() => {
console.log(a); // 2
}, 10);
export const b = 1;
这个例子里面,异步调用,是不会有问题的。
import的时候只是对export的一个引用- 真的调用的时候
export语句已经运行完了。
// -- a.js (entry module) --
import { b } from "./b.js";
export const a = 2;
// -- b.js --
import { a } from "./a.js";
console.log(a); // ReferenceError: Cannot access 'a' before initialization
export const b = 1;
这个例子就无法运行。
- 运行
a.js第一句import { b } from "./b.js" - 加载并执行
b.js b.js第一句import { a } from "./a.js";,a.js已经被导入了,不会再次运行。console.log(a),需要调用a.js的导出,这时候a.js的export语句还没被执行,所有会报错。
// -- a.js (entry module) --
import { b } from "./b.js";
console.log(b); // 1
export const a = 2;
// -- b.js --
import { a } from "./a.js";
setTimeout(() => {
console.log(a); // 2
}, 10);
export const b = 1;
这个例子是可以运行的。
- 运行
a.js第一句import { b } from "./b.js" - 加载并执行
b.js b.js第一句import { a } from "./a.js";- 设置定时器。
b.js导出b=1;- 执行
a.js的console.log(b);,因为b.js已经执行过export了,运行正常。 a.js导出a=1b.js的定时器运行,此时a.js已经export过了,所有可以正常访问。
Script importmap
浏览器加载模块的配置,控制对应模块的加载地址。(有点类似alias)
格式
内部是个JSON格式,支持imports和scopes两个key。
imports path 的匹配模式
- 裸模块
<script type="importmap">
{
imports: {
lodash: 'https://xxxxx',
moduleA: './assets/module.123gde.js',
}
}
</script>
js内部就可以直接import
import _ from 'lodash'; // 等同于 import _ from 'https://xxxxx';
import moduleA form 'moduleA'; // 等同于 import moduleA from './assets/module.123gde.js';
- 前缀匹配
<script type="importmap">
{
imports: {
"shapes/": "./module/shapes/"
}
}
</script>
import circle from 'shapes/circle.js' // 等同于 import circle from './module/shapes/circle.js'
- 路径匹配
<script type="importmap">
{
"imports": {
"modules/shapes/": "./module/src/shapes/",
"modules/shapes/square": "./module/src/other/shapes/square.js",
"https://example.com/modules/square.js": "./module/src/other/shapes/square.js",
"../modules/shapes/": "/modules/shapes/"
}
}
</script>
一个请求如果匹配多个path的时候,取最长的。eg:
import 'modules/shapes/square'; // 等同于 import './module/src/other/shapes/square.js';
import 'modules/shapes/a.js'; // 等同于 import './module/src/shapes/a.js';
scopes
scopes配置的是资源请求的路径匹配,然后对返回的资源内部的资源请求进行module转换。
scopes一般用来做版本控制
<script type="importmap">
{
"imports": {
"box": "./src/b.js"
},
"scopes": {
"custom/": {
"box": "https://example.com/modules/shapes/square.js"
}
}
}
</script>
import './src/a.js';
import './custom/a.js';
// src/a.js
import 'box'; // 等同于 import './src/b.js';
// custom/a.js
import 'box'; // 等同于 import 'https://example.com/modules/shapes/square.js';
拓展
随着前端业务的发展,项目越来越大,微前端被提了出来。
Webpack5提供了一个新功能 - Module Federation
Module Federation就是Webpack利用自己的模块处理机制实现的。针对配置了remote的module,采用异步chunk的形式去加载。
Module Federation的形式很好的对大型的系统进行 切分,独立的部署,组合使用。
但是 想要使用Module Federation依赖项目都使用 Webpack5进行打包。
script module其实是很好的Webpack Module Federation的替代。
- 子项目打包成功后,生成一个
import-map.json - 父项目在html引入
import-map.json,父项目直接import子项目导出的功能。
例子实践
功能:
web-components导出了2个webcomponent组件。app直接使用这两个 webcomponent组件。
步骤:
web-components项目build之后根据vite生成的mainfest.json一个import-map.js的文件。app项目在入口index.html引入这个import-map.js。app项目内部直接import了web-components项目导出的webcomponent,直接使用~
参考资料: