什么是JSDK
在前端项目中我们常常要注入一些第三方的功能,俗称JSDK。常见的有混合开发App的JSBridge
、神策埋点
和一些公司内部所使用的方法。这些方法一般通过<script>
标签放置在body的最后面来为整个前端应用提供特定的功能。
随便打开一个网站就能看到各种各样的:
这些注入的JSDK通常都会作为一个对象挂载到window
上,来方便全局使用。同时类似向JSBridge这样与原生端webview交互的可能会有一些回调函数也挂载到window上。
背景
我司的整体项目技改(由angular技改为react16 + TypeScript4)已经进行了一年多的时间了。但在我接手的项目中发现很多同事在使用全局JSDK时通常采用以下写法,甚至在我被借调到别的组之后另外一个组的同事也是下面这种写法。
以JSBridge举例,处理调用:
tsx
/**
* 写法1:window as any
*/
// eslint-disable-next-line no-unused-expressions
(window as any).JSBridge.xxxxx
/**
* 写法2:全局声明JSBridge为any
*/
// global.d.ts
declare const JSBridge: any;
// index.tsx
JSBridge.xxxxx
/**
* 写法3:ts-ignore
*/
// @ts-ignore
JSBridge.xx();
处理JSBridge在window上的回调:
ts
// global.d.ts
interface Window {
addressCallback: () => void; // 选择地址回调
userInfoCallback: (info) => void; // 用户信息回调
...... // 等等等等诸如此类,很长很长
}
对此我只能表示
我只想说不管是什么开发工具,什么开发语言都是为了更好更快的进行开发而出现的。既然使用上面的这些写法,那使用TypeScript的意义在哪里?仿佛在画蛇添足,还不如直接使用JS方便!!!
解决方案
打一个包
这没什么好说的,作为调用方我完全不知道JSDK中有哪些方法暴露出来。我只能根据文档现有的内容写类型以满足我的需求。
但是作为JSDK的开发者,他们很清楚每个功能的入参、出参,由他们来编写一个ts文件是最好的。就好比现在成熟的包都有他们对应的ts包,例如react、vue、lodash等。
将所有的JSDK统一封装成一个本公司的@types/xxxx
包发布到npm中,然后在项目里一装就可以轻松使用JSDK中的各种功能,既有良好的提示又不会报错。这种是最好的解决方案。
因此我第一时间联系了JSDK的编写相关方处理这个事情,但他们手中任务很多,只说把这件事记录下来了。等不忙了之后给我排期。
自己写一个
说到排期那大家都懂得,基本等于遥遥无期......而且又不是不能用......
既然JSDK出品方不能完成,那只能自己动手丰衣足食先解决眼下的燃眉之急。上面那几种写法主要有以下几个问题:
- 不能直接使用
- 不能通过window调用
- 使用any避免上述两个问题后,ide不提示API,方法名写错了也不知道,像在写js
- window上各种与业务代码无关的、没有实际意义的回调方法冗余,随着挂载的方法越来越多整个文件变的臭不可闻
为JSDK创建一个文件夹模拟npm包
如果想让ide进行提示,那必须要将对应API的类型文件补充齐全。因此需要建立一个单独的文件夹来存放各项JSDK类型,并将里面的API补齐。
当然作为调用方我并不知道有什么API,但是把自己这次用到的API补足进去还是完全可以做到的。这样每个人在调用时先去补充下ts注解,久而久之后面的人在使用时就方便快捷完全不用担心上面的哪些问题。
以下是我个人写的一些JSDK类型:
JSBridge.ts
ts
/* eslint-disable no-unused-vars */
/* eslint-disable no-magic-numbers */
/* eslint-disable no-redeclare */
type JSBridgeFn = (param?: any) => void;
type JSBridgeOthers = Record<string, any>; // 预留字段,暂无使用场景
interface BaseResponse {
respCode: string;
respMsg: string;
data?: Record<string, any>;
}
interface JSBridgeGetUserLoginStatusResp {
data: "0" | "1"; // 1 - 登录成功,0 - 登录失败
respCode: string;
respMessage: string;
}
interface JSBridgePostOthers {
isShowLoading?: 0 | 1; // 1 - 表示弹出loading框 0 - 表示不弹出loading框
timeout?: number; // 超时时间(s)
toastError?: boolean; // 设置api执行失败后是否以toast形式弹出错误信息 true 表示弹出错误信息 false表示不弹出错误信息
}
interface JSBridgeOpenUrlOthers {
urlVersion?: string; // 打开链接的最低内部版本号,不满足设置的内部版本号则会提示版本升级
screenMode?: string; // 全屏设置:screenMode=FullScreen
loadMode?: string; // 进程模式设置,单进程设置loadMode=single,一般无需设置
}
declare function openUrl(url: string, others?: JSBridgeOpenUrlOthers): void;
declare function openUrl(url: string, screenMode: string, others?: JSBridgeOpenUrlOthers): void;
declare function openUrl(url: string, screenMode: string, loadMode: string, others?: JSBridgeOpenUrlOthers): void;
export default interface JSBridge {
readonly ready: (callback: JSBridgeFn) => void;
readonly setTitle: (title: string) => void;
readonly toast: (str: string) => void;
readonly alert: (title: string, msg: string, buttonText?: string, others?: JSBridgeOthers) => void;
readonly close: (others?: JSBridgeOthers) => void;
readonly login: (forceLogin: boolean, xBizScene?: string, others?: JSBridgeOthers) => Promise<void>;
readonly openUrl: typeof openUrl;
readonly getUserLoginStatus: () => Promise<JSBridgeGetUserLoginStatusResp>;
readonly onWebViewDidShow: (callback: JSBridgeFn) => void;
readonly selectAddress: (
sceneId: string, // 业务场景ID
addressId: string, // 地址ID, 传空,相当于是查询收货地址。
others?: {
showAdd?: boolean; // 用户无地址时,是否默认弹出添加地址组件。true为弹出,false或不传为不弹出
operator?: string; // query - 空-同原协议,默认状态 | add-新增 | edit-编辑
}
) => Promise<any>;
readonly selectCity: (
sceneId: string, // 业务场景ID
city: string, // 默认选中城市
others?: JSBridgeOthers
) => Promise<any>;
readonly postData: (
postMode: string, // 本地请求处理方式,用于映射部分参数,目前应用中基本上都是使用应用别名
url: string, // 请求路径,一般是接口路径,如info/getUserInfo.json
data?: Record<string, any>, // 接口请求业务参数,注意,因设计问题,toastError是个特殊交互参数,注意规避
others?: JSBridgePostOthers
) => Promise<BaseResponse>;
}
cipher.ts
ts
abstract class Cipher {
abstract encrypt(str: string): string;
abstract decrypt(str: string): { status: string; data: any };
}
export default interface Ciphersdk {
readonly newCipher: (pubKey: string, pubKeyVersion: string) => Cipher;
}
将写好的类型挂载到window对象
ts中提供了拓展全局对象window的方法global
以及拓展模块的方法module
。拓展之后即可通过window.
的方式来调用。
结合上面的两个文件,项目目录如下:
markdown
- window
- JSBridge.ts
- cipher.ts
- index.ts
index.ts
ts
import type JSBridge from "./JSBridge";
import type Cipher from "./cipher";
declare global {
interface Window {
readonly JSBridge: JSBridge;
readonly cipher: Cipher;
}
}
将写好的类型接入到全局声明文件
我们调用window上的方法通常都是直接调用,很少有使用window.的方式。因此还需在全局文件中告诉ts这个变量是确实存在的,且他具有对应的类型。
ts
// global.d.ts
import type JSBridgeInterface from "./src/constants/window/JSBridge";
import type Cipher from "./src/constants/window/ciper";
// 全局JSDK
declare const JSBridge: JSBridgeInterface;
declare const cipher: Cipher;
看上去好像写的不错,但你发现写完之后代码里各种报错。本来定义到global.d.ts
里的全局类型全部都找不到了.
这是因为在global.d.ts
中使用import
和export
会将其由全局声明文件变为module
模块文件。想要使用其中的类型必须通过导入的方式来使用。
因此我们不能通过import
的方法来将写好的类型导入进来,难道要把写好的类型在复制粘贴一遍???这显然是不能被接受的。
我们可以借助import
的动态导入功能来实现
ts
// global.d.ts
declare const JSBridge: import("./src/constants/window/JSBridge").default;
declare const cipher: import("./src/constants/window/cipher").default;
这样就实现了既能在全局直接使用JSDK的API,又不影响全局暴露的类型。
使用特定类型代表回调函数
最后一个要解决的就是绑定在window上的回调函数。这些函数的特点是并不代表实际的业务意义和业务场景,只是众多API方法中的一环。因此我们并没有必要为他设置向JSDK一样详细的类型,只要保证其使用起来方便不报错即可。
因此可以将众多的回调函数类型抽象为一个简单类型,挂载到window的key
上即可。
ts
// window/index.ts
// 抽象的方法
type WinCallbackType = (param?: any) => void;
declare global {
interface Window {
...
[key: string]: WinCallbackType | null; // 拓展的回调方法
}
}
这样就解决了上述这个问题,使代码变得更有意义。
以上就是如何优雅解决前端项目中集成的JSDK类型报错的全部内容啦!如果看完文章有所收获的话,烦请各位看官动动小手点个赞哦~
您的支持就是对我最大的鼓励❤️❤️❤️