背景

以前 hybrid app 的移动端开发模式下,H5 和客户端通信的 js sdk 代码使用 js 编写,sdk 方法的说明使用文档输出。对于开发的使用来说,在 IDE 中不能得到友好的参数类型提示。于是我们维护一个类型定义包进行 sdk 方法的类型定义。但这样对于维护 sdk 的同学来说,维护源码的同时需要同步更新类型定义,更新如果不及时,开发需要通过类型合并临时解决。加上以前的代码 api 方法越来越多,全部写在一个文件中快一千行了,急需重构。

如果源码使用 ts编写,打包后自动生成.d.ts 文件,不需要维护额外的类型定义文件了,开发者在编辑器中也可以获得参数提示。既然这样,不如动手试试使用 ts 重构。

准备工作

因为代码是纯 js 库,我们使用 rollup+babel 来打包。把原来的代码做了一个简单的梳理,整理出一个初步的项目结构如下:

│  babel.config.js
│ package.json
│ README.md
│ rollup.config.js
│ tsconfig.build.json
│ tsconfig.json
│ typings.d.ts
├─dist // 最终的输出
└─src
│ global.d.ts
│ index.ts // 入口文件,输出最终对外暴露的变量和api方法
├─api // api方法
│ ├─media
│ ├─tool
│ └─ui
├─lib
│ sdk.ts //输出sdk构造函数
└─utils // 工具函数

如何声明回调函数

最常用的是泛型,在下面的例子中,invoke 方法最终会返回我们传入的任何类型的 promise。在'global.user.get'方法中,调用 invoke 方法,就可以获得返回值的 data 有 user_id,is_admin 两个属性。

如何修改原生类型变量

我们在 window 对象上挂载了一些新的属性和方法, ts 报错如下。

先看一下 ts 是如何定义这些对象的,我们安装 typescript 包时,会顺带安装一个 lib.d.ts,包含 js 运行时以及 DOM 中各种常见的环境声明。我们打开 lib.dom.d.ts,发现了 window 的类型定义如下:

解决方案很简单,创建一个 global.d.ts 的全局模块使这些接口与 lib.d.ts 相关联,利用接口的合并特性,重新定义 Window 接口添加需要的属性方法即可。

如果不想污染原始变量类型呢。

比如,我们现在向 sdk 添加一个文件上传方法并且可以取消上传,那么 invoke 方法最终会返回一个扩展了 abort 取消方法的 promise。但像上面这样扩展 promise 时,就会污染类型。更好的写法是创建一个新的 AbortablePromise 类型,像下面这样:

interface AbortablePromise<T extends {}> extends Promise<T> {
abort: () => void;
} const invoke = <T extends {}>(method: string): AbortablePromise<T> => {
let promise = new Promise(resolve => {
window.WebViewJavascriptBridge.registerHandler(method, (res: T) => resolve(res));
}) as AbortablePromise<T>; promise.abort = () => {
window.WebViewJavascriptBridge.registerHandler('media.upload.abort');
}; return promise;
};

那么像现在 invoke 方法传入的参数不同,返回值类型也不同的情况下,想要根据不同的参数约定不同的返回值类型,可以采用函数重载。

function invoke<T extends {}>(method: 'media.file.upload'): AbortablePromise<T>;

function invoke<T extends {}>(method: Method): Promise<T>;

const x1 = invoke('media.file.upload'); // => AbortablePromise
const x2 = invoke('global.user.get'); // => Promise

使用关键字 is 进行类型保护

在 utils 文件夹下,我们通常会定义一些工具函数,当我们把它转换成 ts 的写法时,可能会这样写:

export function isString(arg: any) {
return Object.prototype.toString.call(arg) === '[object String]';
}

如下代码在编译过程中不会报错,因为 a 的类型是 any。但是,如果我们使用 is 进行类型保护,此时,在 if 的判断条件下,类型从 any 缩小至 string,会提示 String 上不存在 join 属性。

let a: any = 2;
if (isString(a)) {
a.join();
} export function isString(arg:any): arg is string {
return Object.prototype.toString.call(arg) === '[object String]';
}

遇到的问题

使用 alias 配置了'@'指向'./src'目录,打包后 d.ts 文件不工作

在开发中,配置了'@'指向'./src'目录下,但是打包后查看 dist 文件夹发现.d.ts 文件中的'@'都未被正确编译。

我们查到一个转换文件路径的插件'@zerollup/ts-transform-paths',用来转换 npm 打包后.d.ts 中不工作的绝对路径,npm 上也介绍了如何使用,配合 ttypescript(Transformer Typescript,支持在编译过程中使用在 tsconfig.json 中配置的自定义转换器),需要用 ttsc 代替 tsc 命令。我们修改 package.json 的命令如下:

"build:types": "ttsc -p tsconfig.build.json"

重新构建试一下,看到'@'已经被正确编译了。

其他实践场景

在应用开发中,如何定义和后端通信返回的数据类型

通常在项目中会有一个 api 文件夹存放各个模块的 service api,现在可以新增一个 types 文件夹,用于存放对应模块的类型,比如 ass.d.ts 对应 ass.ts。在 ass.d.ts 中,使用 declare namespace 声明命名空间,可以提取分页等常用接口。

// api/types/ass.d.ts

declare namespace ass {  // declare namespace后面的全局变量ass是一个对象
interface PageParams {
page: number;
size: number;
} interface CheckinRuleSearchProps {
/** 规则类型 */
rule_type?: string;
...
} interface CheckinRuleListBody extends PageParams, CheckinRuleSearchProps {}
}

在 api 中使用:

// api/ass.ts

export const getLocalCheckinRuleList = (data: ass.CheckinRuleListBody) =>
post < ass.CheckinRuleListResponse > ('/api/v2.0/rule/search', { data });

总结

在重构sdk和项目应用的实际开发过程中,使用 ts 可以直观地获取到组件的接口定义,还能对属性进行自动检测提示,许多低级 bug 在开发阶段就能被发现,对于应用的维护和修改,也不用太担心类型出错。

但是无疑会造成初期开发成本的增加,特别是快速迭代的项目,接口定义耗费大量时间,可能还是写注释变量名更适合。

参考资料:

https://jkchao.github.io/typescript-book-chinese/

福禄·研发中心
福小球

TypeScript 在开发应用中的实践总结的更多相关文章

  1. TypeScript在react项目中的实践

    前段时间有写过一个TypeScript在node项目中的实践. 在里边有解释了为什么要使用TS,以及在Node中的一个项目结构是怎样的. 但是那仅仅是一个纯接口项目,碰巧赶上近期的另一个项目重构也由我 ...

  2. TypeScript在node项目中的实践

    TypeScript在node项目中的实践 TypeScript可以理解为是JavaScript的一个超集,也就是说涵盖了所有JavaScript的功能,并在之上有着自己独特的语法.最近的一个新项目开 ...

  3. Mysql事务探索及其在Django中的实践(二)

    继上一篇<Mysql事务探索及其在Django中的实践(一)>交代完问题的背景和Mysql事务基础后,这一篇主要想介绍一下事务在Django中的使用以及实际应用给我们带来的效率提升. 首先 ...

  4. 在 Typescript 2.0 中使用 @types 类型定义

    在 Typescript 2.0 中使用 @type 类型定义 基于 Typescript 开发的时候,很麻烦的一个问题就是类型定义.导致在编译的时候,经常会看到一连串的找不到类型的提示.解决的方式经 ...

  5. 05-雷海林-mysql备份原理与在TDSQL中的实践

    05-雷海林-mysql备份原理与在TDSQL中的实践 下载地址: http://files.cnblogs.com/files/MYSQLZOUQI/05-%E9%9B%B7%E6%B5%B7%E6 ...

  6. ceph在品高云中的实践

    ceph简介 ceph是业界目前人气最高的开源存储项目之一,关于其定义在官网是这样的:"Ceph is a unified, distributed storage system desig ...

  7. 一致性Hash算法在数据库分表中的实践

    最近有一个项目,其中某个功能单表数据在可预估的未来达到了亿级,初步估算在90亿左右.与同事详细讨论后,决定采用一致性Hash算法来完成数据库的自动扩容和数据迁移.整个程序细节由我同事完成,我只是将其理 ...

  8. 华为云对Kubernetes在Serverless Container产品落地中的实践经验

    华为云容器实例服务,它基于 Kubernetes 打造,对最终用户直接提供 K8S 的 API.正如前面所说,它最大的优点是用户可以围绕 K8S 直接定义运行应用. 这里值得一提是,我们采用了全物理机 ...

  9. React 与 Redux 在生产环境中的实践总结

    React 与 Redux 在生产环境中的实践总结 前段时间使用 React 与 Redux 重构了我们360netlab 的 开放数据平台.现将其中一些技术实践经验总结如下: Universal 渲 ...

随机推荐

  1. 【.NET 与树莓派】六轴飞控传感器(MPU 6050)

    所谓"飞控",其实是重力加速度计和陀螺仪的组合,因为多用于控制飞行器的平衡(无人机.遥控飞机).有同学会问,这货为什么会有六轴呢?咱们常见的不是X.Y.Z三轴吗?重力加速度有三轴, ...

  2. Scrum Meeting 0

    Basic Info where:五号教学楼 when:2020/4/21 target: 明确每次会议基本流程 简要汇报一下已完成任务,下一步计划与遇到的问题 Progress Team Membe ...

  3. ES 6 中的箭头函数及用法

    ES6标准新增了一种新的函数:Arrow Function(箭头函数). 主要的几种写法如下: 组成: 参数 => 语句, 参数不是1个: (参数,参数2)=>语句 语句不止一条: 参数 ...

  4. [Java] SpringBoot

    背景 简化SSM(H)中大量的配置工作,开发人员只关心提供业务功能 可以看成简化了的.按照约定开发的SSM(H) 概念 JavaBean:满足规范的Java类(属性private+默认构造方法+get ...

  5. Linux下script命令录制、回放和共享终端操作script -t 2> timing.log -a output.session # 开始录制

    Linux下script命令录制.回放和共享终端操作 [日期:2018-09-04] 来源:cnblogs.com/f-ck-need-u  作者:骏马金龙 [字体:大 中 小]   另一篇终端会话共 ...

  6. 使用 yum-cron 自动更新 Linux系统

    使用 yum-cron 自动更新 Linux系统   Linux系统技术交流QQ群(1675603)验证问题答案:刘遄 我知道如何使用 yum 命令行 更新系统,但是我想用 cron 任务自动更新软件 ...

  7. Linux创建RAID1_实战

    Linux创建RAID1实战 Linux创建RAID1 RAID1俗称镜像,它最少由两个硬盘组成,且两个硬盘上存储的数据均相同,以实现数据冗余 RAID1读操作速度有所提高,写操作理论上与单硬盘速度一 ...

  8. centos7安装google-chrome

    完整的安装步骤:https://www.tecmint.com/install-google-chrome-on-redhat-centos-fedora-linux/ 1.简单安装测试版:sudo ...

  9. openssl自签发证书

    DOMAIN=www.example.com openssl genrsa -out ${DOMAIN}.key # 生成私有key openssl req -x509 -new -nodes -ke ...

  10. 使用python实现钉钉告警通知功能

    前言:日常工作中告警通知是必不可少的,一般会使用邮件.钉钉.企业微信等,今天分享一下使用python实现钉钉告警 一. 钉钉机器人创建 登录钉钉客户端,创建一个群,把需要收到报警信息的人员都拉到这个群 ...