# babel-plugin-lodash-import

实现 lodash按需加载的 babel插件

# 前言

在平时的开发中,我们一般习惯使用这种方式导入 lodash

import lodash from "lodash";

lodash.debounce()

这种导入方式会将 lodash上的所有的方法都导入到文件中,导致文件的打包体积比较大。实际上,对于 lodash上绑定的属性方法,lodash仓库中都提供了一个对应的文件来导出此方法,如此,对于上面的导入方式,一个优化方式是修改成这样:

- import lodash from "lodash";
+ import debounce from "lodash/debounce";

- lodash.debounce()
+ debounce()

我们只导入了需要使用到的方法 debounce,刨除掉了其他的方法,这会大幅减少打包后的文件体积

# 插件转化

对于开发者而言,当然更喜欢采用简单的导入方式,怎么方便怎么来,但是这样又导致了打包的文件体积过大,那么有什么办法可以兼顾到这二者呢,既想书写方便,又期望减小打包体积? babel插件此时就可以发挥作用了,在打包的时候悄无声息的把代码替换一下即可。这里借鉴 antdbabel-plugin-import的实现思路。

# 实现

这里要解决的问题主要有两个

  1. 转换导入的方式

import lodash from "lodash";转化成 import xxx from "lodash/xxx";

  1. 转换调用的方式

lodash.debounce() 转换成 debounce()的方式调用

实际上,这两个问题可以合并一下,当碰到 import lodash from "lodash";这个语句的时候,直接从 ast树中删除掉即可,当碰到 lodash.debounce()这个语句时候需要做两个事情:

  1. 在文件头部插入 import xxx from "lodash/xxx";这样的语句
  2. 修改调用语句为 xxx()

接下来实现起来就比较简单了

# import xxx from 'lodash'

首先处理 import lodash from 'lodash'import _ from 'lodash' 这样的导入语句

当碰到这样的导入语句时候就删除掉:

ImportDeclaration(path, state, scope) {
  const specifiers = path.get("specifiers");
  const source = path.get("source");
  if (
    specifiers && specifiers.length === 1 &&
    t.isImportDefaultSpecifier(specifiers[0]) &&
    (t.isIdentifier(specifiers[0].node.local, { name: "lodash" }) || t.isIdentifier(specifiers[0].node.local, { name: "_" })) &&
    t.isStringLiteral(source.node, { value: "lodash" })
  ) {
    path.remove()
  }
}

然后对于类似 lodash.debounce()调用,使用如下方法:

{
  pre(state) {
    this.propertyNames = [];
    this.fpPropertyNames = [];
    this.namespaceSpecifier = [];
    this.moduleVals = ['lodash', '_']
  },
  visitor: {
    CallExpression(path, state, scope) {
      if (
        path.node.callee && path.node.callee.object &&
        this.moduleVals.includes(path.node.callee.object.name)
      ) {
        const propertyName = path.get("callee.property").node.name;
        if (!this.propertyNames.includes(propertyName)) {
          // 把属性名存储起来,方便后续添加导入语句
          this.propertyNames.push(propertyName);
        }
        path.get("callee").replaceWith(t.identifier(propertyName));
      }
    },
  }
}

主要方法是这么两句:

1、把 lodash的属性名存储起来,方便后续添加导入语句 import xxx from 'lodash/xxx'

this.propertyNames.push(propertyName);

2、替换语句

path.get("callee").replaceWith(t.identifier(propertyName));

# 添加导入语句

在上面的代码中,我们把 lodash的属性方法 propertyName 存储到 this.propertyNames变量中,现在要使用这些变量在文件头部插入导入语句:

import xxx from "lodash/xxx"

具体实现如下:

module.exports = ({ types: t, template }) => {
  return {
    // ...
    post(state) {
      this.propertyNames.forEach((name) => {
        state.path.node.body.unshift(buildImportDeclaration(t, name, `lodash/${name}`));
      });
    },
  };
};

function buildImportDeclaration(t, name, source) {
  return t.importDeclaration(
    [t.importDefaultSpecifier(t.identifier(name))],
    t.stringLiteral(`${source}`)
  );
}

post方法会在插件遍历完文件后执行,在此方法中,遍历了 this.propertyNames,对于每一个属性名执行 buildImportDeclaration函数,这个函数的作用就是创建一个 import节点,具体使用方法可参考babel-types/import (opens new window)

# 全量导入

以上情况处理了默认导入 import lodash from 'lodash',还会存在一些其他的导入方法,比如全量导入:

import * as lodash from 'lodash'

lodash.debounce()

对于这种情况处理方式和上面的也很类似,需要修改两个地方

  1. 遍历到全量导入的情况需要删除此节点,并把导入的变量存储起来,方便后续在调用语句中判断,是否使用了了该导入变量中的方法:
// import * as lodash from 'lodash'
ImportDeclaration(path, state, scope) {
  if (
    specifiers && specifiers.length === 1 &&
    t.isImportNamespaceSpecifier(specifiers[0]) &&
    t.isStringLiteral(source.node, { value: "lodash" })
  ) {
    // 存储全量导入的变量名
    this.namespaceSpecifier.push(specifiers[0].node.local.name)
    path.remove()
  }
}
  1. 遍历到调用语句,需要添加这个判断 this.namespaceSpecifier.includes(path.node.callee.object.name),这种情况也是需要添加导入语句的
CallExpression(path, state, scope) {
  if (
    path.node.callee && path.node.callee.object &&
    (this.moduleVals.includes(path.node.callee.object.name) || this.namespaceSpecifier.includes(path.node.callee.object.name))
  ) {
    ...
  }
},

# 收效

这里展示一下在项目中实际使用此插件的效果。

使用插件前的打包文件:

prev

使用插件后:

next

可以发现,使用插件后,在引入了lodash的文件打包后可以减少 90kb 左右的体积,收益还是挺明显的。

# 总结

以上就是一个 lodash插件的全部内容了,总的来说还是比较容易的,主要还是需要对 babel的概念和相关 api有一定的了解。

实际上官方也提供了一个类似功能的插件 babel-plugin-lodash (opens new window),实际使用建议使用官方插件,此插件可作为学习实践材料。

本文代码地址可在此查看 (opens new window)

最后更新时间: 6/21/2021, 5:27:24 PM