组件库实现按需引入的原理

前言

已经有好长时间不在掘金冒泡了,固然也是事出有因,3月份动了一次手术,请了3个月的病假。作完手术就一直躺在床上玩switch,一直玩到如今,什么塞尔达传说,火焰纹章也在这段时间打通关了(其实我很想玩ps4,可是作起来费劲只能躺着玩掌机)。css

WechatIMG237.jpeg

发生意外以前,在公司正着手准备作一个内部的ui库,因而就研究了一下一些开源的ui库的方案,这里作一个简单总结和分享。node

各个组件库是怎么作的?

它们一般都会使用 webpack 或者 rollup 打包生成一个入口的js文件,这个文件一般都是在你不须要按需引入组件库时使用。react

好比 iview 组件库中的 dist 目录下的 iview.js 文件。webpack

import ViewUI from 'view-design';

// 引入了整个js文件,里面可能包含了一些你不须要的组件的代码
Vue.use(ViewUI);
复制代码

再好比 rsuite 组件库中 lib 目录下的 index.js 文件。git

// 即便你没有使用其余组件,也会引入一整个js文件
import { Button } from 'rsuite';

function App() {
  return <Button>Hello World</Button>;
}
复制代码

若是咱们不须要引入所有的组件,咱们首先就不能将组件的代码,打包到一个js文件中。咱们能够直接使用 babel 或者借助 gulp 对组件库源码的 src 目录中各个文件直接进行编译, 并写入到目标的 lib 目录。github

// 使用`gulp-babel`对源代码的目录进行编译
function buildLib() {
  return gulp
    .src(源码目录)
    .pipe(babel(babelrc()))
    .pipe(gulp.dest(目标lib目录));
}
复制代码

WX20200512-223522@2x.png

WX20200512-223544@2x.png

编译后的目录结构和源码的目录结构是彻底一致的,可是组件的代码已是通过babel处理过的了。web

这个时候,咱们就能够实现按需引入组件库。可是业务代码中的 import 代码得修改一下。咱们以 rsuite 组件库为例。若是咱们只想使用 Button 组件,咱们就须要指明,只引入 lib\Button 目录下 index.js 文件。gulp

// 只引入 node_modules/rsuite/lib/Button/index.js 文件
import Button from 'rsuite/lib/Button';
复制代码

这样作实在是颇为麻烦,好在已经有了现成的解决方案,babel-plugin-import 插件。假设咱们打包后的目录结构以下图。babel

WX20200512-225547@2x.png

咱们只须要在 .babelrc 中作以下的设置。antd

// .babelrc
{
  "plugins": [
    [
      "import", {
        "libraryName": "react-ui-components-library",
        "libraryDirectory": "lib/components",
        "camel2DashComponentName": false
      }
    ]
  ]
}
复制代码

babel插件就会自动将 import { Button } from '组件库' 转换为 import Button from '组件库/lib/components/Button'

那么 babel-plugin-import 是如何作到的呢?

babel-plugin-import的实现机制

babel-plugin-import 源码我并无仔细研究,只是大概看了下,不少细节并非很了解,若有错误还请多多包含。

在了解babel-plugin-import源码前,咱们还须要了解ast,访问者的概念,这些概念推荐你们阅读下这篇手册,Babel 插件手册

babel-plugin-import 的源码中定义了 import 节点的访问者(babel在处理源码时,若是遇到了import语句,会使用import访问者对import代码节点进行处理)

咱们先看看,import代码节点在babel眼中张什么样子

image.png

图片右边的树,就是访问者函数中的 path参数

ImportDeclaration(path, { opts }) {
    const { node } = path;

    if (!node) return;

    const { value } = node.source;
    const libraryName = this.libraryName;
    const types = this.types;
    // 若是value等于咱们在插件中设置的库的名称
    if (value === libraryName) {
      node.specifiers.forEach(spec => {
        // 记录引入的模块
        if (types.isImportSpecifier(spec)) {
          this.specified[spec.local.name] = spec.imported.name;
        } else {
          this.libraryObjs[spec.local.name] = true;
        }
      });
      // 删除原有的节点,就是删除以前的import代码
      path.remove();
    }
  }
复制代码

在适当的时刻,会插入被修改引入路径的 import 节点

importMethod(methodName, file, opts) {
    if (!this.selectedMethods[methodName]) {
      const libraryDirectory = this.libraryDirectory;
      const style = this.style;
      // 修改模块的引入路径,好比 antd -> antd/lib/Button
      const path = `${this.libraryName}/${libraryDirectory}/${camel2Dash(methodName)}`;
      // 插入被修改引入路径的 import 节点
      this.selectedMethods[methodName] = file.addImport(path, 'default');
      if (style === true) {
        file.addImport(`${path}/style`);
      } else if(style === 'css') {
        file.addImport(`${path}/style/css`);
      }
    }
    return this.selectedMethods[methodName];
}
复制代码
相关文章
相关标签/搜索