前端工程化③Bundless基础设施建设
前言
本文将简述前端通过不打包的方式将代码运行在浏览器上的几种方式
原理与浏览器中的 ESM
当下,主流浏览器已经逐步支持 js 原生模块的导入,出现了很多 bundless 的构建方案,比如 vite,snowpack 等。原理就是利用浏览器对于原生 esm 的支持,进行代码的开发和部署。
Native Import: import from URL
通过script[type=module]
,可以直接在浏览器中使用原生 ESM,使得前端不打包成为可能
1 | <script type="module"> |
由于前端运行在浏览器中,所以他也只能从 URL 中引入 package
- 绝对路径:
http://cdn.skypack.dev/lodash
- 相对路径:
./lib.js
现在打开浏览器控制台,把下面的代码粘贴在控制台中,就可以调试 loadash 了。
1 | > lodash = await import('https://cdn.skypack.dev/lodash') |
Import Map
不过 http Import 每次都要导入完全的 URL,这个肯定是不太合理的,相比以前的裸导入,很不方便,如下:
1 | import lodash from "lodash"; |
他不同于 nodejs 依赖系统文件系统,层层寻找node_modules
1 | /home/app/packages/project-a/node_modules/lodash/index.js |
在 esm 中,可以通过 ImportMap 使得裸导入成为可正常工作
1 | <script type="importmap"> |
此时可以与以前同样的方式进行模块导入
1 | import loadash from 'loadash' |
那么通过裸导入如何导入子路径呢?
1 | <script type="importmap"> |
Import Assertion
通过script[type=module]
,不仅可以引入 js 资源,甚至可以引入 json/css 资源,示例如下:
1 | <script type="module"> |
如何将 CJS 转化为 ESM
Bundless 的兴起,要求所有的模块都是 ESM 模块格式。
目前社区有一部分模块同时支持 ESM 和 CJS,但仍有许多模块仅支持 CJS/UMD,因此将 CJS 转为 ESM 是全部模块 ESM 化的过度阶段
ESM 与 CJS 的导入导出不同
在 ESM 中,导入导出有两种方式
- 具名导出导入
- 默认导出导入
eg
1 | // 具名导入导出 |
而在 CJS 中,导入导出的方法只有一种
1 | module.exports = sum; |
而所谓的 exports 仅仅是 module.exports 的引用而已
1 | exports = module.exports; |
下列举两个例子,来验证他俩的区别
1 | // hello.js |
第二
1 | // hello.js |
正因为它们两者的不同,所以在两者转换的时候有一些兼容问题需要去解决.
exports 的转化
正因为两者不同,所以当 exports 转化的时候,既要转换为exports{}
又要转化为export default{}
1 | // 输入 |
如果只转化为具名导出不转化为默认导出会发生什么?
eg
1 | // Input: CJS |
module.exports
转化
对于module.exports
,我们可以遍历其中的 key,通过 ast,将 key 转化为具名导出,将module.exports
转化为默认导出
1 | // 输入 |
如果module.exports
导出的是函数该如何处理? 特别是exports
和module.exports
的程序逻辑混合在一起?
以下是一个正确的转化结果:1
2
3
4
5
6
7
8
9
10
11
12// 输入
module.exports = ()=>{};
export.a = 3;
export.b = 4;
// 输出
const sum = ()=>{};
sum.a = 3;
sum.b = 4;
export const a = 3;
export const b = 4;
export default = sum;
也可以这么处理:将module.exports
与exports
的代码使用函数包裹起来,此时我们无需关心其中的逻辑细节.
1 | var esm$1 = { export:{} }; |
一些复杂的转换
ESM与CJS不仅仅是语法层次上的不同,本身的思维逻辑就完全不一样.所以有一些较为复杂的转换,不做讨论.
比如:
- 如何处理
_dirname
; - 如何处理
require(dynamicString)
; - 如何处理cjs的编程逻辑
以下代码涉及到编程逻辑,由于exports
是一个动态的js对象,而他自然可以使用两次,那应该如何正确的被编为esm呢?1
2
3
4
5// input: index.cjs
exports.sum = 0;
Promise.resolve().then(() => {
exports.sum = 100;
});
以下是一种不会出现问题的转换结果1
2
3
4
5
6
7const _default = {};
let sum = (_default.sum = 0);
Promise.resolve().then(()=>{
sum = _default.sum = 100;
})
export default _default;
export {sum}
CJS to ESM的构建工具
CJS向ESM转换,自然有构建工具的参与.比如:@rollup/plugin-commonjs
甚至把一些 CommonJS 库转化为 ESM,并且置于 CDN 中,使得我们可以直接使用,而无需构建工具参与
https://cdn.skypack.dev/(opens new window)
https://jspm.org/
Bundless的优势与不足
优势在于 不需要处理打包的问题,直接放到浏览器上面使用.
不足在于 现在还有很多模块没有支持esm,所以转换cjs到esm是个问题