模块化规范,这次我一定弄懂
在学习前端的过程中,一直有看到
CommonJS
、AMD
、CMD
、UMD
、ESM
这些字眼,可总是一知半解?也常常听人提起模块化。模块化是前端工程化的一部分,今天就来静下心,好好的学习和记录关于模块化的知识。
模块化怎么出现的?
首先,大家都知道,JavaScript初期是为了实现简单的页面交互逻辑(如表单校验等),随着前端技术的发展,前端能做的事也越来越多,做的事情也越来越复杂,前端的代码也越来越丰富,代码量也日益增多;此时JavaScript就考虑使用模块化的方式去管理。就这样,前端就去借鉴了很多后台语言,并开启了模块化的历程;
什么是模块化?
- 将一个复杂的整体,按一定的规范封装成几个模块(文件),每块模块负责单一的职能,当它们组合起来时成为一个整体
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化历程
全局函数
- 原因:因为 JS 中函数是有独立作用域的
- 实现:把不同的功能封装成不同的全局函数;
- 效果:实现了代码分离及组织
- 缺点:如果函数过多,并且在多个文件中,容易造成命名冲突而且模块之间看不出直接的关系;
function fn1(){
//...
}
function fn2(){
//...
}
function fn3() {
fn1()
fn2()
}
命名空间(namespace)
- 原因:对象可以有属性,而它的属性既可以是数据,也可以是方法
- 实现:把模块写成一个对象,所有的模块成员都放到这个对象里
- 效果:减少了全局变量,解决命名冲突
- 缺点:数据不安全(外部可以直接修改模块内部的数据)
let myModule = {
name: 'George',
foo() {
console.log(`foo() ${this.name}`)
},
bar() {
console.log(`bar() ${this.name}`)
}
}
myModule.name = 'Paige' //能直接修改模块内部的数据
myModule.foo() // foo() Paige
立即执行函数(IIFE)
- 原因:函数闭包的特性可以很好的实现私有数据和共享方法
- 实现:将数据和行为封装到一个函数内部,向外暴露接口来操作数据
- 效果:数据是私有的,外部只能通过暴露的方法操作
- 缺点:很难对模块进行拆分并完成相互通讯
let myModule = (function () {
let name = 'George'
function getName() {
console.log(name)
}
return { getName }
})()
myModule.name = 'Paige' //不能直接修改模块内部的数据
myModule.getName() // George
那如果这个模块要依赖于另外一个模块呢?如何解决?
let otherModule = (function () {
return {
num1: 10,
num2: 20
}
})()
let myModule = (function (other) {
let name = 'George'
function getName() {
console.log(name)
}
function getSum() {
console.log(other.num1 + other.num2)
}
return { getName, getSum }
})(otherModule)
myModule.getName()
myModule.getSum()
通过这种传参的形式,我们就可以在 myModule
模块中使用其他模块,从而解决了很多问题,这也是现代模块化规范的思想来源
随着前端发展对模块需求越来越大,社区中逐渐出现了一些优秀且被大多数人认同的模块化解决方案,慢慢演变成了通用的社区模块化规范
模块化规范
CommonJS规范
简介
CommonJS
的提出是为了弥补JavaScript对于模块化没有统一标准的缺陷。由于NodeJS
是 以 CommonJs
规范为基础创造,实现了良好的模块化管理,所以NodeJS
就成了CommonJS
的集大成者和代名词。
目前 Commonjs
广泛应用于以下几个场景:
Node
是 CommonJS 在服务器端一个具有代表性的实现;Browserify
是 CommonJS 在浏览器中的一种实现;webpack
打包工具对 CommonJS 的支持和转换;也就是前端应用也可以在编译之前,尽情使用 CommonJS 进行开发。
特点
- 每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
语法
-
暴露模块:
module.exports = any
或exports.xxx = any
;(为什么有了module.exports
还要有一个export
?)引入模块:
require(path)
;require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。如果是引入自定义模块,path为模块路径;如果引入第三方库模块或系统模块,path为模块名
一个例子
// name.js
let name = 'George'
module.exports = function getName() {
return name
}
// userInfo.js
const getName = require('./name.js')
module.exports = function getUserInfo() {
return {
name: getName(),
age: 24
}
}
CommonJS实现原理
每个模块文件上都存在module
,exports
,require
三个变量,我们可以在CommonJS
规范下每一个js模块中使用他们,在NodeJS
中还存在__filename
和 __dirname
变量
module
变量代表当前模块exports
当前模块导出的属性require
引入模块的方法,加载某个模块,其实就是加载该模块的exports
属性__filename
用来动态获取当前文件的绝对路径__dirname
用来动态获取当前文件模块所属目录的绝对路径
问:module
,exports
,require
是没有被定义的,我们为什么可以使用它们?
答:在模块运行时,自动生成的;简单来说,在模块加载的时候,会通过runInThisContext
(可以理解成 eval
)函数执行包装函数,传入require
,exports
,module
等参数;
runInThisContext(modulefunction)(module.exports, require, module, __filename, __dirname)
这个modulefunction
就是js模块中写的内容,不过它是经过处理成字符串形式的,比如上面的userInfo.js中的内容,变成modulefunction
如下
`
const getName = require('./name.js')
module.exports = function getUserInfo() {
return {
name: getName(),
age: 24
}
}
`
runInThisContext(script)
把我们拼成的字符串方法,变成了一个可执行的方法,随后调用并传入参数。
问:module.exports
和exports
有什么不同?
答:先看一下两种导出的写法
- module.exports导出写法
module.exports = {
name: 'George',
age: 24,
job: 'Programmer'
}
- 用exports怎么写上面的导出
exports.name = 'George'
exports.age = 24
exports.job = 'Programmer'
- 导出看看
const obj = require('./exports')
console.log(obj);
两中导出都会输出{ name: 'George', age: 24, job: 'Programmer' }
这个对象,那如果把exports的写法改一下,改成下面这种情况会导出什么呢?
exports = {
name: 'George',
age: 24,
job: 'Programmer'
}
这样导出怎么就变成了{}
了呢,因为:module.exports 和 exports 同指一个对象,但是最终暴露结果以 module.exports 的为准,上面的代码中,exports 改变了指向,而我们又没有为 module.exports 挂载任何的属性或方法,所以就拿到了空对象
问:那既然有了 exports
,为何又出了 module.exports
?
答:如果我们不想在 commonjs 中导出对象,而是只导出一个类或者一个函数再或者其他属性的情况,那么 module.exports
就更方便了,如上我们知道 exports
会被初始化成一个对象,也就是我们只能在对象上绑定属性,但是我们可以通过 module.exports
自定义导出出对象外的其他类型元素。
let value='A'
module.exports = value // 直接导出的是A
// export必须加一个属性导出对象
let value='A'
exports.valA = value // {valA:'A'}
module对象的属性
module.id
模块的识别符,通常是带有绝对路径的模块文件名module.filename
模块的文件名,带有绝对路径module.loaded
返回一个布尔值,表示模块是否已经完成加载module.parent
返回一个对象,表示调用该模块的模块module.children
返回一个数组,表示该模块要用到的其他模块module.exports
表示模块对外输出的值
require文件引入流程
const http = require('http')
//系统模块--从Node核心模块中加载,优先级高,速度快,仅次于缓存加载
const getName = require('./name.js')
// 文件模块--以./ ../ 和 / 开始的标识符,会被当作文件模块处理,require() 方法会将路径转换成真实路径,并以真实路径作为索引,将编译后的结果缓存起来,第二次加载的时候会更快
const monment = require('dayjs')
// 第三方包模块--在当前目录下的 node_modules 目录查找;
// 如果没有,在父级目录的 node_modules 查找,如果没有在父级目录的父级目录的 node_modules 中查找。 沿着路径向上递归,直到根目录下的 node_modules 目录
//在查找过程中,会找 package.json 下 main 属性指向的文件,如果没有 package.json ,在 node 环境下会以此查找 index.js ,index.json ,index.node
require模块加载原理
CommonJS 规范是同步加载并执行模块文件,在执行阶段分析模块依赖,采用深度优先遍历,执行顺序为父->子->父
// a.js
const bModules = require('./b')
console.log('我是a文件');
let value = 'A'
exports.valA = value
exports.changeValA = function () {
value = 'C'
}
// b.js
const aModules=require('./a')
console.log('我是B文件');
exports.valB='B'
// main.js
const aModules = require('./a')
const bModules = require('./b')
console.log('node 入口文件')
console.log(aModules.valA)
aModules.changeValA()
console.log(aModules.valA)
打印结果
从上面的运行结果可以得出以下结论
main.js
和a.js
模块都引用了b.js
模块,但是b.js
模块只执行了一次(正是前面说的特点,只会执行一次)a.js
模块 和b.js
模块互相引用,但是没有造成循环引用的情况(为什么)- 执行顺序是父 -> 子 -> 父;
- commonJS模块输出的是一个值的拷贝,也就是说一旦输出一个值,模块内部的变化就影响不到这个值了;(与ESM不同)
要搞懂上面那个为什么,我们先了解两个概念,module
和Module
;
module
:前面说了每个js文件都是一个module,module上有一个loaded
属性,是一个布尔值,表示模块是否已经完成加载,如果已经加载了那么直接取缓存,可以避免重复加载;
Module
:以 nodejs 为例,整个系统运行之后,会用 Module
缓存每一个模块加载的信息。Module._cache[id] = module
require 的源码大致长如下的样子:
// id 为路径标识符
function require(id) {
/* 查找 Module 上有没有已经加载的 js 对象*/
const cachedModule = Module._cache[id]
/* 如果已经加载了那么直接取走缓存的 exports 对象 */
if(cachedModule){
return cachedModule.exports
}
/* 创建当前模块的 module */
const module = { exports: {} ,loaded: false , ...}
/* 将 module 缓存到 Module 的缓存属性中,路径标识符作为 id */
Module._cache[id] = module
/* 加载文件 */
runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, __filename, __dirname)
/* 加载完成 *//
module.loaded = true
/* 返回值 */
return module.exports
}
require
大致流程是这样的
- require 会接收一个参数——文件标识符,然后分析定位文件,再从 Module 上查找有没有缓存,如果有缓存,那么直接返回缓存的内容
- 如果没有缓存,会创建一个 module 对象,先缓存到 Module 上,后执行文件,加载完文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象
再用这个流程分析上面代码的流程,看看是如何避免重复加载和循环引用的。
-
首先执行
node main.js
,那么开始执行第一行require(a.js)
; -
判断
a.js
是否有缓存,没有缓存,先加入缓存,再执行代码 -
缓存完
a.js
,执行require('./b')
-
判断
b.js
是否有缓存,没有缓存,先加入缓存,再执行代码 -
缓存完
b.js
,执行require('./a')
-
判断
a.js
是否有缓存,有缓存,直接读取a.js
,后执行代码,console.log('我是 b 文件')
,导出值。 -
b.js
执行完毕,回到a.js
文件,打印console.log('我是 a 文件')
,导出值。 -
最后回到
main.js
,打印console.log('node 入口文件')
完成这个流程。注:在第6步中。如果
b.js
中使用到a.js
导出的值,此时b.js
同步上下文中,获取不到 valA,因为a.js
还没执行导出// b.js const aModules=require('./a') console.log('我是B文件'); console.log(aModules.valA); // (node:76176) Warning: Accessing non-existent property 'valA' of module exports inside circular dependency exports.valB='B'
如果要获取ValA的值可采用,异步加载,等主程序执行完,再执行
const aModules = require('./a') console.log('我是B文件'); setTimeout(() => { console.log(aModules.valA); // 会在主程序执行完之后输出A }, 0); exports.valB = 'B'
commonJS就先介绍到这里,接下来介绍AMD
AMD规范
简介
AMD,全称是Asynchronous Module Definition,即异步模块加载机制,是 RequireJS
在推广过程中对模块定义的规范化产出;CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。AMD是专门为浏览器环境设计的,它定义了一套异步加载标准来解决同步的问题;由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现更早。
由于我在工作中很少用到AMD规范,所以我也只是稍稍了解,浅学即止先,需要在深入;
特点
- AMD的核心是预加载,先对依赖的全部文件进行加载,加载完了再进行处理。
- 适合在浏览器环境中异步加载模块。可以并行加载多个模块。 并可以按需加载。
语法
define(id?: String, dependencies?: String[], factory: Function|Object)
id
即模块的名字,字符串,----可选- dependencies
指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入
factory中。如果没有指定
dependencies,那么它的默认值是
["require", "exports", "module"] factory
包裹了模块的具体实现,可为函数或对象,如果是函数,返回值就是模块的输出接口或者值
//定义没有依赖的模块
define(function(){
return .........
})
注:没有 ID 值的匿名模块,此时文件名就是它的标识名,通常都作为启动模块
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return .........
})
一个例子
test.js
// define modules
define('module', [], function(dep1, dep2) {
console.log('setup module1');
return {
api: function() {
console.log('from module1');
}
};
});
define('module2', [], function() {
console.log('setup module2');
var module = require('module');
return {
api2: function() {
console.log('from module2');
}
};
});
define('module3', ['module'], function(module) {
console.log('setup module3');
return {
api3: function() {
console.log('from module3');
}
};
});
define('main', ['module2', 'module'], function(module2, module) {
module2.api2();
module.api();
require('module3').api3();
});
// run main module
console.time('time');
require('main');
console.timeEnd('time');
index.html
<script src="../require.js"></script>
<script src="test.js"></script>
打印
CMD规范
简介
CMD
,全称是Common Module Definition 的出现较为晚一些,即通用模块定义;它汲取了 CommonJS
和 AMD
规范的优点,也是专门用于浏览器的异步模块加载。模块的加载是异步的,模块使用时才会加载执行;在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块;AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了哪些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
特点
- 一个模块就是一个文件,所以经常就用文件名作为模块id,
define
是一个全局函数,用来定义模块。 - CMD推崇依赖就近,所以一般不在define的参数中写依赖,在factory中写
语法
define
接受 factory
参数,factory
可以是一个函数,也可以是一个对象或字符串。
-
factory
为对象和字符串时,表示模块的接口就是该对象、字符串// factory 为JSON数据对象 define({'name': 'George'}) // factory 为字符串模版 define('my name is {{name}}!!!')
-
factory
为函数时,表示是模块的构造方法,执行该构造方法,可以得到模块向外提供的接口,即
function(require, exports, module)require
是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口exports
是一个对象,用来向外提供模块接口module
是一个对象,上面存储了与当前模块相关联的一些属性和方法
define(function(require, exports, module) { var a = require('./a') a.doSomething() // 依赖就近原则:依赖就近书写,什么时候用到什么时候引入 var b = require('./b') b.doSomething() })
更多用法
define(function(require, exports, module) {
// 同步引入
var a = require('./a')
// 异步引入
require.async('./b', function (b) {
})
// 条件引入
if (status) {
var c = requie('./c')
}
// 暴露模块
exports.aaa = 'hahaha'
})
CMD 与 AMD
规范 | 推崇 | 代表作 | 作用 | 推崇 |
---|---|---|---|---|
AMD | 依赖前置 | require.js | 浏览器端模块加载器 | 尽可能的懒加载,也称为延迟加载,即在需要的时候才加载 |
CMD | 依赖就近 | seajs | 浏览器端模块加载器 | 执行过程中会将所有依赖前置执行,即在代码逻辑开始前全部执行 |
很多人说AMD用户体验好,因为没有延迟,依赖模块提前执行了,CMD性能好,因为只有用户需要的时候才执行的原因
UMD规范
简介
UMD,全称是Universal Module Definition,做通用模块定义规范,它是为了让模块同时兼容CommonJs、CMD
甚至是 AMD
的规范而出现的,多被一些需要同时需要支持浏览器端、服务端引用的项目所使用的,UMD是一个时代的产物,当各个环境最终实现ES harmony的统一规范后,它也将推出历史舞台;
特点
- 难看
- 默认多次加载,后一个覆盖前一个
语法
((root, factory) => {
if (typeof define === 'function' && define.amd) {
// AMD
define(factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory();
} else if (typeof define === 'function' && define.cmd){
// CMD
define(function(require, exports, module) {
module.exports = factory()
})
} else {
// 都不是
root.umdModule = factory();
}
})(this, () => {
console.log('我是UMD')
// todo...
});
可以看到,define
是 AMD/CMD
语法,而 exports
只在 CommonJS
中存在,你会发现它在定义模块的时候会检测当前使用环境和模块的定义方式,如果匹配就使用其规范语法,全部不匹配则挂载再全局对象上,我们看到传入的是一个 this
,它在浏览器中指的就是 window
,在服务端环境中指的就是 global
,使用这样的方式将各种模块化定义都兼容。
ESM规范
简介
ESM,全称ECMA Script Modules,ES标准的模块化规范,从 ES6
开始,JavaScript
才真正意义上有自己的模块化规范(现在主流框架用的都是ESM规范,所以要好好学),设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用。等到脚本真正执行的时候。才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。ES6模块是动态引入的,并不会缓存值,模块里变量总是绑定其所在的模块。
特点
-
引入和导出是静态的,
import
会自动提升到代码的顶层 ,import
,export
不能放在块级作用域或条件语句中 -
每个 ES Module 都是运行在单独的私有作用域中
-
值是动态绑定的,通过导出方法修改后,可以直接访问修改结果
-
同一个模块如果加载多次,只会执行一次
-
可以导出多个属性和方法,可以单个导入导出,混合导入导出
-
提前加载并执行模块文件
-
模块脚本自动采用严格模式,所以模块顶层的this关键字返回undefined;
-
可以很容易实现 Tree Shaking 和 Code Splitting
即一些被 import 了但其实没有被使用的代码,不会被打包构建
语法
在 Es Module
中用 export
用来导出模块,import
用来导入模块。但是 export
配合 import
会有很多种组合情况,接下来我们逐一分析一下
正常导出export
// 导出
const name = 'George'
const age = 24
export { name, age }
export const getName = function () {
return name
}
// 导入
import { name, age, getName } from './esm.js'
import { } 内部的变量名称,要与 export { } 完全匹配
默认导出 export default
// 导出
const name = 'George'
const age = 24
const getName = function () {
return name
}
export default {
name,
age,
getName
}
// 导入
import anyName from './esm.js'
console.log(anyName) // { name: 'George',age: 24,getName: Function }
export default anything
导入 module 的默认导出。anything
可以是函数,属性方法,或者对象。- 对于引入默认导出的模块,
import anyName from 'module'
, anyName 可以是自定义名称。
混合导出
// 导出
export const name = 'George'
export const age = 24
const getName = function () {
return name
}
export default {
getName
}
// 导入方式1
import getNameAny, { name, age as secretAge } from './esm.js'
console.log(getNameAny); // ƒ getName()
console.log(name, secretAge); // George 24
// 导出可以重命名
// 导入方式2
import getNameAny, * as obj from './esm.js'
console.log(getNameAny); //ƒ getName()
console.log(obj); // {age: 24,default: ƒ getName(),name: "George"}
导出的属性被合并到 obj
属性上,export default
导出内容被绑定到 default
属性上
重定向导出
把当前模块作为一个中转站,一方面引入 module 内的属性,然后把属性再给导出去
方式1:
// esmmid.js 中间站
export * from './esm.js' // 重定向导出 module 中的所有导出属性, 但是不包括 module 内的 default 属性
import { name, age } from './esmmid.js'
// 或
import * as obj from './esmmid.js'
console.log(name, age); // George 24
console.log(obj); // {age: 24,name: "George"}
方式2
// esmmid.js 中间站
export { name, age } from './esm.js' // 不包括 module 内的 default 属性
// 或者重命名再导出
export { name as pigName, age as pigAge } from './esm.js'
import { name, age } from './esmmid.js'
//或者
import { pigName, pigAge } from './esmmid.js' // 如果导出中间重命名了,一定要用重命名之后的名字
无需导入模块,只运行模块
import 'module'
- 执行 module 不导出值 多次调用
module
只运行一次
动态导入
const promise = import('module')
import('module')
,动态导入返回一个Promise
。为了支持这种方式,需要在 webpack 中做相应的配置处理。Vue的路由懒加载就是这么做的
ES6 模块不是对象
-
ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。import {state, getNum, setPost} form 'api'
以上方法实质上是从 api模块加载了3个方法,api中其他的方法不加载。这种加载就是 编译时加载或者静态加载
常见错误写法
export
命令规定的是模块对外的接口,必须与模块内部的变量建立一一对应关系
export 1; // 报错
// 或者
const a = 1;
export a; // 报错
function fn() {}
export fn; // 报错
export default
:用于模块的默认导出,一个模块中只能存在一个export default
export default function A() {
}
export default function B() {
}
import
命令具有提升效果,会提升到整个模块的头部,首先执行。- 由于
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
- 不能直接修改improt导入的属性
export let num = 1
export const addNumber = ()=>{
num++
}
import { num, addNumber } from './esm.js'
num = 2 // Uncaught TypeError: Assignment to constant variable.
简单总结
- CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案
- AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块;采取依赖前置的方案,提前执行
- CMD规范与AMD规范很相似,都用于浏览器编程,采取依赖就近的方案,延迟执行
- UMD的出现是为了让模块同时兼容
CommonJs、CMD
甚至是AMD
的规范 - ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
规范 | 使用场景 | 模块加载方案 | 实现模块规范 | 引用模块 | 导出接口 |
---|---|---|---|---|---|
CommonJS | 服务端 | 同步加载模块,运行时确认模块依赖关系 | NodeJS | require | module.exports |
AMD | 浏览器 | 异步加载模块,运行时确认模块依赖关系 | RequireJS | require | define函数返回值 return |
CMD | 浏览器 | 异步加载模块,运行时确认模块依赖关系 | SeaJS | require | exports |
ESM | 服务端和浏览器 | 异步加载模块,编译时确认模块依赖关系 | 原生JS | import | export |
参考学习文章
「万字进阶」深入浅出 Commonjs 和 Es Module:juejin.cn/post/699422…
前端模块化详解(完整版):juejin.cn/post/684490…
「前端工程四部曲」模块化的前世今生(上): juejin.cn/post/700794…
转载自:https://juejin.cn/post/7176934558595481656