组件库按需加载深入探讨

为了降低首屏代码大小,对于一些大的第三方库或者团队的基础工具库,需要按需导入模块。如:

import Button from 'antd/lib/button';

但这在需要导入非常多的组件场景时,开发繁琐,体验不友好。在这些组件库的官方文档或者社区会推荐一些babel插件,帮助达到良好的开发体验和性能优化。

本文将详细探究这些工具的原理。

antd等UI组件库按需加载

在使用antd的老版本时,会推荐使用babel-plugin-import工具按需导入组件。工具可以做到如下的转换:

import { Button } from 'antd';
ReactDOM.render();

      ↓ ↓ ↓ ↓ ↓ ↓

var _button = require('antd/lib/button');
require('antd/lib/button/style');
ReactDOM.render(<_button>xxxx);
antd和element-ui都叫做按需加载,但我觉得叫做按需导入更加贴合意境。
eleemnt-ui配套按需导入工具为 babel-plugin-component

babel-plugin-import工具,会在编译时会分析模块导入语句。当为需要导入的目标库组件时,会将原导入删除,生成多个新的导入(如果你配置了style,会额外导入样式)。由于重新生成的导入语句会导致模块变量发生变化(如上文案例演示中Button会被转换为_button),这导致插件程序还会分析当前模块的所有变量,对使用原变量的语句中,将变量名修复。

最新的antd已经推荐使用webpack的tree shaking机制来按需加载。 babel-plugin-import也基本没什么更新。对于vue生态来说,很多组件库为了支持全局注册的方式,无法使用tree shaking。

虽然babel-plugin-import等工具支持一些配置定制,但还是存在下面缺点:

  1. 每个插件都是针对特定的组件库,需要符合特定的目录和文件维护规范。如babel-plugin-import导入的模块需要支持目录为:

    |--component
    |----index.js
    |----*.js
    |----style
    |------index.js
    |------*.css
  2. babel-plugin-import由于底层实现改变了导入的模块变量,然后再全模块枚举语句类型中找到使用变量将其修复,再某一些非常不常见的语句中,会出现没有转变模块变量导致语法错误问题。

    对任意库支持按需导入

    如果也想对项目中公共基础模块(公共组件,公共工具文件等)支持源码中全量导入但实际按需加载的效果,除了可以fork babel-plugin-import等工具调整逻辑来实现,还可以使用工具babel-plugin-transform-imports来支持。

babel-plugin-transform-imports是自己配置转换格式,如可以达到下面效果:

import { Row, Grid as MyGrid } from 'react-bootstrap';
import { merge } from 'lodash';

      ↓ ↓ ↓ ↓ ↓ ↓
      
import Row from 'react-bootstrap/lib/Row';
import MyGrid from 'react-bootstrap/lib/Grid';
import merge from 'lodash/merge';

此时需要的配置为:

{
  "plugins": [
    ["transform-imports", {
      "react-bootstrap": {
        "transform": "react-bootstrap/lib/${member}",
        "preventFullImport": true
      },
    }]
  ]
}
transform是可以支持函数的,实现高级定制,扩展性非常高。

babel-plugin-transform-imports实现的方式是直接对导入语句进行了修复,比babel-plugin-import更加优雅和适用性更广,阅读他的源码也可以发现比babel-plugin-import简洁的多。

babel-plugin-import底层是将原来导入删除,然后生成新的目标导入,导致了导入模块的变量发生了变更。

babel-plugin-transform-imports同样有一些缺陷:它无法由一个导入生成多个导入。这也就意味着使用babel-plugin-transform-imports无法对antd,element-ui这样的UI组件库进行模块按需导入:这些UI组件库,除了js模块的导入外,往往还有一个样式模块。

可以fork babel-plugin-transform-imports扩展其逻辑,如这个库babel-plugin-transform-module-imports。我参照他的实现原理,实现了一个工具babel-plugin-transform-import-module,可以支持更多的配置,基本可以使用它满足所有的模块在源码中全量导入但实际按需导入的效果。

像lodash一样根据调用按需导入

在使用lodash是,可以使用babel-plugin-lodash插件进行导入优化。能够将:

import _ from 'lodash'
import { add } from 'lodash/fp'
 
const addOne = add(1)
_.map([1, 2, 3], addOne)

在编译时转换为:

import _add from 'lodash/fp/add'
import _map from 'lodash/map'
 
const addOne = _add(1)
_map([1, 2, 3], addOne)

利用babel-plugin-lodash可以提升lodash使用时的开发体验:代码中是全量导入使用,无需关注其内部结构,将按需导入对应的工具函数模块交给底层babel插件处理。使用者除了不需要操心是否导入额外的代码外,还可以配合typescipt使用,拥有更好的代码提示,从而降低认知。

当前这个插件只支持lodash库使用。借鉴其理念,可以实现:

import utils from 'utils@'
utils.downloadFile('path/to/file')

      ↓ ↓ ↓ ↓ ↓ ↓

import _downloadFile from 'utils@/downloadFile'
_downloadFile('path/to/file')

如果改造更加深入一点,可以对一些市面上流行库的优化,如antd库的使用:

import * as Antd from 'antd';
ReactDOM.render(xxxx);

      ↓ ↓ ↓ ↓ ↓ ↓

import Button from 'antd/lib/button';
import('antd/lib/button');
ReactDOM.render();

上面的效果在我实现的一个babel插件babel-plugin-module-call-import都可以支持。如果你需要完全像babel-plugin-lodash一样根据文件目录结构自动转换加载器,可以直接fork源码上调整。

我的团队里面多个项目基本使用这种方式。

在vue代码中根据标签按需导入

在vue应用注册组件时,有两种方式:全局注册局部注册。全局注册开发体验式最好的,但会导入UI组件库的所有代码,增加很多无用代码从而首屏激增。如何做到像全局注册组件那样开发友好,又支持按需导入组件提高性能呢?

这里提供一个方案:在vue文件的模板在解析的时候,记录模板文件使用的标签,然后在文件对应的js代码编译的时候,根据标签自动导入UI组件,然后在组件配置对象的components属性中注册上去。

假如代码:



在编译时,将会转换类似于:



我开发了两个工具实现持这个方案:

只支持与vue2版本,vue3可以参照方案自己实现下。

你有什么更好的想法,欢迎讨论。

你可能感兴趣的