应用开发中,各服务的调用使用最多的就是HTTP的形式,使用的HTTP client也从request --> superagent --> axios。axios
中的各类函数都是基于promise
的形式,虽然我也钟情于superagent
的链式调用,但axios
的各类promise
:transform
,interceptor
等特性,只能拥抱无法拒绝~
创建一个新的实例,此实例的公共配置独立与其它实例。一般在后端开发会经常需要对接各类不同的服务,而各服务使用单独的实例是较合理的方法,如下面例子初始化一个用于调用百度服务的实例:
const axios = require('axios');
const baiduService = axios.create({
// 设置接口路径(相对路径将拼接此路径)
baseURL: 'https://www.baidu.com/',
// 根据不同的应用设置默认的超时
timeout: 3 * 1000,
});
async function main() {
try {
const res = await baiduService.get('/');
console.info(res.status);
} catch (err) {
console.error(err);
}
}
main();
在发送请求前,可以对发送的数据做转换处理,默认的transformRequest
中会将提交的数据转换为对应的字符串(json或者querystring),具体代码可查看transformRequest。
我的应用中有一个统计服务,使用的是批量发送统计指标(设置为每次发送200个指标),对带宽的占用较大,因此希望发送指标时做数据压缩,下面看看怎么针对需求实现自定义的transform
。
const axios = require('axios');
const zlib = require('zlib');
const localService = axios.create({
baseURL: 'http://127.0.0.1:3000/',
timeout: 3 * 1000,
transformRequest: [
// 复用原有的转换,实现json --> json string
axios.defaults.transformRequest[0],
(data, header) => {
// 如果已处理过数据,则跳过
if (!header['Content-Encoding']) {
return data;
}
// 如果数据长度1KB(如字符数据并不一定小于1KB),不压缩
if (data.length < 1024) {
return data;
}
// 将数据压缩(可根据需要,只压缩长度大于多少的数据)
// 设置数据类型
header['Content-Encoding'] = 'gzip';
const w = zlib.createGzip();
w.end(Buffer.from(data));
return w;
},
],
});
async function main() {
try {
const arr = [];
for (let index = 0; index < 100; index++) {
// 模拟生成统计数据
arr.push({
category: 'login',
account: 'vicanso',
value: Math.round(Math.random() * 100),
ip: '127.0.0.1',
});
}
const res = await localService.post('/', {
data: arr,
});
console.info(res.status);
} catch (err) {
console.error(err);
}
}
main();
服务端代码:
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();
// body parser中可以解压数据
// 如果希望支持再多类型的压缩数据,可参考https://github.com/stream-utils/inflation调整
app.use(bodyParser());
router.post('/', async (ctx) => {
console.dir(ctx.request.body);
ctx.body = 'OK';
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
通过上面的自定义gzip的transform,带宽的占用节约了70%左右,当然这里会增加了CPU的损耗,根据各自的应用场景选择不压缩或者使用snappy等压缩速度优先的算法。
在接收到响应数据时,可以对响应数据做转换处理,默认的transform
是调用JSON.parse
转换为对应的Object。其的使用方法与transformRequest
类似,不再举例细说。
可实现自定义的请求处理,axios
实现了基于浏览器的xhr
以及nodejs
的两种处理,使其适应于两种运行环境。一般我们不需要自己去实现adapter,主要的使用场景是在测试中mock数据,如下:
const axios = require('axios');
const baiduService = axios.create({
// 设置接口路径(相对路径将拼接此路径)
baseURL: 'https://www.baidu.com/',
// 根据不同的应用设置默认的超时
timeout: 3 * 1000,
});
function mockAdapter(ins, fn) {
const {
adapter,
} = ins.defaults;
ins.defaults.adapter = fn;
return () => {
ins.defaults.adapter = adapter;
};
}
async function main() {
const done = mockAdapter(baiduService, (config) => {
// mock response,只返回状态码与data
return Promise.resolve({
status: 200,
data: 'OK',
});
});
try {
const res = await baiduService.get('/');
// 恢复adapter
console.info(res.status);
} catch (err) {
console.error(err);
} finally {
done();
}
}
main();
指定在nodejs环境中的http(s)的agent,如maxSockets,timeout等。下面例子中启用keepAlive,复用TCP连接,提升性能(默认是未启用)。
const axios = require('axios');
const http = require('http');
const https = require('https')
const localService = axios.create({
baseURL: 'http://127.0.0.1:3000/',
timeout: 3 * 1000,
httpAgent: new http.Agent({
keepAlive: true,
}),
httpsAgent: new https.Agent({
keepAlive: true
}),
});
async function main() {
try {
// 两次顺序调用,复用同样的tcp连接
let res = await localService.get('/');
console.info(res.status);
res = await localService.get('/');
console.info(res.status);
} catch (err) {
console.error(err);
}
}
main();
服务端的代码,展示是否使用同一TCP连接:
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', async (ctx) => {
// 生成socke id,用于标记TCP连接
if (!ctx.socket._id) {
ctx.socket._id = Math.floor(Math.random() * 1000);
}
console.info(ctx.socket._id);
ctx.body = 'OK';
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
Interceptors
是axios
的一大特色,使用拦截器可以对发送请求、接收数据做各类的操作(异步的也支持)。如请求重试,前置认证等等。
后端服务部署,为了高可用,避免单点故障,一般而言都会部署多节点。各服务之间的调用,简单的方式则是使用nginx或haproxy之类做反向代理,应用程序只接入反向代理的节点,这样简单方便,实际上反向代理则成为单点,达不到高可用的目标(实际情况对于大部分公司,访问量不大,反向代理稳定,基本也不出状况)。下面我们来讨论如果在客户端实现高可用的接入方式(如有完善的微服务体系,接入sidecar更简单便捷,无代码入侵性):
const axios = require('axios');
class Backends {
constructor(backends) {
this.backends = backends.map((url) => {
return {
url,
healthy: false,
};
});
}
// 选择其中可用的backend
get(policy) {
let backend = null;
switch (policy) {
case 'first':
this.backends.forEach((item) => {
if (!backend && item.healthy) {
backend = item;
}
});
break;
// 可实现更多的选择策略,如round robin等
default:
break;
}
return backend;
}
doHealthCheck() {
// 可以根据需要调整为更完善的检测方法,
// 如检测5次,3次通过则认为healthy
this.backends.forEach((backend) => {
axios.get(`${backend.url}/ping`).then((res) => {
const {
status,
} = res;
if (status === 200) {
backend.healthy = true;
} else {
backend.healthy = false;
}
}).catch(() => {
backend.healthy = false;
})
});
}
startHealthCheck() {
setInterval(() => {
this.doHealthCheck();
}, 5000).unref();
this.doHealthCheck();
}
}
const localServiceBackends = new Backends([
'http://127.0.0.1:3000',
'http://127.0.0.1:3001',
]);
localServiceBackends.startHealthCheck();
const localService = axios.create({
timeout: 3 * 1000,
});
localService.interceptors.request.use((config) => {
const backend = localServiceBackends.get('first');
if (!backend) {
return Promise.reject(new Error('无可用的服务'))
}
config.baseURL = backend.url;
return config;
})
async function main() {
try {
const res = await localService.get('/');
console.info(res.status);
} catch (err) {
console.error(err);
}
}
// 延时执行,等待首次health check
setTimeout(main, 1000);
服务端代码:
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', async (ctx) => {
ctx.body = 'OK';
});
router.get('/ping', (ctx) => {
ctx.body = 'pong';
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
函数调用出错统一基于Error
对象扩展,后端各服务都限定了标准的出错返回,以JSON的形式返回出错数据{"message": "出错信息", ...},其中message
则是出错信息,因此需要调整axios
以兼容接口的出错响应(默认返回的Error.message为http状态码的描述)。
const axios = require('axios');
const localService = axios.create({
baseURL: 'http://127.0.0.1:3000/',
timeout: 3 * 1000,
});
localService.interceptors.response.use(null, (err) => {
if (err.response && err.response.data) {
const {
data,
} = err.response;
if (data.message) {
// 可以根据后端出错数据的标准,往error中添加再多的属性
err.message = data.message;
}
}
return Promise.reject(err);
});
async function main() {
try {
await localService.get('/');
} catch (err) {
console.error(err.message);
}
}
main();
服务端代码:
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
// 公共的出错处理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
message: err.message,
};
}
});
router.get('/', async (ctx) => {
ctx.throw(400, '出错了')
});
router.get('/ping', (ctx) => {
ctx.body = 'pong';
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
组合使用request
与response
的interceptors
,可以无入侵式的增加接口分析,如性能、接口响应、数据等统计分析。
const axios = require('axios');
const localService = axios.create({
baseURL: 'http://127.0.0.1:3000/',
timeout: 3 * 1000,
});
const stats = (response) => {
// 未考虑各类异常场景
const {
config,
} = response;
const {
method,
url,
_start,
} = config;
// 可输出更多的参数,如post数据,响应数据等
console.info(`${method} ${url} ${Date.now() - _start}ms status:${response.status}`);
};
localService.interceptors.request.use((config) => {
config._start = Date.now();
return config;
});
localService.interceptors.response.use((response) => {
stats(response);
}, (err) => {
stats(err.response);
return Promise.reject(err);
});
async function main() {
try {
await localService.post('/');
await localService.post('/error');
} catch (err) {
console.error(err.message);
}
}
main();
服务端代码:
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
// 公共的出错处理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
message: err.message,
};
}
});
router.post('/', async (ctx) => {
ctx.body = {
foo: 'bar',
};
});
router.post('/error', async (ctx) => {
ctx.throw(400, '出错啦');
});
router.get('/ping', (ctx) => {
ctx.body = 'pong';
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
在认真了解axios
之前,我一直不解为什么其star的数量这么高,比superagent
高了那么多,当时自己没去研究,理所当然认为因为vue推荐使用它
,所以才那么火,深入了解之后,发现它的确有着过人之处。不要让自己的见识误解世界,要以实践了解世界。