按需导入之babel插件转换

为了应用能够快速访问, 需要对构建代码进行"减肥", 将无用代码剔除掉.当前得主流构建框架webpackrollup等都提供了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-componentelement-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-importbabel-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组件库在开发表单时, 需要导入大量的表单组件, 随着业务的变更, 组件也需要变更, 每一次维护都需要重新在导入语句中导入添加需要的模块.

一开始控件中只使用了inputbutton:


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项目时, 组件注册有全局注册和局部注册之分. 全局注册后的组件在每一个其他组件中都可以使用, 无需再次导入注册, 具有良好的开发体验. 但也会导致首屏包过大, 降低用户体验.

可以不可以有一种方式, 让开发人员开发时像全局注册一样, 实际打包又跟局部注册一样支持按需导入呢?

阅读 250

推荐阅读