最近接手了一个 Ant Design 项目,客户试用时提了个需求:表格要支持拖拽列宽度。
好在公司的私有 npm 仓库里早就有人封装了一个带拖拽功能的 Table 组件实现。我心想,这还不简单?花点力气把业务代码里 import { Table } from "antd" 的地方逐个替换成公司仓库的实现就行了。
结果一动手,发现事情没那么简单。
这个实现的 TypeScript 类型写得相当粗糙,很多属性都是 any,或者干脆没导出对应的类型定义。除去替换,还需要加入大量的as any和eslint-disable,要改动的地方还是挺多的。
更头疼的是,刚把表格搞定,产品经理又陆续提了一堆组件调整需求:Modal 要能拖拽、Pagination 要默认显示快速跳转输入框……
如果每一个组件的调整都去业务代码里逐个修改,那工作量简直不敢想象。而且这严重违背了 DRY 原则——同样的逻辑要在几十个页面里重复写。
有没有一种方法,能够"劫持"一个库的某些导出,其余保持原样呢?
有的有的
我拿出了我的“秘密武器” —— rollup-plugin-import。这个插件的核心能力,是把这种写法:
import {
Table } from "antd";
转换成按需引入的形式:
import Table from "antd/lib/table";
它的原理是在编译阶段做 AST 转换,并自动拼接出对应的子包路径。
于是,我兴致勃勃地配置了如下代码:
import importPlugin from "rollup-plugin-import";
import {
paramCase } from "param-case";
export default {
plugins: [
importPlugin({
libraryName: "antd",
libraryDirectory: "lib",
namedComponent: paramCase,
exportName: "default"
})
]
};
按照这套配置,import { Table } from "antd" 会被转换成 import Table from "antd/lib/table"。
接下来,再配合构建工具的别名(alias)功能,把 antd/lib/table 解析到我自己的代理目录下,不就大功告成了吗?
理想很丰满,现实很骨感
正当我准备庆祝时,构建报错了。
原来:项目里还有这样一行代码:
import {
theme } from "antd";
按照上面的插件配置,它会被转换成:
import theme from "antd/lib/theme";
然而,Ant Design 的包结构里根本没有 antd/lib/theme 这个路径!theme 是直接挂在 antd 主包上的导出。看来:Ant Design 的新版本已经不支持 antd/lib/xxx 这种按需引入路径了。
精准打击:只劫持需要的部分
既然"一刀切"的转换会误伤无辜,那我们就改成"精准打击"——只对我们需要改造的组件做转换,其余的保持原样。
rollup-plugin-import 提供了一个 customName 配置项,允许我们自定义转换后的包名。于是我把配置改成了这样:
import importPlugin from "rollup-plugin-import";
export default {
plugins: [
importPlugin({
libraryName: "antd",
customName: function(name) {
switch(name) {
case "Table":
case "Modal":
case "Pagination":
return `@/proxy/antd/${
name}`;
}
// 不在名单里的,保持从 antd 导入
return "antd";
}
// 注意:这里要去掉 exportName,让插件以原名(命名导入)的方式引入
})
]
};
这样一来,构建结果就变成了:
import { Table } from "antd"→import { Table } from "@/proxy/antd/Table"import { theme } from "antd"→ 保持原样,不受影响import { Button } from "antd"→ 保持原样,不受影响
只有我们明确列出的三个组件会被重定向到我们的代理目录,其他的完全不受影响。
编写代理组件
接下来,我们在 src/proxy/antd/ 目录下编写自己的组件实现。
// src/proxy/antd/Table.tsx
import { TableProps } from "antd";
import { ResizableTable } from "@内部仓库/resizable-table";
export function Table(props: TableProps) {
return (
<ResizableTable
{...props}
bordered={props.bordered !== false}
/>
);
}
业务代码里依然写:
import { Table } from "antd";
<Table />
但实际上引入的是我们的代理组件,调整大小和表格边框已经默认开启了。业务代码完全不用改,就能享受到增强功能。
避免循环引用
这里有一个隐藏的坑:由于这个ResizableTable内部是引用antd的Table的,我们必须配置 include 和 exclude,让插件只处理业务代码里的导入,不处理我们代理目录里和node_modules里的导入:
import importPlugin from 'rollup-plugin-import';
import path from "path";
export default {
plugins: [
importPlugin({
// ...
// 只处理 src 下的文件
include: ["src/**/*"],
// 排除代理目录本身,和node_modules
exclude: [
"src/proxy/**/*",
"node_modules/**/*"
]
})
]
};
总结
通过 rollup-plugin-import 的 customName 功能,我们实现了一种优雅的"中间层"方案。这种思路不仅适用于组件库,各种hook库、函数库都同样有效。它的核心思想是:在不破坏原有使用习惯的前提下,通过构建时的路径劫持,悄无声息地替换掉库的特定实现。
业务代码依然是 import { Table } from "antd",但在构建之后,某些组件已经变成了"特供版"。这种"无感替换"正是前端工程化的魅力所在。