官方文档:https://github.com/umijs/babel-plugin-import#usage
组件库按需加载:
组件库以组件为基本单位产出 js、css、less 文件,借助插件或者部分引入的写法,使得项目代码或 babel 编译后的代码中只包含使用到的组件的 js、css、less 等。

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

两者的差别主要在于:
使用组件库:通常是引入min.js及css文件
- import Vue from 'vue';
- import Vant from 'vant';
- import 'vant/lib/index.css';
-
- Vue.use(Vant);
这种写法经过 webpack 构建之后会将组件库产出的 vant.min.js、index.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",
- ...
- }
-
- 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'
- },
- ...
- }
安装 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 收集起来。

可以从这个 ImportDeclaration 语句中提取几个关键点:
local.name 是导入进来的别名,比如 import { Button as MyButton } from 'antd' 的 MyButton
imported.name 是包 antd 真实 `导出` 的变量名
需要做的事情也很简单:
import 的包是不是 antd,也就是 libraryNameButton 和 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,很容易就找到这个节点:

来看代码:
- // 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] };
- }
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中使用的JavaScript解析器。默认启用ES2017,支持JSX,Flow,TypeScript,支持实验性的语言提议(至少是stage-0)
它实现了访问者模式,对AST进行遍历,插件可以通过它获取相应的AST节点,并对对应节点进行具体操作。
它将AST转换成源代码,同时支持SourceMap
参考