按需导入之babel插件转换
为了应用能够快速访问, 需要对构建代码进行"减肥", 将无用代码剔除掉.当前得主流构建框架webpack
和rollup
等都提供了tree shaking
机制, 利用es6
得声明式模块系统语法和语句依赖分析, 进行高精度得代码剔除. 但tree shaking
也存在一些限制, 一般的第三方库都采用es5
语法, 不使用es6
的模块语法,导致tree shaking
失效.对于这些第三方库, 一般采用一些转换导入语句的babel
插件, 如 babel-plugin-import), babel-plugin-component), babel-plugin-transform-imports)等.
本文中的案例中很多采用了antd
的例子, 但其实antd
是支持tree shaking
的, 不需要使用这些插件也能按需导入.
导入语句转换插件
不采用按需导入的插件, 导入一个库如loadash
, 就会这样写:
import { trim, isEqual } from 'loadash';
trim(str);
isEqual(1, 2);
这会将整个lodash
代码都给导入, 如果不像导入不需要的代码, 且当前库支持按需导入, 手动按需导入的代码应该为:
import isEqual from 'lodash/isequal';
import trim from 'lodash/trim';
trim(str);
isEqual(1, 2);
但当前模块中大量使用了这个库的模块(函数)时, 手动按需导入就会非常繁琐, 代码整洁度大大降低了.
如果能够将全量导入代码:
import { trim, isEqual } from 'loadash';
利用工具转换成按需导入代码:
import isEqual from 'lodash/isequal';
import trim from 'lodash/trim';
这样既能享受全量导入的简洁, 又可以不用担心导入过多的无用代码.
而babel-plugin-import
, babel-plugin-component
, babel-plugin-transform-imports
等就是这种提供转换的工具.
babel-plugin-import
是阿里为了antd
组件库量身定做的一套转换工作, 不过在后续的更新中, 适用的范围越来越广. 它除了会导入目标组件外, 还支持导入组件附属样式文件. 转化示意:
import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style/css');
ReactDOM.render(<_button>xxxx</_button>);
babel-plugin-component
是element-ui
团队对babel-plugin-import
一个低版本的fork, 不建议使用, 因为可配置化基地, 基本只能对element-ui
这个UI库按需导入使用.
babel-plugin-transform-imports
是一个非常轻量级的转换插件, 可定制化程度非常高, 可以定制转换后的导入语句, 适应不同的目录结构. 但它只能为全量导入的每一项转换为一个单独的导入, 这不适用的对UI组件库按需导入. 在babel-plugin-component
还很笨重时,babel-plugin-transform-imports
是非常好用的. 它的导入示意:
import { MdCheck, FaCheck } from 'react-icons'
->
import MdCheck from 'react-icons/lib/md/check'
import FaCheck from 'react-icons/lib/fa/check'
从原理来说, 这些插件都是分析具名导入的import
语句, 根据导入项, 转换为按需导入语句. babel-plugin-import
跟babel-plugin-transform-imports
在源码实现的细节上有所不同.
在babel-plugin-import
如对于代码:
import { Button } from 'element-ui';
console.log(Button);
插件解析出来所需要按需导入的模块后, 会在新的一行中添加按需导入语句(使用babel@/helper-module-imports
库中的工具), 此时的代码会变为:
import _Button from 'element-ui/lib/button';
import 'components/lib/button/style.css';
import { Button } from 'element-ui';
console.log(Button);
此时还需要需要将所有使用变量Button
的地方改为_Button
:
import _Button from 'element-ui/lib/button';
import 'components/lib/button/style.css';
import { Button } from 'element-ui';
console.log(_Button);
然后删除原有导入:
import _Button from 'element-ui/lib/button';
import 'components/lib/button/style.css';
console.log(_Button);
为了将Button
变量转为_Button
, 需要对可能使用变量语句转换, 在babel-plugin-import
当前最新代码中, 检测的语句(表达式)类型有:CallExpression
, MemberExpression
, Property
, VariableDeclarator
, ArrayExpression
, LogicalExpression
, ConditionalExpression
, IfStatement
, ExpressionStatement
, ReturnStatement
, ExportDefaultDeclaration
, BinaryExpression
, NewExpression
, ClassDeclaration
等(具体可以参照源码). 这种采用枚举可能存在的语句可能会有遗漏. 从库的变更来看, babel-plugin-component
对应的babel-plugin-import
的版本到最新的版本, 多了几个表达式. 其实这里可以像babel-plugin-lodash
的实现里面一样, 通过作用域查询到所有使用变量的语句, 更加准确和简洁.
从babel-plugin-import
的最新的代码来看, 使用了通过作用域来解决变量重名导致的问题)
而babel-plugin-transform-imports
非常简单, 通过分析目标import
语句,将具名导入语句替换为多条按需导入语句, 且变量名维持跟原样.
就算最新版本的babel-plugin-import
适用范围已经非常广, 但是还是建议使用babel-plugin-transform-imports
, 它的实现轻量,简洁. 对于babel-plugin-transform-imports
不支持按需导入额外的资源, 可以fork
源码, 进行扩展.
根据调用进行按需导入
还有一种babel
转换导入语句的按需导入的机制, 原理来自babel-plugin-loadash
插件, 它可以将下面的代码:
import _ from 'loadash';
_.trim(str);
_.isEqual(1, 2);
转换为:
import isEqual from 'lodash/isequal';
import trim from 'lodash/trim';
trim(str);
isEqual(1, 2);
原理是根据全量导入的变量的调用链, 分析所需要的模块, 然后按需导入.这样的方便之处在于编码时使用模块的全量导入或者默认导入, 避免具名导入那样需要维护每一项. 比如利用antd
组件库在开发表单时, 需要导入大量的表单组件, 随着业务的变更, 组件也需要变更, 每一次维护都需要重新在导入语句中导入添加需要的模块.
一开始控件中只使用了input
和button
:
import { Button, Input, Form, FormItem } from 'antd';
后续扩展了业务时, 需要使用下拉框,需要改写:
import { Button, Input, Form, FormItem, Select } from 'antd';
这在当前文件代码量十分大是, 每一次使用新的组件, 都需要滚动文件顶部维护好新的导入, 然后在回到开发点继续开发, 打断流畅的开发快感.且如果没有配置相应的eslint
规则的话, 还会导致一些没有使用的组件依旧被导入, 导致无用代码被加载.
而一大早使用全量导入就没有这种烦恼:
import * as Ad from 'antd';
return (<Ad.Form>
<Ad.FormItem>
<Ad.Input />
// ...
这里只提供这样一个的思路, 如果觉得这样可以提高开发效率, 可以参考babel-plugin-lodash
实现相应的插件. 笔者所在的团队是这样使用的, 但带来一个苦恼是可能组件前缀过多(Ad.
为前缀), 不简洁.
写在最后
在开发vue项目时, 组件注册有全局注册和局部注册之分. 全局注册后的组件在每一个其他组件中都可以使用, 无需再次导入注册, 具有良好的开发体验. 但也会导致首屏包过大, 降低用户体验.
可以不可以有一种方式, 让开发人员开发时像全局注册一样, 实际打包又跟局部注册一样支持按需导入呢?
0 条评论