升级react@18.3.1后,把我坑惨了

背景

最近晚上在开发marsview功能,12点准备打包部署的时候,突然发现构建失败了,这件事很离奇,因为报错的文件是所有组件库对应的forwardRef语法,可是最近几天开发的内容跟组件库毫无关联。

在这上线前的时刻,居然出这种问题,一时让我很着急,幸亏这只是个人开源项目,继续查找了半个小时,最终才锁定问题,觉得有点坑,今天借此机会记录一下。

报错信息

Marsview是一个纯前端的低代码平台,为了增强组件库的能力,我们给每个物料组件都通过forwardRefuseImperativeHandle暴露了一系列方法。

最近在做行业模板的需求,希望开发者可以直接一键安装一个行业模板,告别从0搭建,然而提交以后,准备部署时,出现了上面的错误,而且几乎所有的组件库都报错。

错误跟踪

鼠标悬浮查看问题

鼠标查看问题,显示缺少属性:idtypenameconfig

查看组件类型定义

ts 复制代码
export type ComponentType<T = any> = {
  id: string;
  type: string;
  name: string | number;
  remoteUrl?: string;
  remoteConfigUrl?: string;
  remoteCssUrl?: string;
  parentId?: string;
  config: ConfigType<T>;
  // 属性中用于展示的事件,跟配置中的事件不同
  events?: Array<{ name: string; value: string }>;
  // 属性中用于展示的方法,跟配置中的方法不同
  methods: ComponentMethodType[];
  apis: { [key: string]: ApiType };
  elements: ComponentType<T>[];
  [key: string]: any;
};

类型里面已经包含了idtypenameconfig,于是,我下载了线上开源版本,发现线上版本正常,只有我当前这个最新开发版本报错。

自我思考

难道是版本问题? 是哪个插件引起的?

于是我分析了两个版本的插件版本,发现我当前开发版本用的是react@18.3.1,而线上GitHub由于提交了pnpm-lock.yaml锁定了版本,所以线上安装的是react@18.2.0,我本地开发时,意外删除了lock文件。

那为什么react@18.3.1版本会报错?

对比了两个版本的报错信息后,发现他们用的泛型不一样,在最新版本中,forwardRef的类型定义,用Omit排除了ref属性,而在老版本却没有。

版本差异分析

18.2类型定义

ts 复制代码
function forwardRef<T, P = {}>(
    render: ForwardRefRenderFunction<T, P>,
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

18.3类型定义

ts 复制代码
function forwardRef<T, P = {}>(
    render: ForwardRefRenderFunction<T, PropsWithoutRef<P>>,
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

type PropsWithoutRef<P> = P extends any ? ("ref" extends keyof P ? Omit<P, "ref"> : P) : P;

原来在18.2版本中,forwardRef的类型定义比较简单,render函数传两个泛型就可以,而新版本对于组件属性用了PropsWithoutRef<P>,也就是说排除了ref属性。

在看一下组件定义

tsx 复制代码
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属性,对不对?是不是有点破绽了?

再来看这段类型定义:

ts 复制代码
function forwardRef<T, P = {}>(
    render: ForwardRefRenderFunction<T, PropsWithoutRef<P>>,
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

type PropsWithoutRef<P> = P extends any ? ("ref" extends keyof P ? Omit<P, "ref"> : P) : P;

"ref" extends keyof P这段代码成立,所以返回:Omit<P, "ref">,相当于Omit<ComponentType>,"ref">,那这段类型有啥问题?其实我第一眼也没看出来有啥问题。

用typescript编译器看一下

在线编译器解析完以后,会发现Omit<P, "ref">返回的是:

ts 复制代码
{
    [x: string]: any;
    [x: number]: any;
}

最终这个类型跟我们组件定义的属性类型明细不一致,所以引发了这个问题。

问题解决

前面给大家分析了报错和分析过程,接下来,我们来解决这个问题。很明显,不能使用[key:string]:any来无脑扩展属性,因为它可能是ref: xxxx,从而变成Omit<P, "ref">,最终变成四不像。

整理思路:我们要什么?

我们需要动态扩展事件,比如:onChangeonClickonSubmitonFinishonReset等等,总之,我们要组件的类型定义支持动态事件。

怎么定义?

ts 复制代码
type OnProps<P extends string> = {
  [key in `on${P}`]: (data?: any) => void;
};

这样的话,就支持了以on开头的事件属性定义,并且值为一个函数,接受可选参数。

ComponentType定义

ts 复制代码
export type ComponentType<T = any> = {
  id: string;
  type: string;
  name: string | number;
  remoteUrl?: string;
  remoteConfigUrl?: string;
  remoteCssUrl?: string;
  parentId?: string;
  config: ConfigType<T>;
  // 属性中用于展示的事件,跟配置中的事件不同
  events?: Array<{ name: string; value: string }>;
  // 属性中用于展示的方法,跟配置中的方法不同
  methods: ComponentMethodType[];
  apis: { [key: string]: ApiType };
  elements: ComponentType<T>[];
} & OnProps<string>;

type OnProps<TKeys extends string> = {
  [P in `on${TKeys}`]: (data?: any) => void;
};

查看typescript编译结果

这一次结果就正常了。最终我们改完ComponentType类型以后,所有的组件库不再报错。

总结

  • 项目最好还是锁定版本,防止版本差异带来的痛苦。
  • 出现问题,不要慌张,仔细查看报错信息,逐步思考可能的报错原因。
  • 多锻炼解决问题的能力,这在工作中极其重要。

Marsview最近更新了不少功能,欢迎体验:www.marsview.com.cn

相关推荐
GISer_Jing19 分钟前
React渲染相关内容——渲染流程API、Fragment、渲染相关底层API
javascript·react.js·ecmascript
山猪打不过家猪20 分钟前
React(五)——useContecxt/Reducer/useCallback/useRef/React.memo/useMemo
前端·javascript·react.js
前端青山22 分钟前
React事件处理机制详解
开发语言·前端·javascript·react.js
科技D人生23 分钟前
Vue.js 学习总结(14)—— Vue3 为什么推荐使用 ref 而不是 reactive
前端·vue.js·vue ref·vue ref 响应式·vue reactive
对卦卦上心27 分钟前
React-useEffect的使用
前端·javascript·react.js
练习两年半的工程师27 分钟前
React的基本知识:事件监听器、Props和State的区分、改变state的方法、使用回调函数改变state、使用三元运算符改变state
前端·javascript·react.js
啵咿傲27 分钟前
在React中实践一些软件设计思想 ✅
前端·react.js·前端框架
GIS好难学1 小时前
《Vue零基础入门教程》第二课:搭建开发环境
前端·javascript·vue.js·ecmascript·gis·web
程序员黄同学2 小时前
Python 中如何创建多行字符串?
前端·python
anyup_前端梦工厂2 小时前
uni-app 认识条件编译,了解多端部署
前端·vue.js·uni-app