型別檢查和智慧提示
作為一個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引數上可以選擇將一些必要(重要)引數用基礎型別直接使用,而將一些配置放入一個定義為介面的物件中。
列舉
你有在程式碼中使用過:
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給大家參考。
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/*"]
}
複製程式碼
tips: 如何處理resolve和reject不同型別的promise回撥
預設的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>;
複製程式碼