0.0.2 • Published 10 months ago

x-tt-request v0.0.2

Weekly downloads
-
License
ISC
Repository
-
Last release
10 months ago

x-tt-request

基于种种痛点,将小程序的请求封装了一份,使之更加易用的同时,还具备更强大的能力。目的在于提高开发效率、提高项目的可维护性和健壮性。 本项目包含两个模块:request 和 login 模块

埋点操作主要放在 请求/响应拦截器 中,可以减少与业务代码的耦合。接入方式多种多样(如 Sentry、 Slardar、APM 等……),请自行选择,通过对应文档接入,本文不再赘述。

小程序测试(预览)地址

https://developer.open-douyin.com/ide/minicode/ihvBGms

代码地址

https://code.byted.org/ies/tt-microapp-components/tree/tt.request?

安装

npm i @x-tt-request
# 或
yarn add @x-tt-request

基本使用

/** utils/request.ts */
import { Request, getToken } from "@x-tt-request";

const req = new Request({
  baseUrl: "http://x.x.x.x",
  method: "POST",
  timeout: 10000,
});
req.interceptors.request(async (arg) => {
  arg.header = {
    "x-token": await getToken({
      url: "http://x.x.x.x/login",
      tokenPath: "data/token",
      reqInterceptor: (code) => {
        return { code };
      },
    }),
  };
});
req.interceptors.response((res) => {
  return new Promise((resolve) => {
    if (res) {
      console.log("响应拦截器 resolve", res);
      resolve(res.data);
    }
  });
});

export default req.send;

/** types.d.ts*/
export namespace API {
  type TestReq = {
    page: number;
    pageSize: number;
  };
  type TestRes = Promise<any>;
}

/** api.ts */
import { API } from "./types";
import request from "./request";

export function test(data: API.TestReq): API.TestRes {
  return request({
    url: "/test",
    data: {
      page: data.page,
      pageSize: data.pageSize,
    },
  });
}

export const Api = {
  test,
};

/** *.ts */
Api.test({ page: 1, pageSize: 1000 }).then((res) => {
  console.log("请求结果: ", res);
  console.log(res?.list);
  console.log(res?.total);

  this.setData({ nameList: res.list });
});

可配置项

Request

constructor

属性名类型默认值说明
baseUrlstring""实际请求 url = baseUrl + 后请求配置的 url
timeoutnumber10000请求超时时间
methodstringPOST网络请求方法
retryCountnumber0请求重试次数
retryTimeoutnumber500请求重试的延迟时间
canRetry((res: ReqRes) => boolean) | null(res) => res?.errNo === 21103是否可以进行重试,retryCount > 0 时才生效,默认只有在 res.errNo 为 21103 时重试

req.interceptors

属性名类型默认值说明
request(arg: CommonReqConfig) => Promise<void | ReqConfig> | void | ReqConfig = () => { };(arg) => arg参数 arg 为 tt.request 参数,你可以直接修改 arg 或者最后返回一个对象,该对象即为 tt.request 的参数,注意不要传入 CB 函数,相关内容请在响应拦截器中执行
responseAnyFunction(res: AnyObject) => res;参数为 tt.request 的 complete 参数,支持异步

req.send

属性名类型默认值说明
req.send(config: CommonReqConfig): Promise-发送请求函数

ComminReqConfig 说明

属性名类型默认值说明
urlstring""请求地址
headerobject{"content-type": "application/json"}请求 Header, referer 不可设置
methodMethods"POST"网络请求方法
dataobject | ArrayBuffer请求的参数
enableCachebooleanfalse开启 cache
responseType'text' | 'arrayBuffer'text期望响应的数据类型
timeoutnumber10000请求超时时间
dataType'json' | 'string'json期望返回的数据格式
retryCountnumber0请求重试次数
retryTimeoutnumber500请求重试的延迟时间,有重试次数时该字段才有效
canRetry(res: AnyObject) => boolean(res) => res?.errNo === 21103是否可以进行重试,默认只有在 res.errNo 为 21103(超时)时重试

getToken

tips: 如果不想展示 toast,传入一个空对象即可

属性名类型默认值说明
*urlstring登录地址
methodMethodsPOST请求 method
*tokenPathstring请求返回的 token 路径,"data/token",表示 res.data.token,组件会以此来获取 token 挂载到 globalData 中
loggingToastToast登录中 Toast 参数登录中的 toast
loginSuccessToastToast登录成功 Toast 参数登录成功的 toast
loginFailToastToast登录失败 Toast 参数登录失败的 toast
retryCountnumber2登录请求重试次数,默认为 2
retryTimeoutnumber500登录请求重试延迟时间,默认为 500 ms
timeoutnumber10000请求登录超时时间,默认为 10000 ms
reqInterceptor(code: string) => any请求拦截器,返回值作为登录请求的 Data
success(res: ReqRes) => void登录成功的回调
fail(res: AnyObject) => void登录失败的回调
complete(res: AnyObject) => void登录结束的回调

模块原理及实践

登录 login

登录是万事开头的第一步,废话不多说直接开干,这里需要注意的点是:

  1. 如何确保用户端上已登录?(solution:force 的值设置为 true 即可)
  2. 端上 code 获取失败怎么办?此处的 code 是由端上通过与抖开平台的网络请求生成的,会有网络问题(solution:前端侧需要支持重试操作,在重试时设置重试次数以及重试时间即可)
  3. 用户的网络问题导致的 login 超时失败怎么办?(solution:前端侧需要支持重试操作,在重试时设置重试次数即可)

请求前获取 token

一般来说,前端除了登录请求外,其他请求都需要用户登录,获取 token 后才能进行前后端交互,步骤如下:

Tips:抖开侧生成的 token 有效期为 24h,用户在小程序的逗留时间 95+% 都不会超过 1h, 所以此处不展开讨论 token 过期的情况,如有需要请自行与服务端沟通(设置请求重试次数,canRetry 中将 globalData 中的 token 去除再返回 true 即可重试请求)

  1. 请求拦截器中,请求 header 加上 token
const req = new Request({
  baseUrl: 'http://10.71.171.232:8083',
  method: 'POST',
  timeout: 10000,
});
req.interceptors.request(async (arg) => {
  arg.header = {
    'x-token': await getToken({ ... })
  };
  console.log('请求拦截器: ', arg)
})

但这样可能会存在近似“并发”的问题,即有可能未登录但同时调用多个接口,如此会使得 getToken 被多次调用,进行多次 login 相关的操作。为避免该种情况的发生,此处需要设计成:

  1. 同一时刻多次调用 getToken,login 操作也只进行一次
  2. login 之后,此时应从缓存中直接返回 token,无需再次进行 login 获取 token
  1. 故此,对 getToken 函数需要设计为:
const globalData = getApp();
const resArr: Array<(value: string) => void> = [];

function getToken(config): Promise<false | string> {
  return new Promise((resolve) => {
    const { token } = globalData;

    if (token) {
      resolve(token);
    } else {
      resArr.push(resolve);
      // 只有第一个 getToken 需要 调用 login 函数,余下的需要进入队列
      resArr?.length === 1 &&
        login(config).then((res) => {
            while (res && resArr.length) {
              resArr.shift()?.(res);
            };
          });
    }
  });
}

第一点:token 放置于 globalData 中,已有 token 情况下直接返回 token

第二点: 熟悉 Promise 原理的同学应该一下就看懂,保存了一个 resolve 函数数组,当 login 成功之后再进行 invoke 即可

请求重试

针对请求失败的情况,做了重试的处理,组件提供了 canRetry?: (res: AnyObject) => boolean 方法简单伪代码如下:

// tt.request 的 complete
newCfg.complete = async (res: AnyObject) => {
  // 是否需要进行请求重试
  if (retryCount > 0 && canRetry(res)) {
    retryCount--;
    tt.request(newCfg as any);
  } else {
    const ret = await this._resInterceptor(res);
    return ret || res;
  }
}

Request 实践

/** utils/request.ts */
import Request from '../core/Request'
import getToken from '../core/getToken'

const req = new Request({
  baseUrl: 'http://x.x.x.x',
  method: 'POST',
  timeout: 10000,
});
req.interceptors.request(async (arg) => {
  arg.retryCount = 3
  arg.header = {
    'x-token': await getToken({
      url: 'http://10.71.171.232:8083/login',
      timeout: 4000,
      tokenPath: 'data/token',
      retryCount: 2,
      loggingToast: {
        title: '登录中~',
        icon: 'loading',
        duration: 50000,
        mask: true
      },
      loginFailToast: {
        title: '登录失败~~~'
      },
      reqInterceptor: (code) => {
        return { code }
      },
    })
  };
  arg.data = {
    ...arg.data,
    name: 'demo'
  }
  console.log('请求拦截器: ', arg)
})
req.interceptors.response((res) => {
  return new Promise(resolve => {
    if (res) {
      console.log('响应拦截器 resolve', res)
      resolve(res.data)
    }
  })
})

export default req.send


/** types.d.ts*/
export namespace API {
  type TestReq = {
    page: number;
    pageSize: number;
  }
  type TestRes = Promise<any>
}


/** api.ts */
import { API } from './types'
import request from './request'

export function test(data: API.TestReq): API.TestRes {
  return request({
    url: '/test',
    data: {
      page: data.page,
      pageSize: data.pageSize
    },
  });
};

export const Api = {
  test,
};


/** *.ts */
Api.test({ page: 1, pageSize: 1000 }).then(res => {
  console.log('请求结果: ', res)
  console.log(res?.list);
  console.log(res?.total);

  this.setData({ nameList: res.list })
})

测试接口

你可以自己在本地开启一个 node 服务, 来测试自己的请求情况

  1. 请先使用 nodemon 运行根目录下的 serve.js 文件(不要在小程序的终端内运行,会提示缺少响应的 require。推荐使用本地终端运行 serve.js 文件)。
  2. 复制 node 运行之后的访问路径
  3. 将访问路径填写进 /utils/request.js 文件中
  4. 开发 IDE 中开启“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”
  5. 开始测试吧
/*
 * 不要在小程序的终端中打开,建议在本机的 iTerm 等终端打开本文件
 * 路径应脱离项目,否则 require 会报错(取了项目的 nodeModules)
*/
const express = require("express");
const bodyParser = require("body-parser");
const os = require("os");
const app = express();
const port = 8083;

function getIPAddress() {
    const interfaces = os.networkInterfaces();
    for (const name of Object.keys(interfaces)) {
        for (const iface of interfaces[name]) {
            if (iface.family === "IPv4" && !iface.internal) {
                return iface.address;
            }
        }
    }
    return "127.0.0.1";
}

// 解析 application/x-www-form-urlencoded 格式的请求体
app.use(bodyParser.urlencoded({ extended: false }));
// 解析 application/json 格式的请求体
app.use(bodyParser.json());

app.listen(port, function () {
    console.log(`服务启动成功, 访问路径:http://${getIPAddress()}:${port}`)
});

app.post('/test', function (req, res) {
    const timeout = 500
    // const timeout = 8000 + Math.random() * 5000
    console.log('收到请求 test, timeout: ', timeout, 'body: ', req.body)
    setTimeout(() => {
        res.send({
            list: [
                '张三', '李四', '王五',
                '张三', '李四', '王五',
                '张三', '李四', '王五',
                '张三',
            ],
            total: 67
        })
    }, timeout);
})

app.post('/login', function (req, res) {
    const timeout = 1000
    // const timeout = 3000 + Math.random() * 5000
    const code = req.body.code
    console.log('收到 login 请求, timeout: ', timeout, 'code: ', code)

    setTimeout(() => {
        res.send({
            token: `${code || ''}`
        })
    }, timeout);
})
0.0.2

10 months ago

0.0.1

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago