本文为作者团队对按需引入的迭代史, 阅读本文可以对组件库按需引入的原理有所了解, 提供一种对项目中所有模块按需导入新思路.
团队中vue项目使用element-ui和react项目使用antd组件库, 为了避免项目打包后首屏过大, 对这些组件库进行了按需导入的配置(没有采用tree shaking).一开始使用的是两个库自带的babel插件, babel-plugin-import和babel-plugin-component.
如果你采用 ES module 的 tree shaking, 不需要阅读本文.
但随着的业务的增长, 发现这两个插件越来越不好用. 且babel-plugin-component是fork自babel-plugin-import, 相当于babel-plugin-import的低版本, 更加难用. babel-plugin-import相当于是对antd等类似组件库定制化的按需导入的插件. 如果想要对项目中的基础的模块(组件也是js模块)进行按需导入时, 就会显得有些笨重. 因为项目中基础组件的css样式文件都是直接导入到js代码中, 且不想维护一个类似antd
组件库那样的目录结构(存放模块的目录下需要有lib
这样的子级目录). babel-plugin-component还有一个缺点, 就是导入时不能使用别名, 在项目中大量使用babel-plugin-component进行按需导入模块时, 如混合, 工具函数等, 由于不能设置别名导致可能会出现重名的情况, 当时的临时解决方案是不同类型的模块采用不同的前缀.在使用babel-plugin-componen时, 它会时刻提醒你, 必须这样维护目录结构, 必须这样写, 不然就会报错.
结论: babel-plugin-import和babel-plugin-component适合对element-ui和antd的按需导入, 但不适合项目中所有的模块.
后面在npm
搜索更好的按需导入组件, 发现babel-plugin-transform-imports, 这个插件扩展性更高, 实现的也更加优雅.这个插件支持定制化转换后的导入源, 也就是由element-ui
到element-ui/lib/button
的规则支持自己定制, 这极大的解脱了开始笨重的目录结构, 立马用这个插件对项目中所有的基础模块(混合, 工具, 基础组件等一切可以视为js模块)进行按需导入.且这个插件支持别名, 使用起来直接无感, 非常酸爽.
此时项目采用babel-plugin-transform-imports
跟babel-plugin-import
和babel-plugin-component
并驾齐驱.
项目继续迭代, 对基础组件库element-ui
和antd
的主题进行定制, 然后定制后的样式文件放在了一个单独npm
库中, 使用babel-plugin-component和babel-plugin-import指向新的样式文件地址, babel-plugin-import可以比较优雅的解决, 大概是阿里也碰到过这种场景, 但是使用babel-plugin-component实现的代码旧比较丑陋.
接而打算放弃使用babel-plugin-import和babel-plugin-component, 只使用babel-plugin-transform-imports. 但在实际使用中发现, babel-plugin-transform-imports限制了一个导入项只能转为一个新的导入, 它只能将:
import { Button } from 'element-ui';
转为:
import Button from 'element-ui/lib/button';
对于button.css
将导入不进来.
而对于element-ui
和antd
这样样式跟逻辑分开放的组件库, 只能全局导入样式.故打算改造babel-plugin-transform-imports, 让它能覆盖babel-plugin-import的功能.
其实这类按需导入插件的原理特别简单, 利用babel对代码进行语法分析后, 对import
语句进行转换.如对:
import { Button } from 'components';
会被转为:
import Button from 'components/lib/button';
import 'components/lib/styleLibraryName/button.css';
故只需要让babel-plugin-transform-imports支持额外的资源导入即可.
这里另外提一点, 在阅读babel-plugin-transform-imports和babel-plugin-import的源码之后, 发现它们的实现方式不一样.
babel-plugin-transform-imports实现原理是, 对目标import
语句进行分析后, 根据分析结果生成新的一个import
语句列表, 然后替换掉原有的import
语句.
而babel-plugin-import实现原理是, 对目标import
语句进行分析, 根据分析结果加入新的import
语句, 但新的import
语句中的变量名会跟原import
语句(目标import
)中的输入变量名不一样, 需要在整个模块中可能使用原import
导入变量的地方都换成新的导入变量名. 最后删除目标导入语句.
比较拗口,用代码说明. 比如待转换的代码为:
import { Button } from 'element-ui';
console.log(Button);
babel-plugin-import会根据配置分析import { Button } from 'element-ui';
,得到需要加载Button
组件和Button
的样式文件, 使用@babel/helper-module-imports的addDefault
和addSideEffect
, 此时代码会变为:
import _Button from 'element-ui/lib/button';
import 'components/lib/button/style.css';
import { Button } from 'element-ui';
console.log(Button);
此时出现了_Button
和Button
, 而Button
是需要丢弃的旧的导入变量, 为了支持import { Button } from 'element-ui';
能够删除,需要插件对当前模块所有使用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';
变为:
import _Button from 'element-ui/lib/button';
import 'components/lib/button/style.css';
import { Button } from 'element-ui';
console.log(_Button);
为了保证Button
都转为_Button
, 需要尽可能的覆盖模块代码, babel-plugin-component检测的语句(表达式)类型有:CallExpression
, MemberExpression
, AssignmentExpression
, ArrayExpression
, Property
, VariableDeclarator
, LogicalExpression
, ConditionalExpression
, IfStatement
,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-import比babel-plugin-component多了几个需要检测的表达式.其实更好的方法是可以通过babel
提供的file.scope.getBinding
作用域来获取所有使用变量的地方, 然后修改对应使用变量的地方.
babel-plugin-import采用的方式很像babel-plugin-lodash使用的方式, file.scope.getBinding
也是babel-plugin-lodash中解决修改旧变量的办法, 但是这里只是对简单的一个导入语句的转换, 个人觉得babel-plugin-transform-imports处理的更加优雅.
模块的按需导入转换到这里就已经完全完毕, 但实际使用时, 虽然按需导入转换插件能够让按需导入模块像全局导入那样写, 避免过多的import
语句, 所有需要导入的模块都可以集中在一条import
语句中.但是并不是最简方式, 它有如下缺点:
*. 每一次导入一个新的模块, 总需要滑到文件最顶端, 添加一个新的模块, 这在一个复杂的表单页面时, 是非常痛苦的.
*. 需要移除一个不需要的模块时, 在没有使用eslint
的情况下可能忘记删除废弃模块导入, 这在多人协同的团队中改小bug时经常会出现.
为了解决这些问题, 团队参照babel-plugin-lodash
, 开发了插件支持对于下面的代码:
import { Row, Col } from 'antd';
(<Row>
<Col></Col>
</Row>)
可以这样写:
import Antd from 'antd';
(<Antd.Row>
<Antd.Col></Antd.Col>
</Antd.Row>)
这样的好处是, 在也不需要频繁的添加新的模块和移除废弃的模块了, 一切皆可以点出来, 特别是配合typescipt的代码提示时, 再也不用担心记不住模块的名字了.
这部分内容后续会有单独文章.
对于vue项目, 由于使用jsx较少, 还是需要根据需在当前组件中局部注册组件, 不够酸爽. 团队开发了自动根据模板中的标签按需导入组件库的插件, 让vue组件的使用就像全局注册一样, 但是打包确是按需导入.
这部分内容后续会有单独文章.
对babel-plugin-transform-imports进行改造后的库, 暂时还没有开源, 等开源后会在文末贴出地址.
网友评论