oakland / tecblog

My tech blogs
4 stars 0 forks source link

Babel✨ #201

Open oakland opened 4 years ago

oakland commented 4 years ago

最近打算写一篇和 babel 相关的文章,好像也没有什么说的,主要就是用法,如何写一个插件,以及原理。如何写一个插件算是中级水平,其实也没有什么,就是一些深入的 api。最深入的内容就是原理部分,就涉及到 AST 和 编译器 相关的内容。作为入门的话,其实先写到原理的 AST 部分就够了。再往深入其实已经不是 babel 了,就是编译器和解释器。可以考虑和 sicp 结合者一起写。如何写插件其实也比较简单,主要是思路,用法很简单。

@babel/core VS babel-core

最近做 TIP 的时候打算用一个新的 babel plugin:babel-plugin-proposal-optional-chaining,按照 npmjs 中的安装方式安装完发现编译时候出错,无法找到 plugin。后来看了下,发现 babel 从 version7.x 之后就采用另外一种安装方式了。原来是 babel-core,后来是 @babel/core 的方式。然后 google 搜了下“babel-core vs @babel/core”,看到了一些相关的内容。 what-is-the-difference-between-babel-core-and-babel-core,这里面引用一段话

Since Babel 7 the Babel team switched to scoped packages, so you now have to use @babel/core instead of babel-core. But in essence, @babel/core is just a newer version of babel-core. This is done to make a better distinction which packages are official and which are third-party.

v7-migration#scoped-packages,官网给出的内容

how-to-upgrade-to-babel-7 给出了一种比较傻瓜的升级方式:

npx babel-upgrade --write

presets VS plugins

difference-between-plugins-and-presets-in-babelrc

Presets are just a collection of plugins. You can include plugins individually in the plugins array, or collection of plugins in the presets array. If a plugin is part of a collection (preset), you don't have to include it individually in plugins. Babel has lots of official and third party plugins. Presets are collections of plugins or as they say: Presets are sharable .babelrc configs or simply an array of babel plugins.

其实 presets 就是 plugins 的集合,就是已经帮你组装好了,你直接用就行了,不需要你自己组合 plugins 了。

plugins 和 presets 的顺序

plugin-ordering

Ordering matters for each visitor in the plugin.

This means if two transforms both visit the "Program" node, the transforms will run in either plugin or preset order.

Plugins run before Presets. Plugin ordering is first to last. Preset ordering is reversed (last to first).

先执行插件,然后执行 presets,插件的顺序是从左到右,而 presets 的执行顺序是从右向左。

这个部分的例子也很好,也说了为什么 presets 要 reverse order。是

This is mostly for ensuring backwards compatibility, since most users list "es2015" before "stage-0". For more information, see notes on potential traversal API changes.

AST

AST 是有 Node 组成的,Node 其实就是拥有 type 属性和其他必要属性,能描述清楚当前节点的 Object。其中 type 属性是必须的。 每个 Node 都有 type 属性,用来表示这个 node 的类型,同时还拥有其他属性,用来描述这个 Node,比如 function 除了 type 之外,就还拥有 id, params, body 三个属性用来描述 function。

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}

同理,二元操作符通过如下属性就可以进行完整的描述:

{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

对于简单的字符串和数字,则通过 type 和 value 两个字段就可以表达完整。例如 'hello'1 用 Node 表示分别是:

{
  type: "StringLiteral",
  value: 'hello'
}
{
  type: "NumericLiteral",
  value: 1
}

babel-types

上面说了 AST 会有很多 Node,每个 Node 都是一个 type,那每个 type 能干什么呢?babel-types 发挥作用了。babel-types 可以判断一个 Node 是否是某个 type,也可以构建一个 Node,例如:

const t = require('@babel/types');
t.isIdentifier(path.node); // 用来判断是否是 Identifier 类型的 node
t.identifier('hello'); 会生成一个 hello StringLiteral 的 node

具体每个 type 怎么生成,以及怎么判断,就去 官网 看文档。

The method name for a builder is simply the name of the node type you want to build except with the first letter lowercased. For example if you wanted to build a MemberExpression you would use t.memberExpression(...).

除了判断一个 Node 的类型之外,可能还需要判断这个 Node 的其他属性,例如可以通过下面的方式判断一个 二元表达式的左节点是否是 Identifier 且命名为 n

BinaryExpression(path) {
  if (t.isIdentifier(path.node.left, { name: "n" })) {
    // ...
  }
}

@babel/parser

The Babel parser generates AST according to Babel AST format. It is based on ESTree spec with the following deviations: 这个包的作用就是把代码解析成 AST ,具体看 https://babeljs.io/docs/en/next/babel-parser

@babel/core

用来转换代码? https://babeljs.io/docs/en/next/options

@babel/generator

astexplorer

可以在本地预览 ast,ast github 仓库地址 https://github.com/fkling/astexplorer,克隆下来,本地启动服务做实验

useBuiltIns and @babel/polyfill

https://babeljs.io/docs/en/babel-polyfill#size

The polyfill is provided as a convenience but you should use it with @babel/preset-env and the useBuiltIns option so that it doesn't include the whole polyfill which isn't always needed. Otherwise, we would recommend you import the individual polyfills manually. 可见通常情况下用 @babel/preset-env 的 useBuiltIns 就行了,没有必要引入 @babel/polyfill。

plugin 中的 path

写 plugin 的时候,肯定会用到 path,那么 path 到底是个什么东西呢? plugin-handbook中有一段描述。

An AST generally has many Nodes, but how do Nodes relate to one another? We could have one giant mutable object that you manipulate and have full access to, or we can simplify this with Paths.

A Path is an object representation of the link between two nodes.

就是说 path 就是一个很大的对象,可以直接修改,并且可以获取两个 node 之间的关联关系。

In a sense, paths are a reactive representation of a node's position in the tree and all sorts of information about the node. Whenever you call a method that modifies the tree, this information is updated. Babel manages all of this for you to make working with nodes easy and as stateless as possible.

path 其实就是包含了当前节点的所有信息的对象,同时还拥有很多方法可以更新当前节点。path.node 就是当前节点,path.parent 就是当前节点的父级节点。这两个属性一定会有,通常感觉用到的最多的也是这两个属性。用到的比较多的方法就是 path.traverse,示例也可以看上面 plugin-handbook 中的示例。

babel-core-apidoc

https://npmdoc.github.io/node-npmdoc-babel-core/build/apidoc.html 这里面有很多相关的 api 说明,而且都是以函数的方式表达出来的。 比如这里面就有我一直不太理解的 scope.maybeGenerateMemoised(node),打开之后跳转到这里,里面写了这个函数到底是干什么的,好像是判断静态节点还是动态节点的。

plugin-proposal vs plugin-syntax

transform-plugin-vs-syntax-plugin-in-babel

Syntax plugins are necessary for step 1: Proposals such as class properties introduce a new syntax, which cannot be parsed by current JavaScript parsers. Syntax plugins extend the parser so it understands the new syntax.

Transform plugins are necessary for step 2: Now that the source was parsed, we need to convert the AST nodes of the new feature into something that is valid in current JavaScript.

Transform plugins will enable the corresponding syntax plugin so you don't have to specify both.

也就是说 plugin-syntax 仅仅让 babel 可以解析这个新的语法,而 plugin-proposal 是可以让这种语法转化成 es5。

深入浅出 Babel 上篇:架构和原理 + 实战中也说到了关于 syntax plugin 的问题:

语法插件(@babel/plugin-syntax-): 上面说了 @babel/parser 已经支持了很多 JavaScript 语法特性,Parser也不支持扩展. 因此plugin-syntax-实际上只是用于开启或者配置Parser的某个功能特性。 一般用户不需要关心这个,Transform 插件里面已经包含了相关的plugin-syntax-*插件了。用户也可以通过parserOpts配置项来直接配置 Parser

作者:荒山 链接:https://juejin.im/post/6844903956905197576 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

optional-chaning 详解

是时候学习/推广一波可选链(Optional chaining)和空值合并(Nullish coalescing )了

注意点 babel 会在每次属性取值时将属性值进行缓存而不是像平时代码中常写的直接 a && a.b && a.b.c,这是为了保证和原生实现的一致性,保证每个属性取值只会取一次,避免在一些 getter 属性获取时造成取值次数不一致差异性。 babel 在判断值是否为空时并没有直接使用 == null 而是使用了比较繁琐的 === null || === void 0,这个主要是为了兼容 document.all,关于 document.all 写在后面。

上面这篇文章里的这两点解答了我心中的一些疑惑。

精读《Optional chaining》 上面这篇文章也很好,非常详细的解释了 optional-chaining 这个提案,看起来一个很简单的功能其实背后有很多复杂的逻辑和设计思想。

一些可以参考的简单 babel 插件

babel原理及插件开发

https://juejin.im/post/6844904038438273032

babel-plugin-playground 可以在线编写 babel 插件实现转换

https://juejin.im/post/6844903582613897223 Babel 在提升前端效率的实践 写个 Babel 插件丰富你的 console 内容

understanding-asts-building-babel-plugin 这篇文章提供了一个很好的写插件的方式,就是先把变化前后的 ast 用图的方式表达出来,然后就可以很方便的分析了。

https://github.com/babel/generator-babel-plugin,没用过,应该是写复杂插件的时候再用吧

非常好的参考文章

深入浅出 Babel 上篇:架构和原理 + 实战,这篇文章除了 babel 之外把很多扩展的东西也说了。

看 @babel/parser 源码看到了一些奇怪的数字表达

const SCOPE_OTHER = 0b0000000000,
       SCOPE_PROGRAM = 0b0000000001,
       SCOPE_FUNCTION = 0b0000000010,

在 mdn 上找到了答案,https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Numbers_and_dates,以 0b 开头的表示 binary number。

如何看 tokens

通过 astexploer 可以看到解析后的 tokens,如下图 image

oakland commented 3 years ago

loose mode VS normal mode

https://2ality.com/2015/12/babel6-loose-mode.html

oakland commented 3 years ago

https://babeljs.io/docs/en/babel-preset-env#modules 看 webpack tree shaking 的时候看到这个配置方式 这个其实就是配置 babel 以什么模块方式去转换 es module 的模块

oakland commented 7 months ago

generate 生产出的代码是 unicode 的格式,需要添加一个配置项:

"generatorOpts": {
  "jsescOption": {
    "minimal": true
  }
}

path.skip() 很重要,参考如下:

https://stackoverflow.com/questions/37539432/babel-maximum-call-stack-size-exceeded-while-using-path-replacewith