We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
平时在使用 antd、element 等组件库的时候,都会使用到一个 Babel 插件:babel-plugin-import,这篇文章通过例子和分析源码简单说一下这个插件做了一些什么事情,并且实现一个最小可用版本。
antd
element
Babel
babel-plugin-import
插件地址:https://github.com/ant-design/babel-plugin-import
antd 和 element 这两个组件库,看它的源码, index.js 分别是这样的:
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 来导出带有命名的各个组件。
ES6 Module
export
所以,我们可以通过 ES6 的 import { } from 的语法来导入单组件的 JS 文件。但是,我们还需要手动引入组件的样式:
ES6
import { } from
JS
// antd import 'antd/dist/antd.css'; // element import 'element-ui/lib/theme-chalk/index.css';
如果仅仅是只需要一个 Button 组件,却把所有的样式都引入了,这明显是不合理的。
Button
当然,你说也可以只使用单个组件啊,还可以减少代码体积:
import Button from 'antd/lib/button'; import 'antd/lib/button/style';
PS:类似 antd 的组件库提供了 ES Module 的构建产物,直接通过 import {} from 的形式也可以 tree-shaking,这个不在今天的话题之内,就不展开说了~
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 的时候自动引入它的样式文件。
简单来说,babel-plugin-import 就是解决了上面的问题,为组件库实现单组件按需加载并且自动引入其样式,如:
import { Button } from 'antd'; ↓ ↓ ↓ ↓ ↓ ↓ var _button = require('antd/lib/button'); require('antd/lib/button/style');
只需关心需要引入哪些组件即可,内部样式我并不需要关心,你帮我自动引入就 ok。
简单来说就需要关心三个参数即可:
{ "libraryName": "antd", // 包名 "libraryDirectory": "lib", // 目录,默认 lib "style": true, // 是否引入 style }
其它的看文档:https://github.com/ant-design/babel-plugin-import#usage
主要来看一下 babel-plugin-import 如何加载 JavaScript 代码和样式的。
JavaScript
以下面这段代码为例:
import { Button, Rate } from 'antd'; ReactDOM.render(<Button>xxxx</Button>);
babel-plubin-import 会在 ImportDeclaration 里将所有的 specifier 收集起来。
babel-plubin-import
ImportDeclaration
specifier
先看一下 ast 吧:
ast
![IMAGE](quiver-image-url/DBC2E9BF0D7FAB9E5ED62A74F63FD19B.jpg =463x815)
可以从这个 ImportDeclaration 语句中提取几个关键点:
需要做的事情也很简单:
import
libraryName
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 类型的节点之后,就收集好了依赖关系,下一步就是如何加载它们了。
babel
收集了依赖关系之后,得要判断一下这些 import 的变量是否被使用到了,我们这里说一种情况。
我们知道,JSX 最终是变成 React.createElement() 执行的:
JSX
React.createElement()
ReactDOM.render(<Button>Hello</Button>); ↓ ↓ ↓ ↓ ↓ ↓ React.createElement(Button, null, "Hello");
没错,createElement 的第一个参数就是我们要找的东西,我们需要判断收集的依赖中是否有被 createElement 使用。
createElement
分析一下这行代码的 ast,很容易就找到这个节点:
![IMAGE](quiver-image-url/D69681FAEC50126D04F5D1F1BB5E0493.jpg =565x664)
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 ,这里就不多说了。
React.createElement(Button)
const btn = Button
[Button]
第一步和第二步主要的工作是找到需要被插件处理的依赖关系,比如:
import { Button, Rate } from 'antd'; ReactDOM.render(<Button>Hello</Button>);
Button 组件使用到了,Rate 在代码里未使用。所以插件要做的也只是自动引入 Button 的代码和样式即可。
我们先回顾一下,当我们 import 一个组件的时候,希望它能够:
并且再回想一下插件的配置 options,只需要将 libraryDirectory 以及 style 等配置用上就完事了。
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
addDefault
addNamed
@babel/helper-module-imports
addSideEffect(path, 'source'); ↓ ↓ ↓ ↓ ↓ ↓ import "source"
addDefault(path, 'source', { nameHint: "hintedName" }) ↓ ↓ ↓ ↓ ↓ ↓ import hintedName from "source"
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....
b,c,d...
CallExpression
importMethod
impport
不过有一些细节这里就没提到,比如如何删除旧的 import 等... 感兴趣的可以自行阅读源码哦。
看完一遍源码,是不是有发现,其实除了 antd 和 element 等大型组件库之外,任意的组件库都可以使用 babel-plugin-import 来实现按需加载和自动加载样式。
没错,比如我们常用的 lodash,也可以使用 babel-plugin-import 来加载它的各种方法,可以动手试一下。
lodash
看了这么多,自己动手实现一个简易版的 babel-plugin-import 吧。
如果还不了解如何实现一个 Babel 插件,可以阅读 【Babel 插件入门】如何用 Babel 为代码自动引入依赖
按照上文说的,最重要的配置项就是三个:
{ "libraryName": "antd", "libraryDirectory": "lib", "style": true, }
所以我们也就只实现这三个配置项。
并且,上文提到,真实情况中会有多种方式来调用一个组件,这里我们也不处理这些复杂情况,只实现最常见的 <Button /> 调用。
<Button />
入口文件的作用是获取用户传入的配置项并且将核心插件代码作用到 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 实现的:
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
本文通过源码解析和动手实践,深入浅出的介绍了 babel-plugin-import 插件的原理,希望大家看完这篇文章之后,都能清楚地了解这个插件做了什么事。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
平时在使用
antd
、element
等组件库的时候,都会使用到一个Babel
插件:babel-plugin-import
,这篇文章通过例子和分析源码简单说一下这个插件做了一些什么事情,并且实现一个最小可用版本。插件地址:https://github.com/ant-design/babel-plugin-import
babel-plugin-import 介绍
Why:为什么需要这个插件
antd
和element
这两个组件库,看它的源码,index.js
分别是这样的:antd
和element
都是通过ES6 Module
的export
来导出带有命名的各个组件。所以,我们可以通过
ES6
的import { } from
的语法来导入单组件的JS
文件。但是,我们还需要手动引入组件的样式:如果仅仅是只需要一个
Button
组件,却把所有的样式都引入了,这明显是不合理的。当然,你说也可以只使用单个组件啊,还可以减少代码体积:
PS:类似
antd
的组件库提供了ES Module
的构建产物,直接通过import {} from
的形式也可以tree-shaking
,这个不在今天的话题之内,就不展开说了~对,这没毛病。但是,看一下如们需要多个组件的时候:
会不会觉得这样的代码不够优雅?如果是我,甚至想打人。
这时候就应该思考一下,如何在引入
Button
的时候自动引入它的样式文件。What:这个插件做了什么
简单来说,
babel-plugin-import
就是解决了上面的问题,为组件库实现单组件按需加载并且自动引入其样式,如:只需关心需要引入哪些组件即可,内部样式我并不需要关心,你帮我自动引入就 ok。
How:这个插件怎么用
简单来说就需要关心三个参数即可:
其它的看文档:https://github.com/ant-design/babel-plugin-import#usage
babel-plugin-import 源码分析
主要来看一下
babel-plugin-import
如何加载JavaScript
代码和样式的。以下面这段代码为例:
第一步 依赖收集
babel-plubin-import
会在ImportDeclaration
里将所有的specifier
收集起来。先看一下
ast
吧:![IMAGE](quiver-image-url/DBC2E9BF0D7FAB9E5ED62A74F63FD19B.jpg =463x815)
可以从这个
ImportDeclaration
语句中提取几个关键点:需要做的事情也很简单:
import
的包是不是antd
,也就是libraryName
Button
和Rate
收集起来来看代码:
待
babel
遍历了所有的ImportDeclaration
类型的节点之后,就收集好了依赖关系,下一步就是如何加载它们了。第二步 判断是否使用
收集了依赖关系之后,得要判断一下这些
import
的变量是否被使用到了,我们这里说一种情况。我们知道,
JSX
最终是变成React.createElement()
执行的:没错,
createElement
的第一个参数就是我们要找的东西,我们需要判断收集的依赖中是否有被createElement
使用。分析一下这行代码的
ast
,很容易就找到这个节点:![IMAGE](quiver-image-url/D69681FAEC50126D04F5D1F1BB5E0493.jpg =565x664)
来看代码:
除了
React.createElement(Button)
之外,还有const btn = Button
/[Button]
... 等多种情况会使用Button
,源码中都有对应的处理方法,感兴趣的可以自己看一下: https://github.com/ant-design/babel-plugin-import/blob/master/src/Plugin.js#L163-L272 ,这里就不多说了。第三步 生成引入代码(核心)
第一步和第二步主要的工作是找到需要被插件处理的依赖关系,比如:
Button
组件使用到了,Rate
在代码里未使用。所以插件要做的也只是自动引入Button
的代码和样式即可。我们先回顾一下,当我们
import
一个组件的时候,希望它能够:并且再回想一下插件的配置 options,只需要将
libraryDirectory
以及style
等配置用上就完事了。小朋友,你是否有几个问号?这里该如何让
babel
去修改代码并且生成一个新的import
以及一个样式的import
呢,不慌,看看代码就知道了:addSideEffect
,addDefault
和addNamed
是@babel/helper-module-imports
的三个方法,作用都是创建一个import
方法,具体表现是:addSideEffect
addDefault
addNamed
更多关于
@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 为代码自动引入依赖最简功能实现
按照上文说的,最重要的配置项就是三个:
所以我们也就只实现这三个配置项。
并且,上文提到,真实情况中会有多种方式来调用一个组件,这里我们也不处理这些复杂情况,只实现最常见的
<Button />
调用。入口文件
入口文件的作用是获取用户传入的配置项并且将核心插件代码作用到
ast
上。核心代码
真正修改
ast
的代码是在plugin
实现的:这样就实现了一个最简单的
babel-plugin-import
插件,可以自动加载单包和样式。完整代码:https://github.com/axuebin/babel-plugin-import-demo
总结
本文通过源码解析和动手实践,深入浅出的介绍了
babel-plugin-import
插件的原理,希望大家看完这篇文章之后,都能清楚地了解这个插件做了什么事。The text was updated successfully, but these errors were encountered: