龙空技术网

一文带你搞懂 babel-plugin-import 插件(上)「源码解析」

Echa攻城狮 225

前言:

现时你们对“jquery的插件开发工具”都比较讲究,你们都需要剖析一些“jquery的插件开发工具”的相关内容。那么小编同时在网络上搜集了一些对于“jquery的插件开发工具””的相关资讯,希望咱们能喜欢,同学们快快来了解一下吧!

作者:axuebin

转发链接:

目录

一文带你搞懂 babel-plugin-import 插件(上)「源码解析」 本篇

一文带你搞懂 babel-plugin-import 插件(上)「源码解析」

前言

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

插件地址:

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}

其它的看文档:

babel-plugin-import 源码分析

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

以下面这段代码为例:

import { Button, Rate } from 'antd';ReactDOM.render(<Button>xxxx</Button>);
第一步 依赖收集

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

先看一下 ast 吧:

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

source.value: antdspecifier.local.name: Buttonspecifier.local.name: Rate

需要做的事情也很简单:

import 的包是不是 antd,也就是 libraryName把 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,源码中都有对应的处理方法,感兴趣的可以自己看一下: ,这里就不多说了。

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

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

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] };}

addSideEffect, addDefault 和 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 上做了一些事情:

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

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

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

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

动手实现 babel-plugin-import

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

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

本篇文章未完结,请见下一篇

推荐JavaScript经典实例学习资料文章

《JavaScript常用API合集汇总「值得收藏」》

《推荐10个常用的图片处理小帮手(上)「值得收藏」》

《推荐10个常用的图片处理小帮手(下)「值得收藏」》

《JavaScript 中ES6代理的实际用例》

《12 个实用的前端开发技巧总结》

《一文带你搞懂搭建企业级的 npm 私有仓库》

《教你如何使用内联框架元素 IFrames 的沙箱属性提高安全性?》

《细说前端开发UI公共组件的新认识「实践」》

《细说DOM API中append和appendChild的三个不同点》

《细品淘系大佬讲前端新人如何上王者「干货」》

《一文带你彻底解决背景跟随弹窗滚动问题「干货」》

《推荐常用的5款代码比较工具「值得收藏」》

《Node.js实现将文字与图片合成技巧》

《爱奇艺云剪辑Web端的技术实现》

《我再也不敢说我会写前端 Button组件「实践」》

《NodeX Component - 滴滴集团 Node.js 生态组件体系「实践」》

《Node Buffers 完整指南》

《推荐18个webpack精美插件「干货」》

《前端开发需要了解常用7种JavaScript设计模式》

《浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务》

《了不起的 Webpack HMR 学习指南(上)「含源码讲解」》

《了不起的 Webpack HMR 学习指南(下)「含源码讲解」》

《10个打开了我新世界大门的 WebAPI(上)「实践」》

《10个打开了我新世界大门的 WebAPI(中)「实践」》

《10个打开了我新世界大门的 WebAPI(下)「实践」》

《「图文」ESLint 在中大型团队的应用实践》

《Deno是代码的浏览器,你认同吗?》

《前端存储除了 localStorage 还有啥?》

《Javascript 多线程编程​的前世今生》

《微前端方案 qiankun(实践及总结)》

《「图文」V8 垃圾回收原来这么简单?》

《Webpack 5模块联邦引发微前端的革命?》

《基于 Web 端的人脸识别身份验证「实践」》

《「前端进阶」高性能渲染十万条数据(时间分片)》

《「前端进阶」高性能渲染十万条数据(虚拟列表)》

《图解 Promise 实现原理(一):基础实现》

《图解 Promise 实现原理(二):Promise 链式调用》

《图解 Promise 实现原理(三):Promise 原型方法实现》

《图解 Promise 实现原理(四):Promise 静态方法实现》

《实践教你从零构建前端 Lint 工作流「干货」》

《高性能多级多选级联组件开发「JS篇」》

《深入浅出讲解Node.js CLI 工具最佳实战》

《延迟加载图像以提高Web网站性能的五种方法「实践」》

《比较 JavaScript 对象的四种方式「实践」》

《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》

《前端如何一次性处理10万条数据「进阶篇」》

《推荐三款正则可视化工具「JS篇」》

《如何让用户选择是否离开当前页面?「JS篇」》

《JavaScript开发人员更喜欢Deno的五大原因》

《仅用18行JavaScript实现一个倒数计时器》

《图文细说JavaScript 的运行机制》

《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》

《推荐Web程序员常用的15个源代码编辑器》

《10个实用的JS技巧「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》

《深入JavaScript教你内存泄漏如何防范》

《手把手教你7个有趣的JavaScript 项目-上「附源码」》

《手把手教你7个有趣的JavaScript 项目-下「附源码」》

《JavaScript 使用 mediaDevices API 访问摄像头自拍》

《手把手教你前端代码如何做错误上报「JS篇」》

《一文让你彻底搞懂移动前端和Web 前端区别在哪里》

《63个JavaScript 正则大礼包「值得收藏」》

《提高你的 JavaScript 技能10 个问答题》

《JavaScript图表库的5个首选》

《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》

《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》

《教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」》

《可视化的 js:动态图演示 Promises & Async/Await 的过程》

《原生JS封装拖动验证滑块你会吗?「实践」》

《如何实现高性能的在线 PDF 预览》

《细说使用字体库加密数据-仿58同城》

《Node.js要完了吗?》

《Pug 3.0.0正式发布,不再支持 Node.js 6/8》

《纯JS手写轮播图(代码逻辑清晰,通俗易懂)》

《JavaScript 20 年 中文版之创立标准》

《值得收藏的前端常用60余种工具方法「JS篇」》

《箭头函数和常规函数之间的 5 个区别》

《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》

《「前端篇」不再为正则烦恼》

《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》

《深入细品浏览器原理「流程图」》

《JavaScript 已进入第三个时代,未来将何去何从?》

《前端上传前预览文件 image、text、json、video、audio「实践」》

《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》

《推荐13个有用的JavaScript数组技巧「值得收藏」》

《前端必备基础知识:window.location 详解》

《不要再依赖CommonJS了》

《犀牛书作者:最该忘记的JavaScript特性》

《36个工作中常用的JavaScript函数片段「值得收藏」》

《Node + H5 实现大文件分片上传、断点续传》

《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》

《【实践总结】关于小程序挣脱枷锁实现批量上传》

《手把手教你前端的各种文件上传攻略和大文件断点续传》

《字节跳动面试官:请你实现一个大文件上传和断点续传》

《谈谈前端关于文件上传下载那些事【实践】》

《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》

《最全的 JavaScript 模块化方案和工具》

《「前端进阶」JS中的内存管理》

《JavaScript正则深入以及10个非常有意思的正则实战》

《前端面试者经常忽视的一道JavaScript 面试题》

《一行JS代码实现一个简单的模板字符串替换「实践」》

《JS代码是如何被压缩的「前端高级进阶」》

《前端开发规范:命名规范、html规范、css规范、js规范》

《【规范篇】前端团队代码规范最佳实践》

《100个原生JavaScript代码片段知识点详细汇总【实践】》

《关于前端174道 JavaScript知识点汇总(一)》

《关于前端174道 JavaScript知识点汇总(二)》

《关于前端174道 JavaScript知识点汇总(三)》

《几个非常有意思的javascript知识点总结【实践】》

《都2020年了,你还不会JavaScript 装饰器?》

《JavaScript实现图片合成下载》

《70个JavaScript知识点详细总结(上)【实践】》

《70个JavaScript知识点详细总结(下)【实践】》

《开源了一个 JavaScript 版敏感词过滤库》

《送你 43 道 JavaScript 面试题》

《3个很棒的小众JavaScript库,你值得拥有》

《手把手教你深入巩固JavaScript知识体系【思维导图】》

《推荐7个很棒的JavaScript产品步骤引导库》

《Echa哥教你彻底弄懂 JavaScript 执行机制》

《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》

《深入解析高频项目中运用到的知识点汇总【JS篇】》

《JavaScript 工具函数大全【新】》

《从JavaScript中看设计模式(总结)》

《身份证号码的正则表达式及验证详解(JavaScript,Regex)》

《浏览器中实现JavaScript计时器的4种创新方式》

《Three.js 动效方案》

《手把手教你常用的59个JS类方法》

《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》

《深入浅出讲解 js 深拷贝 vs 浅拷贝》

《手把手教你JS开发H5游戏【消灭星星】》

《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》

《手把手教你全方位解读JS中this真正含义【实践】》

《书到用时方恨少,一大波JS开发工具函数来了》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《手把手教你JS 异步编程六种方案【实践】》

《让你减少加班的15条高效JS技巧知识点汇总【实践】》

《手把手教你JS开发H5游戏【黄金矿工】》

《手把手教你JS实现监控浏览器上下左右滚动》

《JS 经典实例知识点整理汇总【实践】》

《2.6万字JS干货分享,带你领略前端魅力【基础篇】》

《2.6万字JS干货分享,带你领略前端魅力【实践篇】》

《简单几步让你的 JS 写得更漂亮》

《恭喜你获得治疗JS this的详细药方》

《谈谈前端关于文件上传下载那些事【实践】》

《面试中教你绕过关于 JavaScript 作用域的 5 个坑》

《Jquery插件(常用的插件库)》

《【JS】如何防止重复发送ajax请求》

《JavaScript+Canvas实现自定义画板》

《Continuation 在 JS 中的应用「前端篇」》

作者:axuebin

转发链接:

标签: #jquery的插件开发工具