做者:陈达孚javascript
香港中文大学研究生,《移动Web前端高效开发实战》做者之一,《前端开发者指南2017》译者之一,在中国前端开发者大会,中生代技术大会等技术会议发表过主题演讲, 专一于新技术的调研和使用.前端
本文为原创文章,转载请注明做者及出处java
最近在作公司内部的一个的一个SDK的重构,这里总结一些经验分享给你们。node
做为一个SDK,咱们的目标是让使用者可以减小查看文档的时间,因此咱们须要提供一些类型的检查和智能提示,通常咱们的作法是提供JsDoc,大部分编辑器能够提供快捷生成JsDoc的方式,咱们比较经常使用的vscode可使用Document This。android
另外一种作法是使用Flow或者TypeScript,选择TypeScript的主要缘由是自动生成的JsDoc比较原始,咱们仍然须要在上面进行编辑,因此JsDoc维护和代码开发是脱离的,每每会出现代码更新了,JsDoc忘记更新的状况。webpack
除此以外开发过程当中咱们没法享受到类型检查等对SDK开发比较重要的特性,TypeScript可让咱们减小犯错,减小调试的时间,另外一方面此次开发的SDK在提供出去的时候就会进行一次相对简单的压缩,保证引入后的体积,因此会但愿压缩掉JsDoc,而TypeScript能够经过在tsconfig.json中将declaration设置为true单独的d.ts文件。ios
一个带提示的SDK:git
最后,对于开发同窗来讲,就算不使用TypeScript,也强烈建议使用vscode提供//@ts-check
注解,它会经过一些类型推导来检查你的代码的正确性,能够减小不少开发过程当中的bug。github
还有一个小技巧,若是你使用的库没有提供智能提示,你能够经过NPM/yarn
的-D
安装@types/{pkgname}
,这样你开发过程当中就可以享受到vscode提供的智能提示,而-D
安装到devDependencies
中,也不会增长你在构建时的代码体积。web
既然提到了TypeScript,就提一下TypeScript的语法,基础类型没有必要赘述,而一些曾经的高级语法如今ES6也都能支持,这里提几点经常使用可是JavaScript开发者不太习惯使用的语法。
不少人在开始使用TypeScript的时候,会很迷恋使用any或者默认的any,推荐在开发中打开tsconfig中的strict和noImplicitAny来保证尽可能少的any使用,要知道,滥用any就等于你的类型检查并无实质效果。
对一些暂时不能肯定内容的对象的类型,可使用{[key: string]: any}
,而不要直接使用any,后期能够慢慢扩展这个接口直到彻底消除any,同时TypeScript的类型支持继承,在开发过程当中,能够拆解接口,利用组合继承的方式减小重复定义。
可是接口也会带来一个小痛点,目前vscode的智能提醒不能很好的对应到接口,当你输入到对应变量的时候,虽然会高亮,可是高亮的也只是一个定义了名字的接口。没有办法直接看到接口里定义了什么。可是当你输入了接口里面定义的key的部分时,vscode会给你完整key的提示。虽然这对开发过程当中有一点不够友好,可是vscode开发团队表示这是他们故意设计的,因此在API参数上能够选择将一些必要(重要)参数用基础类型直接使用,而将一些配置放入一个定义为接口的对象中。
你有在代码中使用过:
const Platform = { ios: 0, android: 1 } 复制代码
那你在TypeScript中就应该使用枚举:
enum Platform { ios, android } 复制代码
这样在函数中你就能够为某个参数设置类型为number,而后传入Platform.ios
这样,枚举能够增长代码的维护性,它能够利用智能提示保证你输入的正确,再也不会出现魔数(magic number)。相对于对象,它保证了输入的类型(你定义的对象可能某一天再也不只有number类型的value),再也不须要额外的类型判断。
对于装饰器其实不少开发者既熟悉又陌生,在redux,mobx比较流行的如今,在代码中出现装饰器的调用已经很广泛,可是大多数开发者并无将本身代码逻辑抽成装饰器的习惯。
好比在这个SDK的开发中,咱们须要提供一些facade来兼容不一样的平台(iOS, Android或者Web),而这个facade会经过插件的形式让开发者本身注册,SDK会维护一个注入后的对象,常规的使用方法是到了使用函数后判断环境再判断对象中有没有想有的插件,有就使用插件。
实际来看,插件就是一个拦截器,咱们只要阻止真正的函数运行就能够,大概的逻辑是这样的:
export function facade(env: number) { return function( target: object, name: string, descriptor: TypedPropertyDescriptor<any> ) { let originalMethod = descriptor.value; let method; return { ...descriptor, value(...args: any[]): any { let [arg] = args; let { param, success, failure, polyfill } = arg; // 这部分能够自定义 if ((method = polyfill[env])) { method.use(param, success, failure); return; } originalMethod.apply(this, args); } }; }; } 复制代码
在SDK的开发过程当中另外一个常会遇到的就是不少参数的校验和再封装,咱们也可使用装饰器去完成:
export function snakeParam( target: object, name: string, descriptor: TypedPropertyDescriptor<any> ) { let callback = descriptor.value!; return { ...descriptor, value(...args: any[]): any { let [arg, ...other] = args; arg = convertObjectName(arg, ConvertNameMode.toSnake); callback.apply(this, [arg, ...other]); } }; }÷ 复制代码
泛形能够根据用户的输入决定输出,最简单的例子是
function identity<T>(arg: T): T { return arg; } 复制代码
固然它没有什么特别的意义,可是它代表了返回是根据arg的类型,在通常开发过程当中,你逃不开范型的是Promise或者前面的TypedPropertyDescriptor这种内建的须要类型输入的地方,不要草率的使用any,若是你的后端返回是一个标准结构体相似:
export interface IRes { status: number; message: string; data?: object; } 复制代码
那么你能够这样使用Promise:
function example(): Promise<IRes> { return new Promise ... } 复制代码
固然泛形有不少高级应用,例如泛形约束,泛型建立工厂函数,已经超出了本文的范围,能够去官方文档了解。
若是你的构建工具是Webpack,在SDK的开发中,尽可能使用node方式调用(即webpack.run执行),由于SDK的构建每每会应对不少不一样的参数变化,node方式相比纯配置方式能够更加灵活的调整输入输出的参数,也能够考虑使用rollup,rollup的构建代码更加面向编程方式。
须要注意的是,在Webpack3和rollup中构建中可使用ES6模块化的方式构建,这样业务代码引入你的SDK后,能够经过解构引入的方式减小最终业务代码的体积,若是你只是提供了commonjs的包,那么构建工具的tree sharking是没法生效的,若是使用babel的话注意关闭module的编译。
另一种减小单个包体积的方式,可使用lerna在一个git仓库里构建多个NPM包,比起拆仓库能够更方便的使用公共部分的代码,可是也须要注意对公共部分代码的修改不要影响到别的包。
其实对于大多数的SDK的来讲,Webpack3和rollup使用感觉是差很少的,比较经常使用的插件都有几乎同名的对应。不过rollup有两个优点,一个是rollup的构建更细化,rollup.rollup接受inputOptions生成bundle,还能够generate生成sourcemap,write生成output,在这个过程当中咱们能够作一些细致的工做。
第二点是rollup.rollup会返回一个promise,也就意味着咱们可使用async的方式来写构建代码,而webpack.run仍是使用的回调函数,虽然开发者能够封装成promise,可是我的以为仍是rollup的写法仍是更爽一点。
上周我同事作了一个在线的分享,我发现不少同窗都对单测很感兴趣也很疑惑,在前端开发中,对涉及UI的业务代码开发单测试比较困难的,可是对于SDK,单元测试确定是准出的一个充要条件。固然其实我也很不喜欢写单测,由于单测每每比较枯燥,可是不写单测确定会被老司机们“教育”的~_~。
通常的单测使用mocha做为测试框架,expect做为断言库,使用nyc提供单测报告,一个大概的单测以下:
describe('xxx api test', function() { // 注意若是要用this调用mocha,不要用箭头函数 this.timeout(6000); it('xxx', done => { SDK.file .chooseImage({ count: 10, cancel: () => { console.log('选择图片取消----'); } }) .then(res => { console.dir(res); expect(res).to.be.an('object'); expect(res).to.have.keys('ids'); expect(res.ids).to.be.an('array'); expect(res.ids).to.have.length.above(0); uploadImg(res.ids); done(); }); }); }); 复制代码
一样你能够用TypeScript写单测,固然在执行过程当中,不须要再编译了,咱们能够直接给mocha注册ts-node来直接执行,具体方式能够参考Write tests for TypeScript projects with mocha and chai — in TypeScript!。可是有一点须要提醒你,写单测的时候尽可能依赖文档而不是智能提示,由于你的代码出错,可能会致使你的智能提示也是错误的,你根据错误的智能提示写的单测确定也是。。。
对于网络请求的模拟可使用nock这个库,须要在it以前增长一个beforeEach
方法:
describe('proxy', () => { beforeEach(() => { nock('http://test.com') .post('/test1') .delay(200) .reply(200, { // body test1: 1, test2: 2 }, { 'server-id': 'test' // header }); }); it(... } 复制代码
最后咱们用一个npm script加上nyc在mocha前面,就能够得到咱们的单测报告了。
这里我还提了几个TypeScript使用中的小tips给你们参考。
这个SDK在开发过程会依赖一个内部NPM包,为了让这个NPM支持TypeScript调用,咱们有几种作法:
给原包添加d.ts文件,而后发布.
发布@types包,须要注意的是NPM不支持@types/@scope/{pkgname}
这种写若是是私库包,可使用@types/scope_{pkgname}
这种写法.
此次使用的标注一个文件夹存放对应的d.ts文件,这种方式适合开发中进行,若是你以为你写的d.ts还不够完美,或者这个d.ts文件目前只有这个SDK有须要,能够这么使用,在tsconfig.json中修改:
"baseUrl": "./",
"paths": {
"*": ["/type/*"]
}
复制代码
默认的reject返回的参数类型是any,不必定能知足咱们的须要,这里给一个解决方案,并不是最佳,做为抛砖引玉:
interface IPromise<T, U> { then<TResult1 = T, TResult2 = never>( onfulfilled?: | ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: | ((reason: U) => TResult2 | PromiseLike<TResult2>) | undefined | null ): IPromise<TResult1 , TResult2>; catch<TResult = never>( onrejected?: | ((reason: U) => TResult | PromiseLike<TResult>) | undefined | null ): Promise<TResult>; 复制代码
2019年,iKcamp原创新书《Koa与Node.js开发实战》已在京东、天猫、亚马逊、当当开售啦!