x-tt-request v0.0.2
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
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
baseUrl | string | "" | 实际请求 url = baseUrl + 后请求配置的 url |
timeout | number | 10000 | 请求超时时间 |
method | string | POST | 网络请求方法 |
retryCount | number | 0 | 请求重试次数 |
retryTimeout | number | 500 | 请求重试的延迟时间 |
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 函数,相关内容请在响应拦截器中执行 |
response | AnyFunction | (res: AnyObject) => res; | 参数为 tt.request 的 complete 参数,支持异步 |
req.send
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
req.send | (config: CommonReqConfig): Promise | - | 发送请求函数 |
ComminReqConfig 说明
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
url | string | "" | 请求地址 |
header | object | {"content-type": "application/json"} | 请求 Header, referer 不可设置 |
method | Methods | "POST" | 网络请求方法 |
data | object | ArrayBuffer | 无 | 请求的参数 |
enableCache | boolean | false | 开启 cache |
responseType | 'text' | 'arrayBuffer' | text | 期望响应的数据类型 |
timeout | number | 10000 | 请求超时时间 |
dataType | 'json' | 'string' | json | 期望返回的数据格式 |
retryCount | number | 0 | 请求重试次数 |
retryTimeout | number | 500 | 请求重试的延迟时间,有重试次数时该字段才有效 |
canRetry | (res: AnyObject) => boolean | (res) => res?.errNo === 21103 | 是否可以进行重试,默认只有在 res.errNo 为 21103(超时)时重试 |
getToken
tips: 如果不想展示 toast,传入一个空对象即可
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
*url | string | 无 | 登录地址 |
method | Methods | POST | 请求 method |
*tokenPath | string | 无 | 请求返回的 token 路径,"data/token",表示 res.data.token,组件会以此来获取 token 挂载到 globalData 中 |
loggingToast | Toast | 登录中 Toast 参数 | 登录中的 toast |
loginSuccessToast | Toast | 登录成功 Toast 参数 | 登录成功的 toast |
loginFailToast | Toast | 登录失败 Toast 参数 | 登录失败的 toast |
retryCount | number | 2 | 登录请求重试次数,默认为 2 |
retryTimeout | number | 500 | 登录请求重试延迟时间,默认为 500 ms |
timeout | number | 10000 | 请求登录超时时间,默认为 10000 ms |
reqInterceptor | (code: string) => any | 无 | 请求拦截器,返回值作为登录请求的 Data |
success | (res: ReqRes) => void | 无 | 登录成功的回调 |
fail | (res: AnyObject) => void | 无 | 登录失败的回调 |
complete | (res: AnyObject) => void | 无 | 登录结束的回调 |
模块原理及实践
登录 login
登录是万事开头的第一步,废话不多说直接开干,这里需要注意的点是:
- 如何确保用户端上已登录?(solution:force 的值设置为 true 即可)
- 端上 code 获取失败怎么办?此处的 code 是由端上通过与抖开平台的网络请求生成的,会有网络问题(solution:前端侧需要支持重试操作,在重试时设置重试次数以及重试时间即可)
- 用户的网络问题导致的 login 超时失败怎么办?(solution:前端侧需要支持重试操作,在重试时设置重试次数即可)
请求前获取 token
一般来说,前端除了登录请求外,其他请求都需要用户登录,获取 token 后才能进行前后端交互,步骤如下:
Tips:抖开侧生成的 token 有效期为 24h,用户在小程序的逗留时间 95+% 都不会超过 1h, 所以此处不展开讨论 token 过期的情况,如有需要请自行与服务端沟通(设置请求重试次数,canRetry 中将 globalData 中的 token 去除再返回 true 即可重试请求)
- 请求拦截器中,请求 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 相关的操作。为避免该种情况的发生,此处需要设计成:
- 同一时刻多次调用 getToken,login 操作也只进行一次
- login 之后,此时应从缓存中直接返回 token,无需再次进行 login 获取 token
- 故此,对 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 服务, 来测试自己的请求情况
- 请先使用 nodemon 运行根目录下的 serve.js 文件(不要在小程序的终端内运行,会提示缺少响应的 require。推荐使用本地终端运行 serve.js 文件)。
- 复制 node 运行之后的访问路径
- 将访问路径填写进 /utils/request.js 文件中
- 开发 IDE 中开启“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”
- 开始测试吧
/*
* 不要在小程序的终端中打开,建议在本机的 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);
})