最近在做公司內部的一個的一個SDK的重構,這里總結一些經驗分享給大家。
類型檢查和智能提示
作為一個SDK,我們的目標是讓使用者能夠減少查看文檔的時間,所以我們需要提供一些類型的檢查和智能提示,一般我們的做法是提供JsDoc,大部分編輯器可以提供快捷生成JsDoc的方式,我們比較常用的vscode可以使用Document This。
另一種做法是使用Flow或者TypeScript,選擇TypeScript的主要原因是自動生成的JsDoc比較原始,我們仍然需要在上面進行編輯,所以JsDoc維護和代碼開發是脫離的,往往會出現代碼更新了,JsDoc忘記更新的情況。
除此之外開發過程中我們無法享受到類型檢查等對SDK開發比較重要的特性,TypeScript可以讓我們減少犯錯,減少調試的時間,另一方面這次開發的SDK在提供出去的時候就會進行一次相對簡單的壓縮,保證引入后的體積,所以會希望壓縮掉JsDoc,而TypeScript可以通過在tsconfig.json中將declaration設置為true單獨的d.ts文件。
一個帶提示的SDK:
最后,對於開發同學來說,就算不使用TypeScript,也強烈建議使用vscode提供//@ts-check 注解,它會通過一些類型推導來檢查你的代碼的正確性,可以減少很多開發過程中的bug。
還有一個小技巧,如果你使用的庫沒有提供智能提示,你可以通過NPM/yarn的-D安裝@types/{pkgname},這樣你開發過程中就能夠享受到vscode提供的智能提示,而-D安裝到devDependencies中,也不會增加你在構建時的代碼體積。
接口
既然提到了TypeScript,就提一下TypeScript的語法,基礎類型沒有必要贅述,而一些曾經的高級語法現在ES6也都能支持,這里提幾點常用但是JavaScript開發者不太習慣使用的語法。
很多人在開始使用TypeScript的時候,會很迷戀使用any或者默認的any,推薦在開發中打開tsconfig中的strict和noImplicitAny來保證盡量少的any使用,要知道,濫用any就等於你的類型檢查並沒有實質效果。
對一些暫時不能確定內容的對象的類型,可以使用{[key: string]: any},而不要直接使用any,后期可以慢慢擴展這個接口直到完全消除any,同時TypeScript的類型支持繼承,在開發過程中,可以拆解接口,利用組合繼承的方式減少重復定義。
但是接口也會帶來一個小痛點,目前vscode的智能提醒不能很好的對應到接口,當你輸入到對應變量的時候,雖然會高亮,但是高亮的也只是一個定義了名字的接口。沒有辦法直接看到接口里定義了什么。但是當你輸入了接口里面定義的key的部分時,vscode會給你完整key的提示。雖然這對開發過程中有一點不夠友好,但是vscode開發團隊表示這是他們故意設計的,所以在API參數上可以選擇將一些必要(重要)參數用基礎類型直接使用,而將一些配置放入一個定義為接口的對象中。
枚舉
你有在代碼中使用過:
1 const Platform = { 2 ios: 0, 3 android: 1 4 }
那你在TypeScript中就應該使用枚舉:
1 enum Platform { 2 ios, 3 android 4 }
這樣在函數中你就可以為某個參數設置類型為number,然后傳入Platform.ios這樣,枚舉可以增加代碼的維護性,它可以利用智能提示保證你輸入的正確,不再會出現魔數(magic number)。相對於對象,它保證了輸入的類型(你定義的對象可能某一天不再只有number類型的value),不再需要額外的類型判斷。
裝飾器
對於裝飾器其實很多開發者既熟悉又陌生,在redux,mobx比較流行的現在,在代碼中出現裝飾器的調用已經很普遍,但是大多數開發者並沒有將自己代碼邏輯抽成裝飾器的習慣。
比如在這個SDK的開發中,我們需要提供一些facade來兼容不同的平台(iOS, Android或者Web),而這個facade會通過插件的形式讓開發者自己注冊,SDK會維護一個注入后的對象,常規的使用方法是到了使用函數后判斷環境再判斷對象中有沒有想有的插件,有就使用插件。
實際來看,插件就是一個攔截器,我們只要阻止真正的函數運行就可以,大概的邏輯是這樣的:
1 export function facade(env: number) { 2 return function( 3 target: object, 4 name: string, 5 descriptor: TypedPropertyDescriptor<any> 6 ) { 7 let originalMethod = descriptor.value; 8 let method; 9 10 return { 11 ...descriptor, 12 value(...args: any[]): any { 13 let [arg] = args; 14 let { param, success, failure, polyfill } = arg; // 這部分可以自定義 15 if ((method = polyfill[env])) { 16 method.use(param, success, failure); 17 return; 18 } 19 originalMethod.apply(this, args); 20 } 21 }; 22 }; 23 }
在SDK的開發過程中另一個常會遇到的就是很多參數的校驗和再封裝,我們也可以使用裝飾器去完成:
1 export function snakeParam( 2 target: object, 3 name: string, 4 descriptor: TypedPropertyDescriptor<any> 5 ) { 6 let callback = descriptor.value!; 7 8 return { 9 ...descriptor, 10 value(...args: any[]): any { 11 let [arg, ...other] = args; 12 arg = convertObjectName(arg, ConvertNameMode.toSnake); 13 callback.apply(this, [arg, ...other]); 14 } 15 }; 16 }÷
泛形
泛形可以根據用戶的輸入決定輸出,最簡單的例子是
1 function identity<T>(arg: T): T { 2 return arg; 3 }
當然它沒有什么特別的意義,但是它表明了返回是根據arg的類型,在一般開發過程中,你逃不開范型的是Promise或者前面的TypedPropertyDescriptor這種內建的需要類型輸入的地方,不要草率的使用any,如果你的后端返回是一個標准結構體類似:
1 export interface IRes { 2 status: number; 3 message: string; 4 data?: object; 5 }
那么你可以這樣使用Promise:
1 function example(): Promise<IRes> { 2 return new Promise ... 3 }
當然泛形有很多高級應用,例如泛形約束,泛型創建工廠函數,已經超出了本文的范圍,可以去官方文檔了解。
構建
如果你的構建工具是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提供單測報告,一個大概的單測如下:
1 describe('xxx api test', function() { // 注意如果要用this調用mocha,不要用箭頭函數 2 this.timeout(6000); 3 it('xxx', done => { 4 SDK.file 5 .chooseImage({ 6 count: 10, 7 cancel: () => { 8 console.log('選擇圖片取消----'); 9 } 10 }) 11 .then(res => { 12 console.dir(res); 13 expect(res).to.be.an('object'); 14 expect(res).to.have.keys('ids'); 15 expect(res.ids).to.be.an('array'); 16 expect(res.ids).to.have.length.above(0); 17 uploadImg(res.ids); 18 done(); 19 }); 20 }); 21 });
同樣你可以用TypeScript寫單測,當然在執行過程中,不需要再編譯了,我們可以直接給mocha注冊ts-node來直接執行,具體方式可以參考Write tests for TypeScript projects with mocha and chai — in TypeScript!。但是有一點需要提醒你,寫單測的時候盡量依賴文檔而不是智能提示,因為你的代碼出錯,可能會導致你的智能提示也是錯誤的,你根據錯誤的智能提示寫的單測肯定也是。。。
對於網絡請求的模擬可以使用nock這個庫,需要在it之前增加一個beforeEach方法:
1 describe('proxy', () => { 2 beforeEach(() => { 3 nock('http://test.com') 4 .post('/test1') 5 .delay(200) 6 .reply(200, { // body 7 test1: 1, 8 test2: 2 9 }, { 10 'server-id': 'test' // header 11 }); 12 }); 13 it(... 14 }
最后我們用一個npm script加上nyc在mocha前面,就可以獲得我們的單測報告了。
這里我還提了幾個TypeScript使用中的小tips給大家參考。
tips: 如何在非發包情況下給內部庫添加聲明
這個SDK在開發過程會依賴一個內部NPM包,為了讓這個NPM支持TypeScript調用,我們有幾種做法:
- 給原包添加d.ts文件,然后發布.
- 發布@types包,需要注意的是NPM不支持@types/@scope/{pkgname}這種寫如果是私庫包,可以使用@types/scope_{pkgname}這種寫法.
-
這次使用的標注一個文件夾存放對應的d.ts文件,這種方式適合開發中進行,如果你覺得你寫的d.ts還不夠完美,或者這個d.ts文件目前只有這個SDK有需要,可以這么使用,在tsconfig.json中修改:
1 "baseUrl": "./", 2 "paths": { 3 "*": ["/type/*"] 4 }
tips: 如何處理resolve和reject不同類型的promise回調
默認的reject返回的參數類型是any,不一定能滿足我們的需要,這里給一個解決方案,並非最佳,作為拋磚引玉:
1 interface IPromise<T, U> { 2 then<TResult1 = T, TResult2 = never>( 3 onfulfilled?: 4 | ((value: T) => TResult1 | PromiseLike<TResult1>) 5 | undefined 6 | null, 7 onrejected?: 8 | ((reason: U) => TResult2 | PromiseLike<TResult2>) 9 | undefined 10 | null 11 ): IPromise<TResult1 , TResult2>; 12 catch<TResult = never>( 13 onrejected?: 14 | ((reason: U) => TResult | PromiseLike<TResult>) 15 | undefined 16 | null 17 ): Promise<TResult>;