tt-tio
v0.0.4
Published
Promise based HTTP client for all JavaScript runtimes
Downloads
2
Readme
Tio
基于Promise的,支持所有JavaScript运行时的Http库。Tio是基于axios二次开发的一个项目,它主要有如下功能和特点:
- 兼容axiosAPI.
- 支持所有JavaScript运行时(小程序、Weex等)。
- 支持请求同步。
- 请求重定向;在APP的Webview中,可以将网络请求自动重定向到Native。
安装
使用 npm(推荐):
$ npm install tt-tio
使用bower:
$ bower install tt-tio
源码集成请参考:Tio集成-使用CDN或dist目录文件集成
简介
兼容axiosAPI
Tio兼容axios API,使用方法可以参照axios,不过需要将示例中的axio
换成tio
,如:
const tio = require('tt-tio');
// Make a request for a user with a given ID
tio.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
支持所有JavaScript运行时
Axios目前只支持浏览器和Node,而tio目标之一正是为了弥补这个不足。Tio可以通过适配器的方式,支持各种所有JavaScript运行时。下面是Tio的架构图:上层提供标准、平台统一的API,下层针对不同平台提供不同的适配器:
目前tio支持的平台有头条小程序、微信小程序、支付宝小程序、轻应用、Weex以及浏览器和Node, 下面试各个平台的使用方法:
先引入tio:
const tio = require('tt-tio');
下面是各个平台的导入方法:
注意:示例是在npm环境中引入的,如果某个平台的开发工具不支持npm包管理,请使用源码集成。
头条小程序
const adapter=require('tt-tio/lib/adapters/mp/tt')
tio.defaults.adapter=adapter;
微信小程序
const adapter=require('tt-tio/lib/adapters/mp/wx')
tio.defaults.adapter=adapter;
支付宝小程序
const adapter=require('tt-tio/lib/adapters/mp/al')
tio.defaults.adapter=adapter;
轻应用
const adapter=require('tt-tio/lib/adapters/hap')
tio.defaults.adapter=adapter;
Weex
const adapter=require('tt-tio/lib/adapters/weex')
tio.defaults.adapter=adapter;
Node
const adapter=require('tt-tio/lib/adapters/http')
tio.defaults.adapter=adapter;
浏览器
const adapter=require('tt-tio/lib/adapters/xhr')
tio.defaults.adapter=adapter;
注意:和axios不同,在浏览器环境中,tio必须显式设置xhrAdapter,这是因为tio为了减小包体积,没有将xhrAdapter作为内置默认的adapter.
现在你就可以使用tio来发起网络请求了,使用方法和axios一致,如:
tio.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
支持请求同步
我们知道axios的拦截器可以返回一个promise来执行异步任务,但是axios的拦截器没有办法来同步多个请求,什么意思?我们看一个场景:
由于安全原因,我们需要所有的请求都需要在header中设置一个csrfToken
,如果csrfToken
不存在时,我们需要先请求一个csrfToken
,然后再发起网络请求。
可以想到,在这个场景中,为了保证每次请求都有csrfToken
,我们需要在每次发起网络请求之前都要先检查一下token;很显然,我们不可能没个请求都这么做一下,那怎么解决这个问题?可以想到的一个方法就是在拦截器中去检查,如果没有再请求;用axios实现的流程大概如下:
var csrfToken="";
tio.interceptors.request.use(function (config) {
if(!csrfToken){ //csrfToken不存在, 先获取
return fetchTocken().then((data)=>{
config.headers.csrfToken=csrfToken=data.token;
return config;
})
}else{
config.headers.csrfToken=csrfToken=data.token;
return config;
}
});
上面看起来貌似很完美,但是有一个严重的缺陷,那就是如果页面初始化时同时发起多个网络请求时,csrfToken会请求多次。因为每个请求都会进入请求拦截器,而这时csrfToken都为空,所以没个请求都会走到fetchTocken
,而我们期望的是,只有第一个去请求csrfToken,在请求的过程中,其它请求都先等待,直到csrfToken请求成功后,其它请求再继续,这种场景和多线程同步问题非常类似,所以这种场景我们可以理解为需要“同步请求”(而不是并发)。为了解决“同步”问题,tio引入了一种机制:可以给拦截器加锁。我们先看看用tio如何解决这个问题:
var csrfToken="";
tio.interceptors.request.use(function (config) {
if(!csrfToken){ //csrfToken不存在, 先获取
this.lock(); //锁定请求拦截器,之后,其它请求将在请求拦截器外面等待,
return fetchTocken().then((data)=>{
config.headers.csrfToken=csrfToken=data.token;
this.unlock(); //解锁请求拦截器
return config;
}).catch(()=>this.unlock()) //解锁请求拦截器
}else{
config.headers.csrfToken=csrfToken=data.token
return config;
}
});
解释:
请求拦截器被锁定后(调用
lock
方法),其它请求将不能再进入请求拦截器,此时,其它请求将进入一个等待队列;当请求拦截器解锁后(调用unlock
方法),等待队列中的请求才会进入请求拦截器。上面代码中,我们在请求csrfToken前锁定了请求拦截器,所以即使有多个并发请求,其它请求都得进入等待队列,当我们请求到csrfToken后解锁,其它请求恢复执行,这时csrfToken已经存在,所以就不需要再去请求csrfToken。如果你想取等待消队列里的所有请求(如在请求csrfToken的过程中遇到错误),可以调用
clear(reason)
方法,调用后便会终止请求,进入上层catch方法,reason
将会作为catch的回调参数。如:tio.interceptors.request.use(function (config) { ... //省略无关代码 this.clear("error test") ... });
发起请求:
tio.all([getUserAccount(), getUserPermissions()]) .then(tio.spread(function (acct, perms) { ... })) .catch(e){ console.log(e); // > "error test" }
请求拦截器和响应拦截器是不同的对象,每个拦截器都包含lock/unlock/clear三个方法,加锁、解锁、清空操作用于当前拦截器,比如你只锁定的是响应拦截器,那么请求拦截器依然没有锁,所有并发请求也都会进入请求拦截器,知道他们在需要进入响应拦截器的时,才会到响应拦截器外排队。
注意:在执行拦截器回调时,tio会将当前拦截器对象作为this来call拦截器回调,我们拿请求拦截器来举例:
tio.interceptors.request.use(function (config) { this.lock();// 等价于tio.interceptors.request.lock() });
另外注意,拦截器回调函数如果使用箭头函数,则不能在里面使用
this
来替代拦截器对象。在上面的示例中,
fetchTocken()
方法不能再使用tio
去请求csrfToken,因为在调用fetchTocken()
前tio
的拦截器队列已经锁定了,所以再用它去请求csrfToken的话将会陷入循环等待(死锁),正确的作法很简单,重新创建一个新的tio实例来发起请求即可,如:function fetchTocken(){ return tio.create().get("/token") }
除了上面请求csrfToken的场景,请求同步功能在很多场景也都很实用,再举一个常见的例子,比如登录token自动延时:登录成功后会返回一个token,但token会有一个有效期,如果过期则需要重新请求token。
总结
我们总看看Tio发起网络请求的整个流程图:
请求重定向
在APP内嵌的H5页面中,应该尽可能通过APP发起网络请求;如果使用Webview发起网络请求会有如下问题:
- 不能使用我司的TTNet库
- cookie 同步困难
- 接口安全
- 访问控制
- 性能
- 缓存
详细的分析请查看 为什么在APP内嵌的H5页面中,网络请求应该尽可能通过Native发起?
那如何使用APP发起网络请求呢?
目前我司的APP都有JsBridge,APP内可以实现一个网络请求的JsBridge方法fetch
供H5调用;这样的话前端可以在内嵌的H5页面中直接通过JsBridge方法fetch
方法来发起请求;如果直接手动调用fetch
方法,这样不但太痛苦,而且代码迁移难度会非常大,试想一下:有些H5页面会同时在外部浏览器和APP中打开,对于这些页面我们期望如果在APP中就使用fetch
,如果在浏览器中就使用浏览器发起。
那么现在,救星来了,使用tio,这一切将会变得非常简单;我们只需要定义一个能将请求转发到Native的Adapter,然后再APP环境中便使用Native Adapter,在浏览器环境就仍然使用xhr adapter(内置实现);那么如何定义Native Adapter呢?以F项目举例:APP实现的fetch
方法和主端的fetch方法一致,Native Adapter代码如下(文件名为fAppAdapter.js):
require('byted-ttfe-jsbridge');
const settle = require('tio/lib/core/settle');
const createError = require('tio/lib/core/createError');
module.exports = function (config) {
return new Promise(function (resolve, reject) {
config.header = config.headers;
config.data = config.body;
window.ToutiaoJSBridge.call("fetch", config, function (res) {
if (res.code === 1) {
var response = {
status: res.status,
config: config,
data: res.response,
headers: res.headers
};
settle(resolve, reject, response);
} else {
reject(createError("Network Error!", config, res.status || 0))
}
});
})
}
使用
const tio = require('tt-tio');
var adapter = require('./fAppAdapter')
tio.defaults.adapter=adapter;
接下来就可以正常发起请求了;如果要动态判断是否在App中,代码如下:
const tio = require('tt-tio');
var nativeAdapter = require('./fAppAdapter');
var xhrAdapter= require('tt-io/lib/adapters/xhr');
tio.defaults.adapter=utils.isInAPP()?nativeAdapter:xhrAdapter;
注意,如果确定页面只会在APP中打开,但测试的时候需要在浏览器中测试,请不要使用这种方法,因为这样会将两个adapter都打包;取而代之的方法是使用DefinePlugin,根据不同的打包参数来打包不同的代码。
内置的ttAppAdapter
只要APP实现的fetch
方法也和主端的fetch方法一致,就可以使用上述的adapter,为了使用方便,tio将上述adapter已经内置了,名为"ttAppAdapter", 所以可以直接使用:
const adapter=require('tt-tio/lib/adapters/ttAppAdapter')
tio.defaults.adapter=adapter;
成功案例
目前F项目前端相关项目都在使用:包括M站、C端APP内嵌H5工程、B端APP内嵌H5工程、小程序、B端后台、中台等。
如果你的项目也正在使用,请告知我们 (Lark搜 杜文),谢谢。
FAQ
我的页面时一个纯Web页面,不会在App中打开,有必要使用tio吗?
如果你需要使用Tio的请求同步功能,则需要使用tio;如果没使用,则无所谓,此时tio和axios功能是一致的。
Tio包有多大,和axios相比呢?
Tio和axios包大小基本持平,如果是浏览器环境下,Tio+xhrAdapter为13.8K,而axios为13.3K,Gzip后两者基本持平,都在5K左右;另外值得注意的是,在小程序环境下,Tio+adapterGzip前平均在12K左右,会小于浏览器环境下。
小程序中能使用tio进行文件上传吗?
不可以,小程序中都会有单独的文件上传API,使用其即可;浏览器中Tio可以支持文件上传是因为使用了浏览器内置对象FormData,而小程序中没有该对象。
Tio的请求取消、超时在各个平台下都支持吗?
请求取消、超时取决于平台原生API是否支持,目前浏览器、Node、各种小程序平台都是支持的,所以这些平台下Tio都是支持的。而APP内嵌取决于appAdapter实现,如果APP jsBridge 没有实现或支持请求取消机制,则Tio的取消功能将无效;