babel插件入门-AST

目录

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

Babel简介

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

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

Babel运行原理

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

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

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

解析web

使用 <font color=Chocolate>babylon</font> 解析器对输入的源代码字符串进行解析并生成初始 AST(File.prototype.parse)express

利用 <font color=Chocolate>babel-traverse</font> 这个独立的包对 AST 进行<font color=Chocolate>遍历</font>,并解析出整个树的 <font color=Chocolate>path</font>,经过挂载的 metadataVisitor 读取对应的元信息,这一步叫 set AST 过程浏览器

转换babel

transform 过程:遍历 AST 树并应用各 <font color=Chocolate>transformers(plugin)</font> 生成变换后的 AST 树koa

babel 中最核心的是 <font color=Chocolate>babel-core</font>,它向外暴露出 babel.transform 接口。

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

生成

利用 <font color=Chocolate>babel-generator</font> 将 <font color=Chocolate>AST</font> 树输出为转码后的代码字符串

AST解析

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

节点(node)

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

ES2015箭头函数

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

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

解析代码
let babel = require('babel-core');//babel核心库
let types = require('babel-types');
let code = `codes.map(code=>{return code.toUpperCase()})`;//转换语句

let visitor = {
    ArrowFunctionExpression(path) {//定义须要转换的节点
        let params = path.node.params
        let blockStatement = path.node.body
        let func = types.functionExpression(null, params, blockStatement, false, false)
        path.replaceWith(func) //
    }
}

let arrayPlugin = { visitor }
let result = babel.transform(code, {
    plugins: [
        arrayPlugin
    ]
})
console.log(result.code)
注意: ArrowFunctionExpression() { ... } 是 ArrowFunctionExpression: { enter() { ... } } 的简写形式。

<font color=Chocolate>Path 是一个对象,它表示两个节点之间的链接。</font>

解析步骤
  • 定义须要转换的节点
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中新建文件夹 <font color=Chocolate>babel-plugin-extraxt</font>
注意:babel插件文件夹的定义方式是 babel-plugin-插件名
咱们能够在.babelrc的plugin中引入自定义插件 或者在webpack.config.js的loader options中加入自定义插件
  1. 在babel-plugin-extraxt新建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. 修改<font color=Chocolate>webpack.prod.config.js</font>中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>env>stage-0
  1. 运行引入了自定义插件的webpack.config.js

打包文件如今为21.4KiB,明显减少,自定义插件成功!~

插件文件目录

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

以为好玩就关注一下~欢迎你们收藏写评论~~~

相关文章
相关标签/搜索