由于文档已经在 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 里 Chain
的一段代码, 也是整个工具运行的其中一个关键, 字段 interceptors
是当前剩余的拦截器, 而字段 stack
是当前的调用栈. 可以从图片中看出整个责任链是通过 interceptors-stack 两者来维护的, 同时把自身( 即 Chain
)作为参数传入当前调用的拦截器, 从而确保拦截器在调用 chain.proceed( chain.request() )
时, 调用流程会重入到当前的 Chain
.
要注意的是, 这实际上是一个递归调用, 所以拦截器太多的话也会出现溢出, 当然这是极端情况了.
另外在调用一个被 RetrofitJs 代理的方法时, 实际的调用如下:
- 装饰器被激活, 收集所有不存在的元数据, 如果已经收集过( 比如第二次调用 )则直接返回
- 代理函数( 即 Proxy.apply )获取当前元数据, 并且不断遍历原型链获取构造器元数据( 也就是类的元数据 )并组合
- 检查元数据并且根据当前传入的 parameters 创建出 request 对象
- 把 request 对象传入拦截链并返回一个 Promise 对象
为啥不截图呢? 因为整个代码太多了, 好几份文件.
说实话, 这个工具相对于前端而言, 特性有点激进, 当然也解决一部分问题, 最直观的就是代码更简洁, 尤其是调用的时候给人一种本地函数调用的错觉( 有点像RPC? ).
都看到这里了, 不介意的话可以试试哇, 目前项目在 npm 提供.