babel-plugin-import 实现按需引入

news2024/10/2 8:34:28

 官方文档:https://github.com/umijs/babel-plugin-import#usage

为什么需要这个插件?
在 antd 和 element 两个组件库中,index.js 分别是这样的:

// antd
export { default as Button } from './button';
export { default as Table } from './table';

// element
import Button from '../packages/button/index.js';
import Table from '../packages/table/index.js';
export default {
  Button,
  Table
}

antd 和 element 都是通过 ES6 module 的 export 来导出带有命名的各个组件,因此我们可以通过 import 导入单组件的 JS 文件,但是我们还需要手动引入组件的样式:

import Button from 'antd/lib/button';
import 'antd/dist/antd.css';

在上面的代码中,我们仅仅只需要一个 Button 组件,却把所有的样式都引入了,这明显是不合理的,会增加代码包的体积。

当然我们也可以只引入单个组件的样式:

import Button from 'antd/lib/button';
import 'antd/lib/button/style';

这样看上去没毛病,但是如果需要多个组件的时候,代码就显得不够优雅:

import { Affix, Avatar, Button, Rate } from 'antd';

import 'antd/lib/affix/style';
import 'antd/lib/avatar/style';
import 'antd/lib/button/style';
import 'antd/lib/rate/style';

这时候就应该思考一下,如何在引入组件的时候自动引入它的样式文件

这个插件做了什么?
简单来说,babel-plugin-import 就是解决了上面的问题,为组件库实现单组件按需加载并且自动引入其样式。

import { Button } from 'antd';

// 插件会自动将代码转换为按需引入的形式
import Button from 'antd/lib/button';
import 'antd/lib/button/style';

这个插件怎么用
简单来说只需关系三个参数即可。

module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant', // 包名
      libraryDirectory: 'es', // 目录,默认 lib
      style: true // 是否引入 style
    }, 'vant']
  ]
};

babel-plugin-import 具体实现按需加载思路如下:将代码解析成AST,去AST里面找ImportDeclaration,若是source对应的value为是该插件配置的libraryName,那么就是要处理的,之后看该引用有没有真正使用,未使用直接删掉引用,使用了,删掉引用语句,借助于babel相应plugin重新插入引用语句,比如引用对应目录下的组件及样式。

第一步 依赖收集

babel-plubin-import 会在 ImportDeclaration 里将所有的 specifier 收集起来。

可以从这个 ImportDeclaration 语句中提取几个关键点:

  • source.value: antd
  • specifier.local.name: Button
  • specifier.local.name: Rate

需要做的事情也很简单:

  1. import 的包是不是 antd,也就是 libraryName
  2. 把 Button 和 Rate 收集起来

 

来看代码:

ImportDeclaration(path, state) {
  const { node } = path;
  if (!node) return;
  // 代码里 import 的包名
  const { value } = node.source;
  // 配在插件 options 的包名
  const { libraryName } = this;
  // babel-type 工具函数
  const { types } = this;
  // 内部状态
  const pluginState = this.getPluginState(state);
  // 判断是不是需要使用该插件的包
  if (value === libraryName) {
    // node.specifiers 表示 import 了什么
    node.specifiers.forEach(spec => {
      // 判断是不是 ImportSpecifier 类型的节点,也就是是否是大括号的
      if (types.isImportSpecifier(spec)) {
        // 收集依赖
        // 也就是 pluginState.specified.Button = Button
        // local.name 是导入进来的别名,比如 import { Button as MyButton } from 'antd' 的 MyButton
        // imported.name 是真实导出的变量名
        pluginState.specified[spec.local.name] = spec.imported.name;
      } else {
        // ImportDefaultSpecifier 和 ImportNamespaceSpecifier
        pluginState.libraryObjs[spec.local.name] = true;
      }
    });
    pluginState.pathsToRemove.push(path);
  }
}

待 babel 遍历了所有的 ImportDeclaration 类型的节点之后,就收集好了依赖关系,下一步就是如何加载它们了。

第二步 判断是否使用

收集了依赖关系之后,得要判断一下这些 import 的变量是否被使用到了,我们这里说一种情况。

我们知道,JSX 最终是变成 React.createElement() 执行的:

ReactDOM.render(<Button>Hello</Button>);

      ↓ ↓ ↓ ↓ ↓ ↓

React.createElement(Button, null, "Hello");

没错,createElement 的第一个参数就是我们要找的东西,我们需要判断收集的依赖中是否有被 createElement 使用。

分析一下这行代码的 ast,很容易就找到这个节点:

 

来看代码:

CallExpression(path, state) {
  const { node } = path;
  const file = (path && path.hub && path.hub.file) || (state && state.file);
  // 方法调用者的 name
  const { name } = node.callee;
  // babel-type 工具函数
  const { types } = this;
  // 内部状态
  const pluginState = this.getPluginState(state);

  // 如果方法调用者是 Identifier 类型
  if (types.isIdentifier(node.callee)) {
    if (pluginState.specified[name]) {
      node.callee = this.importMethod(pluginState.specified[name], file, pluginState);
    }
  }

  // 遍历 arguments 找我们要的 specifier
  node.arguments = node.arguments.map(arg => {
    const { name: argName } = arg;
    if (
      pluginState.specified[argName] &&
      path.scope.hasBinding(argName) &&
      path.scope.getBinding(argName).path.type === 'ImportSpecifier'
    ) {
      // 找到 specifier,调用 importMethod 方法
      return this.importMethod(pluginState.specified[argName], file, pluginState);
    }
    return arg;
  });
}

除了 React.createElement(Button) 之外,还有 const btn = Button / [Button] ... 等多种情况会使用 Button,源码中都有对应的处理方法,感兴趣的可以自己看一下: babel-plugin-import/Plugin.js at master · umijs/babel-plugin-import · GitHub ,这里就不多说了。

第三步 生成引入代码(核心)

第一步和第二步主要的工作是找到需要被插件处理的依赖关系,比如:

import { Button, Rate } from 'antd';
ReactDOM.render(<Button>Hello</Button>);

Button 组件使用到了,Rate 在代码里未使用。所以插件要做的也只是自动引入 Button 的代码和样式即可。

我们先回顾一下,当我们 import 一个组件的时候,希望它能够:

import { Button } from 'antd';

      ↓ ↓ ↓ ↓ ↓ ↓

var _button = require('antd/lib/button');
require('antd/lib/button/style');

并且再回想一下插件的配置 options,只需要将 libraryDirectory 以及 style 等配置用上就完事了。

小朋友,你是否有几个问号?这里该如何让 babel 去修改代码并且生成一个新的 import 以及一个样式的 import 呢,不慌,看看代码就知道了:

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

importMethod(methodName, file, pluginState) {
  if (!pluginState.selectedMethods[methodName]) {
    // libraryDirectory:目录,默认 lib
    // style:是否引入样式
    const { style, libraryDirectory } = this;

    // 组件名转换规则
    // 优先级最高的是配了 camel2UnderlineComponentName:是否使用下划线作为连接符
    // camel2DashComponentName 为 true,会转换成小写字母,并且使用 - 作为连接符
    const transformedMethodName = this.camel2UnderlineComponentName
      ? transCamel(methodName, '_')
      : this.camel2DashComponentName
      ? transCamel(methodName, '-')
      : methodName;
    // 兼容 windows 路径
    // path.join('antd/lib/button') == 'antd/lib/button'
    const path = winPath(
      this.customName
        ? this.customName(transformedMethodName, file)
        : join(this.libraryName, libraryDirectory, transformedMethodName, this.fileName),
    );
    // 根据是否有导出 default 来判断使用哪种方法来生成 import 语句,默认为 true
    // addDefault(path, 'antd/lib/button', { nameHint: 'button' })
    // addNamed(path, 'button', 'antd/lib/button')
    pluginState.selectedMethods[methodName] = this.transformToDefaultImport
      ? addDefault(file.path, path, { nameHint: methodName })
      : addNamed(file.path, methodName, path);
    // 根据不同配置 import 样式
    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) {
      addSideEffect(file.path, `${path}/style`);
    } else if (style === '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] };
}

addSideEffectaddDefault 和 addNamed 是 @babel/helper-module-imports 的三个方法,作用都是创建一个 import 方法,具体表现是:

addSideEffect

addSideEffect(path, 'source');

      ↓ ↓ ↓ ↓ ↓ ↓

import "source"

addDefault

addDefault(path, 'source', { nameHint: "hintedName" })

      ↓ ↓ ↓ ↓ ↓ ↓

import hintedName from "source"

addNamed

addNamed(path, 'named', 'source', { nameHint: "hintedName" });

      ↓ ↓ ↓ ↓ ↓ ↓

import { named as _hintedName } from "source"

更多关于 @babel/helper-module-imports 见:@babel/helper-module-imports

总结

一起数个 1 2 3,babel-plugin-import 要做的事情也就做完了。

我们来总结一下,babel-plugin-import 和普遍的 babel 插件一样,会遍历代码的 ast,然后在 ast 上做了一些事情:

  1. 收集依赖:找到 importDeclaration,分析出包 a 和依赖 b,c,d....,假如 a 和 libraryName 一致,就将 b,c,d... 在内部收集起来
  2. 判断是否使用:在多种情况下(比如文中提到的 CallExpression)判断 收集到的 b,c,d... 是否在代码中被使用,如果有使用的,就调用 importMethod 生成新的 impport 语句
  3. 生成引入代码:根据配置项生成代码和样式的 import 语句

不过有一些细节这里就没提到,比如如何删除旧的 import 等... 感兴趣的可以自行阅读源码哦。

看完一遍源码,是不是有发现,其实除了 antd 和 element 等大型组件库之外,任意的组件库都可以使用 babel-plugin-import 来实现按需加载和自动加载样式。

没错,比如我们常用的 lodash,也可以使用 babel-plugin-import 来加载它的各种方法,可以动手试一下。

核心支持类:

@babel/parser

它是Babel中使用的JavaScript解析器。默认启用ES2017,支持JSX,Flow,TypeScript,支持实验性的语言提议(至少是stage-0)

@babel/traverse

它实现了访问者模式,对AST进行遍历,插件可以通过它获取相应的AST节点,并对对应节点进行具体操作。

@babel/generator

它将AST转换成源代码,同时支持SourceMap

参考

简单实现 babel-plugin-import 插件 - axuebin - 博客园

用 babel-plugin 实现按需加载 - 知乎

庖丁解牛:最全babel-plugin-import源码详解 - 掘金

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7560.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智能云门禁解决方案来了

传统门禁存在的问题 01、安全性差&#xff1a;传统门禁卡易被复制和盗用无法精准识别进出人员身份造成较大安全隐患。 02、通行不便&#xff1a;要求人员近距离操作&#xff0c;当使 用者双手被占用时通行不便 门禁卡丢失或密码遗忘造成 无法开门。 03、管理困难&#xff1a…

Multilevel Cooperative Coevolution for Large Scale Optimization

0、论文背景 本文在CCEA_G的基础上&#xff0c;提出了MLCC框架。在MLCC中&#xff0c;基于不同组大小的随机分组策略构造了一组问题分解器。演化过程分为若干个循环&#xff0c;在每个周期开始时&#xff0c;MLCC使用自适应机制根据其历史性能选择分解器。由于不同的组大小捕获…

数据结构-图的存储结构

目录 图的存储结构邻接矩阵邻接表图的邻接矩阵和邻接表两种存储结构各有什么优缺点?图的存储结构 邻接矩阵 邻接矩阵的主要特点:

spring7:总结56

1.handler的形参解析&#xff08;即如何把请求参数转化为形参&#xff09; 注解参数解析原理 model and map解析原理 自定义对象处理原理 2.数据响应原理&#xff08;即如何把返回值传给前端&#xff09; 整体返回原理 详解其中的内容协商流程&#xff08;基于请求头&#x…

数据库分区的通俗解释

关于数据库分区&#xff0c;分表&#xff0c;分库&#xff0c;我通俗易懂的来举几个栗子&#xff0c;看过还不懂&#xff0c;你打我。。。 村里一家四口人(老爹叫A)有两儿子(分别是A1&#xff0c;A2)&#xff0c;长大了要自己种地了&#xff0c;就嚷嚷着要分家&#xff0c;把村…

虚拟机(Vmware)磁盘扩容(xfs格式)

先将虚拟机关机&#xff0c;按上图调整虚拟磁盘大小。 1.开启并进入虚拟机&#xff0c;打开终端&#xff0c;输入命令 df -Th 查看格式&#xff0c;图示中 /dev/mapper/centos-root 类型为xfs。 [mangolocalhost ~]$ df -Th Filesystem Type Size Used Ava…

luffy-(9)

内容概览 redis图形化客户端redis字符串操作redis hash操作redis列表操作redis管道redis其他操作django中集成rediscelery介绍 redis图形化客户端 安装图形化客户端redis-desktop-manager 新版本收费&#xff0c;可以使用老版本 QT平台&#xff1a;可以写图形化界面 python&…

腾讯云~Kafka 监控 Kafka Eagle 图形化版本

文章目录1. 安装包下载2. 开启kafka JMX3. 安装JDK&#xff0c;配置JAVA_HOME4. 上传安装包、解压5. 配置Kafka-eagle环境变量6. 配置Kafka_eagle7. 配置ke.sh8. 启动Kafka_eagle9. 防火墙10. 访问Kafka eagle1. 安装包下载 官网地址&#xff1a;EFAK 本文使用3.0.1版本 2. …

【保姆级·创建对象】如何利用resolveBeforeInstantiation()在预处理阶段返回一个Bean的实例对象

前情回顾 之前有篇文章我们有详细介绍了prepareMethodOverrides()方法并详细例举了一个lookup-method标签的例子 【保姆级】lookup-method标签实践与分析_AQin1012的博客-CSDN博客 本文我们来盘盘prepareMethodOverrides()方法后面的resolveBeaforeInstantiation()的函数&…

Android中SQLite数据库增删改查/使用ListView显示数据库内容(有完整源码)

android作业笔记 文章目录效果展示一、前言源码获取实验功能描述注意事项实现步骤二、代码展示activity_main.xml布局文件MyOpenHelper.javaMainActivity.javaList_item.xml三、&#xff08;补充&#xff09;ListView实现数据列表显示效果展示 编写SQLite数据库相关操作的代码…

【附源码】Python计算机毕业设计数据时代下的疫情管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

达梦数据库,数据库重置主键id从1开始

一、前言 今天中国国寿XX项目XC环境达梦遇到id主键自增顺序不对的问题&#xff0c;那么如何修改一个表的自增主键顺序呢&#xff1f;下边通过具体测试案例进行深入分析&#xff0c;通过delete/update/truncate/alter观察数据的变换总结出结论&#xff0c;欢迎各位喜欢达梦数据…

值得推荐的小型 C 语言开源项目:Triggerhappy

这几天在知乎上看到了一个好问题&#xff1a; 有哪些值得推荐的小型 C 语言开源项目&#xff1f; 题主很可能是想要一个这样的开源项目&#xff1a;功能小巧、代码质量高&#xff0c;可读性好&#xff0c;以便自己循序渐进地学习 C 语言。 作为一个嵌入式开发人员&#xff0…

趁年轻,大胆闯

趁年轻&#xff0c;大胆闯如果我是20岁&#xff0c;我会拿出未来的十年&#xff0c;全力已赴的赚钱&#xff0c;折腾&#xff0c;不要任何安全感。 出来创业&#xff0c;就是为100倍以上的赔率来的。

HTTPS

一、HTTPS是什么 HTTPS也是一个应用层协议&#xff0c;是在HTTP协议的基础上引入了一个加密层。 由于HTTP协议内容一般都是本文方式明文传输的&#xff0c;这就导致它在传输过程中会出现被篡改的情况。 经典案例就是万恶的“运营商劫持”&#xff01; 除了运营商可以劫持&a…

如何快速从零开始搭建一个前端项目

2022 年了&#xff0c;如何快速从零开始搭建一个合适的前端项目&#xff1f; 准备工作 首先本地需要安装好 node 环境以及包管理工具&#xff0c;推荐直接使用 pnpm&#xff0c;也可以通过 pnpm 来直接管理 nodejs 版本。 pnpm 安装&#xff1a; # Mac or Linux curl -fsSL…

SpringMVC 环境配置

文章目录引入1、MVC的概念2、Spring MVC基本原理一、导入坐标&#xff08;导包&#xff09;导入Spring MVC所需要的jar包二、新建springmvc-config.xml文件三、配置web.xml四、 创建Controller五、配置SpringMVC配置文件六、配置页面其他引入 Spring Web MVC是一种基于Java的实…

cv算法工程师学习教程

前言一&#xff0c;计算机系统 1.1&#xff0c;计算机系统书籍1.2&#xff0c;设计模式教程 二&#xff0c;编程语言 2.1&#xff0c;C 学习资料2.2&#xff0c;Python 学习资料 三&#xff0c;数据结构与算法 3.1&#xff0c;数据结构与算法课程3.2&#xff0c;算法题解 四&am…

数字信号处理及python实现(三)

数字信号处理及python实现三抽样引起的混叠抽样的频域视图样本重建信号拟合正弦波线性与多项式内插理想低通滤波器这是参考知乎的数字信号处理及matlab实现的python实现版本&#xff0c;参考连接 上一期:数字信号处理及python实现(二) 项目文件结构 test为测试文件&#xff…

【Vue】Vue开发实战之我的笔记(ch18-ch27)--20221115

参考https://blog.csdn.net/yfm120750310/article/details/111353963 18 | 为什么需要Vuex 18.1 为什么需要Vuex provide和inject虽然能够实现层层传递的数据管理&#xff0c;但对于一个大的管理系统而言会显得有些繁琐&#xff0c;我们需要一个大型的状态管理系统。 Vuex不…