0.0.4 • Published 5 years ago

tt-tio v0.0.4

Weekly downloads
3
License
MIT
Repository
github
Last release
5 years ago

Tio

基于Promise的,支持所有JavaScript运行时的Http库。Tio是基于axios二次开发的一个项目,它主要有如下功能和特点:

  1. 兼容axiosAPI.
  2. 支持所有JavaScript运行时(小程序、Weex等)。
  3. 支持请求同步。
  4. 请求重定向;在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;
   }
});

解释:

  1. 请求拦截器被锁定后(调用lock方法),其它请求将不能再进入请求拦截器,此时,其它请求将进入一个等待队列;当请求拦截器解锁后(调用unlock方法),等待队列中的请求才会进入请求拦截器。上面代码中,我们在请求csrfToken前锁定了请求拦截器,所以即使有多个并发请求,其它请求都得进入等待队列,当我们请求到csrfToken后解锁,其它请求恢复执行,这时csrfToken已经存在,所以就不需要再去请求csrfToken。

  2. 如果你想取等待消队列里的所有请求(如在请求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"
         }
  3. 请求拦截器和响应拦截器是不同的对象,每个拦截器都包含lock/unlock/clear三个方法,加锁、解锁、清空操作用于当前拦截器,比如你只锁定的是响应拦截器,那么请求拦截器依然没有锁,所有并发请求也都会进入请求拦截器,知道他们在需要进入响应拦截器的时,才会到响应拦截器外排队。

    注意:在执行拦截器回调时,tio会将当前拦截器对象作为this来call拦截器回调,我们拿请求拦截器来举例:

    tio.interceptors.request.use(function (config) {
     this.lock();// 等价于tio.interceptors.request.lock()
    });

    另外注意,拦截器回调函数如果使用箭头函数,则不能在里面使用this来替代拦截器对象。

  4. 在上面的示例中,fetchTocken() 方法不能再使用tio去请求csrfToken,因为在调用fetchTocken()tio的拦截器队列已经锁定了,所以再用它去请求csrfToken的话将会陷入循环等待(死锁),正确的作法很简单,重新创建一个新的tio实例来发起请求即可,如:

    function fetchTocken(){
       return tio.create().get("/token")
    }

除了上面请求csrfToken的场景,请求同步功能在很多场景也都很实用,再举一个常见的例子,比如登录token自动延时:登录成功后会返回一个token,但token会有一个有效期,如果过期则需要重新请求token。

总结

我们总看看Tio发起网络请求的整个流程图:

Tio request flow

请求重定向

在APP内嵌的H5页面中,应该尽可能通过APP发起网络请求;如果使用Webview发起网络请求会有如下问题:

  1. 不能使用我司的TTNet库
  2. cookie 同步困难
  3. 接口安全
  4. 访问控制
  5. 性能
  6. 缓存

详细的分析请查看 为什么在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的取消功能将无效;