手把手教你开发一个babel-plugin

需求

在最近的开发过程当中,不一样的项目、不一样的页面都须要用到某种UI控件,因而很天然的将这些UI控件拆出来,单独创建了一个代码库进行维护。下面是个人组件库大体的目录结构以下:javascript

...
- lib
    - components
        - componentA
            - index.vue
        - componentB
            - index.vue
        - componentC
            - index.vue
- index.js
...

整个组件库的出口在index.js,里面的内容差很少是下面这样的:vue

import A from './lib/componentA';
import B from './lib/componentB';
import C from './lib/componentC';

export {
    A,
    B,
    C
}

个人代码库的name为:kb-bi-vue-component。在项目中引用这个组件库的时候,代码以下:java

import { A, B } from 'kb-bi-vue-component';
....

这个时候,问题出现了,我在页面中,仅仅使用了AB两个组件,可是页面打包后,整个组件库的代码都会被打进来,增长了产出的体积,包括了很多的冗余代码。很容易想到的一个解决方案是按照如下的方式引用组件。node

import A from 'kb-bi-vue-component/lib/componentA';
import B from 'kb-bi-vue-component/lib/componentB';

这种方法虽然解决了问题,我想引用哪一个组件,就引用哪一个组件,不会有多余的代码。可是我总以为这种写法看起来不太舒服。有没有还能像第一种写法同样引用组件库,而且只引用须要的组件呢?写一个babel-plugin好了,自动将第一种写法转换成第二种写法。git

Babel的原理

本文只是简单介绍。想要深刻理解代码编译,请学习<<编译原理>>

这里有一个不错的Babel教程:https://github.com/jamiebuild...github

Babel是Javascript编译器,更确切地说是源码到源码的编译器,一般也叫作『转换编译器』。也就是说,你给Babel提供一些Javascript代码,Babel更改这下代码,而后返回给你新生成的代码。数组

AST

在这整个过程当中,都是围绕着抽象语法树(AST)来进行的。在Javascritp中,AST,简单来讲,就是一个记录着代码语法结构的Object。好比下面的代码:babel

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

转换成AST后以下,函数

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

AST是分层的,由一个一个的 节点(Node) 组成。如:工具

{
  ...
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}
{
  type: "Identifier",
  name: ...
}

每个节点都有一个必需的 type 字段表示节点的类型。如上面的FunctionDeclaration

Identifier等等。每种类型的节点都会有本身的属性。

Babel的工做过程

Babel的处理过程主要为3个:解析(parse)转换(transform)生成(generate)

  • 解析

    解析主要包含两个过程:词法分析和语法分析,输入是代码字符串,输出是AST。

  • 转换

    处理AST。处理工具、插件等就是在这个过程当中介入,将代码按照需求进行转换。

  • 生成

    遍历AST,输出代码字符串。

解析和生成过程,都有Babel都为咱们处理得很好了,咱们要作的就是在 转换 过程当中搞事情,进行个性化的定制开发。

开发一个babel-plugin

这里有详细的介绍: https://github.com/jamiebuild...

开发方式概述

首先,须要大体了解一下babel-plugin的开发方法。

babel使用一种 访问者模式 来遍历整棵语法树,即遍历进入到每个Node节点时,能够说咱们在「访问」这个节点。访问者就是一个对象,定义了在一个树状结构中获取具体节点的方法。简单来讲,咱们能够在访问者中,使用Node的type来定义一个hook函数,每一次遍历到对应type的Node时,hook函数就会被触发,咱们能够在这个hook函数中,修改、查看、替换、删除这个节点。提及来很抽象,直接看下面的内容吧。

开始开发吧

  • 下面,根据咱们的需求,来开发一个plugin。怎么配置使用本身的babel-plugin呢?个人项目中,是使用.babelrc来配置babel的,以下:
{
    "presets": [
        ["es2015"],
        ["stage-0"]
    ]
}

上面的配置中,只有两个预设,并无使用插件。首先加上插件的配置。因为是在本地开发,插件直接写的本地的相对地址。

{
    "presets": [
        ["es2015"],
        ["stage-0"]
    ],
    "plugins":["./my-import-babel-plugin"]
}

仅仅像上面这样是有问题的,由于需求是须要针对具体的library,因此确定是须要传入参数的。改为下面这样:

{
    "presets": [
        ["es2015"],
        ["stage-0"]
    ],
    "plugins":[
        ["./my-import-babel-plugin", { "libraryName": "kb-bi-vue-component", "alias": "kb-bi-vue-component/lib/components"}]
    ]
}

咱们给plugin传了一个参数,libraryName表示须要处理的library,alias表示组件在组件库内部的路径。

  • 下面是插件的代码./my-import-babel-plugin.js
module.exports = function ({ types: t }) {
    return {
        visitor: {
            ImportDeclaration(path, source){
                const { opts: { libraryName, alias } } = source;
                if (!t.isStringLiteral(path.node.source, { value: libraryName })) {
                    return;
                }
                console.log(path.node);
                // todo
            }
        }    
    }
}

函数的参数为babel对象,对象中的types是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑很是有用。咱们单独把这个types拿出来。返回的visitor就是咱们上文提到的访问者对象。此次的需求是对 import 语句的修改,因此咱们在visitor中定义了import的type:ImportDeclaration。这样,当babel处理到代码里的import语句时,就会走到这个ImportDeclaration函数里面来。

这里查看Babel定义的全部的AST Node: https://github.com/babel/babe...

ImportDeclaration接受两个参数,

  1. path表示当前访问的路径,path.node就能取到当前访问的Node.
  2. source表示PluginPass,即传递给当前plugin的其余信息,包括当前编译的文件、代码字符串以及咱们在.babelrc中传入的参数等。

在插件的代码中,咱们首先取到了传入插件的参数。接着,判断若是不是咱们须要处理的library,就直接返回了

这里能够查看babel.types的使用方法: https://babeljs.io/docs/en/ba...
  • 假设咱们的业务代码中的代码以下:
...
import { A, B } from 'kb-bi-vue-component'
...

咱们运行一下打包工具,输出一下path.node,能够看到,当前访问的Node以下:

Node {
    type: 'ImportDeclaration',
    start: 9,
    end: 51,
    loc: SourceLocation {
        start: Position {
            line: 10,
            column: 0
        },
        end: Position {
            line: 10,
            column: 42
        }
    },
    specifiers: [Node {
            type: 'ImportSpecifier',
            start: 18,
            end: 19,
            loc: [Object],
            imported: [Object],
            local: [Object]
        },
        Node {
            type: 'ImportSpecifier',
            start: 21,
            end: 22,
            loc: [Object],
            imported: [Object],
            local: [Object]
        }
    ],
    source: Node {
        type: 'StringLiteral',
        start: 30,
        end: 51,
        loc: SourceLocation {
            start: [Object],
            end: [Object]
        },
        extra: {
            rawValue: 'kb-bi-vue-component',
            raw: '\'kb-bi-vue-component\''
        },
        value: 'kb-bi-vue-component'
    }
}

稍微解释一下这个Node. specifiers是一个数组,包含两个Node,对应的是代码import后面的两个参数AB。这两个Node的local值都是Identifier类型的Node。source表示的是代码from后面的library。

  • 接下来,按照需求把这个ImportDeclaration类型的Node替换掉,换成咱们想要的。使用path.replaceWithMultiple这个方法来替换一个Node。此方法接受一个Node数组。因此咱们首先须要构造出Node,装进一个数组里,而后扔给这个path.replaceWithMultiple方法。

    查阅文档,

    t.importDeclaration(specifiers, source)
    
    specifiers: Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier> (required)
    source: StringLiteral (required)

    能够经过t.importDeclaration来构造importNode,参数如上所示。构造importNode,须要先构造其参数须要的Node。最终,修改插件的代码以下:

    module.exports = function ({ types: t }) {
        return {
            visitor: {
                ImportDeclaration(path, source) {
                    const { opts: { libraryName, alias } } = source;
                    if (!t.isStringLiteral(path.node.source, { value: libraryName })) {
                        return;
                    }
                    const newImports = path.node.specifiers.map( item => {
                        return t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${alias}/${item.local.name}`))
                    });
                    path.replaceWithMultiple(newImports);
                }
            }
        }
    }

    开发基本结束

    好了,一个babel-plugin开发完成了。咱们成功的实现了如下的编译:

    import { A, B } from 'kb-bi-vue-component';
    
    ↓ ↓ ↓ ↓ ↓ ↓
    
    import A from 'kb-bi-vue-component/lib/components/A';
    import B from 'kb-bi-vue-component/lib/components/B';

    babel在工做时,会优先执行.babelrc中的plugins,接着才会执行presets。咱们优先将源代码进行了转换,再使用babel去转换为es5的代码,整个过程是没有问题的。

    固然,这是最简单的babel-plugin,还有不少其余状况没有处理,好比下面这种,转换后就不符合预期。

    import { A as aaa, B } from 'kb-bi-vue-component';
    
    ↓ ↓ ↓ ↓ ↓ ↓
    
    import aaa from 'kb-bi-vue-component/lib/components/aaa';
    import B from 'kb-bi-vue-component/lib/components/B';

    要完成一个高质量的babel-plugin,还有不少的工做要作。

    附:阿里已经开源了一个成熟的babel-plugin-import

    参考连接:

    一、https://github.com/jamiebuild...
    二、https://babeljs.io/docs/en/ba...

相关文章
相关标签/搜索