完整视频系列及倍数请移步 bilibili
模块化背景
那么我们为什么要用到打包工具呢? 原因之一是:在浏览器支持 ES 模块之前,前端没有原生支持以模块化的方式开发 js。打包工具的出现也是为了帮助开发让浏览器支持的模块化方案。
那不使用模块化开发的体验会是什么样子呢?
- 容易造成变量污染(无意声明相同变量造成使用时不是预期变量)
- js文件之间有依赖关系时,因为不能在js中引js,只能统一写到script引入,需要注意顺序问题。而且被依赖的js一般不希望被其他js调用,script只能全局引入,所有js都能调用不需要引用
- 开发无法分块编写,将项目分成更小的独立部分,是跟优雅的开发模式,更好维护代码和更好的复用。
这也是“打包”
这个概念出现的原因:使用打包工具抓取收集依赖资源、处理成script或是其他形式链接资源文件到引用方文件中,实现模块化。
接下来将主要讲解
Commonjs、ES Module
模块化并且不会讲基础使用,其他如AMD、UMD
可能提及但不会细讲,感兴趣请自行搜索🔍学习
模块化方案环境支持
开发环境(nodejs
)
- 支持
Commonjs
- ES6后通过
.mjs
支持ES Module
浏览器环境
- 不支持
Commonjs
- ES6后支持
ES Module
CommonJs
Commonjs是一种模块化规范(思想)
Commonjs
是一种规范并不是一种具体的代码,类似 promise A+
规范,只要符合规范里所有的条件,那它就是 Commonjs
。
而nodejs就是借鉴这种思想实现自己的模块化的,所以也不能完全说nodejs就是 Commonjs
AMD模块化同样脱胎于Commonjs规范,浏览器支持的Commonjs一般是使用require.js的库,注意这个库和nodejs的require不是一个东西,这个库的Commonjs是AMD实现的。
为了解决变量污染的问题,Commonjs规定每一个js都是独立的模块,即依赖的文件没有抛出的变量,是无法获到的。可以看作每个文件都是一个闭包。
另外,
webpack
作为以nodejs为底层语言(强调这个是因为ESbuild以GO为底层语言)的打包工具,是依靠nodejs的Commonjs做依赖收集进行整合后用方法插入script来协助打包的。所以webpack工程下,编写Commonjs不需要额外支持,可以直接编译识别。-- TODO: 这里补一个文章跳转各种打包工具的模块化原理
从0实现简易Commonjs
0. 前期准备
开始前先转变以前的固有印象,“引入”
和“抛出”
在Commonjs
引入
的本质是存储/缓存抛出
的本质是读取缓存
实现模块化的本质就是,把每个文件存储到Modules
的大对象中,每个文件就是一个子对象module
,且每个文件module对象
内有各种信息
打印module和Module
const Module = {
[module] = {
id: '',
exports: {},
loaded: false,
module: {},
...
...
},
...
}
抛出的内容就是往对象里的exports
存储深拷贝的值,引入就是获取整个exports
(一般解构赋值取出来使用)
由此可以看出一个 Commonjs
的特性:引入(也就是获取缓存)的值取决于当时存储时的值,也就不是动态可变的了
1. 让js文件直接支持myexport、myrequire等
支持Commonjs的环境(nodejs),会在编译运行js时(运行 node xx.js
)包装js
即我们写的js不是最终执行的js,而是会被包裹起来,进行统一处理
如 node index.js
,执行index.js,nodejs就会先对index.js进行包装
注入进去export、models
等变量或函数
这就是在js里全局支持使用export的原因
接下来手写实现一个 Commonjs
模块化,不会使用nodejs模块化相关的api,但是还是需要用到nodejs其他的api来帮助我们实现,如读取文件内容的 fs
,遇到不会的nodejs API请自行搜索🔍学习
由👆分析,我们需要读取require引入的资源文件内容,进行包装并且注入模块化api或其他工具方法
// 包装js文件 注入api的期望方式
function (){
wrap(fileContent)(myrequire); // 柯里化函数方式,第一次调用传递引入的资源文件内容,返回一个函数,传递要注入的东西并运行
}
👆包装js文件 注入api的期望方式
function wrap(fileContent) {
// 拼接函数进行包装 注意要包一层()才不会被eval立即执行,而是返回一个字符串()内的函数
const newfileContent = `(function (myrequire){
${fileContent}
})`
return eval(newfileContent)
}
👆 资源文件内容是一个js文件的所有字符串,里面可以直接使用模块化api 先包装字符串,然后执行,注意执行字符串的结果要返回一个函数而不是直接执行(会报错myrequire undefined)
const fs = requuire('fs')
function myrequire(filePath) {
console.log('开始引入',filePath)
let fileContent;
try {
fileContent = fs.readFile(filePath,'utf8');
}catch(err) {
console.log(`没有找到引用资源${filePath}`)
}
if(!fileContent) return;
wrap(fileContent)(myrequire);
}
myrequire('./index.js')
这样就能运行引入的资源文件了,但是还没实现模块化抛出和接收的动作
// index.js
myrequire('./a.js')
myrequire('./b.js')
console.lof('执行index.js')
// b.js
console.log('执行b.js')
// 没有a.js
打印内容会发现执行顺序
开始引入index.js
执行index.js
开始引入a.js
开始引入b.js
没有找到a.js
执行b.js
可以看到,index.js引入a和b,执行index并没有等a和b引入结束
而运行一次原生Commonjs如下
开始引入index.js
开始引入a.js
error 没有找到a.js
原生是会等a和b引入结束才执行index的,并且遇到引入异常会中断运行
Commonjs
模块化是同步加载的
我们自己写的 Commonjs
不是同步的原因是fs读取文件内容的api fs.readFile()
用了异步加载
// 读取文件内容是异步的,外面调用myrequire是不会等内部异步结束
// 所以读取文件内容要用同步
fs.readFile(filePath)
fs.readFileSync(filePath)
并且处理读取文件内容失败,要中断nodejs运行,抛出错误throw
改造后的 myrequire()
function myrequire(filePath) {
console.log('开始引入',filePath)
let fileContent;
try {
fileContent = fs.readFileSync(filePath,'utf8');
}catch(err) {
throw `没有找到引用资源${filePath}`
}
if(!fileContent) return;
wrap(fileContent)(myrequire);
}
2. myrequire识别资源方式
识别资源路径的几种格式
- xxx node内置模块
- /xxx 当前目录绝对路径
- ./ ../ 当前目录相对路径
- xxx node_modules下的目录
/**
* 文件路径几种格式
* xxx node内置模块
* /xxx 当前目录绝对路径
* ./ ../ 当前目录相对路径
* xxx node_modules下的目录
*/
const enterPublicPath = 'example'
function dealFilePath(filePath) {
const firstString = filePath.charAt(); // 路径首字符
if (firstString === '.') {
// 当前目录相对路径转为绝对路径(模拟的都是从example中引入的,跟当前core文件夹是同级所以相对路径也是)
// console.log('当前文件core的绝对路径',__dirname);
return path.resolve(__dirname, `../${enterPublicPath}/${filePath}`);
} else if (firstString === '/') {
// 当前目录绝对路径
} else if (node内置模块.includes(filePath)) {
// node内置模块
} else {
// node_module模块
}
}
👆这里仿照webpack定义一个 publicPath
,用于处理相对路径时的公共路径前缀,配合我们把代码抽离到core文件夹和在expample文件夹运行
关于资源路径属于 node_module
的情况,会逐级查找资源,如下规则,这里不做实现
- 在当前目录下的 node_modules 目录查找。
- 如果没有,在父级目录的 node_modules 查找,如果没有在父级目录的父级目录的 node_modules 中查找。
- 沿着路径向上递归,直到根目录下的 node_modules 目录。
- 在查找到第三方模块后,会找
package.json
下 main 属性指向的文件,如果没有package.json
,在 nodejs 环境下会依次查找index.js ,index.json ,index.node
。
改造后 myrequire()
function myrequire(filePath) {
console.log('开始引入',filePath)
const newPath = dealFilePath(filePath) // 处理资源路径
let fileContent
try {
fileContent = fs.readFileSync(newPath,'utf8');
}catch(err) {
throw `没有找到引用资源${newPath}`
}
if(!fileContent) return;
wrap(fileContent)(myrequire);
}
3. require处理资源防止套娃引用
reuqire顺序是深度优先遍历
这里的深度优先并不是模块化工具在做递归,而是资源引用的写法:每次在不同文件下
require
,因为是同步执行自然而然形成的嵌套调用
index引用a、b,a文件引用b,b文件引用a,node原生Commonjs执行顺序
开始引入index.js
开始引入a.js
开始引入b.js (b有引a,但是不会触发开始引入和执行a)
执行b.js
执行a.js
执行index.js
可以看出不会重复引用
实现原理:和递归遍历类似,可以用一个weakmap或者一个变量存储调用过的资源文件,因为做引入抛出也是存储的时候要用到Modules的变量,所以直接用一个Modules存储引用过的文件,把资源路径作为数组id存储
const Modules = []; // 做全局变量
function cacheModule(filePath) {
const isExist = Modules.some(item=>item.id === filePath);
if(!isExist) {
Modules.push({
id: filePath
})
}
return isExist;
}
改造后 myrequire()
function myrequire(filePath) {
console.log('开始引入',filePath)
const newPath = dealFilePath(filePath)
const isExist = cacheModule(newPath); // 判断是否加载过
if(isExist) return; // 加载过不再重复加载
let fileContent
try {
fileContent = fs.readFileSync(newPath,'utf8');
}catch(err) {
throw `没有找到引用资源${newPath}`
}
if(!fileContent) return;
wrap(fileContent)(myrequire);
}
4. 引用和抛出变量
抛出和接收是使用模块化编程时的直观感受 实际上“抛出”的动作是文件把值存到export里 而“接收”就是require去export对象中取值(require还有抓取、包装文件内容的作用) 前面提到引用和抛出的本质是存储和获取,且每次加载资源都有一个数组存储资源的信息,我们往Modules全局变量中存储相应要抛出的变量即可
const Modules = []
function cacheModule(filePath) {
const isExist = Modules.some(item=>item.id === filePath);
if(!isExist) {
Modules.push({
id: filePath,
exports:{} // 加一个存储变量的地方
})
}
return isExist;
}
原生nodejs的Commonjs的使用方法是:
module.exports = {
a:''
}
exports.a = ''
👆这里两种存储变量的形式,本质都是往全局变量Modules里的exports进行存储,因此两者使用的区别,请在实现之后自行思考,本质区别就是操作对象赋值的区别而已
我们往js里注入方法的时候只注入了myrequire,这次再往里注入module和exports
// myrequire() 的注入变量代码片段
const index = Modules.findIndex(item=>item.id===newPath)
// 注入多两个变量 module 和 exports
wrap(fileContent)(myrequire,Modules[index],Modules[index].exports);
👆这样我们实现了资源文件的存储变量,接下来要实现获取存储的变量,也就是在myrequire()
之后返回出资源存储的整个对象exports即可
改造后 myrequire()
function myrequire(filePath) {
console.log('开始引入',filePath)
const newPath = dealFilePath(filePath)
const isExist = cacheModule(newPath);
if(isExist) return;
let fileContent = null
try {
fileContent = fs.readFileSync(newPath,'utf8');
}catch(err) {
throw `没有找到引用资源${newPath}`
}
// 注入 存储用的两个变量 module 和 exports
const index = Modules.findIndex(item=>item.id===newPath)
wrap(fileContent)(myrequire,Modules[index],Modules[index].exports);
// 返回资源文件存储的变量exports
return Modules[index].exports
}
5. 整理
至此,我们已经从0到1实现了一个简易版的 Commonjs
并且由此可知几个Commonjs的特性:
- 加载资源是同步加载的,即如果资源很大还是会阻塞后面资源的加载
- 资源套娃引用或重复引用,不会重复加载
- exports抛出的变量是值的拷贝,取决于加载资源时进行存储的变量是什么,后续不会更新存储的变量(即抛出的值是静态的)
- 执行require,会加载资源存储的所有变量到本文件
Commonjs获取动态值
// lib.js
var counter = 3;
function changeCounter() {
counter++;
}
module.exports = {
getCounter() {
return counter
},
changeCounter,
};
这样就可以拿到深拷贝的值,因为并不是拿拷贝的值,而是通过函数作用域去取值,作用域内的值变了,取到值就变了。
关于Commonjs的require是动态引用
Commonjs的
require()
发生在运行时 首先明确一点,动态的概念不等于异步的概念
动态引用是在条件作用域里require,只有在调用该函数才会运行引用资源,运行时才去获取对应的内容
而动态引用分两种情况
- 不需要执行就已知资源路径的情况
- 需要执行才知道资源路径的情况 如:
// 1.不需要执行就已知资源路径的情况
setTimeout(()=>{
require('./a.js')
},1000)
// 2.需要执行才知道资源路径的情况
function a(x) {
const filePath = x;
require(x)
}
function onclick(){
a('./a.js')
}
在webpack里require的资源是个未知的变量(运行时才可知)的话,上线之后会加载不到该文件。为什么nodejs可以加载,webpack不行?
是因为在本地node环境下,动态require运行时可以找到文件,如果动态require的文件是本地不存在的话,也是会像webpack一样报错找不到的 而webpack打包不会运行未知的代码(会收集已知的require资源),所以不会知道动态require的文件是什么,就不会把目标文件打包进项目,最后运行时服务器中找不到该文件就会报错
关于异步加载: 希望点击才加载a文件,在Commonjs里,index依赖a即使在条件作用域里,也必须初始化的时候就加载进来,点击时只是获取并执行a,并不是点击时才去找a文件进行加载和执行
不用exports.xx='' 而是exports={xx:''} 来存储变量会怎样
module.exports = {}
、exports.xx=''
同样是操作exports存储对象,那可不可以exports = {}
呢?
我们知道exports的来源是注入到函数里的形参 也就是👇的形式:
const exports = {a:'a'}
function a(exports) {
exports = {b:'b'}
}
a(exports)
console.log(exports) // {a:'a'}
👆函数的引用类型形参做重新赋值,在函数体中将会是个新的变量,而不是外部的变量 是js原生这么干的,不关nodejs的Commonjs机制
的事
在require中,exports是文件资源信息对象里的一个属性,如果在文件中自己重新赋值了exports,因为一个文件就是一个单独的作用域函数,exports将是一个作用域下全新的变量,而不再是外部文件对象里的属性,那么require就取不到文件抛出的信息了
这也就是为什么抛出数据只能exports.xx 一个个抛出(存储)而不能直接exports =
const module = {
myexports: {a:'1'}
}
// 传入的是module.myexports引用类型,如果重新赋值了myexports将不再有引用作用而是一个新的对象
function a(myexports) {
myexports = {a:'2'} // -->{ myexports: {a:'1'} }
// myexports.b = 'b' // -->{ myexports: {b: 'b'} }
}
a(module.myexports)
console.log(module) // -->{ myexports: {a:'1'} }
ES Module
node环境
node 8.5
开始支持mjs,但是还是实验性的所以需要参数--experimental-modules
执行.mjs 会提示ESM是实验性
node --experimental-modules index.mjs
(node:9076) ExperimentalWarning: The ESM module loader is experimental.
node13
开始不需要实验参数,可以直接执行.mjs .mjs可以用ESM,并且还是支持原来Commonjs
浏览器环境
和node环境用mjs后缀的方式不同,在浏览器环境不认识mjs,mjs是node自己新增的
浏览器需要不影响以往的没有模块化的js,所以要支持模块化时在<srcipt>
标签加上type
script标签的async、defer
在讲type=module
之前,我们看看其他属性 默认scipt标签都是同步加载,即按顺序加载script标签并会阻塞后面的加载 浏览器提供scipt识别为需要异步加载资源async、defer、module
都是异步加载的
<script src="./1.js" defer></script>
<script src="./2.js" async></script>
defer
要等到整个页面在内存中正常渲染结束(DOM
结构完全生成,以及其他脚本执行完成),才会执行(即使已经下载完成了)async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再恢复渲染
浏览器的V8引擎,渲染页面和执行js是共用一个线程的,所以会交错执行如👆async
的场景。
defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
type=module
因为type="module"
默认就是异步加载的,所以也要有个异步规则,那就是defer
所以👇其实就是一种省略默认值的写法
<script type="module" src="./1.js"></script>
<!-- 等同于 -->
<script type="module" src="./1.js" defer></script>
和defer
基本相同,等页面渲染结束,才按顺序执行资源(即使已经下载完资源)
而如果是async
<script type="module" src="./1.js" async></script>
则会是一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再恢复渲染,并且不保证顺序
由👆type="module"
,更多的是让文件内部可以使用ESM模块化,控制异步规则还是靠defer、async
ES Module
除了加载规则跟异步加载资源相同外,内部是支持直接使用import的。而这是只开启异步加载的资源文件无法做到的
ESM识别的资源路径
回忆一下 Commonjs
识别资源的路径有相对路径
、绝对路径
、nodejs内置模块
、node_module第三方资源
那ESM的资源路径:相对路径
、绝对路径
、第三方资源URl
import
第三方资源只能引用完整的 URL
,相对以前的裸导入 (bare import specifiers)(人话就是直接通过模块名导入),很不太方便,如下例:
import lodash from 'lodash'
它不同于 Node.JS 可以依赖系统文件系统,层层寻找 node_modules
在 ESM 中,可通过 importmap
使得裸导入可正常工作:
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.sykpack.dev/lodash",
"ms": "https://cdn.sykpack.dev/ms"
}
}
</script>
<script src="lodash" type="module"></script>
加了上面的map,浏览器就支持通过别名直接引入模块了
补充一点:平时可以到这里 https://npm.devtool.tech/ 查看各种第三方包的cdn,看看别人的导出方式 👇浏览器的控制台可以直接使用ESM的CDN
> lodash = await import('https://cdn.skypack.dev/lodash')
> lodash.get({ a: 3 }, 'a')
接收的是动态的值
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量 foo ,值为 bar ,500 毫秒之后变成 baz 。
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新(如果是引用类型也不行,因为是运行时进行存储的值的拷贝)
ESM自动严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
;。
严格模式主要有以下限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值- arguments不会自动反映函数参数的变化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局对象
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈
- 增加了保留字(比如protected、static和interface)
其中,尤其需要注意this的限制。ES6 模块之中,顶层的this
指向undefined
,即不应该在顶层代码使用this。
👇因此利用顶层的 this
等于 undefined
这个语法点,可以侦测当前代码是否在 ES6
模块之中。
const isNotModuleScript = this !== undefined;
编译时、运行时、静态化
- ESM发生在编译时且是静态化的
- CJS发生在运行时
编译时报错是语法/句法错误 运行时报错是各种情况 👆平时可以留意一下两种报错的区别
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及抛出和接收的变量
ESM的编译时
编译时:如 import
命令是编译阶段执行的,即在代码运行之前。 编译过程不能使用表达式和变量,表达式和变量是只有在运行时才能得到的动态结果
因为ESM是命令式语法,因此不做源码解读和伪代码的编写,请彻底理解👇发生在编译时的模块化原理
编译流程是:
JS 引擎对脚本静态分析的时候,遇到命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的文件里面去取值(由于ES6 import
的变量,只是一个“符号连接(指针)”
,所以这个变量是只读的,相当于“抛出”的变量都是const,对它进行重新赋值会报错。
浏览器通过html的
<script type="module">
,加载第一个js,这个js是入口文件 后续模块都是js中的import,而不是浏览器中的<script>
浏览器加载文件的功能我们称为加载器,加载器开始加载文件时,会把文件路径作为key记录到模块映射(缓存)中 不等文件加载完成,标记为加载中。继续开始下一个文件。 加载完成后触发解析文件即
编译
,识别静态语法中import。继续触发加载器,以此形成了一种深度遍历的效果,不过是不等待的那种 有了模块映射(缓存),加载器就可以跳过加载重复模块,直接导出模块的运行结果 另外,导出值的引用是在递归加载文件之后执行的,即会到最深层再往上抛出的顺序导出值的引用
ES6 模块“抛出”的变量是活的,ES6 模块是动态取值,并不是通过存储值来做抛出
import {a,b} from 'fs'`
👆不是函数,是js语法支持的读取命令,且命令实现只读取fs文件中抛出的a和b,并且这里的{ }
并不是解构赋值,而是命令解析的字符串。
另外import
不能写在条件代码块之中,没法做静态优化了,违背了 ES6 模块的设计初衷
// 报错
if (x === 2) {
import MyModual from './myModual';
}
编译过程处理 import 语句,这时不会去分析或执行 if 语句,所以 import 语句放在 if 代码块之中毫无意义,报句法错误,而不是执行时错误
CJS的运行时
CommonJS
和 AMD
模块化,都只能在运行时确定模块的依赖关系,以及抛出和接收的变量 如Commonjs
const {a,b} = require("fs")`
上面讲原理的时候也讲到,rqeuire
是个函数,是运行函数才能读取资源的,并且只能整个文件的存储都读取下来,再解构赋值给a和b
nodejs中共用两种模块化方式会怎样
通过 Babel 转码,CommonJS 模块的 require 命令和 ES6 模块的 import 命令,可以写在同一个文件里面,但是最好不要这样做。因为import 在静态解析阶段执行,所以它是一个模块之中最早执行的,出现不按代码顺序执行的情况:
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
core-js/modules
是给react实例化用的插件,需要在react之前加载好,但是这样写会先实例化react
ESM可以运行时的import()
import()
函数可以用在任何地方。它是运行时执行,什么时候运行到这一句,也会加载指定的模块。另外,import()
函数与所加载的模块没有静态连接关系(人话就是import()不会返回指定变量,而是返回整个文件的输出),这点也是与 import
语句不相同。
import()
类似于 nodejs
的 require
方法,区别主要是前者是异步加载,后者是同步加载(就是require一定会加载结束才往下走,而import()不await住的话会异步执行)
所以nodejs环境
Commonjs
可以被await import()
完全替代了?
ES6与CommonJS的区别
- CommonJS 模块输出的是一个值的拷贝(深拷贝)
- ES6 模块输出的是值的引用(变量的指针)
- CommonJS 模块是运行时加载
- ES6 模块是编译时输出接口
ESM对工程化最大的好处是浏览器直接支持模块化代码的编写 如下依靠ESM不安装任何依赖直接使用react框架,
<script type="module">
import { html, Component, render } from 'https://unpkg.com/htm/preact/standalone.module.js';
class App extends Component {
state = {
count: 0
}
add = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return html`
<div class="app">
<div>count: ${this.state.count}</div>
<button onClick=${this.add}>Add Todo</button>
</div>
`;
}
}
render(html`<${App} page="All" />`, document.body);
</script>
esm的限制与解决方案
代码需要基于es开发
第三方资源的导出要支持ESM
第三方资源node_module或者CDN的模块要可以通过import加载到
不支持esm的浏览器
不支持esm的浏览器会跳过type=module
的js 这时再html写多一个bundle.js
commonjs规范
的代码资源兼容即可 浏览器提供一个标签属性<script nomodule src="">
会判断是否支持esm,不支持则加载该标签
systemjs实现兼容方案
<script src="system.js"></script>
<script type="systemjs-importmap">
{
"imports": {
"lodash": "https://unpkg.com/[email protected]/lodash.js"
}
}
</script>
<script type="systemjs-module" src="">
加载这个库后,用type="systemjs-module",会根据浏览器支持esm的情况处理代码
思考
1. webpack工程下怎么同时支持CommonJs和ESM规范
提示:webpack通过编译(不等于上面说的js引擎的编译)CMD和ESM的源代码做资源收集合并,再利用js插入script的形式来做整合过的模块化
开发编写的是Commonjs和ESM,最后上线的代码既不是CMD也不是ESM,而是打包后的插入script方法(webpack干的)
2. babel如何把esm编译成commonjs,在node环境中运行
3. webpack、rollup是怎么支持tree shaking
提示:ESM因为命令式语法发生在编译时根据指针接收变量,即本身就是不引入额外的变量,也就是不需要做tree shaking
这个动作,但是打包工具最终上线的不是ESM,且即使代码写的CJS,上线后也能实现tree shaking