- 原文地址:How to build an RPC based API with node.js
- 原文做者:Alloys Mila
- 译文出自:阿里云翻译小组
- 译文连接:github.com/dawn-teams/…
- 译者:牧曈
- 校对者:也树,灵沼
API 在它存在的很长时间内都不断地侵蚀着咱们的开发工做。不管是构建仅供其余微服务访问的微服务仍是构建对外暴露的服务,你都须要开发 API。javascript
目前,大多数 API 都基于 REST 规范,REST 规范通俗易懂,而且创建在 HTTP 协议之上。 可是在很大程度上,REST 可能并不适合你。许多公司好比 Uber,facebook,Google,netflix 等都构建了本身的服务间内部通讯协议,这里的关键问题在于什么时候作,而不是应不该该作。java
假设你想使用传统的 RPC 方式,可是你仍然想经过 http 格式传递 json 数据,这时要怎么经过 node.js 来实现呢?请继续阅读本文。node
阅读本教程前应确保如下两点linux
v4.0.0
以上版本。在本教程中,咱们将为 API 设置以下两个约束:git
本教程的完整源代码能够在 Github 上找到,所以你能够 clone 下来方便查看。 首先,咱们须要首先定义类型以及将对它们进行操做的方法(这些将是经过 API 调用的相同方法)。github
建立一个新目录,并在新目录中建立两个文件,types.js
和 methods.js
。 若是你正在使用 linux 或 mac 终端,能够键入如下命令。express
mkdir noderpc && cd noderpc
touch types.js methods.js
复制代码
在 types.js
文件中,输入如下内容。npm
'use strict';
let types = {
user: {
description:'the details of the user',
props: {
name:['string', 'required'],
age: ['number'],
email: ['string', 'required'],
password: ['string', 'required']
}
},
task: {
description:'a task entered by the user to do at a later time',
props: {
userid: ['number', 'required'],
content: ['string', 'require'],
expire: ['date', 'required']
}
}
}
module.exports = types;
复制代码
乍一看很简单,用一个 key-value
对象来保存咱们的类型,key
是类型的名称,value
是它的定义。该定义包括描述(是一段可读文本,主要用于生成文档),在 props 中描述了各个属性,这样设计主要用于文档生成和验证,最后经过 module.exports
暴露出来。json
在 methods.js
有如下内容。api
'use strict';
let db = require('./db');
let methods = {
createUser: {
description: `creates a new user, and returns the details of the new user`,
params: ['user:the user object'],
returns: ['user'],
exec(userObj) {
return new Promise((resolve) => {
if (typeof (userObj) !== 'object') {
throw new Error('was expecting an object!');
}
// you would usually do some validations here
// and check for required fields
// attach an id the save to db
let _userObj = JSON.parse(JSON.stringify(userObj));
_userObj.id = (Math.random() * 10000000) | 0; // binary or, converts the number into a 32 bit integer
resolve(db.users.save(userObj));
});
}
},
fetchUser: {
description: `fetches the user of the given id`,
params: ['id:the id of the user were looking for'],
returns: ['user'],
exec(userObj) {
return new Promise((resolve) => {
if (typeof (userObj) !== 'object') {
throw new Error('was expecting an object!');
}
// you would usually do some validations here
// and check for required fields
// fetch
resolve(db.users.fetch(userObj.id) || {});
});
}
},
fetchAllUsers: {
released:false;
description: `fetches the entire list of users`,
params: [],
returns: ['userscollection'],
exec() {
return new Promise((resolve) => {
// fetch
resolve(db.users.fetchAll() || {});
});
}
},
};
module.exports = methods;
复制代码
能够看到,它和类型模块的设计很是相似,但主要区别在于每一个方法定义中都包含一个名为 exec
的函数,它返回一个 Promise
。 这个函数暴露了这个方法的功能,虽然其余属性也暴露给了用户,但这必须经过 API 抽象。
咱们的 API 须要在某处存储数据,可是在本教程中,咱们不但愿经过没必要要的 npm install
使教程复杂化,咱们建立一个很是简单、原生的内存中键值存储,由于它的数据结构由你本身设计,因此你能够随时改变数据的存储方式。
在 db.js
中包含如下内容。
'use strict';
let users = {};
let tasks = {};
// we are saving everything inmemory for now
let db = {
users: proc(users),
tasks: proc(tasks)
}
function clone(obj) {
// a simple way to deep clone an object in javascript
return JSON.parse(JSON.stringify(obj));
}
// a generalised function to handle CRUD operations
function proc(container) {
return {
save(obj) {
// in JS, objects are passed by reference
// so to avoid interfering with the original data
// we deep clone the object, to get our own reference
let _obj = clone(obj);
if (!_obj.id) {
// assign a random number as ID if none exists
_obj.id = (Math.random() * 10000000) | 0;
}
container[_obj.id.toString()] = _obj;
return clone(_obj);
},
fetch(id) {
// deep clone this so that nobody modifies the db by mistake from outside
return clone(container[id.toString()]);
},
fetchAll() {
let _bunch = [];
for (let item in container) {
_bunch.push(clone(container[item]));
}
return _bunch;
},
unset(id) {
delete container[id];
}
}
}
module.exports = db;
复制代码
其中比较重要是 proc
函数。经过获取一个对象,并将其包装在一个带有一组函数的闭包中,方便在该对象上添加,编辑和删除值。若是你对闭包不够了解,应该提早阅读关于 JavaScript
闭包的内容。
因此,咱们如今基本上已经完成了程序功能,咱们能够存储和检索数据,而且能够实现对这些数据进行操做,咱们如今须要作的是经过网络公开这个功能。 所以,最后一部分是实现 HTTP 服务。
这是咱们大多数人但愿使用express的地方,但咱们不但愿这样,因此咱们将使用随节点一块儿提供的http模块,并围绕它实现一个很是简单的路由表。
正如预期的那样,咱们继续建立 server.js
文件。在这个文件中咱们把全部内容关联在一块儿,以下所示。
'use strict';
let http = require('http');
let url = require('url');
let methods = require('./methods');
let types = require('./types');
let server = http.createServer(requestListener);
const PORT = process.env.PORT || 9090;
复制代码
文件的开头部分引入咱们所须要的内容,使用 http.createServer
来建立一个 HTTP 服务。requestListener
是一个回调函数,咱们稍后定义它。 而且咱们肯定下来服务器将侦听的端口。
在这段代码以后咱们来定义路由表,它规定了咱们的应用程序将响应的不一样 URL 路径。
// we'll use a very very very simple routing mechanism
// don't do something like this in production, ok technically you can...
// probably could even be faster than using a routing library :-D
let routes = {
// this is the rpc endpoint
// every operation request will come through here
'/rpc': function (body) {
return new Promise((resolve, reject) => {
if (!body) {
throw new (`rpc request was expecting some data...!`);
}
let _json = JSON.parse(body); // might throw error
let keys = Object.keys(_json);
let promiseArr = [];
for (let key of keys) {
if (methods[key] && typeof (methods[key].exec) === 'function') {
let execPromise = methods[key].exec.call(null, _json[key]);
if (!(execPromise instanceof Promise)) {
throw new Error(`exec on ${key} did not return a promise`);
}
promiseArr.push(execPromise);
} else {
let execPromise = Promise.resolve({
error: 'method not defined'
})
promiseArr.push(execPromise);
}
}
Promise.all(promiseArr).then(iter => {
console.log(iter);
let response = {};
iter.forEach((val, index) => {
response[keys[index]] = val;
});
resolve(response);
}).catch(err => {
reject(err);
});
});
},
// this is our docs endpoint
// through this the clients should know
// what methods and datatypes are available
'/describe': function () {
// load the type descriptions
return new Promise(resolve => {
let type = {};
let method = {};
// set types
type = types;
//set methods
for(let m in methods) {
let _m = JSON.parse(JSON.stringify(methods[m]));
method[m] = _m;
}
resolve({
types: type,
methods: method
});
});
}
};
复制代码
这是整个程序中很是重要的一部分,由于它提供了实际的接口。 咱们有一组 endpoint,每一个 endpoint 都对应一个处理函数,在路径匹配时被调用。根据设计原则每一个处理函数都必须返回一个 Promise。
RPC endpoint 获取一个包含请求内容的 json 对象,而后将每一个请求解析为 methods.js
文件中的对应方法,调用该方法的 exec
函数,并将结果返回,或者抛出错误。
describe endpoint 扫描方法和类型的描述,并将该信息返回给调用者。让使用 API 的开发者可以轻松地知道如何使用它。
如今让咱们添加咱们以前讨论过的函数 requestListener
,而后就能够启动服务。
// request Listener
// this is what we'll feed into http.createServer
function requestListener(request, response) {
let reqUrl = `http://${request.headers.host}${request.url}`;
let parseUrl = url.parse(reqUrl, true);
let pathname = parseUrl.pathname;
// we're doing everything json
response.setHeader('Content-Type', 'application/json');
// buffer for incoming data
let buf = null;
// listen for incoming data
request.on('data', data => {
if (buf === null) {
buf = data;
} else {
buf = buf + data;
}
});
// on end proceed with compute
request.on('end', () => {
let body = buf !== null ? buf.toString() : null;
if (routes[pathname]) {
let compute = routes[pathname].call(null, body);
if (!(compute instanceof Promise)) {
// we're kinda expecting compute to be a promise
// so if it isn't, just avoid it
response.statusCode = 500;
response.end('oops! server error!');
console.warn(`whatever I got from rpc wasn't a Promise!`);
} else {
compute.then(res => {
response.end(JSON.stringify(res))
}).catch(err => {
console.error(err);
response.statusCode = 500;
response.end('oops! server error!');
});
}
} else {
response.statusCode = 404;
response.end(`oops! ${pathname} not found here`)
}
})
}
// now we can start up the server
server.listen(PORT);
复制代码
每当有新请求时调用此函数并等待拿到数据,以后查看路径,并根据路径匹配到路由表上的对应处理方法。而后使用 server.listen
启动服务。
如今咱们能够在目录下运行 node server.js
来启动服务,而后使用 postman 或你熟悉的 API 调试工具,向 http://localhost{PORT}/rpc
发送请求,请求体中包含如下 JSON 内容。
{
"createUser": {
"name":"alloys mila",
"age":24
}
}
复制代码
server 将会根据你提交的请求建立一个新用户并返回响应结果。一个基于 RPC、文档完善的 API 系统已经搭建完成了。
注意,咱们还没有对本教程接口进行任何参数验证,你在调用测试的时候必须手动保证数据正确性。