乐趣区

关于javascript:数栈史上最全-babelpluginimport源码详解

本文将率领大家解析 babel-plugin-import 实现按需加载的残缺流程,解开业界所认可 babel 插件的面纱。

首先供上 babel-plugin-import 插件

一、初见萌芽

首先 babel-plugin-import 是为了解决在打包过程中把我的项目中援用到的内部组件或性能库全量打包,从而导致编译完结后容纳量过大的问题,如下图所示:

babel-plugin-import 插件源码由两个文件形成

  • Index 文件即是插件入口初始化的文件,也是笔者在 Step1 中着重阐明的文件
  • Plugin 文件蕴含了解决各种 AST 节点的办法集,以 Class 模式导出

先来到插件的入口文件 Index:

import Plugin from './Plugin';
export default function({types}) {
  let plugins = null;
  /**
   *  Program 入口初始化插件 options 的数据结构
   */
  const Program = {enter(path, { opts = {} }) {assert(opts.libraryName, 'libraryName should be provided');
      plugins = [
        new Plugin(
          opts.libraryName,
          opts.libraryDirectory,
          opts.style,
          opts.styleLibraryDirectory,
          opts.customStyleName,
          opts.camel2DashComponentName,
          opts.camel2UnderlineComponentName,
          opts.fileName,
          opts.customName,
          opts.transformToDefaultImport,
          types,
        ),
      ];
      applyInstance('ProgramEnter', arguments, this);
    },
    exit() {applyInstance('ProgramExit', arguments, this);
    },
  };
  const ret = {visitor: { Program}, // 对整棵 AST 树的入口进行初始化操作
  };
  return ret;
}

首先 Index 文件导入了 Plugin,并且有一个默认导出函数,函数的参数是被解构出的名叫 types 的参数,它是从 babel 对象中被解构进去的,types 的全称是 @babel/types,用于解决 AST 节点的办法集。以这种形式引入后,咱们不须要手动引入 @babel/types。进入函数后能够看见 观察者 (visitor) 中初始化了一个 AST 节点 Program,这里对 Program 节点的解决应用残缺插件构造,有进入(enter) 与来到(exit)事件,且需注意:

个别咱们缩写的 Identifier() { …} 是 Identifier: {enter() {…} } 的简写模式。

这里可能有同学会问 Program 节点是什么?见下方 const a = 1 对应的 AST 树 (简略局部参数)

{
  "type": "File",
  "loc": {
    "start":... ,
    "end": ...
  },
  "program": {
    "type": "Program", // Program 所在位置
    "sourceType": "module",
    "body": [
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {
              "type": "Identifier",
              "name": "a"
            },
            "init": {
              "type": "NumericLiteral",
              "value": 1
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []},
  "comments": [],
  "tokens": [...]
}

Program 相当于一个 根节点,一个残缺的源代码树。个别在进入该节点的时候进行初始化数据之类的操作,也可了解为该节点先于其余节点执行,同时也是最晚执行 exit 的节点,在 exit 时也能够做一些”善后“的工作。既然 babel-plugin-importProgram 节点处写了残缺的构造,必然在 exit 时也有十分必要的事件须要解决,对于 exit 具体是做什么的咱们稍后进行探讨。咱们先看 enter,这里首先用 enter 形参 state 构造出用户制订的插件参数,验证必填的 libraryName [库名称] 是否存在。Index 文件引入的 Plugin 是一个 class 构造,因而须要对 Plugin 进行实例化,并把插件的所有参数与 @babel/types 全副传进去,对于 Plugin 类会在下文中进行论述。接着调用了 applyInstance 函数:

export default function({types}) {
  let plugins = null;
  /**
   * 从类中继承办法并利用 apply 扭转 this 指向,并传递 path , state 参数
   */
  function applyInstance(method, args, context) {for (const plugin of plugins) {if (plugin[method]) {plugin[method].apply(plugin, [...args, context]);
      }
    }
  }
  const Program = {enter(path, { opts = {} }) {
      ...
      applyInstance('ProgramEnter', arguments, this);
    },
      ...
   }
}

此函数的次要目标是继承 Plugin 类中的办法,且须要三个参数

  1. method(String):你须要从 Plugin 类中继承进去的办法名称
  2. args:(Arrray):[Path, State]
  3. PluginPass(Object):内容和 State 统一,确保传递内容为最新的 State

次要的目标是让 Program 的 enter 继承 Plugin 类的 ProgramEnter 办法,并且传递 path 与 state 形参至 ProgramEnterProgram 的 exit 同理,继承的是 ProgramExit 办法。

当初进入 Plugin 类:

export default class Plugin {
  constructor(
    libraryName,
    libraryDirectory,
    style,
    styleLibraryDirectory,
    customStyleName,
    camel2DashComponentName,
    camel2UnderlineComponentName,
    fileName,
    customName,
    transformToDefaultImport,
    types, // babel-types
    index = 0, // 标记符
  ) {
    this.libraryName = libraryName; // 库名
    this.libraryDirectory = typeof libraryDirectory === 'undefined' ? 'lib' : libraryDirectory; // 包门路
    this.style = style || false; // 是否加载 style
    this.styleLibraryDirectory = styleLibraryDirectory; // style 包门路
    this.camel2DashComponentName = camel2DashComponentName || true; // 组件名是否转换以“-”链接的模式
    this.transformToDefaultImport = transformToDefaultImport || true; // 解决默认导入
    this.customName = normalizeCustomName(customName); // 解决转换后果的函数或门路
    this.customStyleName = normalizeCustomName(customStyleName); // 解决转换后果的函数或门路
    this.camel2UnderlineComponentName = camel2UnderlineComponentName; // 解决成相似 time_picker 的模式
    this.fileName = fileName || ''; // 链接到具体的文件,例如 antd/lib/button/[abc.js]
    this.types = types; // babel-types
    this.pluginStateKey = `importPluginState${index}`;
  }
  ...
}

在入口文件实例化 Plugin 曾经把插件的参数通过 constructor 后被初始化结束啦,除了 libraryName 以外其余所有的值均有相应默认值,值得注意的是参数列表中的 customeName 与 customStyleName 能够接管一个函数或者一个引入的门路,因而须要通过 normalizeCustomName 函数进行统一化解决。

function normalizeCustomName(originCustomName) {if (typeof originCustomName === 'string') {const customeNameExports = require(originCustomName);
    return typeof customeNameExports === 'function'
      ? customeNameExports
      : customeNameExports.default;// 如果 customeNameExports 不是函数就导入{default:func()}
  }
  return originCustomName;
}

此函数就是用来解决当参数是门路时,进行转换并取出相应的函数。如果解决后 customeNameExports 依然不是函数就导入 customeNameExports.default,这里牵扯到 export default 是语法糖的一个小知识点。

export default something() {}
// 等效于
function something() {}
export (something as default)

回归代码,Step1 中入口文件 Program 的 Enter 继承了 Plugin 的 ProgramEnter 办法

export default class Plugin {constructor(...) {...}

  getPluginState(state) {if (!state[this.pluginStateKey]) {
      // eslint-disable-next-line no-param-reassign
      state[this.pluginStateKey] = {}; // 初始化标示}
    return state[this.pluginStateKey]; // 返回标示
  }
  ProgramEnter(_, state) {const pluginState = this.getPluginState(state);
    pluginState.specified = Object.create(null); // 导入对象汇合
    pluginState.libraryObjs = Object.create(null); // 库对象汇合 (非 module 导入的内容)
    pluginState.selectedMethods = Object.create(null); // 寄存通过 importMethod 之后的节点
    pluginState.pathsToRemove = []; // 存储须要删除的节点
    /**
     * 初始化之后的 state
     * state:{
     *    importPluginState「Number」: {*      specified:{},
     *      libraryObjs:{},
     *      select:{},
     *      pathToRemovw:[]
     *    },
     *    opts:{
     *      ...
     *    },
     *    ...
     * }
     */
  }
   ...
}

ProgramEnter 中通过 getPluginState** 初始化 state 构造中的 importPluginState 对象,getPluginState 函数在后续操作中呈现十分频繁,读者在此须要注意此函数的作用,后文不再对此进行赘述。然而为什么须要初始化这么一个构造呢?这就牵扯到插件的思路。正像开篇流程图所述的那样,babel-plugin-import 具体实现按需加载思路如下:通过 import 节点后收集节点数据,而后从所有可能援用到 import 绑定的节点处执行按需加载转换方法。state 是一个援用类型,对其进行操作会影响到后续节点的 state 初始值,因而用 Program 节点,在 enter 的时候就初始化这个收集依赖的对象,不便后续操作。负责初始化 state 节点构造与取数据的办法正是 getPluginState。这个思路很重要,并且贯通前面所有的代码与目标,请读者务必了解再往下浏览。

二、惟恍惟惚

借由 Step1,当初曾经理解到插件以 Program 为出发点继承了 ProgramEnter 并且初始化了 Plugin 依赖,如果读者还有尚未梳理分明的局部,请回到 Step1 认真消化下内容再持续浏览。首先,咱们再回到外围的 Index 文件,之前只在观察者模式中注册了 Program 的节点,没有其余 AST 节点入口,因而至多还需注入 import 语句的 AST 节点类型 ImportDeclaration

export default function({types}) {
  let plugins = null;
  function applyInstance(method, args, context) {...}
  const Program = {...}
  const methods = [ // 注册 AST type 的数组
    'ImportDeclaration' 
  ]

  const ret = {visitor: { Program}, 
  };

  // 遍历数组,利用 applyInstance 继承相应办法
  for (const method of methods) {ret.visitor[method] = function() {applyInstance(method, arguments, ret.visitor);
    };
  }

}

创立一个数组并将 ImportDeclaration 置入,通过遍历调用 applyInstance_ _和 Step1 介绍同理,执行结束后 visitor 会变成如下构造

visitor: {Program: { enter: [Function: enter], exit: [Function: exit] },
  ImportDeclaration: [Function],
}

当初回归 Plugin,进入 ImportDeclaration

export default class Plugin {constructor(...) {...}
  ProgramEnter(_, state) {...}

  /**
   * 主指标,收集依赖
   */
  ImportDeclaration(path, state) {const { node} = path;
    // path 有可能被前一个实例删除
    if (!node) return;
    const {source: { value}, // 获取 AST 中引入的库名
    } = node;
    const {libraryName, types} = this;
    const pluginState = this.getPluginState(state); // 获取在 Program 处初始化的构造
    if (value === libraryName) { //  AST 库名与插件参数名是否统一,统一就进行依赖收集
      node.specifiers.forEach(spec => {if (types.isImportSpecifier(spec)) { // 不满足条件阐明 import 是名称空间引入或默认引入
          pluginState.specified[spec.local.name] = spec.imported.name; 
          // 保留为:{别名 :  组件名} 构造
        } else {pluginState.libraryObjs[spec.local.name] = true;// 名称空间引入或默认引入的值设置为 true
        }
      });
      pluginState.pathsToRemove.push(path); // 取值结束的节点增加进预删除数组
    }
  }
  ...
}

ImportDeclaration 会对 import 中的依赖字段进行收集,如果是名称空间引入或者是默认引入就设置为 {别名:true},解构导入就设置为 {别名:组件名}。getPluginState 办法在 Step1 中曾经进行过阐明。对于 import 的 AST 节点构造 用 babel-plugin 实现按需加载 中有具体阐明,本文不再赘述。执行结束后 pluginState 构造如下

// 例:import {Input, Button as Btn} from 'antd'

{
  ...
  importPluginState0: {
     specified: {
      Btn : 'Button',
      Input : 'Input'
    },
    pathToRemove: {[NodePath]
    }
    ...
  }
  ...
}

这下 state.importPluginState 构造曾经收集到了后续帮忙节点进行转换的所有依赖信息。目前曾经万事俱备,只欠东风。东风是啥?是能让转换 import 工作开始的 action。在 用 babel-plugin 实现按需加载 中收集到依赖的同时也进行了节点转换与删除旧节点。所有工作都在 ImportDeclaration 节点中产生。而 babel-plugin-import 的思路是 寻找所有可能援用到 Import 的 AST 节点,对他们全副进行解决。有局部读者兴许会间接想到去转换援用了 import 绑定 的 JSX 节点,然而转换 JSX 节点的意义不大,因为可能援用到 import 绑定的 AST 节点类型 (type) 曾经够多了,所有应尽可能的放大须要转换的 AST 节点类型范畴。而且 babel 的其余插件会将咱们的 JSX 节点进行转换成其余 AST type,因而能不思考 JSX 类型的 AST 树,能够等其余 babel 插件转换后再进行替换工作。其实下一步能够开始的入口有很多,但还是从咱最相熟的 React.createElement 开始。

class Hello extends React.Component {render() {return <div>Hello</div>}
}

// 转换后

class Hello extends React.Component {render(){return React.createElement("div",null,"Hello")
    }
}

JSX 转换后 AST 类型为 CallExpression(函数执行表达式),构造如下所示,相熟构造后能不便各位同学对之后步骤有更深刻的了解。

{
  "type": "File",
  "program": {
    "type": "Program",
    "body": [
      {
        "type": "ClassDeclaration",
        "body": {
          "type": "ClassBody",
          "body": [
            {
              "type": "ClassMethod",
              "body": {
                "type": "BlockStatement",
                "body": [
                  {
                    "type": "ReturnStatement",
                    "argument": {
                      "type": "CallExpression", // 这里是解决的终点
                      "callee": {
                        "type": "MemberExpression",
                        "object": {
                          "type": "Identifier",
                          "identifierName": "React"
                        },
                        "name": "React"
                      },
                      "property": {
                        "type": "Identifier",
                        "loc": {"identifierName": "createElement"},
                        "name": "createElement"
                      }
                    },
                    "arguments": [
                      {
                        "type": "StringLiteral",
                        "extra": {
                          "rawValue": "div",
                          "raw": ""div""
                        },
                        "value": "div"
                      },
                      {"type": "NullLiteral"},
                      {
                        "type": "StringLiteral",
                        "extra": {
                          "rawValue": "Hello",
                          "raw": ""Hello""
                        },
                        "value": "Hello"
                      }
                    ]
                  }
                ],
                "directives": []}
            }
          ]
        }
      }
    ]
  }
}

因而咱们进入 CallExpression 节点处,持续转换流程。

export default class Plugin {constructor(...) {...}
  ProgramEnter(_, state) {...}

  ImportDeclaration(path, state) {...}

  CallExpression(path, state) {const { node} = path;
    const file = path?.hub?.file || state?.file;
    const {name} = node.callee;
    const {types} = this;
    const pluginState = this.getPluginState(state);
    // 解决个别的调用表达式
    if (types.isIdentifier(node.callee)) {if (pluginState.specified[name]) {node.callee = this.importMethod(pluginState.specified[name], file, pluginState);
      }
    }
    // 解决 React.createElement
    node.arguments = node.arguments.map(arg => {const { name: argName} = arg;
      // 判断作用域的绑定是否为 import
      if (pluginState.specified[argName] &&
        path.scope.hasBinding(argName) &&
        types.isImportSpecifier(path.scope.getBinding(argName).path)
      ) {return this.importMethod(pluginState.specified[argName], file, pluginState); // 替换了援用,help/import 插件返回节点类型与名称
      }
      return arg;
    });
  } 
  ...
}

能够看见源码调用了importMethod 两次,此函数的作用是触发 import 转换成按需加载模式的 action,并返回一个全新的 AST 节点。因为 import 被转换后,之前咱们人工引入的组件名称会和转换后的名称不一样,因而 importMethod 须要把转换后的新名字(一个 AST 构造)返回到咱们对应 AST 节点的对应地位上,替换掉老组件名。函数源码稍后会进行详细分析。回到一开始的问题,为什么 CallExpression 须要调用 importMethod 函数?因为这两处示意的意义是不同的,CallExpression 节点的状况有两种:

  1. 方才曾经剖析过了,这第一种状况是 JSX 代码通过转换后的 React.createElement
  2. 咱们应用函数调用一类的操作代码的 AST 也同样是 CallExpression 类型,例如:
import lodash from 'lodash'

lodash(some values)

因而在 CallExpression 中首先会判断 node.callee 值是否是 Identifier,如果正确则是所述的第二种状况,间接进行转换。若否,则是 React.createElement 模式,遍历 React.createElement 的三个参数取出 name,再判断 name 是否是先前 state.pluginState 收集的 import 的 name,最初查看 name 的作用域状况,以及追溯 name 的 绑定 是否是一个 import 语句。这些判断条件都是为了防止谬误的批改函数本来的语义,避免谬误批改因 闭包等个性的块级作用域 中有雷同名称的变量。如果上述条件均满足那它必定是须要解决的 import 援用 了。让其持续进入importMethod 转换函数,importMethod 须要传递三个参数:组件名,File(path.sub.file),pluginState

import {join} from 'path';
import {addSideEffect, addDefault, addNamed} from '@babel/helper-module-imports';

 export default class Plugin {constructor(...) {...}
   ProgramEnter(_, state) {...}
   ImportDeclaration(path, state) {...}
   CallExpression(path, state) {...} 

  // 组件原始名称 , sub.file , 导入依赖项
   importMethod(methodName, file, pluginState) {if (!pluginState.selectedMethods[methodName]) {const { style, libraryDirectory} = this;
      const transformedMethodName = this.camel2UnderlineComponentName // 依据参数转换组件名称
        ? transCamel(methodName, '_')
        : this.camel2DashComponentName
        ? transCamel(methodName, '-')
        : methodName;
       /**
       * 转换门路,优先依照用户定义的 customName 进行转换,如果没有提供就依照惯例拼接门路
       */
      const path = winPath(
        this.customName
          ? this.customName(transformedMethodName, file)
          : join(this.libraryName, libraryDirectory, transformedMethodName, this.fileName), // eslint-disable-line
      );
      /**
       * 依据是否是默认引入对最终门路做解决, 并没有对 namespace 做解决
       */
      pluginState.selectedMethods[methodName] = this.transformToDefaultImport // eslint-disable-line
        ? addDefault(file.path, path, { nameHint: methodName})
        : addNamed(file.path, methodName, path);
      if (this.customStyleName) { // 依据用户指定的门路引入款式文件
        const stylePath = winPath(this.customStyleName(transformedMethodName));
        addSideEffect(file.path, `${stylePath}`);
      } else if (this.styleLibraryDirectory) { // 依据用户指定的款式目录引入款式文件
        const stylePath = winPath(join(this.libraryName, this.styleLibraryDirectory, transformedMethodName, this.fileName),
        );
        addSideEffect(file.path, `${stylePath}`);
      } else if (style === true) {  // 引入 scss/less 
        addSideEffect(file.path, `${path}/style`);
      } else if (style === 'css') { // 引入 css
        addSideEffect(file.path, `${path}/style/css`);
      } else if (typeof style === 'function') { // 若是函数,依据返回值生成引入
        const stylePath = style(path, file);
        if (stylePath) {addSideEffect(file.path, stylePath);
        }
      }
    }
    return {...pluginState.selectedMethods[methodName] };
  }
  ...
}

进入函数后,先别着急看代码,留神这里引入了两个包:path.join 和 @babel/helper-module-imports,引入 join 是为了解决按需加载门路快捷拼接的需要,至于 import 语句转换,必定须要产生全新的 import AST 节点实现按需加载,最初再把老的 import 语句删除。而新的 import 节点应用 babel 官网保护的 @babel/helper-module-imports 生成。当初持续流程,首先忽视一开始的 if 条件语句,稍后会做阐明。再捋一捋 import 处理函数中须要解决的几个环节:

  • 对引入的组件名称进行批改,默认转换以“-”拼接单词的模式,例如:DatePicker 转换为 date-picker,解决转换的函数是 transCamel。
function transCamel(_str, symbol) {const str = _str[0].toLowerCase() + _str.substr(1); // 先转换成小驼峰, 以便正则获取残缺单词
  return str.replace(/([A-Z])/g, $1 => `${symbol}${$1.toLowerCase()}`); 
  // 例 datePicker,正则抓取到 P 后,在它后面加上指定的 symbol 符号
}

转换到组件所在的具体门路,如果插件用户给定了自定义门路就应用 customName 进行解决,babel-plugin-import 为什么不提供对象的模式作为参数?因为 customName 批改是以 transformedMethodName 值作为根底并将其传递给插件使用者,如此设计就能够更准确的匹配到须要按需加载的门路。解决这些动作的函数是 withPath,withPath 次要兼容 Linux 操作系统,将 Windows 文件系统反对的 ” 对立转换为 ‘/’。

function winPath(path) {return path.replace(//g, '/'); 
  // 兼容门路: windows 默认应用‘’, 也反对‘/’,但 linux 不反对‘’,遂对立转换成‘/’}

对 transformToDefaultImport 进行判断,此选项默认为 true,转换后的 AST 节点是默认导出的模式,如果不想要默认导出能够将 transformToDefaultImport 设置为 false,之后便利用 @babel/helper-module-imports 生成新的 import 节点,最初 ** 函数的返回值就是新 import 节点的 default Identifier,替换掉调用 importMethod 函数的节点,从而把所有援用旧 import 绑定的节点替换成最新生成的 import AST 的节点。

最初,依据用户是否开启 style 按需引入与 customStyleName 是否有 style 门路额定解决,以及 styleLibraryDirectory(style 包门路)等参数解决或生成对应的 css 按需加载节点。

到目前为止一条最根本的转换线路曾经转换结束了,置信大家也曾经理解了按需加载的根本转换流程,回到 importMethod 函数一开始的if 判断语句,这与咱们将在 step3 中的工作非亲非故。当初就让咱们一起进入 step3。

三、一目了然

在 step3 中会进行按需加载转换最初的两个步骤:

  1. 引入 import 绑定的援用必定不止 JSX 语法,还有其余诸如,三元表达式,类的继承,运算,判断语句,返回语法等等类型,咱们都得对他们进行解决,确保所有的援用都绑定到最新的 import,这也会导致 importMethod 函数 被从新调用,但咱们必定不心愿 import 函数被援用了 n 次,生成 n 个新的 import 语句,因而才会有先前的判断语句。
  2. 一开始进入 ImportDeclaration 收集信息的时候咱们只是对其进行了依赖收集工作,并没有删除节点。并且咱们尚未补充 Program 节点 exit 所做的 action

接下来将以此列举须要解决的所有 AST 节点,并且会给每一个节点对应的接口(Interface)与例子(不关注语义):

MemberExpression

MemberExpression(path, state) {const { node} = path;
    const file = (path && path.hub && path.hub.file) || (state && state.file);
    const pluginState = this.getPluginState(state);
    if (!node.object || !node.object.name) return;
    if (pluginState.libraryObjs[node.object.name]) {
      // antd.Button -> _Button
      path.replaceWith(this.importMethod(node.property.name, file, pluginState));
    } else if (pluginState.specified[node.object.name] && path.scope.hasBinding(node.object.name)) {const { scope} = path.scope.getBinding(node.object.name);
      // 全局变量解决
      if (scope.path.parent.type === 'File') {node.object = this.importMethod(pluginState.specified[node.object.name], file, pluginState);
      }
    }
  }

MemberExpression(属性成员表达式),接口如下

interface MemberExpression {
    type: 'MemberExpression';
    computed: boolean;
    object: Expression;
    property: Expression;
}
/**
 * 解决相似:* console.log(lodash.fill())
 * antd.Button
 */

如果插件的选项中没有敞开 transformToDefaultImport,这里会调用 importMethod 办法并返回@babel/helper-module-imports 给予的新节点值。否则会判断以后值是否是收集到 import 信息中的一部分以及是否是文件作用域下的全局变量,通过获取作用域查看其父节点的类型是否是 File,即可防止谬误的替换其余同名变量,比方闭包场景。

VariableDeclarator

VariableDeclarator(path, state) {const { node} = path;
   this.buildDeclaratorHandler(node, 'init', path, state);
}

VariableDeclarator(变量申明),十分不便了解解决场景,次要解决 const/let/var 申明语句

interface VariableDeclaration : Declaration {
    type: "VariableDeclaration";
    declarations: [VariableDeclarator];
    kind: "var" | "let" | "const";
}
/**
 * 解决相似:* const foo = antd
 */

本例中呈现 buildDeclaratorHandler 办法,次要确保传递的属性是根底的 Identifier 类型且是 import 绑定的援用后便进入 importMethod 进行转换后返回新节点笼罩原属性。

buildDeclaratorHandler(node, prop, path, state) {const file = (path && path.hub && path.hub.file) || (state && state.file);
    const {types} = this;
    const pluginState = this.getPluginState(state);
    if (!types.isIdentifier(node[prop])) return;
    if (pluginState.specified[node[prop].name] &&
      path.scope.hasBinding(node[prop].name) &&
      path.scope.getBinding(node[prop].name).path.type === 'ImportSpecifier'
    ) {node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState);
    }
  }

ArrayExpression

ArrayExpression(path, state) {const { node} = path;
    const props = node.elements.map((_, index) => index);
    this.buildExpressionHandler(node.elements, props, path, state);
  }

ArrayExpression(数组表达式),接口如下所示

interface ArrayExpression {
    type: 'ArrayExpression';
    elements: ArrayExpressionElement[];}
/**
 * 解决相似:* [Button, Select, Input]
 */

本例的解决和方才的其余节点不太一样,因为数组的 Element 自身就是一个数组模式,并且咱们须要转换的援用都是数组元素,因而这里传递的 props 就是相似 [0, 1, 2, 3] 的纯数组,不便后续从 elements 中进行取数据。这里进行具体转换的办法是 buildExpressionHandler,在后续的 AST 节点解决中将会频繁呈现

buildExpressionHandler(node, props, path, state) {const file = (path && path.hub && path.hub.file) || (state && state.file);
    const {types} = this;
    const pluginState = this.getPluginState(state);
    props.forEach(prop => {if (!types.isIdentifier(node[prop])) return;
      if (pluginState.specified[node[prop].name] &&
        types.isImportSpecifier(path.scope.getBinding(node[prop].name).path)
      ) {node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState); 
      }
    });
  }

首先对 props 进行遍历,同样确保传递的属性是根底的 Identifier 类型且是 import 绑定的援用后便进入 importMethod 进行转换,和之前的 buildDeclaratorHandler 办法差不多,只是 props 是数组模式

LogicalExpression

LogicalExpression(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['left', 'right'], path, state);
  }

LogicalExpression(逻辑运算符表达式)

interface LogicalExpression {
    type: 'LogicalExpression';
    operator: '||' | '&&';
    left: Expression;
    right: Expression;
}
/**
 * 解决相似:* antd && 1
 */

次要取出逻辑运算符表达式的左右两边的变量,并应用 buildExpressionHandler 办法进行转换

ConditionalExpression

ConditionalExpression(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['test', 'consequent', 'alternate'], path, state);
  }

ConditionalExpression(条件运算符)

interface ConditionalExpression {
    type: 'ConditionalExpression';
    test: Expression;
    consequent: Expression;
    alternate: Expression;
}
/**
 * 解决相似:* antd ? antd.Button : antd.Select;
 */

次要取出相似三元表达式的元素,同用 buildExpressionHandler 办法进行转换。

IfStatement

IfStatement(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['test'], path, state);
    this.buildExpressionHandler(node.test, ['left', 'right'], path, state);
  }

IfStatement(if 语句)

interface IfStatement {
    type: 'IfStatement';
    test: Expression;
    consequent: Statement;
    alternate?: Statement;
}
/**
 * 解决相似:* if(antd){ }
 */

这个节点绝对比拟非凡,但笔者不明确为什么要调用两次 buildExpressionHandler,因为笔者所想到的可能性,都有其余的 AST 入口能够解决。望通晓的读者可进行科普。

ExpressionStatement

ExpressionStatement(path, state) {const { node} = path;
    const {types} = this;
    if (types.isAssignmentExpression(node.expression)) {this.buildExpressionHandler(node.expression, ['right'], path, state);
    }
 }

ExpressionStatement(表达式语句)

interface ExpressionStatement {
    type: 'ExpressionStatement';
    expression: Expression;
    directive?: string;
}
/**
 * 解决相似:* module.export = antd
 */

ReturnStatement

ReturnStatement(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['argument'], path, state);
  }

ReturnStatement(return 语句)

interface ReturnStatement {
    type: 'ReturnStatement';
    argument: Expression | null;
}
/**
 * 解决相似:* return lodash
 */

ExportDefaultDeclaration

ExportDefaultDeclaration(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['declaration'], path, state);
  }

ExportDefaultDeclaration(导出默认模块)

interface ExportDefaultDeclaration {
    type: 'ExportDefaultDeclaration';
    declaration: Identifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration;
}
/**
 * 解决相似:* return lodash
 */

BinaryExpression

BinaryExpression(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['left', 'right'], path, state);
  }

BinaryExpression(二元操作符表达式)

interface BinaryExpression {
    type: 'BinaryExpression';
    operator: BinaryOperator;
    left: Expression;
    right: Expression;
}
/**
 * 解决相似:* antd > 1
 */

NewExpression

NewExpression(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['callee', 'arguments'], path, state);
  }

NewExpression(new 表达式)

interface NewExpression {
    type: 'NewExpression';
    callee: Expression;
    arguments: ArgumentListElement[];}
/**
 * 解决相似:* new Antd()
 */

ClassDeclaration

ClassDeclaration(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['superClass'], path, state);
  }

ClassDeclaration(类申明)

interface ClassDeclaration {
    type: 'ClassDeclaration';
    id: Identifier | null;
    superClass: Identifier | null;
    body: ClassBody;
}
/**
 * 解决相似:* class emaple extends Antd {...}
 */

Property

Property(path, state) {const { node} = path;
    this.buildDeclaratorHandler(node, ['value'], path, state);
  }

Property(对象的属性值)

/**
 * 解决相似:* const a={
 *  button:antd.Button
 * }
 */

解决完 AST 节点后,删除掉本来的 import 导入,因为咱们曾经把旧 import 的 path 保留在 pluginState.pathsToRemove 中,最佳的删除的机会便是 ProgramExit,应用 path.remove() 删除。

ProgramExit(path, state) {this.getPluginState(state).pathsToRemove.forEach(p => !p.removed && p.remove());
}

祝贺各位保持看到当初的读者,曾经到最初一步啦,把咱们所解决的所有 AST 节点类型注册到观察者中

export default function({types}) {
  let plugins = null;
  function applyInstance(method, args, context) {...}
  const Program = {...}

  // 补充注册 AST type 的数组
  const methods = [ 
    'ImportDeclaration'
    'CallExpression',
    'MemberExpression',
    'Property',
    'VariableDeclarator',
    'ArrayExpression',
    'LogicalExpression',
    'ConditionalExpression',
    'IfStatement',
    'ExpressionStatement',
    'ReturnStatement',
    'ExportDefaultDeclaration',
    'BinaryExpression',
    'NewExpression',
    'ClassDeclaration',
  ]

  const ret = {visitor: { Program}, 
  };

  for (const method of methods) {...}

}

到此曾经残缺剖析完 babel-plugin-import 的整个流程,读者能够从新捋一捋解决按需加载的整个解决思路,其实抛去细节,主体逻辑还是比拟简单明了的。

四、一些思考

笔者在进行源码与单元测试的浏览后,发现插件并没有对 Switch 节点进行转换,遂向官网仓库提了 PR,目前曾经被合入 master 分支,读者有任何想法,欢送在评论区畅所欲言。笔者次要补了 SwitchStatementSwitchCase 与两个 AST 节点解决。

SwitchStatement

SwitchStatement(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['discriminant'], path, state);
}

SwitchCase

SwitchCase(path, state) {const { node} = path;
    this.buildExpressionHandler(node, ['test'], path, state);
}

五、小小总结

这是笔者第一次写源码解析的文章,也因笔者能力无限,如果有些逻辑论述的不够清晰,或者在解读过程中有谬误的,欢送读者在评论区给出倡议或进行纠错。

当初 babel 其实也出了一些 API 能够更加简化 babel-plugin-import 的代码或者逻辑,例如:path.replaceWithMultiple,但源码中一些看似多余的逻辑肯定是有对应的场景,所以才会被加以保留。

此插件禁受住了工夫的考验,同时对有须要开发 babel-plugin 的读者来说,也是一个十分好的事例。不仅如此,对于性能的边缘化解决以及操作系统的兼容等细节都有做欠缺的解决。

如果仅仅须要应用 babel-plugin-import,此文展现了一些在 babel-plugin-import 文档中未裸露的 API,也能够帮忙插件使用者实现更多扩大性能,因而笔者推出了此文,心愿能帮忙到各位同学。


本文首发于:数栈研习社

数栈是云原生—站式数据中台 PaaS,咱们在 github 上有一个乏味的开源我的项目:FlinkX。FlinkX 是一个基于 Flink 的批流对立的数据同步工具,既能够采集动态的数据,比方 MySQL,HDFS 等,也能够采集实时变动的数据,比方 MySQL binlog,Kafka 等,是全域、异构、批流一体的数据同步引擎,大家如果有趣味,欢送来 github 社区找咱们玩~

退出移动版