babel插件入门-AST

目录

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

Babel简介

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

image

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

Babel运行原理

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

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

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

解析

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

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

转换

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

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

image
image
解析代码
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文档链接

image
  • 在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);
image

所以我们这次的目的是将

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
image

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

插件文件目录

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容