amis
是百度开源的一个前端低代码框架,使用 JSON
配置来生成页面,有120+的内置组件。但是组件太齐全也有一个坏处,就是包体积过大,但是实际上一个页面可能只使用了少数几个组件,大多数组件都是没有被使用到的,因此我们尝试了一次组件的分包构建
大致思路
首先肯定是需要将amis
和amis-ui
中的所有组件都使用vite
单独打成iife
包,然后分析amis schema
配置,找到使用了哪些组件,然后只引入这些组件以及其所依赖的组件。下面是具体步骤。
1、amis
和amis-ui
单独打包
1.1 分析amis
和amis-ui
组件
由于每个组件都是一个单独的入口,所以这一步我们从文件系统中找到所有的组件
const amisRootDir = [
`packages/amis/src/renderers`,
];
const amisUIRootDir = [
`packages/amis-ui/src/components`,
`packages/amis-ui/src/hooks`,
`packages/amis-ui/src/locale`,
`packages/amis-ui/src/themes`,
];
具体实现就是从上面的入口中递归找到所有的组件,需要注意的是amis-ui/src/hooks/index.ts
和amis-ui/src/components/index.tsx
是汇总文件,需要被忽略
最终这个步骤将会输出一个类似于下面的json
{
"amis": [
{
"name": "amis.Action", // iife的全局变量名称
"srcPath": "amis/src/renderers/Action.tsx", // 组件在文件系统上的相对路径
"impPath": "amis/lib/renderers/Action" // 组件的import路径
}
],
"amisUi": [
{
"name": "amisUi.NotFound",
"srcPath": "amis-ui/src/components/404.tsx",
"impPath": "amis-ui/lib/components/404"
}
]
}
1.2 分析amis-ui
的组件导入方式
这个步骤我们使用es-module-lexer
来处理,但是es-module-lexer
不能分析ts
,所以需要确保amis-ui
被rollup
构建过,生成了esm
目录,我们来分析packages/amis-ui/esm/index.js
中的内容
有这个步骤的原因是因为在
amis schema
配置中不直接依赖amis-ui
,都是通过在amis
中导入的amis-ui
,但是我们无法通过这种导入语句来找到组件具体位置,也无法确定导入形式。如:无法通过import { Table } form 'amis-ui'
找到Table
组件的具体位置,找到了位置也无法确定是该用import Table from 'amis-ui/lib/table'
还是用import { Table } from 'amis-ui/lib/table'
来导入
在packages/amis-ui/esm/index.js
中有四种组件导入方式
名称 | 描述 | 例子 |
---|---|---|
name | 命名导入 | import { A } from 'B'; |
rename | 重命名导入 | import { A as AA } from 'B'; |
default | 默认导入 | import A from 'B'; |
all | 导入所有 | import * as A from 'B'; |
最终这个步骤将会输出一个类似于下面的json
{
"NotFound": {
"importsWay": "default", // 导入方式
"impPath": "amis-ui/lib/components/404", // 组件的import路径
"srcPath": "amis-ui/src/components/404.tsx" // 组件在文件系统上的相对路径
},
"AlertComponent": {
"importsWay": "rename",
"originName": "FinnalAlert", // 如果是重命名导入,还需要一个原始名称
"impPath": "amis-ui/lib/components/Alert",
"srcPath": "amis-ui/src/components/Alert.tsx"
}
}
1.3 使用vite
将amis
和amis-ui
分包构建
这个步骤主要就是写rollup插件了
- 1.3.1 在
transform
钩子中将所有amis-ui
和amis
的导入路径改为步骤1.1中生成的iife的全局变量名称
- 1.3.2 在
external
配置项中将id
为iife的全局变量名称
返回true
- 1.3.3 还要处理下动态导入,在
renderDynamicImport
钩子中返回{ left: 'Promise.resolve().then(() => ', right: ')' };
1.4 找到amis
组件的iife的全局变量名称和renderer
组件类型之间的对应关系
有两个解决方案,我是使用的第二种
- 正则表达式。大部分的
renderer
都是类似下面这样的,少部分的可以特殊处理
@Renderer({
type: 'avatar'
})
@withBadge
export class AvatarFieldRenderer extends AvatarField {}
- 在
moduleParsed
钩子中通过ast
来寻找
最终这个步骤将会输出一个类似于下面的json
{
"action": "amis.Action",
"button": "amis.Action",
"submit": "amis.Action",
"reset": "amis.Action",
"crud": "amis.CRUD",
"calendar": "amis.Calendar",
"card2": "amis.Card2",
"{\"pattern\":\"(^|\\\\/)(?:crud\\\\/body\\\\/grid|cards)$\",\"flags\":\"\"}": "amis.Cards", // 正则表达式
"collapse-group": "amis.CollapseGroup",
"collapse": "amis.Collapse",
"carousel": "amis.Carousel",
}
1.5 分析组件依赖数据
这个步骤主要是在external
配置项中,最终生成类似下面的json。
{
"amis.AnchorNav": [
"amisUi.AnchorNav"
],
"amis.BarCode": [
"amisUi.BarCode"
],
"amis.Avatar": [
"amisUi.Avatar",
"amisUi.Badge"
],
"amis.Alert": [
"amisUi.Alert2"
],
"amis.Action": [
"amisUi.Button",
"amis.Remark",
"amisUi.Badge",
"amisUi.TooltipWrapper"
]
}
按理来说这是除了发包外的最后一步了,但在实践中发现amis-ui
出现了循环依赖!!组件的依赖链陷入了死循环!!
1.6 解决循环依赖
- 找到循环依赖
在这个步骤中我们使用到了类似N叉树的数据结构
class Node {
constructor(val) {
this.val = val;
}
/** @type {string} */
val;
/** @type {Node[]} */
children;
/** @type {string[]?} */
ancestor;
setChildren(children) {
this.children = children;
this.#setParent();
}
#setParent() {
if (this.children) {
for (const child of this.children) {
// child.parent = this;
child.ancestor = [...(this.ancestor || []), this.val]; // 祖先
}
}
}
getLoopDep() {
if (this.ancestor) {
const index = this.ancestor.indexOf(this.val);
if (index !== -1) {
const arr = this.ancestor.splice(index);
arr.push(this.val);
return arr;
}
}
return null;
}
}
最终找到了5组循环依赖
amis-ui/src/components/condition-builder/Group.tsx
amis-ui/src/components/condition-builder/GroupOrItem.tsx
amis-ui/src/components/condition-builder/Expression.tsx
amis-ui/src/components/condition-builder/Func.tsx
amis-ui/src/components/formula/Editor.tsx
amis-ui/src/components/formula/plugin.ts
amis-ui/src/components/json-schema/Item.tsx
amis-ui/src/components/json-schema/Array.tsx
amis-ui/src/components/json-schema/Object.tsx
amis-ui/src/components/schema-editor/Item.tsx
amis-ui/src/components/schema-editor/Array.tsx
amis-ui/src/components/schema-editor/Object.tsx
- 解决循环依赖
还好不是很多,解决方案为将有循环依赖的组件打包在一起,在amis-ui
中新建5个文件分别导入有循环依赖的组件再导出,如:
import * as Expression from '../../condition-builder/Expression';
import * as Func from '../../condition-builder/Func';
export default {
Expression,
Func
};
并且生生成一个json文件来标识这些循环依赖项,以便特殊处理
[
"amisUi.loopImport.conditionBuilder.Group_GroupOrItem",
"amisUi.loopImport.conditionBuilder.Expression_Func",
"amisUi.loopImport.formula.Editor_plugin",
"amisUi.loopImport.jsonSchema.Item_Array_Object",
"amisUi.loopImport.schemaEditor.Item_Array_Object"
]
终于到了发包前的最后一步了,但事实证明我还是想多了
1.7 再次寻找依赖项
在amis
代码中还有很多使用render
函数而产生的依赖项,类下面这些代码:
同样有两种解决方案,这次我是使用的第一种
- 正则表达式
- 在
moduleParsed
钩子中通过ast
来寻找
最终这个步骤将会输出一个类似于下面的json
{
"amis.CRUD": [
"amis.Action",
"amis.Pagination",
"amis.Dialog"
],
"amis.App": [
"amis.Action"
],
"amis.Card": [
"amis.Action"
],
}
1.8 发包
终于来到这个部分的最后一步了,生成package.json
,把所有组件发到npm上就完事了
最终的文件目录如下
amis-splitPkg/
├── amis
│ ├── Action
│ │ └── index.iife.js
│ ├── Alert
│ │ └── index.iife.js
│ └── ...
├── amis-ui // amis-ui组件目录
│ ├── Alert
│ │ └── index.iife.js
│ ├── Avatar
│ │ └── index.iife.js
│ └── ...
├── depsData.json
├── globalUmdPkg.json
├── loopImportKey.json
├── metaData.json
├── renderDepsData.json
├── README.md
└── package.json
1.8.1 文件和文件目录说明
-
amis
amis
组件目录 -
amisUi
amis-ui
组件目录 -
depsData.json
通过
import
语句分析出来的组件间依赖关系,不允许存在循环依赖,且组件导入有严格的顺序要求 -
renderDepsData.json
在
amis
组件中通过调用render
函数所产生的依赖关系,可以存在循环依赖,且没有组件导入顺序要求 -
metaData.json
schema
中的renderer
类型和组件名的对应关系,其中以{
开头的key
为正则表达式,需要特殊处理 -
loopImportKey.json
有循环依赖的组件一起打包后的组件名
-
globalUmdPkg.json
公共组件库的
umd
包的路径,如:react、react-dom等
针对amis
的构建部分终于结束了