- 作者:老汪软件技巧
- 发表时间:2024-11-26 17:03
- 浏览量:
背景
最近晚上在开发marsview功能,12点准备打包部署的时候,突然发现构建失败了,这件事很离奇,因为报错的文件是所有组件库对应的forwardRef语法,可是最近几天开发的内容跟组件库毫无关联。
在这上线前的时刻,居然出这种问题,一时让我很着急,幸亏这只是个人开源项目,继续查找了半个小时,最终才锁定问题,觉得有点坑,今天借此机会记录一下。
报错信息
Marsview是一个纯前端的低代码平台,为了增强组件库的能力,我们给每个物料组件都通过forwardRef和useImperativeHandle暴露了一系列方法。
最近在做行业模板的需求,希望开发者可以直接一键安装一个行业模板,告别从0搭建,然而提交以后,准备部署时,出现了上面的错误,而且几乎所有的组件库都报错。
错误跟踪鼠标悬浮查看问题
鼠标查看问题,显示缺少属性:id、type、name、config。
查看组件类型定义
export type ComponentTypeany> = {
id: string;
type: string;
name: string | number;
remoteUrl?: string;
remoteConfigUrl?: string;
remoteCssUrl?: string;
parentId?: string;
config: ConfigType;
// 属性中用于展示的事件,跟配置中的事件不同
events?: Array<{ name: string; value: string }>;
// 属性中用于展示的方法,跟配置中的方法不同
methods: ComponentMethodType[];
apis: { [key: string]: ApiType };
elements: ComponentType[];
[key: string]: any;
};
类型里面已经包含了id、type、name、config,于是,我下载了线上开源版本,发现线上版本正常,只有我当前这个最新开发版本报错。
自我思考
难道是版本问题? 是哪个插件引起的?
于是我分析了两个版本的插件版本,发现我当前开发版本用的是react@18.3.1,而线上GitHub由于提交了pnpm-lock.yaml锁定了版本,所以线上安装的是react@18.2.0,我本地开发时,意外删除了lock文件。
那为什么react@18.3.1版本会报错?
对比了两个版本的报错信息后,发现他们用的泛型不一样,在最新版本中,forwardRef的类型定义,用Omit排除了ref属性,而在老版本却没有。
版本差异分析
18.2类型定义
function forwardRef(
render: ForwardRefRenderFunction,
): ForwardRefExoticComponent<PropsWithoutRef & RefAttributes>;
18.3类型定义
function forwardRef(
render: ForwardRefRenderFunctionPropsWithoutRef>,
): ForwardRefExoticComponent<PropsWithoutRef
& RefAttributes>;
type PropsWithoutRef = P extends any ? ("ref" extends keyof P ? Omit
"ref"> : P) : P;
原来在18.2版本中,forwardRef的类型定义比较简单,render函数传两个泛型就可以,而新版本对于组件属性用了PropsWithoutRef
,也就是说排除了ref属性。
在看一下组件定义
const Input = (props:ComponentType,ref:any)=>{
return <input />
}
export default forwardRef(Input)
在泛型里面,T对应ref对象,P对应组件属性props,就算这样,那又怎样?继续分析...
原因分析
类型ComponentType中,有一个动态属性定义:[key:string]:any,这个大家可能比较疑惑,你为什么要定义这个? 这个是有背景的,你听我说:
在低代码平台中,事件交互是最重要的能力之一,我需要给每个组件添加事件回调,从而在触发事件的时候去执行事件流中的行为。为了方便调用,我在组件渲染方法里面,遍历事件流,把事件回调统一挂在了组件上面,要不然我就要在组件内部循环查找来实现回调。
你跟我说着干啥?这跟类型定义有啥关系??
别激动,因为挂载的事件名称是动态的,所以ComponentType上面才有了[key:string]:any的类型定义。由于key是任何字符串,也就意味着它可以是ref属性,对不对?是不是有点破绽了?
再来看这段类型定义:
function forwardRef(
render: ForwardRefRenderFunctionPropsWithoutRef>,
): ForwardRefExoticComponent<PropsWithoutRef
& RefAttributes>;
type PropsWithoutRef = P extends any ? ("ref" extends keyof P ? Omit
"ref"> : P) : P;
"ref" extends keyof P这段代码成立,所以返回:Omit
,相当于Omit,"ref">,那这段类型有啥问题?其实我第一眼也没看出来有啥问题。
用typescript编译器看一下
在线编译器解析完以后,会发现Omit
返回的是:
{
[x: string]: any;
[x: number]: any;
}
最终这个类型跟我们组件定义的属性类型明细不一致,所以引发了这个问题。
问题解决
前面给大家分析了报错和分析过程,接下来,我们来解决这个问题。很明显,不能使用[key:string]:any来无脑扩展属性,因为它可能是ref: xxxx,从而变成Omit
,最终变成四不像。
整理思路:我们要什么?
我们需要动态扩展事件,比如:onChange、onClick、onSubmit、onFinish、onReset等等,总之,我们要组件的类型定义支持动态事件。
怎么定义?
type OnPropsextends string> = {
[key in `on${P}`]: (data?: any) => void;
};
这样的话,就支持了以on开头的事件属性定义,并且值为一个函数,接受可选参数。
ComponentType定义
export type ComponentTypeany> = {
id: string;
type: string;
name: string | number;
remoteUrl?: string;
remoteConfigUrl?: string;
remoteCssUrl?: string;
parentId?: string;
config: ConfigType;
// 属性中用于展示的事件,跟配置中的事件不同
events?: Array<{ name: string; value: string }>;
// 属性中用于展示的方法,跟配置中的方法不同
methods: ComponentMethodType[];
apis: { [key: string]: ApiType };
elements: ComponentType[];
} & OnProps<string>;
type OnProps<TKeys extends string> = {
[P in `on${TKeys}`]: (data?: any) => void;
};
查看typescript编译结果
这一次结果就正常了。最终我们改完ComponentType类型以后,所有的组件库不再报错。
总结
Marsview最近更新了不少功能,欢迎体验: