学习写一个babel插件

前言

babel做为现代前端项目的标配,工做中常常会用到。可是,不多人会去研究它的底层实现和设计。这篇文章是平常工做中实践总结,将会由浅入深地和你们一块儿学习下babel的一些基础知识,以及编写属于本身的babel插件,并在项目中使用。php

AST简介

抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构前端

AST生成过程

  1. 分词 / 词法分析: 将一个语句中的关键词进行提取, 例如let a = 3; 分词提取以后获得let, a, =, 3node

  2. 解析 / 语法分析: 在对上面已经被拆分提取过的关键词进行分析以后创建一课语法树(AST)git

  3. 底层代码生成: 获得语法树以后执行引擎(例如 chrome 的 v8引擎)会对这颗树进行必定的优化分析, 而后生成更底层的代码或者机器指令交由机器执行github

babel工具简介

Babel is a compiler for writing next generation JavaScriptchrome

babel三件套

  • 解析:@babel/parse
    • 词法解析
    • 语法解析
  • 遍历:@babel/traverse
  • 生成:@babel/generator
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from '@babel/generator';

//源代码
const code = `function square(n) {
  return n * n;
}`;

//解析为ast结构
const ast = parser.parse(code, {
  // parse in strict mode and allow module declarations
  sourceType: "module",

  plugins: [
    // enable jsx and flow syntax
    "jsx",
    "flow"
  ]
});

//进行遍历,修改节点
//第二个参数是一个访问者对象,定义遍历时的具体转换规则,囊括本文95%的重点
traverse(ast, {
  enter(path) {
    if (path.isIdentifier({ name: "n" })) {
      path.node.name = "x";
    }
  }
});
//将修改后的ast结构,生成从新的代码
const output = generate(ast, { /* options */ }, code);
复制代码

整个流程最核心的就是traverse部分,接下来咱们回顾下traverse的核心知识npm

如何去编写一个babel插件

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,一般也叫作“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,而后返回给你新生成的代码编程

遍历

Babel 或是其余编译器中最复杂的过程 同时也是插件将要介入工做的部分。小程序

首先熟悉下常见的js结构对应的ast节点类型数组

//functionDeclaration
function square(n) {
  return n * n;
}

let a = {
	test(){},  //ObjectMethod
  	setOnly: function(){}   //ObjectProperty
}

let b = 3;  //VariableDeclaration
b = 5;   //AssignmentExpression
复制代码

访问者(visitor)

访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法

const MyVisitor = {
  //完整写法
  functionDeclaration: {
    enter(path) {
      console.log("Entered!");
    },
    exit(path) {
      console.log("Exited!");
    }
  },
  //经常使用写法
  functionDeclaration(path){
  },
  
  ObjectMethod | ObjectProperty: {
    enter(path) {
      console.log("Entered!");
    },
    exit(path) {
      console.log("Exited!");
    }
  }

  ...
};
复制代码

Path:

visitor对象每次访问节点方法时,都会传入一个path参数。Path 是表示两个节点之间链接的对象。这个对象不只包含了当前节点的信息,也有当前节点的父节点的信息,同时也包含了添加、更新、移动和删除节点有关的其余不少方法

+ 属性      
  - node   当前节点
  - parent  父节点
  - parentPath 父path
  - scope   做用域
  - context  上下文
  - ...
+ 方法
  - findParent  向父节点搜寻节点
  - getSibling 获取兄弟节点
  - replaceWith  用AST节点替换该节点
  - replaceWithSourceString  用代码字符串替换该节点
  - replaceWithMultiple 用多个AST节点替换该节点
  - insertBefore  在节点前插入节点
  - insertAfter 在节点后插入节点
  - remove   删除节点

复制代码

AST实战讲解

1. 打开在线AST工具

高亮的是对应的代码段,左边是一个对象的属性,右边对应ast中的节点信息。

注意:js中不一样的数据类型,对应的ast节点信息也不竟相同。以图中为例,externalClasses对象的节点信息中类型(type)是ObjectProperty,包含key ,value等关键属性(其余类型节点可能就没有)

2. 打开transform开关,选择转换引擎,发现了新大陆

图片
这里咱们选择babel和配套的babylon7,能够根据实际须要本身选择,这只是推荐。

注意选择最新的babel7版本,否则下面例子中的类型会匹配不上,

3. 如今的界面结构展现以下图,接下来就开始进行转换逻辑的代码编写

假设咱们的目标是要把properties属性中key为‘current’的属性改成myCurrent。let's go!

原始代码:

/*eslint-disable*/
/*globals Page, getApp, App, wx,Component,getCurrentPages*/
Component({
  externalClasses: ['u-class'],

  relations: {
    '../tab/index': {
      type: 'child',
      linked() {
        this.changeCurrent();
      },
      linkChanged() {
        this.changeCurrent();
      },
      unlinked() {
        this.changeCurrent();
      }
    }
  },

  properties: {
    current: {
      type: String,
      value: '',
      observer: 'changeCurrent'
    }
  },

  methods: {
    changeCurrent(val = this.data.current) {
      let items = this.getRelationNodes('../tab/index');
      const len = items.length;

      if (len > 0) {
        items.forEach(item => {
          item.changeScroll(this.data.scroll);
          item.changeCurrent(item.data.key === val);
          item.changeCurrentColor(this.data.color);
        });
      }
    },
    emitEvent(key) {
      this.triggerEvent('change', { key });
    }
  }
});

复制代码

首先在原始代码中选中'current',查看右边ast的节点结构,如图:

这是一个对象属性(ObjectProperty),关键节点信息为key和value,key自己也是一个ast节点,类型为Identifier(准确的应该是StringIdentifer,经常使用的还有NumberIdentifer等),'curent'是里面的name属性。因此咱们的第一步就是找到改节点,而后修改它。

查找

export default function (babel) {
  const { types: t } = babel;
  
  return {
    name: "ast-transform", // not required
    visitor: {
      Identifier(path) {
        //path.node.name = path.node.name.split('').reverse().join('');
      },
       ObjectProperty(path) {
         if (path.node.key.type === 'StringIdentifier' && 
             path.node.key.name === 'current') {
         	console.log(path,'StringIdentifier')
         }
  	   }
    }
  };
}

复制代码

这里须要用到@babel/typesbabeljs.io/docs/en/bab…来辅助咱们进行类型判断,开发中会很是依赖这个字典进行查找

在控制台会看见,path下面的节点信息不少,关键字段为node和parentPath,node记录了该节点下数据信息,例如以前提到过的key和value。parentPath表明父级节点,此例中表示ObjectExpression中properties节点信息,有时咱们须要修改父节点的数据,例如常见的节点移除操做。接下来咱们修改该节点信息。

修改

@babel/types中找到该ObjectProperty的节点信息以下,咱们须要须要构造一个新的同类型节点(ObjectProperty)来替换它。

能够看到关键信息是key和value,其余使用默认就好。value里面的信息咱们能够照搬,从原有的path里面获取,咱们更改的只是key里面的标识符'current'。由于key自己也是一个ast节点,因此咱们还须要查看字典,看看生成Identifier节点须要什么参数,步骤同样。修改代码以下:

ObjectProperty(path) {
         console.log(path,'ObjectProperty--')
         if (path.node.key.type === 'Identifier' && 
             path.node.key.name === 'current') {
            //替换节点
           path.replaceWith(t.objectProperty(t.identifier('myCurrent'), path.node.value));
         }
  	   }
复制代码

其中咱们用到了replaceWith方法,这个方法表示用一个ast节点来替换当前节点。 还有一个经常使用的replaceWithSourceString方法,表示用一个字符串来代替该ast节点,参数为一串代码字符串,如:'current : {type:String};',感兴趣的,能够本身试试。

最后查看转换后的代码,发现'current'已经被咱们替换成了'myCurrent'。

到这里,一个完整的例子就演示完了。这里补充说明一下,在实际中可能会遇到嵌套结构比较深的ast结构。咱们须要嵌套类型判断,好比:

ObjectProperty(path) {
     console.log(path,'ObjectProperty--')
      MemberExpression(memberPath) {
          console.log(path,'memberPath--')
      }
 }
复制代码

由于遍历中的path指定的是当前匹配的节点信息。因此能够为不一样的类型遍历指定不一样的path参数,来获取当前遍历的节点信息,避免path覆盖,例如上面的path和memberPath。

到这里,babel的基本用法就差很少介绍完了,想要熟练掌握,还须要你在项目中反复练习和实践。想系统学习babel,并在实际项目中使用的同窗能够先看看这篇babel的介绍文档,边写边查,巩固学习

Babel的实际应用

小程序的主要差别对比:

  1. 自定义组件不支持relations的关系申明

  2. 不支持getRelationNodes 的API调用

  3. transition动画数据结构不一样

  4. onLaunch, onShow, onLoad中不支持使用selectComponentselectAllComponents

  5. 微信的wxs语法

  6. 登陆流程,百度系使用passport,非百度系使用Oauth

代码展现

relations为例,进行演示,完整项目请查看互转工程项目

微信的使用demo:

relations: {
    './custom-li': {
      type: 'child', // 关联的目标节点应为子节点
      linked: function(target) {
        // 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期以后
      },
      linkChanged: function(target) {
        // 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期以后
      },
      unlinked: function(target) {
        // 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期以后
      }
    }
  },
复制代码

互转源码:

let linkedBody = '';
if (path.node.type === 'ObjectProperty' && path.node.key.name === 'relations') {
        //获取到relations属性中type的value
        //获取到relations属性中linked函数
        let componentName = '';
        let relationsValue = '';
        path.traverse({
            ObjectMethod(path) {
                if (path.node.key.name === 'linked') {
                    linkedBody = path.node.body;
                }
            },
            ObjectProperty(path) {
                if (path.node.key.type === 'StringLiteral' && path.node.key.value) {
                    relationsValue = path.node.key.value || '';
                    let index = relationsValue.lastIndexOf('./');
                    let lastIndex = relationsValue.lastIndexOf('/');
                    componentName = relationsValue.substring(index + 2, lastIndex);
                }
                // '../grid/index''grid'
                if (path.node.key.name === 'type') {
                    if (context.isDesgin) {
                        //添加组件库前缀
                        componentName = 'u-' + componentName;
                    }
                    let action = path.node.value.value === 'parent' ? 'relationComponentsParent' : 'relationComponentsChild';
                    contextStore.dispatch({
                        action,
                        payload: componentName
                    });
                    relationsMap[relationsValue] = path.node.value.value;
                }
            }
        });
        if (!linkedBody) {
            path.remove();
            return;
        } else {
            path.replaceWith(t.objectMethod('method', t.identifier('attached'), [], linkedBody, false));
        }
    }
复制代码

组件库的按需加载:

使用组件库的时候,不想打包全部组件,只打包项目中引入的组件

按需实现源码:

visitor: {
    ImportDeclaration(path, {opts})
    {
        const specifiers = path.node.specifiers;
        const source = path.node.source;

        // 判断传入的配置参数是不是数组形式
        if (Array.isArray(opts)) {
            opts.forEach(opt => {
                assert(opt.libraryName, 'libraryName should be provided');
            });
            if (!opts.find(opt => opt.libraryName === source.value)) return;
        } else {
            assert(opts.libraryName, 'libraryName should be provided');
            if (opts.libraryName !== source.value) return;
        }

        const opt = Array.isArray(opts) ? opts.find(opt => opt.libraryName === source.value) : opts;
        opt.camel2UnderlineComponentName = typeof opt.camel2UnderlineComponentName === 'undefined'
            ? false
            : opt.camel2UnderlineComponentName;
        opt.camel2DashComponentName = typeof opt.camel2DashComponentName === 'undefined'
            ? false
            : opt.camel2DashComponentName;

        if (!t.isImportDefaultSpecifier(specifiers[0]) && !t.isImportNamespaceSpecifier(specifiers[0])) {
            // 遍历specifiers生成转换后的ImportDeclaration节点数组
            const declarations = specifiers.map((specifier) => {
                // 转换组件名称
                const transformedSourceName = opt.camel2UnderlineComponentName
                    ? camel2Underline(specifier.imported.name)
                    : opt.camel2DashComponentName
                        ? camel2Dash(specifier.imported.name)
                        : specifier.imported.name;
                // 利用自定义的customSourceFunc生成绝对路径,而后建立新的ImportDeclaration节点
                return t.ImportDeclaration([t.ImportDefaultSpecifier(specifier.local)],
                    t.StringLiteral(opt.customSourceFunc(transformedSourceName)));
            });
            // 将当前节点替换成新建的ImportDeclaration节点组
            path.replaceWithMultiple(declarations);
        }
    }
}
复制代码

而后安装babel-cli工具,将代码打包,发布到npm,就能够在项目中使用了。若是再优化完善下,是否是就能够把现有项目中ant-design的按需加载功能移除了。。。

在项目中设置.babelrc文件,增长自定义插件配置

效果:

//以前
import { button, table } from 'union-design';

//如今
import button from 'union-design/src/components/button/index.js';
import table from 'union-design/src/components/table/index.js';
复制代码
相关文章
相关标签/搜索