内容简介:API 在它存在的很长时间内都不断地侵蚀着我们的开发工作。无论是构建仅供其他微服务访问的微服务还是构建对外暴露的服务,你都需要开发 API。目前,大多数 API 都基于 REST 规范,REST 规范通俗易懂,并且建立在 HTTP 协议之上。 但是在很大程度上,REST 可能并不适合你。许多公司比如 Uber,facebook,Google,netflix 等都构建了自己的服务间内部通信协议,这里的关键问题在于何时做,而不是应不应该做。假设你想使用传统的 RPC 方式,但是你仍然想通过 http 格式传递
API 在它存在的很长时间内都不断地侵蚀着我们的开发工作。无论是构建仅供其他微服务访问的微服务还是构建对外暴露的服务,你都需要开发 API。
目前,大多数 API 都基于 REST 规范,REST 规范通俗易懂,并且建立在 HTTP 协议之上。 但是在很大程度上,REST 可能并不适合你。许多公司比如 Uber,facebook,Google,netflix 等都构建了自己的服务间内部通信协议,这里的关键问题在于何时做,而不是应不应该做。
假设你想使用传统的 RPC 方式,但是你仍然想通过 http 格式传递 json 数据,这时要怎么通过 node.js 来实现呢?请继续阅读本文。
阅读本教程前应确保以下两点
v4.0.0
设计原则
在本教程中,我们将为 API 设置如下两个约束:
- 保持简单(没有外部包装和复杂的操作)
- API 和接口文档,应该一同编写
现在开始
本教程的完整源代码可以在 Github 上找到,因此你可以 clone 下来方便查看。 首先,我们需要首先定义类型以及将对它们进行操作的方法(这些将是通过 API 调用的相同方法)。
创建一个新目录,并在新目录中创建两个文件, types.js
和 methods.js
。 如果你正在使用 linux 或 mac 终端,可以键入以下命令。
mkdir noderpc && cd noderpc touch types.js methods.js 复制代码
在 types.js
文件中,输入以下内容。
'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
暴露出来。
在 methods.js
有以下内容。
'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 抽象。
db.js
我们的 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 系统已经搭建完成了。
注意,我们尚未对本教程接口进行任何参数验证,你在调用测试的时候必须手动保证数据正确性。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 在 Android Studio 里使用构建分析器提升构建性能
- 使用 Docker 构建
- 使用 webpack 构建应用
- 使用Dockerfile构建镜像
- 使用模式构建:总结
- 使用 AutoAI 自动构建模型
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。