babel插件入门-AST(抽象语法树)

16,703 阅读6分钟

目录

  • Babel简介
  • Babel运行原理
  • AST解析
  • AST转换
  • 写一个Babel插件

Babel简介

Babel 是一个 JavaScript 编译器,它能将es2015,react等低端浏览器无法识别的语言,进行编译。

上图的左边代码中有箭头函数,Babel将进行了源码转换,下面我们来看Babel的运行原理。

Babel运行原理

Babel 的三个主要处理步骤分别是:

解析(parse),转换(transform),生成(generate)。

Babel三个步骤

其过程分解用语言描述的话,就是下面这样:

解析

使用 babylon 解析器对输入的源代码字符串进行解析并生成初始 AST(File.prototype.parse)

利用 babel-traverse 这个独立的包对 AST 进行遍历,并解析出整个树的 path,通过挂载的 metadataVisitor 读取对应的元信息,这一步叫 set AST 过程

转换

transform 过程:遍历 AST 树并应用各 transformers(plugin) 生成变换后的 AST 树

babel 中最核心的是 babel-core,它向外暴露出 babel.transform 接口。

let result = babel.transform(code, {
    plugins: [
        arrayPlugin
    ]
})

生成

利用 babel-generator 将 AST 树输出为转码后的代码字符串

AST解析

AST解析会把拿到的语法,进行树形遍历,对语法的每个节点进行响应的变化和改造再生产新的代码字符串

节点(node)

AST将开头提到的箭头函数转根据节点换为节点树

ES2015箭头函数

codes.map(code=>{
	return code.toUpperCase()
})

map+箭头函数+返回其大写字母 看上去是很简单的函数,对应的抽象语法树(AST)通常情况下也比较复杂,尤其是一些复杂的程序。我们不要试图自己去分析抽象语法树(AST),可以通过astexplorer.net可以在线看到不同的parser解析js代码后得到的AST,网站帮助我们来完成转换,它允许我们在左边输入 JavaScript代码,右侧会出可浏览的抽象语法树(AST),我们可以通过这个工具辅助理解和试验一些代码。

JavaScript AST visualizer 可以在线可视化的看到AST。

AST树形遍历转换后的结构

{
    type:"ExpressionStatement",
    expression:{
        type:"CallExpression"
        callee:{
            type:"MemberExpression",
            computed:false
            object:{
                type:"Identifier",
                name:"codes"
            }
            property:{
                type:"Identifier",
                name:"map"
            }
            range:[]
        }
        arguments:{
            {
                type:"ArrowFunctionExpression",
                id:null,
                params:{
                    type:"Identifier",
                    name:"code",
                    range:[]
                }
                body:{
                    type:"BlockStatement"
                    body:{
                        type:"ReturnStatement",
                        argument:{
                            type:"CallExpression",
                            callee:{
                                type:"MemberExpression"
                                computed:false
                                object:{
                                    type:"Identifier"
                                    name:"code"
                                    range:[]
                                }
                                property:{
                                    type:"Identifier"
                                    name:"toUpperCase"
                                }
                                range:[]
                            }
                            range:[]
                        }
                    }
                    range:[]
                }
                generator:false
                expression:false
                async:false
                range:[]
            }
        }
    }
}

我们从 ExpressionStatement开始往树形结构里面走,看到它的内部属性有callee,type,arguments,所以我们再依次访问每一个属性及它们的子节点。

于是就有了如下的顺序

进入  ExpressionStatement
进入  CallExpression
进入  MemberExpression
进入  Identifier
离开  Identifier
进入  Identifier
离开  Identifier
离开  MemberExpression
进入  ArrowFunctionExpression
进入  Identifier
离开  Identifier
进入  BlockStatement
进入  ReturnStatement
进入  CallExpression
进入  MemberExpression
进入  Identifier
离开  Identifier
进入  Identifier
离开  Identifier
离开  MemberExpression
离开  CallExpression
离开  ReturnStatement
离开  BlockStatement
离开  ArrowFunctionExpression
离开  CallExpression
离开  ExpressionStatement
离开  Program

Babel 的转换步骤全都是这样的遍历过程。(有点像koa的洋葱模型??)

AST转换

解析好树结构后,我们手动对箭头函数进行转换。

对比两张图,发现不一样的地方就是两个函数的arguments.type

解析代码
//babel核心库,用来实现核心的转换引擎
let babel = require('babel-core');
//可以实现类型判断,生成AST节点
let types = require('babel-types');
let code = `codes.map(code=>{return code.toUpperCase()})`;//转换语句
//visitor可以对特定节点进行处理
let visitor = {
    ArrowFunctionExpression(path) {//定义需要转换的节点,这里拦截箭头函数
        let params = path.node.params
        let blockStatement = path.node.body
        //使用babel-types的functionExpression方法生成新节点
        let func = types.functionExpression(null, params, blockStatement, false, false)
        //替换节点
        path.replaceWith(func) //
    }
}
//将code转成ast
let result = babel.transform(code, {
    plugins: [
        { visitor }
    ]
})
console.log(result.code)

注意: ArrowFunctionExpression() { ... } 是 ArrowFunctionExpression: { enter() { ... } } 的简写形式。

Path 是一个对象,它表示两个节点之间的连接。

解析步骤
  • 定义需要转换的节点
    ArrowFunctionExpression(path) {
        ......
    }
  • 创建用来替换的节点
types.functionExpression(null, params, blockStatement, false, false)

babel-types文档链接

  • 在node节点上找到需要的参数
  • replaceWith(替换)

写一个Babel插件

从一个接收了 babel 对象作为参数的 function 开始。

export default function(babel) {
  // plugin contents
}

接着返回一个对象,其 visitor 属性是这个插件的主要节点访问者。

export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
    }
  };
};

我们日常引入依赖的时候,会将整个包引入,导致打包后的代码太冗余,加入了许多不需要的模块,比如index.js三行代码,打包后的文件大小就达到了483 KiB,

index.js

import { flatten, join } from "lodash";
let arr = [1, [2, 3], [4, [5]]];
let result = _.flatten(arr);

所以我们这次的目的是将

import { flatten, join } from "lodash";

转换为从而只引入两个lodash模块,减少打包体积

import flatten from "lodash/flatten";
import join from "lodash/join";

实现步骤如下:

  1. 在项目下的node_module中新建文件夹 babel-plugin-extract

注意:babel插件文件夹的定义方式是 babel-plugin-插件名 我们可以在.babelrc的plugin中引入自定义插件 或者在webpack.config.js的loader options中加入自定义插件

  1. 在babel-plugin-extract新建index.js
module.exports = function ({types:t}) {
    return {
        // 对import转码
        visitor:{
            ImportDeclaration(path, _ref = { opts: {} }) {
                const specifiers = path.node.specifiers;
                const source = path.node.source;
                // 只有libraryName满足才会转码
                if (_ref.opts.library == source.value && (!t.isImportDefaultSpecifier(specifiers[0]))) { //_ref.opts是传进来的参数
                    var declarations = specifiers.map((specifier) => {      //遍历  uniq extend flatten cloneDeep
                        return t.ImportDeclaration(                         //创建importImportDeclaration节点
                            [t.importDefaultSpecifier(specifier.local)],
                            t.StringLiteral(`${source.value}/${specifier.local.name}`)
                        )
                    })
                    path.replaceWithMultiple(declarations)
                }
            }
        }
    };
}
  1. 修改webpack.prod.config.js中babel-loader的配置项,在plugins中添加自定义的插件名
rules: [{
    test: /\.js$/,
    loader: 'babel-loader',
    options: {
        presets: ["env",'stage-0'],
        plugins: [
            ["extract", { "library":"lodash"}],
            ["transform-runtime", {}]
        ]
    }
}]

注意:plugins 的插件使用顺序是顺序的,而 preset 则是逆序的。所以上面的执行方式是extract>transform-runtime>stage-0>env

  1. 运行引入了自定义插件的webpack.config.js

打包文件现在为21.4KiB,明显减小,自定义插件成功!~

插件文件目录

YUAN-PLUGINS
|
| - node_modules
|   |
|   | - babel-plugins-extract
|           |
|           index.js
|   
| - src
|   | - index.js
|
| - webpack.config.js

觉得好玩就关注一下~欢迎大家收藏写评论~~~

招聘贴

字节跳动招人啦!

职位描述:前端开发(高级)ToB方向—视频云(Base: 上海、北京)

1、负责音视频点播/直播/实时通信等多媒体服务产品化以及业务云平台建设;

2、负责多媒体质量体系、运维体系建设及系统开发工作;

3、擅长抽象设计、工程化思维,专注交互、打造极致用户体验。

职位要求

1、计算机、通信和电子信息科学等相关专业优先;

2、熟练掌握各种前端技术,包括 HTML/CSS/JavaScript/Node.js 等;

3、深入了解 JavaScript 语言,使用过 React 或 Vue.js 等主流开发框架;

4、熟悉 Node.js,了解 Express/KOA 等框架,有大型服务端程序开发经验者优先;

5、对用户体验、交互操作及用户需求分析等有一定了解,有产品或界面设计经验者优先;

6、有自己的技术产品、开源作品或活跃的开源社区贡献者优先。

职位亮点

视频云团队依托抖音、西瓜视频等产品的音视频技术积累和基础资源,为客户提供极致的一站式音视频多媒体服务,包括音视频点播、直播、实时通信、图片处理等。对内作为视频技术中台,服务内部业务;对外打造产品化的音视频多媒体服务解决方案,服务企业级用户。

团队具备规范的项目迭代流程、完善的项目角色配置;技术氛围浓厚,拥抱开源社区,定期分享,让大家能够伴随业务快速成长,用技术改变世界!

投递方式

可直接发送简历至:yuanyuan.wallace@bytedance.com

也可以扫描内推二维码在线投递,期待你的加入!~