坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.3

babel入门学习

前言

从我开始接触前端工程化到现在工作一年多,babel这个词就时不时出现在各种代码里,但是见到了并不意味着就知道了,尤其是各种babel的包结合起来配置,头都晕了,所以这次写个文章加深下自己的印象。

官方文档

Babel · The compiler for next generation JavaScriptbabeljs.io/

简介

开头直接简洁明了,就是一个javascript的解析器。主要就是将不兼容的ES6以上的api、语法等转换为大部分浏览器都兼容的es5

主要能用于一下地方

  1. 语法转换,就是上面说的
  2. 帮你兼容一些低版本没有的api
  3. 源码转换,比如你用的jsx,帮你转换为js
  4. 其它。。。。

配置

说的再多也不如直接实战

我们先创建一个文件夹和一个index.js文件,然后初始下环境

然后在我们的index.js文件中随便写点es6的东西,比如箭头函数或者Array.reduce之类的api

然后在终端安装babel的核心包@babel/core、@babel/preset-env以及能让这两个包跑起来的命令工具@babel/cli

安装完包之后直接执行运行命令

然而你发现lib目录下生成的index.js里的代码依旧没改变,还是es6的东西,为什么没有转换呢?因为我们并没有做配置,命令读不到配置自然就按原来的输出了。

在介绍通过配置文件配置之前,我们先来看下通过参数传入,我们先安装@babel/plugin-transform-arrow-functions ,这个包一看就知道是用来转换箭头函数的。

那么我们要怎么用这个plugin呢?@babel/cli中提供了--pluginsoption

这里就转换完成了。但每次这样传参就麻烦,所以官方又提供了一种方式,通过配置文件。

让我们来创建一个babel.config.js的配置文件,当然建议用.json(>=7.8.0)的,js是旧版中的。

箭头函数正常转换了。但你应该注意到了reduce方法和...解构都没有转换为es5

让我们改下配置

我们再重新跑一下指令。

箭头函数是转换了,...结构也通过apply的方式转换了,但是这个reduce并没有变。

在低版本是可以引入@babel/polyfill处理的,但是在7.4.0之后就废弃迁移到core-js包中了。

另外为什么没有自动转换呢?因为现在默认是不需要的,需要我们做其他配置。

首先,我们先安装下core-js,这个包中的代码都是参与打包的,所以不能放dev中。

然后我们改下babel.config.js文件中的配置

  • useBuiltIns: 默认是false,不使用任何的polyfill。不为false的时候会直接从core-js中引入polyfill,其他无相关的将不会被引入。 其中usage 会分析每一个module中需要引入的polyfill然后自动引入,是按需加载,同时代码是放在module内部的,不会污染全局,但当所有的module合并后自然会出现很多重复的导入(指编译后的代码,不影响实际效果,因为同一个polyfill只会被导入一次)。而entry需要开发者自己去全局入口引入core-js,这样全局就只有一份polyfill,但这份polyfill是全量的,会污染全局,不过你会有重复的导入问题。怎么用还是得看具体的场景,并不是说usage就会比较好。
  • corejs: 你安装的core-js包版本,其中有23的版本,其中2的版本比3少了一些实例自身的属性。

另外,我们并不需要补充regenarator-runtime这个包,因为这个包如果没有配置会自动给我们加上。

让我们重新跑下指令

result

这下就正常转换了。并且只引入了我们需要的。

我们来看下useBuiltInsusageentry的不同。

usage

首先新建src文件夹,然后新增a.js、b.js、c.js以及index.js四个文件,其中abc三个文件导出方法除了名字其它都相同,index.js导入这三个文件导出的方法。

然后改下我们package.jsonbuild的指令

然后终端直接执行npm run build

可以看到编译后的文件中两个polyfill被重复引用。

entry

首先将babel.config.js中的useBuildIns改为entry,然后在我们的index.js中将core-js包导入,然后重新执行我们的npm run build指令。

可以看到所有的polyfill都被引入了。

这里越过了@babel/polyfill这个包的使用,因为目前已经是不推荐了,但如果你接手了一个老项目,那还是得看一下的,这里放下官网链接

https://babeljs.io/docs/en/usage#polyfillbabeljs.io/docs/en/usage#polyfill


关于配置文件

配置文件支持多种

  1. babel.onfig.js/json这两种都是一样的,推荐json格式,上边已经介绍过了这种方式,就不多说了。作用范围是整个项目。
  2. .babelrc.json ,作用范围局部,当前文件夹下,执行转换时会先往上找最近的一个.babelrc.json文件直到根目录,然后将这个.babelrc.json和全局的合并之后再获取配置信息。用法如下:
  1. 嫌麻烦,可以直接将配置信息写到package.json文件中。

一些常用的配置

已经被内置了的就不说了。

  1. @babel/preset-react,看名字就知道是用于react的配置,我们先安装

然后将这个配置加到babel.config.js文件中

接着创建react的文件夹,新建index.jsx,随便写点react的代码

然后package.json中新增script

然后直接执行指令npm run build-react

  1. @babel/preset-typescript,看名字也知道这是个用来识别转换typescript的配置

然后新增一个ts的文件夹,创建一个index.ts,随便搞点东西

接着在package.json中新增指令,需要注意ts文件需要提供extensionsoption

最后在babel.config.js中新增一个预设

然后运行指令npm run build-ts


实现一个babel插件

如果大家用过webpack的插件,应该能发现都有同一个格式,需要定义class.apply才能被调用。

babel也有自己的格式。

我们先创建一个plugins文件夹,新建log.js文件

当找到nameres的变量时,打印这个节点。

然后我们将它引入到babel.config.js中。

然后我们执行npm run build,目标既是我们的index.js文件

这里面标记了行列的位置,注释,起始结束位置等。

正好就是第一行第六列到第九列。

通过这个例子可以看出这个格式

这里就只展示这一个小demo,实际上一个plugin能做的事情很多,可以监听的东西也很多。因为传入visitor中的是被core编译转换后的ast(抽象语法树)


@babel/cli

你是否觉得这个指令的执行者./node_modules/.bin/babel很奇怪?实际上node_modules/.bin中存放的是所有第三方包里的命令执行文件入口,如果这个包配合commander和全局安装,就能做到类似npm这样的全局命令执行工具。可以看下我之前的文章。

https://zhuanlan.zhihu.com/p/569233377

而这个.bin中的命令来源是包自身带有的bin文件夹中的,比如这个@babel/cli中的bin文件夹。

要怎么分辨呢?直接去看package.json中如何配置的即可。

如果你嫌弃./node_modules...这种执行太冗长,你可以在package.json中新增一个script

为什么这样会生效呢?因为@bael/cli这个包配置了这个npm指令即上面提到的package.json文件中的bin.babel

我们跟着进入babel.js中,发现是导入cli/lib/babel这个包,入口自然是index.js文件

这段代码很简单,就是将命令行带上的参数传入options.js中,转换后判断是否有配置--out-dir,如果有则按照传入的来,否则使用默认输出路径。

简单的说下options.js中做了什么。 就不看代码了,毕竟这不是源码分析。。。

其实这里面也用了commander,装载了一堆的option,唯独没有配置指令的名字。。。

抛去commander的配置,还剩200多行代码。

主要做了以下几件事。

  1. 获取命令行中传入的参数
  2. 判断参数是否符合要求,不符合报错return
  3. 以对象的格式返回转换后的参数

然后参数传入dir.js中。

这个文件中代码写的很绕,一个套一个的,最终找到的compile方法居然在utils里。。。。

这里面主要做了以下的俩件事

  1. 读文件,没有读怎么写呢,是吧?doge
  2. 遍历读到的符合要求的文件(比如非ignore的文件),调用compile方法,引入core包,将这些文件里的源码转换成我们想要的兼容低版本的代码。

@babel/preset-env

这个包相信大家也差不多猜到了,在学的过程中需要通过它来做兼容判断。

先来看下官方对它的描述

简单的说就是可以帮你自动去做兼容等,不需要你去做额外的处理比如引入插件等。当然,有些东西需要额外配置才能生效,比如polyfill,需要配置useBuiltIns以及corejs

同样不分析源码,简单的说先是判断当前环境,比如node,然后通过options判断加载不同plugin

来看下常用的option

  • targets:你希望支持的哪些浏览器的哪些版本,会根据你的配置,往你的bundle中加点兼容性东西,让你的代码可以正常运行在你想要的浏览器中。 当然,如果你配置了browserlist,也会根据browserlist的来做判断。默认不配置,会根据默认情况执行,尽量帮你兼容所有浏览器的低版本。
  • include:和下面的exclude同类型的作用,用于让某些plugin一定会被调用。
  • exclude:让某些plugin一定不会被调用。
  • useBuiltIns:用来处理polyfill的,默认false不加载polyfill。其它不多说了,配置 这一块里有对usageentry的分析。
  • corejs:只有配置了useBuiltIns: usage/entry这个字段才会生效。默认是版本22不可以传入具体的版本 ,3则可以。用于加载core-js的包,而core-js需要自行安装并确保包版本和你这配置里的是一致的(指的是core-js2只能配置2,不能配置3core-js3同样道理)。
  • browserlistEnv:基于browserlist的配置。

@babel/core

看名字就知道这个包的重要性,是用来处理源码转换代码的核心包。

我们直接来实战

我们先创建一个transform的文件夹,新建index.js文件。

然后我们在index.js中将@babel/core包中的transform方法导入。

我们执行下这个文件node transform/index.js

可以看到我们的箭头函数确实被转换成function的形式了。

这里依旧不会去分析源码,只需要知道这里面有一个解析器@babel/parser,将代码转换成object,经过@babel/traverse加工处理后,再调用@babel/generatorobject转换为最终的代码。


@babel/parser

上面说到了,将你的代码"字符串·"转换成抽象语法树(AST),至于怎么转的,这里就不说了,因为这块逻辑是很复杂的,简单的说就是将你的code进行遍历,将每个字符都过一遍,当然这不是简单的遍历。它里面内置了几十个javscript特殊字段(比如const、let、var这些,尤其是空格),根据这些字段,将“不稳定”的状态 转变为稳定状态(比如你定义一个变量,最开始是不确定你这个是不是变量的,但是遍历过前后之后再配上这些特殊字段判断就能确定了),最终将你的code string变成一颗”树“objectAST的应用是非常非常常见的,比如你的vue的虚拟dom就是一种AST,还有你现在正在用的IDE,都是基于AST的。

我们来调用下这个包

又或者你可以将上面@babel/core中的transform传入的参数第二个插入一个options,里面传入ast: true,也是一样的。


@babel/generator

这个core包中也提到了,主要是将你的ast转换为string。当然,这里拿到的ast不是原版从parser中拿到的ast,是经过@babel/traverse执行各种plugin加工过后的。

我们直接来调用下这个包

这里的code里的箭头函数居然没有转换成function的形式,这是为什么呢?因为我们没有配置。。。。。之前@babel/core能正常转换是因为里面做了读取配置的处理,而generate/parser都是功能单一的包,不可能会做读取的处理。 这也符合官方的描述 --“默认不处理代码,处理都交由插件“。那么要怎么变成我们想要的呢?

你应该注意到了generator处理的是ast,所以如果我们改动ast的话到时它输出也就是我们改动了的,官方包里也是这么做的。

而改动我们需要用到另一个包@babel/traverse来帮助我们将箭头函数变为function的形式。


@babel/traverse

这个包是用来更新ast的。

接着我们上面的代码

然后终端执行下node transform/index.js

转换成功!


@babel/types

提供了babel内置的类型,比如上面提到过的IdentifierArrowFunctionExpression

这里就不多说了,感兴趣的大佬可自行点下【18】去官网看下。


@babel/template

看名字就知道是和模板有关的。

我们简单的搞下即可,因为重要的包前面几个已经讲完。

官方提供了两种替换变量的方式

  1. %%xxx%% 的方式
  2. XXX的方式


@babel/runtime

还记得上面提到过的polyfill和配置不?是通过注入代码参与打包来实现转换的,而这些一般叫做runtime

babel也是需要注入一些runtime来帮助代码进行兼容的。

我们先在transform文件夹中创建一个文件test-runtime.js

然后package.json新增script

接着npm run build-runtime

输出的buildRuntime.js中多了一堆意义不明的玩意儿。

而这些东西就是async await的辅助函数。

让我们在创建一个test-runtime2.js,然后让这个文件也参与编译

发现这些辅助函数每次都会被引入,这也就意味着存在大量的重复代码。

为了处理这个问题,官方就将这些个runtime都抽出来放到了这个@babel/runtime包中。

那么要这么做才能处理掉这些重复代码呢?

我们直接在入口引入对应的包即可。

但这样每次都得手动去引入,很麻烦。有没有办法能做到和core-js一样自动引入?

有的,官方又提供了一个包@babel/plugin-transform-runtime,这个包能自动引入。

[https://babeljs.io/docs/en/v7-migration#babelruntime-babelplugin-transform-runtimebabeljs.io/docs/en/v7-migration#babelruntime-babelplugin-transform-runtime

我们先安装

然后在我们的babel.config.js中配置下

这回就自动引入啦


@babel/code-frame

这个包提供了自定义行列位置的log,就不多说了,感兴趣的大佬可以点击【19】看下。

随便搞下。


那么babel的基础就学完了,如果觉得对你有用麻烦点个赞~谢谢!

参考

  1. core-js https://www.npmjs.com/package/core-js
  2. useBuiltIns https://babeljs.io/docs/en/babel-preset-env#usebuiltins
  3. @babel/preset-react https://babeljs.io/docs/en/babel-preset-react
  4. @babel/preset-typescript https://babeljs.io/docs/en/babel-preset-typescript
  5. preset-env https://babeljs.io/docs/en/babel-preset-env
  6. @babel/preset-env_targets https://babeljs.io/docs/en/babel-preset-env#targets
  7. @babel/preset-env_include https://babeljs.io/docs/en/babel-preset-env#include
  8. @babel/preset-env_exclude https://babeljs.io/docs/en/babel-preset-env#exclude
  9. useBuiltIns https://babeljs.io/docs/en/babel-preset-env#usebuiltins
  10. @babel/preset-env_corejs https://babeljs.io/docs/en/babel-preset-env#corejs
  11. browserlistEnv https://babeljs.io/docs/en/babel-preset-env#browserslistenv
  12. @babel/core https://babeljs.io/docs/en/babel-core
  13. @babel/parser https://babeljs.io/docs/en/babel-parser
  14. @babel/generator https://babeljs.io/docs/en/babel-generator
  15. @babel/parser https://babeljs.io/docs/en/babel-parser
  16. @babel/generator https://babeljs.io/docs/en/babel-generator
  17. @babel/traverse https://babeljs.io/docs/en/babel-traverse
  18. @babel/types https://babeljs.io/docs/en/babel-types
  19. @babel/code-frame https://babeljs.io/docs/en/babel-code-frame

发布于 2022-10-24 18:17