组件库按需加载深入探讨

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

import Button from 'antd/lib/button';

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

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

antd等UI组件库按需加载

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

import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);

      ↓ ↓ ↓ ↓ ↓ ↓

var _button = require('antd/lib/button');
require('antd/lib/button/style');
ReactDOM.render(<_button>xxxx</_button>);

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
  1. 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(<Antd.Button>xxxx</Antd.Button>);

      ↓ ↓ ↓ ↓ ↓ ↓

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

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

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

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

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

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

假如代码:

<template>
  <div>
    <el-button>按钮</el-button>
  </div>
</template>

<script>
  export default {
    created() {
    },
  };
</script>

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

<template>
  <div>
    <el-button>按钮</el-button>
  </div>
</template>

<script>
  import ElButton from 'element-ui/lib/form-item';
  
  export default {
    components: {
      ElButton,
    },
    created() {
    },
  };
</script>

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容