babel插件笔记 - 基础

平时喜欢看一些文章,从中可以学到一些零零散散的东西,但是不够全面与详细,而且实战也不够,于是决定开始系统学babel,并记录下来,然后做些实战练习,。

关于babel,好像是之前学习webpack时接触到的,我还叫babel [ˈbʌbl],然后百度搜索的正确读法是babel [ˈbeɪbl], 之前大概的了解是es6 to es5,是有这作用,但不止,现在就来一探究竟。

babel的用途

转译

这个是最常用的功能,用来把代码中的 esnext 的新的语法、typescriptflow 的语法转成基于目标环境支持的语法的实现。并且还可以把目标环境不支持的api进行 polyfill

代码转换

babel是一个转译器,暴露了很多api,用这些api可以完成代码到ASTparseAST的转换,以及目标代码的生成。

babel的编译流程

babelsource to source 的转换,整体编译流程分为三步:

  • parse:通过 parser 把源码转成抽象语法树(AST)
  • transform:遍历 AST,调用各种transform插件对 AST进行增删改
  • generate:把转换后的 AST打印成目标代码,并生成 sourcemap

为了让计算机理解代码需要先对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串。

parse

parse 阶段的目的是把源码字符串转换成机器能够理解的 AST,这个过程分为词法分析、语法分析。

比如 const name = 'miku39'; 这样一段源码,我们要先把它分成一个个不能细分的单词(token),也就是 const, name, =, 'miku39',这个过程是词法分析,按照单词的构成规则来拆分字符串成单词

之后要把 token 进行递归的组装,生成 AST,这个过程是语法分析,按照不同的语法结构,来把一组单词组合成对象。

transform

transform阶段是对 parse 生成的 AST的处理,会进行 AST 的遍历,遍历的过程中处理到不同的 AST节点会调用注册的相应的 visitor函数,visitor 函数里可以对AST节点进行增删改,返回新的 AST(可以指定是否继续遍历新生成的 AST)。这样遍历完一遍 AST 之后就完成了对代码的修改。

generate

generate 阶段会把AST 打印成目标代码字符串,并且会生成 sourcemap。不同的 AST 对应的不同结构的字符串。比如 IfStatement 就可以打印成 if(test) {} 格式的代码。这样从AST根节点进行递归打印,就可以生成目标代码的字符串。sourcemap记录了源码到目标代码的转换关系,通过它我们可以找到目标代码中每一个节点对应的源码位置。

parse阶段

parse 阶段有@babel/parser@babel/parser是babel的一个api,功能是把源码转成 AST, babel parser 叫 babylon,是基于 acorn 实现的,扩展了很多语法,可以支持 es next(现在支持到 es2020)、jsx、flow、typescript 等语法的解析,其中 jsx、flow、typescript 这些非标准的语法的解析需要指定语法插件。

它提供了有两个 api:parseparseExpression。两者都是把源码转成 AST,不过 parse 返回的 AST 根节点是 File(整个 AST),parseExpression 返回的 AST 根节点是是 Expression(表达式的 AST),粒度不同。

function parse(input: string, options?: ParserOptions): File
function parseExpression(input: string, options?: ParserOptions): Expression
复制代码

详细的 options 可以查看文档。其实主要分为两类,一是 parse 的内容是什么,二是以什么方式去 parse

parse 的内容是什么:

  • plugins: 指定jsx、typescript、flow 等插件来解析对应的语法
  • allowXxx: 指定一些语法是否允许,比如函数外的 await、没声明的 export等
  • sourceType: 指定是否支持解析模块语法,有 module、script、unambiguous 3个取值,module 是解析 es module 语法,script 则不解析 es module语法,当作脚本执行,unambiguous` 则是根据内容是否有 import 和 export 来确定是否解析 es module 语法。

以什么方式 parse

  • strictMode 是否是严格模式
  • startLine 从源码哪一行开始 parse
  • errorRecovery 出错时是否记录错误并继续往下 parse
  • token parse 的时候是否保留 token 信息
  • range 是否在 ast 节点中添加 ranges 属性

其实最常用的 option 就是 plugins、sourceType 这两个,比如要 parse tsx 模块,那么就可以这样来写

require("@babel/parser").parse("code", {
  sourceType: "module",
  plugins: [
    "jsx",
    "typescript"
  ]
});
复制代码

transform阶段

transform阶段是将 parse 出的 AST 由 @babel/traverse 来遍历和修改,babel traverse 包提供了 traverse 方法:

function traverse(parent, opts)
复制代码

常用的就前面两个参数,parent 指定要遍历的 AST 节点,opts 指定 visitor 函数。babel 会在遍历 parent 对应的 AST 时调用相应的 visitor 函数。

遍历过程

visitor 对象的 value 是对象或者函数:

  • 如果 value 为函数,那么就相当于是 enter 时调用的函数。
  • 如果 value 为对象,则可以明确指定 enter 或者 exit 时的处理函数。

函数会接收两个参数 path 和 state。

visitor: {
    Identifier (path, state) {},
    StringLiteral: {
        enter (path, state) {},
        exit (path, state) {}
    }
}
复制代码

enter 时调用是在遍历当前节点的子节点前调用,exit 时调用是遍历完当前节点的子节点后调用。

可以为单个节点的类型,也可以是多个节点类型通过 | 连接,还可以通过别名指定一系列节点类型。

// 进入 FunctionDeclaration 节点时调用
traverse(ast, {
  FunctionDeclaration: {
      enter(path, state) {}
  }
})

// 默认是进入节点时调用,和上面等价
traverse(ast, {
  FunctionDeclaration(path, state) {}
})

// 进入 FunctionDeclaration 和 VariableDeclaration 节点时调用
traverse(ast, {
  'FunctionDeclaration|VariableDeclaration'(path, state) {}
})

// 通过别名指定离开各种 Declaration 节点时调用
traverse(ast, {
  Declaration: {
      exit(path, state) {}
  }
})
复制代码

具体的别名有哪些在babel-types 的类型定义可以查。

path

path 是遍历过程中的路径,会保留上下文信息,有很多属性和方法,比如:

  • path.node 指向当前 AST 节点
  • path.get、path.set 获取和设置当前节点属性的 path
  • path.parent 指向父级 AST 节点
  • path.getSibling、path.getNextSibling、path.getPrevSibling 获取兄弟节点
  • path.find 从当前节点向上查找节点

这些属性和方法是获取当前节点以及它的关联节点的

  • path.scope 获取当前节点的作用域信息

这个属性可以获取作用域的信息

  • path.isXxx 判断当前节点是不是 xx 类型
  • path.assertXxx 判断当前节点是不是 xx 类型,不是则抛出异常

isXxx、assertXxx 系列方法可以用于判断 AST 类型

  • path.insertBefore、path.insertAfter 插入节点
  • path.replaceWith、path.replaceWithMultiple、replaceWithSourceString 替换节点
  • path.remove 删除节点

这些方法可以对 AST 进行增删改

  • path.skip 跳过当前节点的子节点的遍历
  • path.stop 结束后续遍历

这俩方法可以跳过一些遍历,当然,path 的 api 不是只有这些

state

第二个参数 state 则是遍历过程中在不同节点之间传递数据的机制,插件会通过 state 传递 options 和 file 信息,我们也可以通过 state 存储一些遍历过程中的共享数据。

generate 阶段

AST 转换完之后就要打印成目标代码字符串,通过 @babel/generator 包的 generate api

function (ast: Object, opts: Object, code: string): {code, map} 
复制代码

第一个参数是要打印的 AST

第二个参数是 options,指定打印的一些细节,比如通过 comments 指定是否包含注释,通过 minified 指定是否包含空白字符

第三个参数当多个文件合并打印的时候需要用到

options 中常用的是 sourceMaps,开启了这个选项才会生成 sourcemap

const { code, map } = generate(ast, { sourceMaps: true })
复制代码

总结

babel是一个 js 转译器,用于 es next、typescript等代码的转换,同时还暴露出了 api 让开发者可以进行特定用途的转换,babel 编译流程的三个步骤 parse、transform、generate,分别用到babel的@babel/parser@babel/traverse@babel/generator api

  • @babel/parser 对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法
  • @babel/traverse 通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据
  • @babel/generator 打印 AST 成目标代码字符串,支持 comments、minified、sourceMaps 等选项。

当然bebel的api不止这些, 还得一点一点去学习扩展

猜你喜欢

转载自juejin.im/post/7035210940727443470