• 作者:老汪软件技巧
  • 发表时间: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,这个大家可能比较疑惑,你为什么要定义这个? 这个是有背景的,你听我说:

_react升级_react新版本

在低代码平台中,事件交互是最重要的能力之一,我需要给每个组件添加事件回调,从而在触发事件的时候去执行事件流中的行为。为了方便调用,我在组件渲染方法里面,遍历事件流,把事件回调统一挂在了组件上面,要不然我就要在组件内部循环查找来实现回调。

你跟我说着干啥?这跟类型定义有啥关系??

别激动,因为挂载的事件名称是动态的,所以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 OnProps

extends 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最近更新了不少功能,欢迎体验: