如何手写一个自己的babel插件,以及umi集成踩坑记

1,611 阅读4分钟

如果不是近期项目需要,急需通过babel这种自动化手段解决一些项目场景,可能还将会在很长一段时间被babel蒙在鼓里,什么ast一大串看不懂的玩意,对此深有误解,以为门槛太高,不敢轻易尝试,心想着以我目前的技术(菜鸟水平)写不出这么高深的东西。但是身处前端岗位,岂有不卷的道理,终于皇天不负有心人,今天也算是摸到了点babel插件的门道,真香;

背景

先来说说此次研究的背景,为啥突然卷起了babel插件。其实需求很简单,验收标准中有一项功能————所有表单输入框要做特殊字符处理,好家伙,所有?听到需求的我都惊呆了。然后搜索了一下Input,好家伙,上百个文件,都是直接从antd引入的input,即便是封装一个自己的Input框,也要改上百个文件,太吓人了;然后就整理了一下目前的诉求:

  1. 批量处理,把antd input换成 MyInput
  2. 解决后面的新人不知道这需求,还是直接从antd引入,需要二次加工的问题 思前想后,再没有比babel plugins更合适了,一键式解决所有后顾之忧;剩下的就是理清babel插件需要做的事情(把antd input 替换成自己封装的),步骤如下:
  3. 遇到import { Input } from 'antd',则将Input删除
  4. 基于ast语法生成语句:import myInput from 'myInputPath';
  5. 在下一行的位置插入生成后的语句;
  6. 灵活性,假如将来不只替换input,替换目标不只是antd,于是提供配置参数,动态获取 原理不多说了,直接上成品代码, ast在线解析入口

/**
 * 1,useage,在babelrc添加
 * {
 *   plugins: [
 *     ['./plugins/antdInputToCustom', {
 *       'antd': { // 需要转换的ui框架
 *         'Input': '@/component/Input', 需要替换的组件
 *       }
 *     }]
 *   ]
 * }
 *
 * 2, state,在babelrc中第二个参数的值,通过state.opts获取
 *
 *
 * @param t
 * @returns {{name: string, visitor: {ImportSpecifier(*, *): void}}}
 */
module.exports = function ({ types: t, template: template }) {
  return {
    name: 'antd-input-to-custome',
    visitor: {
      ImportSpecifier(path, state) {
        const parentBody = path.parentPath.parentPath.get('body');
        const uiType = path.parent.source.value;
        const componentType = path.node.local.name;
        const optional = state.opts || {};
        if (optional.hasOwnProperty(uiType) && optional[uiType][componentType]) {
           // 基于template写法,推荐,简单
            const templaStr = template(`import LEFT from RIGHT`);
            path.replaceWidth(templaStr({
                LEFT: componentType,
                RIGHT: optional[uiType][componentType],
            }))
          /*path.remove(); // 1. 将匹配目标删除
          // 2. types写法,基于ast语句生成 import xxx from 'xxx'
          const node = t.ImportDeclaration(
            [
              t.ImportDefaultSpecifier(t.Identifier(componentType)),
            ],
            t.StringLiteral(optional[uiType][componentType]) // 替换成目标文件
          );
          // 3. 在目标位置的下一行插入语句
          parentBody[0].insertAfter(node);
          */
        }
      },
    },
  };
};

umi集成babel的坑

普通的webpack项目,配置babel直接在项目根目录添加一个.babelrc,plugins写成本地插件的相对路径就完事了,但是umi这种零配置,开箱即用的框架,由于封装性太多,直接添加babelrc/babel.config.js不生效,那只能重温官网api,查看对外暴露的配置,终于发现一个叫extraBabelPlugins的,往项目里一加,

    import myBabelPlugin from 'myBabelPluginPath'; // 本地插件路径
    extraBabelPlugins: [
        myBabelPlugin, // 必须是import进来的实例,path.resolve('相对路径')将会报错;
    ]

添加完extraBabelPlugins,发现一个问题,插件是运行了,但是转换不彻底,经过一番周折,发现是跟dynamicImport冲突了,dynamicImport先于babel插件执行,导致动态加载部分的转换就失败了,关闭dynamicImport:false后虽然能正常,但是只生成一个umi.js,显示包太大了,不是我们想要的结果; 解决umi集成babelrc的终极方案(任选其一):

  1. 新建.env文件,开启umi对babelrc的识别开关,加入:BABELRC=true
  2. 新建babel.config.js,添加plugins

附码

## babel.config.js
module.exports = {
  "plugins": [
    ["./plugins/antdInputToCustom", {
      "antd": {
        "Input": "@/components/Input"
      }
    }]
  ]
}

## .env
BABELRC=true

总结

umi集成babel方案,归根到底是对webpack的上层封装,最终的配置项都会解析成webpack配置,所以单看umi api并不能很好地解决我们的问题,问题的本质还是得回到webpack本身,webpack纯天然支持.babelrc/babel.config.js,所以抛开umi api配置来看,如何让umi识别babelrc就是一个解决思路的方向,之前自己也是跑偏了方向,较真怎么通过umi自带功能如何解决,转换不彻底时如何调整插件执行顺序等,折腾了很久,绕了这么多弯路,结果就是这么一个BABELRC的问题,果然卷得还不够,思路不够开阔,继续卷吧

学习链接

github.com/jamiebuilds…