前端打包(编译器)方面的思考
背景
最近在对打包、编译方面做一些思考,于是提了一些问题,自己也是做了一些总结,记录在此。形式主要是一问一答,个人理解,供你参考
Q1:打包器是什么?
打包器可以理解为是一个node脚本,把开发源代码(带框架、目录结构、npm包)打包成浏览器可以识别的规范代码(html)
例如:
Q2:为什么需要打包器?
因为:
-
开发的源代码是 带框架、目录结构、npm包等,是没有办法直接在浏览器端跑起来的
-
当你需要发布一个npm包时,为了节省使用者的时间,并且避免打包工具的版本冲突,所以也需要提前打好包
-
在可以打包阶段可以顺便处理很多问题,比如:浏览器兼容,性能问题,语法编译等
-
打包器可以集成编译器,比如把react-loader(react DSL的编译器)集成到webpack中使用
- 在打包阶段把框架语法(.vue .jsx)给解析成浏览器可以识别的规范js
-
-
相当于,把给人看的代码(.vue .jsx .tsx .scss),转换成,给机器(浏览器)看的代码(.js .css .html)
盗用一张webpack官网的图:
Q3:打包器原理?
大致思路是:通过一个入口,递归的去找到所有的相关依赖,最终会把js,css分别都是打在一个或多个包里,生成入口index.html,包含这些依赖(外链的形式),这些都是静态资源,可以直接部署
- ps:也可以是多入口,最终生成的html,也会是多html,本质是一样的
原理这么讲的话,还是太浅了,需要多深入了解一些细节,于是又多提了一些问题
-
依赖之间的作用域怎么隔离?否则jS会相互污染
-
如果一个依赖被多个文件引用,怎么做拆包?
-
如何处理umd、cjs、esm模块?
-
按需加载,怎么获取精准的依赖?
以上问题以webpack5来做分析,只是一个思路、一个视角,前端的打包工具不止是有webpack(以下内容是我查看webpack打包产物 做的分析)
1. 依赖之间的作用域怎么隔离?否则jS会相互污染
- 用块作用域隔离(让两个模块的a变量互不影响)
(() => {
const a = 1
console.log(a)
})()
(() => {
const a = 1
console.log(a)
})()
- 如果会打包到同一个块作用域内的话,通过改变量名来解决变量名冲突(编译过程中改源代码)
const a = 1
console.log(a)
const util_name_b_a = 1 // 假设文件来源于:'./util/name-b.js',会取: 文件路径名_变量名,文件路径名有唯一性保证
console.log(util_name_b_a)
2. 如果一个依赖被多个文件引用,怎么做拆包?
递归分析依赖的时候,会维护一个map,如果一个依赖被多个文件引用,那么会被单独拆出来,统一放在一个公共js包里面。因为都在一个js里面,所以依赖之间也需要互相隔离,依赖会被统一放到__webpack_modules__这个对象里,格式如下:(请细看代码、注释和变量命名)
// 依赖经常是esm、cjs、umd同时存在的
// 当依赖是 cjs 或 umd 时,会被单独拆一个块作用域,放到__webpack_modules__对象内
var __webpack_modules__ = ({
834: ((module) => { // 浏览器里面是没有 module对象的,所以module.exports会报错。这里是webpack自己传的module参数,webpack本身来兼容module对象的逻辑
// 如果是cjs的包,直接module.exports (以'lodash.difference'为例)
module.exports = difference;
// 如果是umd的包,会有类似兼容代码
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.React = {}));
}),
432: (() => {
...
})(),
...: ...
})
// 以'lodash.difference'为例
var lodash_difference = __webpack_require__(834);
var lodash_difference_default = /*#__PURE__*/__webpack_require__.n(lodash_difference); // __webpack_require__.n是做缓存用的
console.log((lodash_difference_default())[[1,2], [2,3]])
// 当依赖是 esm 时,会直接放到最外层
var __webpack_modules__ = ({
...
})
// esm 包,直接放最外层
const unit_name = 1 // 当变量名没有冲突时,直接用
const util_name_b_a = 1 // 当变量名冲突时,假设文件来源于:'./util/name-b.js',会取: 文件路径名_变量名,文件路径名有唯一性保证
console.log(unit_name, util_name_b_a)
3. 如何处理umd、cjs、esm模块?
- 问:umd、cjs、esm模块依赖是怎么打包的,可以看上面
- 问:如果我要用webpack来打包输出一个npm库?
- 如果要输出esm,那么会把cjs的语法转成esm,比如module.exports转成export
- 如果要输出cjs,那么会把esm语法转成cjs,比如export转成module.exports
- 如果要输出umd,则输出类似兼容代码
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.React = {}));
4. 按需加载,怎么获取精准的依赖?
可以先参考上面:2. 如果一个依赖被多个文件引用,怎么做拆包?
思路:按需加载的依赖会把 _webpack_modules_ 作为中转对象,比如 (请细看代码、注释和变量命名)
// 以'lodash.difference'为例,会以__webpack_modules__.834作为挂载点
var __webpack_modules__ = ({
834: ((module) => {
// 初始的时候,这里面的内容是空的
// 等'lodash.difference'这个懒加载包真正被加载时,会被挂到 __webpack_modules__.834 这里。 还没加载时,此处会为这个懒加载包留位置,后续的代码已经关联上了此处
}),
432: (() => {
...
})(),
...: ...
})
// 提前关联好 __webpack_modules__.834
var lodash_difference = __webpack_require__(834);
var lodash_difference_default = /*#__PURE__*/__webpack_require__.n(lodash_difference); // __webpack_require__.n是做缓存用的
console.log((lodash_difference_default())[[1,2], [2,3]])
Q4:前端框架利用打包器做了什么?
- 利用打包器,可以实现对框架语法的编译
- 比如:浏览器不认识 .jsx .vue 这类的文件,jsx和vue的语法也没有任何工具知道如何解析,因为是框架作者自定义的,所以编译框架语法这一块,肯定需要框架作者自己来写解释器。
-
解释器写完之后,要嵌入开发流程内去,最好是类似npm run build 一条命令 既可以编译好框架语法,又可以实现打包,让打包的产物可以开箱即用,这样无疑是最容易被大家接受的
-
并且webpack正好提供loader这种钩子函数,那么就很自然的诞生了 jsx-loader、vue-loader
-
- 把打包器包一层,集成到脚手架(create-react-app、vue-cli)内去,更好的开箱即用
-
因为webpack从0到1配置在webpack的版本早期,成本还是比较高的,而且很多普通开发者也不熟悉webpack,这个问题可能会阻塞vue、react这类框架的发展,提高他们的使用成本。
-
所以,干脆就把webpack包一层,直接把一些好的实践集成进去,让开发者无感,可以开箱即用
05:在打包各阶段我们能做些什么有趣的事情?
这里肯定讲不全,只能提几个思路,看官可以自行发挥创造力去思考
-
可以做编译
- 自定义loader,理论上可以把任意DSL(语法)转成合法的js或css。比如:ts,babel,sass,vue、react语法的解析。都在这个阶段
-
输出产物阶段
- 可以做和性能相关的,比如:包压缩,产物名字控制、对根html加载资源作优化、资源上传到CDN 等等
webpack的生态也很繁荣,可以自行从webpack生态里的loader和plugin里去找灵感
另外提个思考题:vue和react在打包阶段能否互转?
(个人意见)在打包阶段,理论上是可以做到的,由ast作为中间态是可以实现的,react/vue -> ast -> vue/react,但是有几大缺陷:
- 本身实现成本比较高
- 维护成本非常非常高,这点应该是最致命的
- 因为:vue、react都在频繁更新迭代,甚至有break change,比如vue3、react18,这样会造成项目很难很难维护,几乎相当于要重写。而且用户的环境非常复杂,用的版本也不一样,很难做到各种情况都兼容。并且两种框架本身也有一些api差异非常大,很难对这类api做兼容
如果要解决vue和react互相兼容的问题,不考虑打包阶段解决的话,有其他几个方向可以解决
-
源代码互转:左侧贴入你的vue/react代码,右侧输出对应的转换,不保证100%成功,需人工check,新语法不一定支持。
- 这个解决思路和上面类似,只不过是抽出来了,可以可视化。缺点也和上面类似
-
项目内同时把vue、react实例共存,不做代码转换,直接让对应的框架实例去解析
- 有类似开源项目,比如github.com/akxcv/vuera
- 实现原理类似我写的另外两篇:如何在react项目中运行vue组件?,如何在vue项目中运行react组件? (运行时解析,对dom节点做替换)
码字不易,点赞鼓励!!
转载自:https://juejin.cn/post/7244174365184589881