当前位置: 首页 > 科技观察

Babel剖析——迈向前端架构师的一小步

时间:2023-03-13 17:05:40 科技观察

谈到Babel的作用,很多人的第一反应是:用来实现APIpolyfill。其实Babel作为前端工程的基石,做的远不止这些。作为一个庞大的家族,Babel生态中有很多概念,比如:preset、plugin、runtime等,这些概念让Babel初学者望而生畏,理解也就止步于webpack的babel-loader配置。本文将从Babel的核心功能入手,一步步揭开Babel家族的神秘面纱,向前端架构师迈出一小步。什么是BabelBabel是一个JavaScript编译器。Babel作为一个JS编译器,接收输入的JS代码,经过内部处理过程,最终输出修改后的JS代码。在Babel内部,会执行以下步骤:ParsetheInputCodeintoAST(AbstractSyntaxTree),这一步叫做解析EdittheAST,这一步叫做transforming将编辑好的AST输出为OutputCode,这一步叫做打印FromBabelrepository[1]的源代码可以找到:Babel是一个由几十个项目组成的Monorepo。其中babel-core提供了上述三个步骤的能力。babel-core内部,更详细:babel-parser实现第一步babel-generator实现第三步要理解第二步,我们需要简单了解一下AST。AST的结构进入ASTexplorer[2],选择@babel/parser作为解析器,在左侧输入:constname=['ka','song'];可以解析出如下结构的AST,它是一个JSON格式的树形结构:babel-core内部:babel-traverse可以“深度优先”的方式遍历AST树。对于遍历的每条路径,babel-types都提供节点类型数据,用于修改AST节点。因此,整个Babel底层编译Capabilities由以下几个部分组成:在我们了解了Babel的底层能力之后,我们来看看上层基于这些能力可以实现哪些功能。Babel的上层能力是基于Babel编译处理JS代码的能力。Babel最常见的上层能力有:polyfillDSL转换(比如解析JSX)语法转换(比如把高级语法解析成当前可用的实现)限于篇幅,这里只介绍polyfill和“语法转换”相关的功能.以polyfill为前端,最常见的Babel生态库肯定是@babel/polyfill和@babel/preset-env。使用@babel/polyfill或@babel/preset-env来实现高级语法的回退实现以及API的polyfill。从上面我们知道Babel本身只是一个JS编译器。上面两个的转换函数是谁实现的?答案是:core-jscore-js简介core-js是一套模块化的JS标准库,包括:直到ES2021的polyfillpromise、symbols、iterators等特性都实现了。ES提案中的特性实现了跨平台的WHATWG/W3C特性。例如URLcore-js作者DenisPushkarev从co??re-js仓库[3]看到,core-js也是一个由多个库组成的Monorepo,包括:core-js-buildercore-js-bundlecore-js-compatcore-js-purecore-js我们介绍了其中的几个库:core-jscore-js提供了polyfill核心实现。import'core-js/features/array/from';import'core-js/features/array/flat';import'core-js/features/set';import'core-js/features/promise';数组。from(newSet([1,2,3,2,1]));//=>[1,2,3][1,[2,3],[4,[5]]].flat(2);//=>[1,2,3,4,5]Promise.resolve(32).then(x=>console.log(x));//=>32直接使用core-js会污染全局命名空间和对象原型。比如上面的例子,修改了Array的原型,支持数组实例的flat方法。core-js-purecore-js-pure提供了一个独立的命名空间:importfromfrom'core-js-pure/features/array/from';importflatfrom'core-js-pure/features/array/flat';importSetfrom'core-js-pure/features/set';importPromisefrom'core-js-pure/features/promise';from(newSet([1,2,3,2,1]));//=>[1,2,3]flat([1,[2,3],[4,[5]]],2);//=>[1,2,3,4,5]Promise.resolve(32).then(x=>console.log(x));//=>32这种使用不会污染全局命名空间和对象原型。core-js-compatcore-js-compat根据Browserslist维护了在不同主机环境和版本下需要支持的特性集合。Browserslist[4]提供了对不同浏览器和节点版本下的ES特性的支持。Browserslist,例如:"browserslist":["notIE11","maintainednodeversions"]表示:非IE11版本和Node.js基金会维护的所有版本。@babel/polyfill和core-js@babel/polyfill的关系可以看成:core-js加上regenerator-runtime。regenerator-runtime是generator和async/await的运行时依赖。单独使用@babel/polyfill会引入所有的core-js,导致项目打包体积过大。从Babelv7.4.0[5]开始,不推荐使用@babel/polyfill。你可以直接引用core-js和regenerator-runtime来代替。为了解决全面引入core-js导致的打包体积过大的问题,我们需要使用@babel/preset-env。preset的含义在介绍@babel/preset-env之前,我们先了解一下preset的含义。最初,Babel没有任何额外的能力,它的工作流程可以描述为:constbabel=code=>code;它提供了通过插件干预babel-core的能力,类似webpack的插件提供了干预webpack编译过程的能力。插件分为几类:@babel/plugin-syntax-*与语法相关的插件,用于新的语法支持。例如babel-plugin-syntax-decorators[6]为装饰器提供语法支持@babel/plugin-proposal-*用于ESproposal特性支持,比如babel-plugin-proposal-optional-chaining是可选的链运算符特性支持@babel/plugin-transform-*用于转换代码。转换插件会使用语法插件对应的多个插件的组合,形成一个集合,称为预设。@babel/preset-env使用@babel/preset-env,可以“按需”打包core-js中的特性,可以显着减少最终打包的体积。这里的“按需”分为两个粒度:宿主环境的粒度。根据不同的托管环境,环境中需要的所有功能都按照用例的粒度进行打包。把用到的特性打包一下,我们依次看一下。宿主环境的粒度当我们根据如下参数配置项目目录下的browserslist文件时(或者在@babel/preset-env的targets属性中设置,或者在package.json的browserslist属性中设置):notIE11maintainednodeversions将“非IE11”和“Node.js基金会维护的所有节点版本”下所需的功能放入最终包中。很明显,这是对刚才介绍的monorepocore-js下的core-js-compat能力的使用。用例的粒度理想情况下是只打包我们使用的功能。这时候可以设置@babel/preset-env的useBuiltIns属性为usage。例如:a.js:vara=newPromise();b.js:varb=newMap();当宿主环境不支持promise和Map时,输出文件为:a.js:import"core-js/modules/es.promise";vara=newPromise();b.js:import"core-js/modules/es.map";varb=newMap();当主机环境支持这两个功能时,输出文件是:js:vara=newPromise();b.js:varb=newMap();进一步优化打包体积打开babelplayground[7],输入:classApp{}会发现编译结果为:function_classCallCheck(instance,Constructor){if(!(instanceinstanceofConstructor)){thrownewTypeError("Cannotcallaclassasafunction");}}varApp=functionApp(){"usestrict";_classCallCheck(this,App);};其中_classCallCheck是辅助方法。如果多个文件使用class特性,每个文件包对应的模块都会包含_classCallCheck。为了减少打包体积,更好的办法是:所有需要使用“辅助方法”的模块都从同一个地方引用,而不是自己维护一份。@babel/runtime包含所有Babel“辅助方法”和regenerator-runtime。仅仅引入@babel/runtime是不够的,因为Babel不知道什么时候引用@babel/runtime中的“辅助方法”。因此,还需要引入@babel/plugin-transform-runtime。这个插件会把所有使用“helpermethods”的地方从“自己维护一个”改成编译时从@babel/runtime导入。所以我们需要将@babel/plugin-transform-runtime设置为devDependence,因为它是在编译时使用的。将@babel/runtime设置为依赖项,因为它在运行时使用。总结本文自下而上介绍前端日常业务开发会接触到的Babel家族成员。它们包括:底层的@babel/core(由@babel/parser、@babel/traverse、@babel/types、@babel/generator等组成)它们提供了Babel编译JS的能力。注意:这里的@babel/core是库名。上一篇中babel-core就是仓库中对应的文件名。中间层@babel/plugin-*Babel对外暴露API,开发者可以干预其编译JS的能力。上层@babel/preset-*日常开发中会用到的插件集合。对于立志成为前端架构师的同学来说,Babel是前端工程的基石,学习使用是很有必要的。看到这里不容易,给自己鼓掌。参考资料[1]Babel库:https://github.com/babel/babel/tree/main/packages[2]ASTexplorer:https://astexplorer.net/[3]core-js库:https://github.com/zloirock/core-js/tree/master/packages[4]浏览器列表:https://github.com/browserslist/browserslist[5]Babelv7.4.0:https://babeljs.io/docs/en/babel-polyfill#docsNav[6]babel-plugin-syntax-decorators:https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-decorators[7]babel游乐场:https:://babeljs.io/repl#?browsers=&buildins=&buildins=&spec=false&sege=false&code_lz=mygwhgzhaeccao9og8c8c-q&debug=&debug=&forcealltransforms=&forceallTransforms=false&forcealltransforms=false&precterapl=quidproposal7.13.7&externalPlugins=babel-plugin-transform-regenerator%406.26.0