前几个月写的一篇文章,在公众号补发一下,可能之前你们在其它平台已经看过了。

前言

平时在使用 antdelement 等组件库的时候,都会使用到一个 Babel 插件:babel-plugin-import,这篇文章通过例子和分析源码简单说一下这个插件做了一些什么事情,并且实现一个最小可用版本。

插件地址:https://github.com/ant-design/babel-plugin-import[1]

babel-plugin-import 介绍

Why:为什么需要这个插件

antd 和 element 这两个组件库,看它的源码, index.js 分别是这样的:

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

antd 和 element 都是通过 ES6 Module 的 export 来导出带有命名的各个组件。

所以,我们可以通过 ES6 的 import { } from 的语法来导入单组件的 JS 文件。但是,我们还需要手动引入组件的样式:

// antdimport 'antd/dist/antd.css';// elementimport 'element-ui/lib/theme-chalk/index.css';

如果仅仅是只需要一个 Button 组件,却把所有的样式都引入了,这明显是不合理的。

当然,你说也可以只使用单个组件啊,还可以减少代码体积:

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

PS:类似 antd 的组件库提供了 ES Module 的构建产物,直接通过 import {} from 的形式也可以 tree-shaking,这个不在今天的话题之内,就不展开说了~

对,这没毛病。但是,看一下如果我们需要多个组件的时候:

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';

会不会觉得这样的代码不够优雅?如果是我,甚至想打人。

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

What:这个插件做了什么

简单来说,babel-plugin-import 就是解决了上面的问题,为组件库实现单组件按需加载并且自动引入其样式,如:

import { Button } from 'antd';

      ↓ ↓ ↓ ↓ ↓ ↓

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

只需关心需要引入哪些组件即可,内部样式我并不需要关心,你帮我自动引入就 ok。

How:这个插件怎么用

简单来说就需要关心三个参数即可:

{  "libraryName": "antd",     // 包名  "libraryDirectory": "lib", // 目录,默认 lib  "style": true,             // 是否引入 style}

其它的看文档:https://github.com/ant-design/babel-plugin-import#usage[2]

babel-plugin-import 源码分析

主要来看一下 babel-plugin-import 如何加载 JavaScript 代码和样式的。

以下面这段代码为例:

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

第一步 依赖收集

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

先看一下 ast 吧:


可以从这个 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>HelloButton>);

      ↓ ↓ ↓ ↓ ↓ ↓

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,源码中都有对应的处理方法,感兴趣的可以自己看一下: https://github.com/ant-design/babel-plugin-import/blob/master/src/Plugin.js#L163-L272[3] ,这里就不多说了。

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

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

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

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

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

import { Button } from 'antd';

      ↓ ↓ ↓ ↓ ↓ ↓

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

并且再回想一下插件的配置 options[4],只需要将 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[5]

总结

一起数个 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-plugin-import

看了这么多,自己动手实现一个简易版的 babel-plugin-import 吧。

如果还不了解如何实现一个 Babel 插件,可以阅读 【Babel 插件入门】如何用 Babel 为代码自动引入依赖

最简功能实现

按照上文说的,最重要的配置项就是三个:

{  "libraryName": "antd",  "libraryDirectory": "lib",  "style": true,}

所以我们也就只实现这三个配置项。

并且,上文提到,真实情况中会有多种方式来调用一个组件,这里我们也不处理这些复杂情况,只实现最常见的  调用。

入口文件

入口文件的作用是获取用户传入的配置项并且将核心插件代码作用到 ast 上。

import Plugin from './Plugin';

export default function ({ types }) {  let plugins = null;

  // 将插件作用到节点上  function applyInstance(method, args, context) {    for (const plugin of plugins) {      if (plugin[method]) {        plugin[method].apply(plugin, [...args, context]);      }    }  }

  const Program = {    // ast 入口    enter(path, { opts = {} }) {      // 初始化插件实例      if (!plugins) {        plugins = [          new Plugin(            opts.libraryName,            opts.libraryDirectory,            opts.style,            types,          ),        ];      }      applyInstance('ProgramEnter', arguments, this);    },    // ast 出口    exit() {      applyInstance('ProgramExit', arguments, this);    },  };

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

  // 插件只作用在 ImportDeclaration 和 CallExpression 上  ['ImportDeclaration', 'CallExpression'].forEach(method => {    ret.visitor[method] = function () {      applyInstance(method, arguments, ret.visitor);    };  });

  return ret;}

核心代码

真正修改 ast 的代码是在 plugin 实现的:

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

/** * 转换成小写,添加连接符 * @param {*} _str   字符串 * @param {*} symbol 连接符 */function transCamel(_str, symbol) {  const str = _str[0].toLowerCase() + _str.substr(1);  return str.replace(/([A-Z])/g, $1 => `${symbol}${$1.toLowerCase()}`);}

/** * 兼容 Windows 路径 * @param {*} path */function winPath(path) {  return path.replace(/\\/g, '/');}

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

  /**   * 获取内部状态,收集依赖   * @param {*} state   */  getPluginState(state) {    if (!state) {      state = {};    }    return state;  }

  /**   * 生成 import 语句(核心代码)   * @param {*} methodName   * @param {*} file   * @param {*} pluginState   */  importMethod(methodName, file, pluginState) {    if (!pluginState.selectedMethods[methodName]) {      // libraryDirectory:目录,默认 lib      // style:是否引入样式      const { style, libraryDirectory } = this;      // 组件名转换规则      const transformedMethodName = transCamel(methodName, '');      // 兼容 windows 路径      // path.join('antd/lib/button') == 'antd/lib/button'      const path = winPath(join(this.libraryName, libraryDirectory, transformedMethodName));      // 生成 import 语句      // import Button from 'antd/lib/button'      pluginState.selectedMethods[methodName] = addDefault(file.path, path, { nameHint: methodName });      if (style) {        // 生成样式 import 语句        // import 'antd/lib/button/style'        addSideEffect(file.path, `${path}/style`);      }    }    return { ...pluginState.selectedMethods[methodName] };  }

  ProgramEnter(path, state) {    const pluginState = this.getPluginState(state);    pluginState.specified = Object.create(null);    pluginState.selectedMethods = Object.create(null);    pluginState.pathsToRemove = [];  }

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

  /**   * ImportDeclaration 节点的处理方法   * @param {*} path   * @param {*} state   */  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);    }  }

  /**   * React.createElement 对应的节点处理方法   * @param {*} path   * @param {*} state   */  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;    });  }}

这样就实现了一个最简单的 babel-plugin-import 插件,可以自动加载单包和样式。

完整代码:https://github.com/axuebin/babel-plugin-import-demo[6]

总结

本文通过源码解析和动手实践,深入浅出的介绍了 babel-plugin-import 插件的原理,希望大家看完这篇文章之后,都能清楚地了解这个插件做了什么事。

更多文章可以关注公众号「前端试炼」,分享每日前端精选文章。

关于 Babel 你会用到的一些链接:

  • Babel 用户手册[7]
  • Babel 插件手册[8]
  • ast 分析[9]
  • 节点规范[10]

参考资料

[1]

https://github.com/ant-design/babel-plugin-import: https://github.com/ant-design/babel-plugin-import

[2]

https://github.com/ant-design/babel-plugin-import#usage: https://github.com/ant-design/babel-plugin-import#usage

[3]

https://github.com/ant-design/babel-plugin-import/blob/master/src/Plugin.js#L163-L272: https://github.com/ant-design/babel-plugin-import/blob/master/src/Plugin.js#L163-L272

[4]

options: https://github.com/ant-design/babel-plugin-import#options

[5]

@babel/helper-module-imports: https://babeljs.io/docs/en/next/babel-helper-module-imports.html

[6]

https://github.com/axuebin/babel-plugin-import-demo: https://github.com/axuebin/babel-plugin-import-demo

[7]

Babel 用户手册: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/user-handbook.md

[8]

Babel 插件手册: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

[9]

ast 分析: https://astexplorer.net/

[10]

节点规范: https://github.com/estree/estree

欢迎关注公众号 玩相机的程序员

我是 axuebin

用键盘和相机记录生活

react 引入轮播插件_简单实现 babelpluginimport 插件相关推荐

  1. react 引入轮播插件_React.js实现轮播图

    react.js的基本思想,是通过改变state或props的值,重新渲染用户界面(不用操作DOM).截图GIF效果如下(只截取了三页效果): GIF1.gif 1.文件列表: 1490606951( ...

  2. vue3学习(引入轮播图插件)

    Vue引入第三方 以轮播图为例 Swiper开源.免费.强大的触摸滑动插件 Swiper是纯javascript打造的滑动特效插件,面向手机.平板电脑等移动终端 Swiper能实现触屏焦点图.触屏ta ...

  3. 响应式html轮播图,最简单的响应式jQuery轮播图插件

    easySlider.js是一款轻量级.简单易用的响应式jQuery轮播图插件.easySlider.js可以根据视口的大小来动态修改轮播图的尺寸.它压缩后的版本仅5K大小,简单实用. 使用方法 在页 ...

  4. jquery手写轮播图_用jQuery如何手写一个简单的轮播图?(附代码)

    用jQuery如何手写一个简单的轮播图?下面本篇文章通过代码示例来给大家介绍一下.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 用 jQuery 手写轮播图 先上个效果截图: 主要 ...

  5. html幻灯片图片轮播w3,支持30+种类型幻灯片|轮播图|旋转木马的强大jQuery插件

    jssor slider是一款功能非常强大的可制作超过30种不同类型的幻灯片|轮播图|旋转木马的jQuery插件.jssor具有高性能,轻量级,跨浏览器等特点,它可以支持IE6+的浏览器,并且可以支持 ...

  6. react引入多个图片_重新引入React:v16之后的每个React更新都已揭开神秘面纱。

    react引入多个图片 In this article (and accompanying book), unlike any you may have come across before, I w ...

  7. 关于轮播图的简单介绍

    一.引入图标字体 图标字体(iconfont) 网页中会有很多小的图片,例如,购物车,小箭头,定位等, 这些内容可以直接用图片代替,但图片一般都是比较大的,而且修改起来也不方便 所以可以直接使用图标字 ...

  8. vue element ui 走马灯轮播图(简单几句话实现自动缩放效果)

    实现效果 element ui 简单实现轮播图 本文,中间叙述的是过程,完整代码在最后面. 最近在写公司官网,用的是element ui 走马灯组件写的轮播图,ui想要自动缩放的效果,如上视频.在这里 ...

  9. 微信小程序vue轮播图_微信小程序使用swiper组件实现类3D轮播图

    Swiper是纯javascript打造的滑动特效插件,面向手机.平板电脑等移动终端. Swiper能实现触屏焦点图.触屏Tab切换.触屏多图切换等常用效果. Swiper开源.免费.稳定.使用简单. ...

最新文章

  1. C++拓展笔记2-3:C++中this指针用法简介
  2. 王爽《汇编语言第三版》实验10-1代码实现
  3. Extjs不错的博客
  4. docker rabbitmq_使用Docker集成Rabbitmq与安装elasticsearch教程
  5. [转载]超酷代码-使用 ASP.NET AJAX 进行拖放
  6. PHP no input file specified 三种解决方法
  7. 使用MATLAB转换图片为数据进行vga显示
  8. 手机号码检测开通微信方法
  9. 三年建模师告诉你3DMAX有没有前途
  10. PV、UV、IV的概念
  11. 新版标准日本语高级_第9课
  12. 我的世界租赁服自定义服务器,我的世界租借服务器(我的世界技巧教程 租赁服务器玩家间传送功能)...
  13. 每个人心里都有一只孟加拉虎
  14. 【Rust日报】 2019-04-27
  15. http协议各个版本之解说
  16. 【PyTorch】7 文本分类TorchText实战——AG_NEWS四类别新闻分类
  17. 云速建站:关于企业版的几点说明
  18. 艾伟_转载:VS 2010 和 .NET 4.0 系列之《在ASP.NET 4 Web Forms中实现URL导向》篇
  19. 指纹辨识传感器解决方案
  20. 转 GitHub上史上最全的Android开源项目分类汇总

热门文章

  1. 多级报表 php,电力设备生产数据的多层分组统计报表实现
  2. 使用C#模拟键盘输入
  3. java导出文件大数据量一种处理思路:多文件压缩导出
  4. linux wubi安卓,安卓搜狗输入法升级:五笔前所未有的爽!
  5. 2022年超实用蓝牙耳机推荐,适合学生党的无线蓝牙耳机品牌推荐
  6. 【编程100%】22-05 字符串之分割字符串
  7. 单招计算机主板图解,磐正系列主板刷bios图解新.pdf
  8. 有点污,23 种设计模式的通俗解释,我居然秒懂了!
  9. matlab小作业答案,MATLAB所有作业及答案
  10. C语言常用文件读、写、定位函数