TypeScript 在开发应用中的实践总结
<h2 id="背景">背景</h2><p>以前 hybrid app 的移动端开发模式下,H5 和客户端通信的 js sdk 代码使用 js 编写,sdk 方法的说明使用文档输出。对于开发的使用来说,在 IDE 中不能得到友好的参数类型提示。于是我们维护一个类型定义包进行 sdk 方法的类型定义。但这样对于维护 sdk 的同学来说,维护源码的同时需要同步更新类型定义,更新如果不及时,开发需要通过类型合并临时解决。加上以前的代码 api 方法越来越多,全部写在一个文件中快一千行了,急需重构。</p>
<p>如果源码使用 ts编写,打包后自动生成.d.ts 文件,不需要维护额外的类型定义文件了,开发者在编辑器中也可以获得参数提示。既然这样,不如动手试试使用 ts 重构。</p>
<h2 id="准备工作">准备工作</h2>
<p>因为代码是纯 js 库,我们使用 rollup+babel 来打包。把原来的代码做了一个简单的梳理,整理出一个初步的项目结构如下:</p>
<pre><code class="language-javascript">│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 // 工具函数
</code></pre>
<h2 id="如何声明回调函数">如何声明回调函数</h2>
<p>最常用的是泛型,在下面的例子中,invoke 方法最终会返回我们传入的任何类型的 promise。在'global.user.get'方法中,调用 invoke 方法,就可以获得返回值的 data 有 user_id,is_admin 两个属性。</p>
<p><img src="https://fulu-item11-zjk.oss-cn-zhangjiakou.aliyuncs.com/images/1623854207231.png" alt="1623854207231" loading="lazy"></p>
<h2 id="如何修改原生类型变量">如何修改原生类型变量</h2>
<p>我们在 window 对象上挂载了一些新的属性和方法, ts 报错如下。</p>
<p><img src="https://fulu-item11-zjk.oss-cn-zhangjiakou.aliyuncs.com/images/1622532435156-7b65457a-e304-4527-971f-645b93d75cff.png" alt="1622532435156-7b65457a-e304-4527-971f-645b93d75cff" loading="lazy"></p>
<p>先看一下 ts 是如何定义这些对象的,我们安装 typescript 包时,会顺带安装一个 lib.d.ts,包含 js 运行时以及 DOM 中各种常见的环境声明。我们打开 lib.dom.d.ts,发现了 window 的类型定义如下:</p>
<p><img src="https://fulu-item11-zjk.oss-cn-zhangjiakou.aliyuncs.com/images/1622538459076-d0caa2c9-20b1-44f4-83cf-bea0da077e19.png" alt="1622538459076-d0caa2c9-20b1-44f4-83cf-bea0da077e19" loading="lazy"></p>
<p>解决方案很简单,创建一个 global.d.ts 的全局模块使这些接口与 lib.d.ts 相关联,利用接口的合并特性,重新定义 Window 接口添加需要的属性方法即可。</p>
<p>如果不想污染原始变量类型呢。</p>
<p>比如,我们现在向 sdk 添加一个文件上传方法并且可以取消上传,那么 invoke 方法最终会返回一个扩展了 abort 取消方法的 promise。但像上面这样扩展 promise 时,就会污染类型。更好的写法是创建一个新的 AbortablePromise 类型,像下面这样:</p>
<pre><code class="language-javascript">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;
};
</code></pre>
<p>那么像现在 invoke 方法传入的参数不同,返回值类型也不同的情况下,想要根据不同的参数约定不同的返回值类型,可以采用函数重载。</p>
<pre><code class="language-javascript">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
</code></pre>
<h2 id="使用关键字-is-进行类型保护">使用关键字 is 进行类型保护</h2>
<p>在 utils 文件夹下,我们通常会定义一些工具函数,当我们把它转换成 ts 的写法时,可能会这样写:</p>
<pre><code class="language-javascript">export function isString(arg: any) {
return Object.prototype.toString.call(arg) === '';
}
</code></pre>
<p>如下代码在编译过程中不会报错,因为 a 的类型是 any。但是,如果我们使用 is 进行类型保护,此时,在 if 的判断条件下,类型从 any 缩小至 string,会提示 String 上不存在 join 属性。</p>
<pre><code class="language-javascript">let a: any = 2;
if (isString(a)) {
a.join();
}
export function isString(arg:any): arg is string {
return Object.prototype.toString.call(arg) === '';
}
</code></pre>
<h2 id="遇到的问题">遇到的问题</h2>
<h3 id="使用-alias-配置了指向src目录打包后-dts-文件不工作">使用 alias 配置了'@'指向'./src'目录,打包后 d.ts 文件不工作</h3>
<p>在开发中,配置了'@'指向'./src'目录下,但是打包后查看 dist 文件夹发现.d.ts 文件中的'@'都未被正确编译。</p>
<p><img src="https://fulu-item11-zjk.oss-cn-zhangjiakou.aliyuncs.com/images/1622532587939-63665f53-59b8-4f39-bdfa-d1629f2f42d1.png" alt="1622532587939-63665f53-59b8-4f39-bdfa-d1629f2f42d1" loading="lazy"></p>
<p>我们查到一个转换文件路径的插件'@zerollup/ts-transform-paths',用来转换 npm 打包后.d.ts 中不工作的绝对路径,npm 上也介绍了如何使用,配合 ttypescript(Transformer Typescript,支持在编译过程中使用在 tsconfig.json 中配置的自定义转换器),需要用 ttsc 代替 tsc 命令。我们修改 package.json 的命令如下:</p>
<pre><code class="language-javascript">"build:types": "ttsc -p tsconfig.build.json"
</code></pre>
<p>重新构建试一下,看到'@'已经被正确编译了。</p>
<p><img src="https://fulu-item11-zjk.oss-cn-zhangjiakou.aliyuncs.com/images/1622532665017-395b593c-ce49-4e4d-96b2-4b39381a43fd.png" alt="1622532665017-395b593c-ce49-4e4d-96b2-4b39381a43fd" loading="lazy"></p>
<h2 id="其他实践场景">其他实践场景</h2>
<h3 id="在应用开发中如何定义和后端通信返回的数据类型">在应用开发中,如何定义和后端通信返回的数据类型</h3>
<p>通常在项目中会有一个 api 文件夹存放各个模块的 service api,现在可以新增一个 types 文件夹,用于存放对应模块的类型,比如 ass.d.ts 对应 ass.ts。在 ass.d.ts 中,使用 declare namespace 声明命名空间,可以提取分页等常用接口。</p>
<pre><code class="language-javascript">// 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 {}
}
</code></pre>
<p>在 api 中使用:</p>
<pre><code class="language-javascript">// api/ass.ts
export const getLocalCheckinRuleList = (data: ass.CheckinRuleListBody) =>
post < ass.CheckinRuleListResponse > ('/api/v2.0/rule/search', { data });
</code></pre>
<h2 id="总结">总结</h2>
<p>在重构sdk和项目应用的实际开发过程中,使用 ts 可以直观地获取到组件的接口定义,还能对属性进行自动检测提示,许多低级 bug 在开发阶段就能被发现,对于应用的维护和修改,也不用太担心类型出错。<br>
但是无疑会造成初期开发成本的增加,特别是快速迭代的项目,接口定义耗费大量时间,可能还是写注释变量名更适合。</p>
<h2 id="参考资料">参考资料:</h2>
<p>https://jkchao.github.io/typescript-book-chinese/</p>
<div style="display: none">
<span id="fulu-org">福禄·研发中心</span>
<span id="fulu-author">福小球</span>
</div><br><br>
来源:https://www.cnblogs.com/fulu/p/14921280.html
頁:
[1]