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

发生意外以前,在公司正着手准备作一个内部的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目录)); } 复制代码


编译后的目录结构和源码的目录结构是彻底一致的,可是组件的代码已是通过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

咱们只须要在 .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眼中张什么样子

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