RetrofitJs – TypeScript實現的宣告式HTTP客戶端

itfinally發表於2019-02-14

由於文件已經在 github 裡寫好了, 這裡並不是很想再寫一次中文版文件, 本文將著重於解析工具的設計.( 好像看國人寫的英文也是件蠻痛苦的事哇? 本人英文渣渣. )

實際上 Retrofit 是 Java 的一款基於 OkHttp 開發的, 型別安全的宣告式HTTP客戶端, 在 Java 用慣了這一類的工具, 自然也會想在其他地方使用類似的工具, 於是乎自己寫了一款, 加個 Js 當作字尾就當專案名了, 即 RetrofitJs.

當然主要還是因為懶, 假如在專案的每個頁面都要寫一大串 client.get({ ...config })來發起請求, 一來感覺太亂, 畢竟每個頁面都要寫就感覺很難管理. 二來重複度高, 因為同一個介面呼叫兩次就要寫兩次配置( 說實話直接封裝成函式我也覺得有點拙 ), 介面並不能重用. 本著懶惰是第一生產力的原則就寫了個工具來解決上述問題.

Talk is cheap, 先來看個 demo, 這是 TypeScript 下的程式碼:

// This is typescript demo, also javascript demo( it is if you remove all type define )

// In the first step, you must create your retrofit object. 
let client = Retrofit.getBuilder()
  .setConfig<RetrofitConfig>( { /** config, you can use retrofit config or axios config */ } )
  .addInterceptor( /** your interceptor */ )
  .setErrorHandler( /** define your error handler */ )
  .build();

// This is the part of define any interface what you need.
@HTTP( "/testing" )
@Headers( [ "Cache-Control: no-store" ] )
class TestingClient {
  
  @GET( "/demo1/:callByWho/:when" )
  public demo1( @Path( "callByWho" ) name: string, @Path( "when" ) time: number ): RetrofitPromise<string> & void {
  }
  
  @POST( "/demo2/:file" )
  public demo2( @Path( "file" ) file: string, @Header( "cookie" ) val: string, @Config localConfig: AxiosConfig ): RetrofitPromise<string> & void {
  }
}

// The final step, create your client.
export let testingClient = client.create( TestingClient );

// When you are calling this method, it is a http call actually. 
testingClient.demo1( "itfinally", Date.now() ).then( response => {
    // any code
} ).catch( reason => {
	// any code
} );

// And you can also get axios instance.
let axios: AxiosInstance = client.getEngine();
複製程式碼

上面的例子其實還少了個東西, 實際上介面是可以繼承的. 例如這樣:

class Parent {
  @GET( "/a1_testing/:a1" )
  public demo1(): RetrofitPromise<string> & void {
  }
}

@HTTP( "/test" )
class TestingClient extends Parent {
}
複製程式碼

基於 Axios 的當然是同時支援 browser/nodejs 啦.

不過需要注意的是, 專案核心功能依賴於 Proxy, 所以並不能在非原生支援 ES6 的環境下使用. 同時 decorator 特性仍然在 stage 2, 並且最重要的 parameter decorator 在 17 年尾才被人提出並加入討論( 這些事件我都有在專案文件開頭說明, 需要了解的可以移步到專案的README.md ), 所以這個工具目前只能在 TypeScript 環境中使用.( 不要怪我等你脫了褲才告訴你這個事 = =. )

在設計前其實看過 Axios 和 OkHttp 的使用文件, 當然是更偏向於 OkHttp 的方式, 尤其是攔截器方面的設計.

// Axios 使用方式
// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Do something with response data
    return response;
  }, function (error) {
    // Do something with response error
    return Promise.reject(error);
  });

// OkHttp 使用方式
class MyInterceptor implements Interceptor {
    public Response intercept(Interceptor.Chain chain) throws IOException {
        chain.proceed( chain.request() );
    }
}
複製程式碼

明顯是 OkHttp 在同一個作用域中處理一切的方式優於 Axios 把 request/response 隔離成兩個作用域的方式, 當然我沒詳細看過 Axios 原始碼, 這裡不予過多評論, 說不定別人是有原因的.

不管怎樣, 當時是查了很多關於 OkHttp 原始碼解析的文件和部落格, 其實關鍵就是實現一個責任鏈, 並且每個節點都可以把 request 傳遞給下一個節點.

於是就有了如下程式碼:

InterceptorChainActor 排程流程

這是 InterceptorChainActor 裡 Chain 的一段程式碼, 也是整個工具執行的其中一個關鍵, 欄位 interceptors 是當前剩餘的攔截器, 而欄位 stack 是當前的呼叫棧. 可以從圖片中看出整個責任鏈是通過 interceptors-stack 兩者來維護的, 同時把自身( 即 Chain )作為引數傳入當前呼叫的攔截器, 從而確保攔截器在呼叫 chain.proceed( chain.request() ) 時, 呼叫流程會重入到當前的 Chain.

要注意的是, 這實際上是一個遞迴呼叫, 所以攔截器太多的話也會出現溢位, 當然這是極端情況了.

另外在呼叫一個被 RetrofitJs 代理的方法時, 實際的呼叫如下:

  1. 裝飾器被啟用, 收集所有不存在的後設資料, 如果已經收集過( 比如第二次呼叫 )則直接返回
  2. 代理函式( 即 Proxy.apply )獲取當前後設資料, 並且不斷遍歷原型鏈獲取構造器後設資料( 也就是類的後設資料 )並組合
  3. 檢查後設資料並且根據當前傳入的 parameters 建立出 request 物件
  4. 把 request 物件傳入攔截鏈並返回一個 Promise 物件

為啥不截圖呢? 因為整個程式碼太多了, 好幾份檔案.

說實話, 這個工具相對於前端而言, 特性有點激進, 當然也解決一部分問題, 最直觀的就是程式碼更簡潔, 尤其是呼叫的時候給人一種本地函式呼叫的錯覺( 有點像RPC? ).

都看到這裡了, 不介意的話可以試試哇, 目前專案在 npm 提供.

相關文章