這篇是 js-interface 的 README,雖然並非很複雜的一個東西,若是有人看的話我就寫寫源碼思路了 ORZjavascript
在作一個先後分離的項目時,有些頭疼 Api 之類的東西要怎麼管理,在閱讀 《JavaScript 設計模式》 一書時,第二章提到了在 JavaScript 中模擬接口 (interface) 的概念,以方便使用衆多設計模式,所以嘗試着作一個接口的模擬。因爲我本職是一名後端 Java 開發,所以但願在這個模擬層能夠加入 接口默認實現、接口繼承、方法重載 等能力,雖然這些東西加上以後不可避省得會在性能上有所犧牲,但對我來講能夠提高一些開發體驗(我知道 TypeScript,只是想搞個輪子試試 :P)。java
既然初衷是爲了方便管理 Api,那麼就作一個關於 Api 的 demo。node
const config = { // 接口的名字 name: 'IApi', // 是否打開此接口的 debug 開關 // 開發時必須打開,不然不會啓動 (方法聲明、方法實現等)入參的類型檢查。 // 打開這個的狀況下,還會得到一些調試用的信息。 debug: true, } let IApi = new Interface(config)
最簡單的聲明方式:webpack
IApi.method({name: 'getName'}) // 等價於 IApi.method({name: 'getName', args: undefined})
這樣就聲明瞭 IApi
接口含有一個 getName
方法,它沒有任何參數,也沒有默認實現,這就要求在後面任何 IApi
的子接口或實現類必須實現該方法,不然會拋出一個異常。ios
若是想指定方法的參數列表:git
IApi.method({ name: 'getName', args: null })
注意!github
args
爲 null
時表示該方法能夠接受任意數量的任意參數,若是重載了一個方法(詳細的請參閱後面關於重載的說明):web
// 聲明一個空參方法 IApi.method({ id: 0, name: 'getName', args: null }) // 重載上面的方法,使其有且只有一個 'string' 類型,名爲 name 的參數 IApi.method({ id: 1, name: 'getName', args: [ {name: 'name', type: 'string', support: val => typeof val === 'string'} ] })
注意!axios
在參數描述中,type
屬性只是一個字符串值,它並不真的表明參數的實際類型。它跟 name
屬性同樣只是提供用於調試的信息,所以你能夠填入任何你認爲合適的、足以標記該參數一些信息的字符串值。後端
真正決定方法是否接受該參數的是 support
屬性,當它返回 true
時會檢查下一個參數直到全部參數檢查完畢或某個位置的參數不被接受。
若是須要,能夠在 support
中對實際入參進行特殊處理,好比轉換對象、特定屬性檢查等等。
若是想爲方法提供默認實現:
IApi.method({ name: 'getName', // 默認實現,不能爲箭頭函數! implement: function() { return "IApi" } })
回到咱們的 demo,綜合運用一下:
// 聲明兩個方法,它們都沒有參數,也不須要重載所以這樣就能夠了 IApi .method({ name: 'getName' }) // 項目中使用 Axios,所以這裏須要一個方法來獲取 axios 實例 .method({ name: 'getAxios' }) // 聲明四個請求方式對應的方法 const methods = ['get', 'post', 'put', 'delete'] methods.forEach(method => { IApi.method({ name: method, args: null, implement: function() { // 處理了 this 指向問題,放心用吧 return this.getAxios()[method].apply(this, arguments) .then(responseHandler) .catch(errorHandler) } }) })
假定咱們要建立接口 A,要繼承 B、C、D、E 等接口,使用以下語句:
const A = new Interface({ name: 'A', debug: true }).extends([B, C, D, E])
extends
方法會將傳入的接口所持有的全部方法聲明(即經過 Interface.method(config)
所聲明的那些方法 )拷貝至接口 A,包括那些方法聲明的默認實現。
注意!
通常來講,不會在多個接口中重載同一個方法簽名,但若是真的有這樣的需求,能夠自行設置 id
的規則,好比:
const B = new Interface(...) .method({ id: 'B00', name: 'getName', args = [...] }) .method({ id: 'B01', name: 'getName', args = [...] }) const C = new Interface(...) .method({ id: 'C00', name: 'getName', args = [...] })
而後實現該方法時指定要實現哪個聲明:
// 注意!若是一個方法被重載,則不能在 class 中聲明該方法。 class AImpl { ... } const AInstance = new AImpl(...) B.implement({ object: AInstance, id: 'B00', // 指定要實現的方法聲明 name: 'getName' })
再次回到咱們的 demo,綜合運用一下:
const IAuthenticationApi = new Interface({ name: 'IAuthentication', debug: true }) // 指明 IAuthenticationApi 繼承自 IApi 接口 .extends(IApi) IAuthenticationApi // 重載方法 login // loin (username :string, password :string) .method({ id: 0, name: 'login', args: [ {name: 'username', type: 'string', support: val => typeof val === 'string'}, {name: 'password', type: 'string', support: val => typeof val === 'string'} ] }) // login() .method({ id: 1, name: 'login' })
// 編寫一個實現類 class AuthenticationApi { constructor(axios) { this.axios = axios } // 直接實現 getName 方法 getName() { return "AuthenticationApi" } // 直接實現 getAxios 方法 getAxios() { return this.axios } } // 實現重載方法 IAuthenticationApi .implement({ // 指定掛載實現到 AuthenticationApi 上 object: AuthenticationApi, // 指定此實現是對應 id 爲 0 的方法聲明 id: 0, name: 'login', implement: function(username, password) { console.log('帶參數的 login') // 還記得咱們在 IApi 接口中定義了 get 方法(包括默認實現)嗎? this.get('https://www.baidu.com') return Promise.resolve('hello') } }) .implement({ object: AuthenticationApi, id: 1, name: 'login', implement: function () { console.log('無參數的 login') }, }) IAuthenticationApi.ensureImplements(AuthenticationApi)
let authenticationService = new AuthenticationApi(axios) // 掛載代理函數到實例上,不然會提示 // Uncaught TypeError: authenticationService.login is not a function IAuthenticationApi.ensureImplements(authenticationService) authenticationService .login('sitdownload', '1498696873') // login(string, string) 會返回一個 Promise 還記得嗎 :P .then(str => alert(`${str} world!`)) authenticationService.login()
首先確保在建立接口時打開了 debug 開關({ debug: true }
)。
上面的 demo 運行正常的話你將會獲得下面的日誌:
// 註冊方法 Interface 註冊方法: IApi.getName() Interface 註冊方法: IApi.getAxios() Interface 註冊方法: IApi.get(any) Interface 註冊方法: IApi.post(any) Interface 註冊方法: IApi.put(any) Interface 註冊方法: IApi.delete(any) Interface 註冊方法: IAuthentication extends IApi.getName() Interface 註冊方法: IAuthentication extends IApi.getAxios() Interface 註冊方法: IAuthentication extends IApi.get(any) Interface 註冊方法: IAuthentication extends IApi.post(any) Interface 註冊方法: IAuthentication extends IApi.put(any) Interface 註冊方法: IAuthentication extends IApi.delete(any) Interface 註冊方法: [0]IAuthentication.login(username :string, password :string) Interface 註冊方法: [1]IAuthentication.login() // 實現方法 Interface 實現方法: 保存 [0]IAuthentication.login(...) 實現: ƒ implement(username, password) Interface 實現方法: 保存 [1]IAuthentication.login(...) 實現: ƒ implement() // 匹配方法 Interface 方法匹配: 精準匹配 IAuthentication.login({ username: "sitdownload" } :string, { password: "1498696873" } :string). // 在控制檯這行是能夠打開實現的具體位置的 ƒ implement(username, password) // 方法輸出 AuthenticationApi.js?7b55:25 帶參數的 login // 匹配方法 Interface 方法匹配: 沒法精準匹配 IAuthentication.get("https://www.baidu.com"),使用 any 實現匹配: ƒ implement() Interface 方法匹配: 精準匹配 IAuthentication.login(). ƒ implement() // 方法輸出 AuthenticationApi.js?7b55:35 無參數的 login // AuthenticationApi.login(username, password) 中請求了 'https://www.baidu.com' Failed to load https://www.baidu.com/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1' is therefore not allowed access. // IApi.get(any) 中將異常直接向下拋了 Uncaught (in promise) {type: "network", payload: Error: Network Error at createError (webpack-internal:///./node_modules/_axios@0.18.0@axios/lib/…}
若是要發版了,確認全部的接口方法都正確實現後,就能夠把 debug 關掉,這樣就不會有 Interface
內部的一些入參檢查和調試輸出。