资讯专栏INFORMATION COLUMN

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

zsirfs / 2622人阅读

摘要:下面是我的组件库大致的目录结构如下整个组件库的出口在,里面的内容差不多是下面这样的我的代码库的为。改成下面这样我们给传了一个参数,表示需要处理的,表示组件在组件库内部的路径。要完成一个高质量的,还有很多的工作要做。

需求

在最近的开发过程中,不同的项目、不同的页面都需要用到某种UI控件,于是很自然的将这些UI控件拆出来,多带带建立了一个代码库进行维护。下面是我的组件库大致的目录结构如下:

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

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

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

export {
    A,
    B,
    C
}

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

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

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

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

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

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

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

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

AST

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

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接受两个参数,

path表示当前访问的路径,path.node就能取到当前访问的Node.

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 (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

参考链接:

1、https://github.com/jamiebuild...
2、https://babeljs.io/docs/en/ba...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97902.html

相关文章

  • 把手教你学Dapr

    摘要:配置配置使用概率抽样。采样率定义了对跟踪跨度进行采样的概率,其值可以介于和含之间。例如,以下配置对象将采样率更改为即每个跨度都被采样,并使用协议将跟踪发送到位于的服务器文件路径注将采样率更改为会完全禁用跟踪。目录手把手教你学Dapr - 1. .Net开发者的大时代手把手教你学Dapr - 2. 必须知道的概念手把手教你学Dapr - 3. 使用Dapr运行第一个.Net程序手把手教你学Da...

    qqlcbb 评论0 收藏0
  • 把手教你用echarts和SovitChart开发带渐变色的柱状图

    摘要:我们在开发应用的时候经常美工会设计一些样式比较特殊的图表,这对于前端开发人员来说会增加开发量,如下图就是笔者开发过程中要求制作的带渐变色效果的柱状图今天在这里教大家如何用原生和如何用图表开发工具来实现。 我们在开发web应用的时候经常美工会设计一些样式比较特殊的图表,这对于前端开发人员来说会...

    RaoMeng 评论0 收藏0
  • 把手教你开发现代PHP框架

    摘要:本文将从零开始搭建一个现代化的框架,该框架会拥有现代框架的一切特征,如单入口,路由,依赖注入,类自动加载机制等等,如同时下最流行的框架一样。执行控制器文件中的逻辑代码,最终将数据通过对应的视图层显示出来。 本文将从零开始搭建一个现代化的PHP框架,该框架会拥有现代框架的一切特征,如单入口,路由,依赖注入,composer类自动加载机制等等,如同时下最流行的Laravel框架一样。 一、...

    raise_yang 评论0 收藏0
  • 把手教你学Vue-2(组件开发)

    摘要:组件声明组件分为全局的和局部的。父组件通过给子组件下发数据,子组件通过事件给父组件发送消息。以下实例中子组件已经和它外部完全解耦了。 1.vue 组件-声明 组件分为全局的和局部的。 全局声明 Vue.component(tagName, options) ** 引用组件 // 注册 Vue.comp...

    lansheng228 评论0 收藏0
  • 把手教你学Vue-2(组件开发)

    摘要:组件声明组件分为全局的和局部的。父组件通过给子组件下发数据,子组件通过事件给父组件发送消息。以下实例中子组件已经和它外部完全解耦了。 1.vue 组件-声明 组件分为全局的和局部的。 全局声明 Vue.component(tagName, options) ** 引用组件 // 注册 Vue.comp...

    Null 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<