按需引入组件原理

184 阅读3分钟

各个组件库是怎么做的? 它们通常都会使用 webpack 或者 rollup 打包生成一个入口的js文件,这个文件通常都是在你不需要按需引入组件库时使用。 比如 iview 组件库中的 dist 目录下的 iview.js 文件。 import ViewUI from 'view-design';

// 引入了整个js文件,里面可能包含了一些你不需要的组件的代码 Vue.use(ViewUI); 复制代码再比如 rsuite 组件库中 lib 目录下的 index.js 文件。 // 即使你没有使用其他组件,也会引入一整个js文件 import { Button } from 'rsuite';

function App() { return Hello World; } 复制代码如果我们不需要引入全部的组件,我们首先就不能将组件的代码,打包到一个js文件中。我们可以直接使用 babel 或者借助 gulp 对组件库源码的 src 目录中各个文件直接进行编译, 并写入到目标的 lib 目录。

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

编译后的目录结构和源码的目录结构是完全一致的,但是组件的代码已经是经过babel处理过的了。 这个时候,我们就可以实现按需引入组件库。但是业务代码中的 import 代码得修改一下。我们以 rsuite 组件库为例。如果我们只想使用 Button 组件,我们就需要指明,只引入 lib\Button 目录下 index.js 文件。 // 只引入 node_modules/rsuite/lib/Button/index.js 文件 import Button from 'rsuite/lib/Button'; 复制代码这样做实在是颇为麻烦,好在已经有了现成的解决方案,babel-plugin-import 插件。假设我们打包后的目录结构如下图。

我们只需要在 .babelrc 中做如下的设置。

// .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眼中张什么样子

图片右边的树,就是访问者函数中的 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]; }