在项目中有一个需求,当我们调试本地代码时,不会直接在项目代码中去mock本地数据,而是通过node的代理层去mock数据。前端应用通过本地起的一个node服务代理请求api接口,因此,在代理阶段,可以对数据进行拦截,既可以对从远端返回的数据进行修改后返回给前端应用,也可以在代理层面做一些优化,比如本文要讲到的缓存。
node代理
node是非常轻量级的服务,特别适合用于快速创建应用和后端服务的接入层。所谓接入层,除了进行数据处理外,其实更多时候是为了解决在应用和后端接口之间,实现某些特殊功能,例如打日志,例如对数据进行校验和校正,例如在发生异常时对上下游服务进行信息收集等等。而且node适合异步编程,在不影响正常服务的情况下,还可以通过异步编程,对某个动作后面的跟随动作做进一步的处理。
而常用的方案,就是利用node便捷的代理能力。node的http模块提供了便捷的网络请求接口,基于此,中间件的node服务编程模式被广泛应用。知名框架express就是一个中间件的运作器,它的一切动作都可以通过中间件机制实现。
这种模式被广泛运用在现代框架编程中,一个请求和响应被中间件兜住,并一层层的处理数据。
而要实现node代理,只需要一个代理中间件即可。http-proxy-middelware这个包被广泛应用在这样的场景需求中。关于http-proxy-middelware可以通过它的说明文档详细了解它的用法。通过http-proxy-middelware,我们可以让前端应用发来的ajax请求转发到后端服务的api去,在得到后端接口数据之后,通过代理又返回给前端,这样做的一个好处是,可以在一个node服务中同时提供接口和界面输出,解决跨域问题。
level数据库
leveldb在我之前的文章中已经介绍过了,这里正好是它发挥作用的实用场景。为什么要选择leveldb,而非redis这样更知名的kv数据库?有两个原因,一是redis需要另外安装和启动服务,而level不需要,通过npm安装后会自动编译在本地,甚至可以通过打包level目录共享数据,二是level提供了超级方便的api进行操作,对于开端开发者而言,简直无需学习(和localStorage极其相似)。
但是leveldb只能存取基础类型数据,而不能存对象,因此,还需要做一层封装。今天在看文档时,发现实际上leveldb是支持直接存储对象的。恰巧,我所开发的hello-storage可以帮助我们瞬间解决这个问题。
const { HelloStorage } = require('hello-storage'); const level = require('level'); const ldb = level('leveldb'); const ldbSotrage = { getItem: key => ldb.get(key).catch(() => null), setItem: (key, value) => ldb.put(key, value), removeItem: key => ldb.del(key) }; const store = new HelloStorage({ storage: ldbSotrage, stringify: false, async: true });
可以看到,整个过程非常方便,这样,就可以让leveldb支持对象以及支持过期时间等能力。
请求唯一性
kv数据库的特点是,key值一定要唯一。而我们前端发出去的请求,即使同一个资源,也有可能会通过传一些特别的参数而不同,要解决这个问题,我们可以通过使用一些方法,使每一个请求,都能得到一个唯一的hash。
const queryString = require('query-string'); const { getObjectHashcode } = require('object-hashcode'); function handle(req, res, next) { const url = URL.parse(req.originalUrl); const { pathname, query } = url; const search = Object.assign({}, queryString.parse(query)); // 删除不需要的参数 delete search.request_id; const hash = getObjectHashcode({ pathname, search }); store.get(hash).then((data) => { if (!data) { next(); } else { let output = JSON.stringify(data); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(output); } }); } }
通过求一个hash,保证了请求对应的唯一性。
代理缓存
在上一段代码中,我们创建了一个handle函数,而这个handle函数,就是一个中间件,可以结合各类基于http模块开发的框架使用(中间件机制由框架实现,而非http模块本身)。
根据中间件机制的特点,中间件的执行和拦截是有一定顺序的,通过next函数来控制,因此我们需要在中间件序列中合理安排顺序。当一个请求进来之后,先被handle函数兜住,如果next函数被执行,那么就进入代理中间件(http-proxy-middleware)进行处理。我们在代理的响应接收阶段记录缓存。
onProxyRes(proxyRes, req, res) {
const url = URL.parse(req.originalUrl);
const { pathname, query } = url;
const search = Object.assign({}, queryString.parse(query));
// 删除不需要的参数
delete search.request_id;
const hash = getObjectHashcode({ pathname, search });
const ret = [];
proxyRes.on('data', (chunk) => {
ret.push(chunk.toString());
});
proxyRes.on('end', () => {
let json = ret.join('');
try {
let data = JSON.parse(json);
if (!data.error) {
store.set(hash, data);
}
}
catch(e) {
console.error(e);
}
});
}
}),
在数据返回后,将它作为缓存保存起来。但是由于整个操作是异步的,因此,并不影响数据直接输出给前端的性能。
小结
代理模式是开发中常用的模式,把它运用到实际开发过程中,也是非常的有趣。之前一直在介绍leveldb,算是一个狂热粉,这次正好通过本文把它运用到实际工作中。除了上文提到的这些点外,你还可以在中间件思想的基础上继续添加,达到你想要的功能。另外,上面的整个过程中,没有配置一个缓存的快关和过期时间,这也是一个需要完善的地方。