babel-plugin-import 实现按需引入

这篇具有很好参考价值的文章主要介绍了babel-plugin-import 实现按需引入。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

组件库按需加载原理分析 - 掘金

组件库按需加载:

 组件库以组件为基本单位产出 js、css、less 文件,借助插件或者部分引入的写法,使得项目代码或 babel 编译后的代码中只包含使用到的组件的 js、css、less 等。 

babel-plugin-import 实现按需引入

 webpack 懒加载: webpack 将源码中的 import、require 引入的文件编译之后再根据动态加载语法配置(通常以页面路由为基本单位)将较大的代码拆分并构建出较小的 chunk 包,应用在运行时执行到相应业务逻辑时才去加载执行对应 chunk 代码。 webpack 懒加载主要发生在下图的 JS 拆分出不同的 Chunk 这一过程中。

babel-plugin-import 实现按需引入

 两者的差别主要在于:

  1. 两者执行时机不同,组件库按需加载是在源码编写阶段或者 babel 编译 js 阶段,而 webpack 懒加载则是在构建生成打包产物时,组件库按需加载在前,webpack 懒加载在后;
  2. 两者原理不同,组件库按需加载是在源码阶段就去掉了无关代码,而 webpack 懒加载则是将经过 tree-shaking 优化过后的大文件包进行拆分在适当的运行时进行按需加载。

使用组件库:通常是引入min.js及css文件

import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';

Vue.use(Vant);

这种写法经过 webpack 构建之后会将组件库产出的 vant.min.jsindex.css 引入并打包至构建产物中,而引入的 vant.min.js 文件是包含组件库全部组件的 js 部分,index.css 包含全部组件的 css 部分。因此,这会导致构建打包产物增大。

组件库按需加载:

方式一:手动加载

手动引入需要使用到的组件以及其对应的样式文件即可,在 webpack 构件时组件库中其他未被引入的文件不会被打包。

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

组件库怎么实现产出为lib下对应组件?

elementUI:

//webpack.component.js
const Components = require('../components.json');
const webpackConfig = {
  mode: 'production',
  entry: Components,
  output: {
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/',
    filename: '[name].js',
    chunkFilename: '[id].js',
    libraryTarget: 'commonjs2'
  },
...
}


//components.json:
{
  "pagination": "./packages/pagination/index.js",
  "dialog": "./packages/dialog/index.js",
  "autocomplete": "./packages/autocomplete/index.js",
  "dropdown": "./packages/dropdown/index.js",
  "dropdown-menu": "./packages/dropdown-menu/index.js",
...
}

lib下还会生成整体的组件min.js及css:


module.exports = {
  mode: 'production',
  entry: {
    app: ['./src/index.js']
  },
  output: {
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/',
    filename: 'element-ui.common.js',
    chunkFilename: '[id].js',
    libraryExport: 'default',
    library: 'ELEMENT',
    libraryTarget: 'commonjs2'
  },
...
}

antd: 

方式二:自动加载

安装 babel-plugin-import 插件

npm i babel-plugin-import -D

修改 babel 插件配置

module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
};

在项目代码中按需引入要用到的组件

import { Button } from 'vant';
Vue.use(Button);

自动转换为

import "vant/es/button/style";
import _Button from "vant/es/button";

为什么需要这个插件?
在 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';

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

//在 babel 配置文件 .babelrc or babel-loader 中配置
module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant', // 包名
      libraryDirectory: 'es', // 目录,默认 lib
      style: true // 是否引入 style
    }, 'vant']
  ]
};


//多个包
// 如果是 babel@6 版本,可以将 import.options 配置为一个数组:
[
  {
    "libraryName": "antd",
    "libraryDirectory": "lib",
    "style": true
  },
  {
    "libraryName": "antd-mobile"
  },
]

// 如果是 babel@7+ 版本,可以配置多个 `import` 插件实例:
{
  "plugins": [
    ["import", { "libraryName": "antd", "libraryDirectory": "lib"}, "antd"],
    ["import", { "libraryName": "antd-mobile", "libraryDirectory": "lib"}, "antd-mobile"]
  ]
}

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

转换为 AST 语法树后结构如下:(语法转换可以在这里尝试:astexplorer.net)

 

{
  "type": "Program",
  "body": [
    {
      "type": "ImportDeclaration",
      "specifiers": [
        {
          "type": "ImportSpecifier",
          "imported": {
            "type": "Identifier",
            "name": "Button"
          },
          "local": {
            "type": "Identifier",
            "name": "Button"
          }
        },
        {
          "type": "ImportSpecifier",
          "imported": {
            "type": "Identifier",
            "name": "Input"
          },
          "local": {
            "type": "Identifier",
            "name": "Input"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "value": "antd",
      }
    }
  ],
  "sourceType": "module"
}

以上 JSON 数据中,我们关注以下几个跟 import 导入有关的信息:

  • source.value:antd;
  • specifiers.imported.name:Button;
  • specifiers.local.name: Button;

babel-plugin-import 的核心实现都在 Plugin 中:

  • 收集 import 语句 { xxx } 中的模块名称;
  • 分析模块导入后,是否被 call 使用到
  • 如果有被使用到,改写 import 语句,使得 path 具体到模块的所在目录。
// Plugin.js

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

function transCamel(_str, symbol) {
  const str = _str[0].toLowerCase() + _str.substr(1);
  return str.replace(/([A-Z])/g, $1 => `${symbol}${$1.toLowerCase()}`);
}

function winPath(path) {
  return path.replace(/\\/g, '/');
}

export default class Plugin {
  constructor(
    libraryName, // 需要使用按需加载的包名
    libraryDirectory = 'lib', // 按需加载的目录
    style = false, // 是否加载样式
    types, // babel-type 工具函数
    index = 0,
  ) {
    this.libraryName = libraryName;
    this.libraryDirectory = libraryDirectory;
    this.style = style;
    this.types = types;
    this.pluginStateKey = `importPluginState${index}`;
  }

  // 获取内部状态,收集依赖,state 指向 plugin.visitor
  getPluginState(state) {
    if (!state[this.pluginStateKey]) {
      state[this.pluginStateKey] = {};
    }
    return state[this.pluginStateKey];
  }

  // 生成按需引入 import 语句(核心代码)
  importMethod(methodName, file, pluginState) {
    ...
  }

  ProgramEnter(path, state) {
    const pluginState = this.getPluginState(state);
    // 初始化插件实例的 state 对象
    pluginState.specified = Object.create(null);
    pluginState.libraryObjs = Object.create(null);
    pluginState.selectedMethods = Object.create(null);
    pluginState.pathsToRemove = [];
  }

  ProgramExit(path, state) {
    // 删除旧的 import
    this.getPluginState(state).pathsToRemove.forEach(p => !p.removed && p.remove());
  }

  // import 语句的处理方法,收集 import { xxx } 中的模块
  ImportDeclaration(path, state) {
    ...
  }

  // import 的模块被使用时的处理方法。上面收集了依赖之后,得判断这些 import 的变量是否被用到
  // 比如使用方式为:React.createElement(Button, null, "Hello"); 可将这行代码转换为 AST 节点树结合更容易理解 CallExpression 做的事情
  CallExpression(path, state) {
    ...
  }
}

第一步 依赖收集

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

babel-plugin-import 实现按需引入

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

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

local.name 是导入进来的别名,比如 import { Button as MyButton } from 'antd' 的 MyButton 

imported.name 是包 antd 真实 `导出` 的变量名 

需要做的事情也很简单:

  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 类型的节点之后,就收集好了依赖关系,下一步就是如何加载它们了。

第二步 判断是否使用

查找模块是否被使用 调用 CallExpression 分析被使用到的模块名,调用 importMethod 方法改写 import 路径.

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

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

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

      ↓ ↓ ↓ ↓ ↓ ↓

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

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

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

babel-plugin-import 实现按需引入

来看代码:

// import 的模块被使用时的处理方法。上面收集了依赖之后,得判断这些 import 的变量是否被用到
// 比如使用方式为:React.createElement(Button, null, "Hello"); 可将这行代码转换为 AST 节点树结合更容易理解 CallExpression 做的事情
CallExpression(path, state) {
  const { node } = path;
  const file = (path && path.hub && path.hub.file) || (state && state.file);
  // 方法调用者的 name,如:Button
  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);
    }
  }

  // 参数形式,如 React.createElement(Button, null, "Hello"),会将 Button 作为第一个参数
  node.arguments = node.arguments.map(arg => {
    const { name: argName } = arg;
    if (
      pluginState.specified[argName] &&
      path.scope.hasBinding(argName) &&  // 检查当前作用域内是否存在 Button 变量
      path.scope.getBinding(argName).path.type === 'ImportSpecifier' // 并且变量通过 import 方式创建
    ) {
      // 找到 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-import 使用 - 掘金 

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

庖丁解牛:最全babel-plugin-import源码详解 - 掘金文章来源地址https://www.toymoban.com/news/detail-425437.html

到了这里,关于babel-plugin-import 实现按需引入的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • React UI组件库——如何快速实现antd的按需引入和自定义主题

    大家上午好呀~ 今天来学习一下React的UI组件库以及antd的使用相关的知识点。 感兴趣的小伙伴可以给个三连哦~ material-ui(国外) ant-design(国内蚂蚁金服) antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。 安装antd组件库: 默认按需引入antd组件

    2024年02月02日
    浏览(38)
  • vue3 vue.config.js配置Element-plus组件和Icon图标实现按需自动引入

    打包时,报警告,提示包太大会影响性能 在页面直接使用,直接使用 SVG 图标,当做一般的 svg 使用 icon使用时需要用以下两种方式方式: 如果用在el-button里面的icon属性上使用,用SVG方式无效,还是需要引入再使用(不知道有没有其他方式) 注意: 使用 :icon=\\\"Edit\\\" 则icon的大

    2024年02月06日
    浏览(70)
  • 引入element-ui步骤(按需引入和全局引入)

    两种引入方式 1)完整引入 2)按需引入 借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。 a)先安装 babel-plugin-component: b)修改 babel.config.js 的内容 c)创建文件 element.js(名字自定义) d)最后在main.js中添加代码

    2024年02月16日
    浏览(52)
  • element UI 按需引入

    1、npm安装【全局安装element】    2、按需引入,借助  babel-plugin-component,         2.1、首先安装 babel-plugin-component,         2.2、将.babelrc修改为:【是项目中的babel.config.js文件】         2.3、在main.js中引入需要的组件 比如:Button,Select         2.4、或者挂载在

    2024年02月16日
    浏览(46)
  • vue运行报错Error: Cannot find module ‘@vue/cli-plugin-babel‘

    今天将VUE项目迁移到新的电脑上,配置不一样,导致报错: Error: Cannot find module \\\'@vue/cli-plugin-babel\\\' Require stack: - D:WWWVUExxxxxxnode_modules.store@vue+ cli-service@5.0.8- D:WWWVUExxxxxxnode_modules.store@vue+cli-service@5.0.8node_modules@vuecli-servicelibService.js - D:WWWVUExxxxxxnode_modules.store@vue+

    2024年01月19日
    浏览(65)
  • element-ui按需引入

    1、安装elment-ui组件库 官方网站 在项目的根目录下安装 element-ui 官方网页: 2、找到官方文档 官方网站 3、安装 babel-plugin-component 在你的项目根目录下运行该: 如: 4、修改 .babelrc 配置文件 vue-li 的 .babelrc 文件里为 babel.config.js 原来的 babel.config.js 文件: 修改后的 babel.config.j

    2024年02月04日
    浏览(59)
  • Vue项目中实现ElementUI按需引入

    🐱 个人主页: 不叫猫先生 🙋‍♂️ 作者简介:2022年度博客之星前端领域TOP 2,前端领域优质作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀! 💫优质专栏:vue3从入门到精通、TypeScript从入门到实践 📢 资料领取:前端进阶资料可以找我免

    2024年02月04日
    浏览(44)
  • 按需引入ant-design-vue组件

    一、首先创建一个新的Vue项目 选择default含有babel和eslint。或者自定义Manually select features。 babel是一个转码器,主要用于ES2015+ 代码转换为 JavaScript 向后兼容版本的代码 eslint是一个代码检测工具。用来规范编码规范用。 自定义可选的插件有,Babel,TypeScript,Progressive Web App (P

    2024年02月13日
    浏览(56)
  • Vue3中按需引入ECharts(一看就会)

    使用背景:比如做一个工程化项目,且只使用到柱状图和折线图,如果把所有的echarts组件都引入到项目中的话,会影响用户打开页面的速度和项目的性能。所以对于做一个高逼格的程序工程师,我们需要按需引入。   1:废话不多说 老步骤安装   2:自己新建一个 js 文件(名

    2024年02月16日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包