一文读懂Babel原理及用途,及如何进行插件开发

5,135 阅读7分钟

作者:吴韩

前置阅读:Babel 插件手册

目录

  • Babel介绍
  • Babel用途
  • Babel怎么转义代码的?
  • Babel开发API
  • Babel的内置功能
  • Babel plugin的例子

Babel介绍

Babel 的前身是 6to5,6to5 是 2014 年 发布的,主要功能是 就是 ES6 转成 ES5。后改名babel。

Babel 指的是 通天塔,是巴比伦文明里面的 通天塔 当时地上的人们都说同一种语言,当人们离开东方之后,他们来到了示拿之地。在那里,人们想方设法烧砖好让他们能够造出一座城和一座高耸入云的塔来传播自己的名声,以免他们分散到世界各地。上帝来到人间后看到了这座城和这座塔,说一群只说一种语言的人以后便没有他们做不成的事了;于是上帝将他们的语言打乱,这样他们就不能听懂对方说什么了,还把他们分散到了世界各地,这座城市也停止了修建。这座城市就被称为“巴别城”。 -- 《创世记》

Babel用途

1. 转译 esnext、typescript 等到目标环境支持的 js

高级语言到到低级语言叫编译,高级语言到高级语言叫转译

2. 代码转换

taro就是用babel做语法转译的,后面还有一个例子。

3. 代码分析

模块分析、tree-shaking 、 代码压缩、linter等

Babel怎么转译代码的?

1.jpg

对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串。

Babel插件开发的API

Babel 包

babel转译的不同阶段使用了不同的api

1.parse 阶段使用@babel/parser,作用是把源码转成 AST


require('@babel/parser').parse(source, {

sourceType: 'module',

plugins: ['jsx', 'flow', 'classProperties', 'decorator', 'decorators-legacy'],

});

plugins: 指定jsx、typescript、flow 等插件来解析对应的语法

sourceType: module、script、unambiguous 3个取值,module 是解析 es module 语法

Babel的AST

Babel AST树查看

2.transform 阶段使用 @babel/traverse,可以遍历 AST,并调用 visitor 函数修改 AST,修改 AST 涉及到 AST 的判断、创建、修改等,这时候就需要 @babel/types 了,当需要批量创建 AST 的时候可以使用 @babel/template 来简化 AST 创建逻辑。


require('@babel/traverse').default

function traverse(ast,vistor)

//vistor可以是函数可以是对象,函数就是enter节点时调用的函数vistor名字为节点名称,对象就是enter和exit调用的函数。tips多个节点可以'FunctionDeclaration|VariableDeclaration'


require('@babel/traverse').default(ast, {

Program(path,state) {

}

})


// path api

{

"parent": {...},

"node": {...},

"hub": {...},

"contexts": [],

"data": {},

"shouldSkip": false,

"shouldStop": false,

"removed": false,

"state": null,

"opts": null,

"skipKeys": null,

"parentPath": null,

"context": null,

"container": null,

"listKey": null,

"inList": false,

"parentKey": null,

"key": null,

"scope": null,

"type": null,

"typeAnnotation": null,

get(key)

set(key, node)

inList()

getSibling(key)

getNextSibling()

getPrevSibling()

getAllPrevSiblings()

getAllNextSiblings()

isXxx(opts)

assertXxx(opts)

find(callback)

findParent(callback)

insertBefore(nodes)

insertAfter(nodes)

replaceWith(replacement)

replaceWithMultiple(nodes)

replaceWithSourceString(replacement)

remove()

traverse(visitor, state)

skip()

stop()

}

2.jpg

@babel/types 用于创建、判断 AST 节点,提供了 xxx、isXxx、assertXxx 的 api

@babel/template例子

3.generate 阶段会把 AST 打印为目标代码字符串,同时生成 sourcemap,需要 @babel/generate包,中途遇到错误想打印代码位置的时候,使用@babel/code-frame


const { code,map } = generator(ast, { sourceMaps: true });

//设置sourceMaps为true才有sourceMaps

@babel/code-frame例子

Babel的内置功能

1.首先我们看看babel需要做的功能

从高版本语法和 api 转换成低版本的语法并自动 polyfill 缺少的 api。

babel转换

a. TC39 是制定 javascript 语言标准的组织,每年都会公布加入到语言标准的特性,[es2015]262.ecma-international.org/6.0/)、es201… 等。

b. 提议

  • 阶段 0 : 只是一个想法,可能用 babel plugin 实现

  • 阶段 1: 值得继续的建议

  • 阶段 2: 建立 spec

  • 阶段 3: 完成 spec 并且在浏览器实现

  • 阶段 4: 会加入到下一年的 es20xx spec

c.react、flow、typescript

由于这些要转换的语法

babel7 内置的实现这些特性的插件分为 syntax、transform、proposal 3类,syntax声明要转译的语法,transform对AST的转换,proposal未加入语言标准的特性的 AST 转换,当插件太多很多共同的逻辑需要共享所以这部分在helper中实现。

Babel 包

正因为babel的插件太多,配置困难,用门面模式设计了presets让配置简单。

不同版本的语言标准支持: preset-es2015、preset-es2016 等,babel7 后用 preset-env 代替

未加入标准的语言特性的支持: 用于 stage0、stage1、stage2 的特性,babel7 后单独引入 proposal plugin

用于 react、jsx、flow 的支持:分别封装相应的插件为 preset-react、preset-jsx、preset-flow,直接使用对应 preset 即可

d.每个特性的实现用一个 babel 插件实现,当 babel 插件多了,自然会有一些共同的逻辑。这部分逻辑怎么共享呢?

babel 设计了插件之间共享逻辑的机制,就是 helper。

helper 分为两种:

  • 一种是注入到 AST 的运行时用的全局函数(如class)

  • 一种是操作 AST 的工具函数,比如变量提升这种通用逻辑(如常量定义)

babel runtime 里面放运行时加载的模块,会被打包工具打包到产物中。分为:regenerator、corejs、helper。

  • corejs 这就是新的 api 的 polyfill,分为 2 和 3 两个版本,3 才实现了实例方法的polyfill

  • regenerator 是 facebook 实现的 aync 的 runtime 库,babel 使用 regenerator-runtime来支持实现 async await 的支持。

  • helper 是 babel 做语法转换时用到的函数,比如 _typeof、_extends 等

2.babel的使用


"presets": [

[

"es2015",

{

"modules": false

}

],

"react",

"stage-2"

]

//babel6


"presets": [

[

"@babel/preset-env",

{

"targets": "> 0.25%, not dead",

"useBuiltIns": "usage",

"corejs": 3

}

]

],

"plugins": [[

"@babel/plugin-transform-runtime",

{

"corejs": 3

}

]]

//babel7

这里我们发现一个问题为什么有targets的目标浏览器呢?如果没有这个有可能你目标浏览器的语言本来就支持你却增加了babel增加了包的大小

babel/preset-env会导致使用到新特性的地方注入 helper 到 AST 中,@babel/plugin-transform-runtime的用处是把这些抽离出来模块引入。

babel 的 runtime 代码包括 2 部分:

一部分是 polyfill 用的,也就是 corejs 和 regenerator(实现 async await),这俩都是第三方的实现,babel 做了集成

另一部分就是转换代码的时候的 helper,比如实现继承、实现 class 的那些代码,这部分默认是注入到代码里的

如果使用了 @babel/plugin-transform-runtime,会做两个改动:

  1. 把 helper 部分的代码从注入的方式改为从 @babel/runtime 包引入的方式

  2. polyfill 部分的代码也不再是全局引入,会改为模块引入。

所以 transform runtime 的好处就有两个:

  1. 抽离重复注入的 helper 代码,减少产物体积

  2. polyfill 不污染全局

但是也有相对应的问题

babel 会按照如下顺序处理插件和 preset:

  1. 先应用 plugin,再应用 preset

  2. plugin 从前到后,preset 从后到前

这样就会造成,还没有到targets就执行了@babel/plugin-transform-runtime,不需要的babel包被增加了,这个babel8会解决这个问题。

最后说下上回文涛分享ts的时候关于用@babel/preset-typescript、ts-loader选择的一个问题

babel 是单文件编译的,每个文件处理方式都一样。ts-loader 在内部是调用了 TypeScript 的官方编译器 – tsc, tsc 是多文件一起编译的,因为在编译过程中会解析模块语法,去做类型推导和检查,这是两者流程上最大的区别。所以 tsc 不可避免的在多文件的时候会卡,而 babel 就和规模没有关系。 babel 不会解析 ts 类型,所以 namespace合并、const enum 这种需要解析类型内容的就不支持。整体来说 babel 编译 ts 代码会快很多。而且 babel 生成的代码也能根据 targets 按需编译以及引入 polyfill,而 tsc 不支持。需要类型检查单独跑 tsc --noEmit 就行。


// tsconfig.json

{

...

"compilerOptions":{

"noEmit":true // 不输出文件,只做类型检查

}

}

babel 编译 typescript 是更好的选择

Babel plugin的例子

先看下需求

!Wslq5ub7vhIy2RT11.png

1.小程序的页面预览页面要在b端再写一遍

2.ui后面改动也要俩边同时改动(有可能漏改忘改)

taro的c端代码要应用到web端

利用babel编译到b端解决代码公用问题,利用postcss解决样式问题

第一个版本

无mobx只是组件传值 修改2倍距离的问题

第二个版本

有mobx 优化css vh等

在了解下taro转换的思路

4.png

5.jpg

taro组件库的设计

taro的目标实现 React、Vue 等框架都可以使用的组件库。

Web Components

taro跨端组件库设计

参考链接