如何使用 NodeJS 构建基于 RPC 的 API 系统

简介: > * 原文地址:[How to build an RPC based API with node.js](https://scotch.io/@alloys/how-to-build-an-rpc-based-api-with-nodejs) > * 原文作者:[Alloys Mila](https://scotch.io/@alloys) > * 译文出自:[阿里云翻译小组](https:

如何使用 NodeJS 构建基于 RPC 的 API 系统

API 在它存在的很长时间内都不断地侵蚀着我们的开发工作。无论是构建仅供其他微服务访问的微服务还是构建对外暴露的服务,你都需要开发 API。

目前,大多数 API 都基于 REST 规范,REST 规范通俗易懂,并且建立在 HTTP 协议之上。 但是在很大程度上,REST 可能并不适合你。许多公司比如 Uber,facebook,Google,netflix 等都构建了自己的服务间内部通信协议,这里的关键问题在于何时做,而不是应不应该做。

假设你想使用传统的 RPC 方式,但是你仍然想通过 http 格式传递 json 数据,这时要怎么通过 node.js 来实现呢?请继续阅读本文。

阅读本教程前应确保以下两点

  • 你至少应该具备 Node.js 的实战经验
  • 为了获得 ES6 支持,需要安装 Node.js v4.0.0 以上版本。

设计原则

在本教程中,我们将为 API 设置如下两个约束:

  • 保持简单(没有外部包装和复杂的操作)
  • API 和接口文档,应该一同编写

现在开始

本教程的完整源代码可以在 Github 上找到,因此你可以 clone 下来方便查看。
首先,我们需要首先定义类型以及将对它们进行操作的方法(这些将是通过 API 调用的相同方法)。

创建一个新目录,并在新目录中创建两个文件,types.jsmethods.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 系统已经搭建完成了。

注意,我们尚未对本教程接口进行任何参数验证,你在调用测试的时候必须手动保证数据正确性。

目录
相关文章
|
15天前
|
安全 API 数据安全/隐私保护
自学记录HarmonyOS Next DRM API 13:构建安全的数字内容保护系统
在完成HarmonyOS Camera API开发后,我深入研究了数字版权管理(DRM)技术。最新DRM API 13提供了强大的工具,用于保护数字内容的安全传输和使用。通过学习该API的核心功能,如获取许可证、解密内容和管理权限,我实现了一个简单的数字视频保护系统。该系统包括初始化DRM模块、获取许可证、解密视频并播放。此外,我还配置了开发环境并实现了界面布局。未来,随着数字版权保护需求的增加,DRM技术将更加重要。如果你对这一领域感兴趣,欢迎一起探索和进步。
77 18
|
11天前
|
监控 算法 JavaScript
基于 Node.js Socket 算法搭建局域网屏幕监控系统
在数字化办公环境中,局域网屏幕监控系统至关重要。基于Node.js的Socket算法实现高效、稳定的实时屏幕数据传输,助力企业保障信息安全、监督工作状态和远程技术支持。通过Socket建立监控端与被监控端的数据桥梁,确保实时画面呈现。实际部署需合理分配带宽并加密传输,确保信息安全。企业在使用时应权衡利弊,遵循法规,保障员工权益。
25 7
|
1月前
|
JSON JavaScript 前端开发
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发作为连接用户与数据的桥梁,扮演着至关重要的角色。本文将引导您步入Node.js的奇妙世界,通过实践操作,掌握如何使用这一强大的JavaScript运行时环境构建高效、可扩展的RESTful API。我们将一同探索Express框架的使用,学习如何设计API端点,处理数据请求,并实现身份验证机制,最终部署我们的成果到云服务器上。无论您是初学者还是有一定基础的开发者,这篇文章都将为您打开一扇通往后端开发深层知识的大门。
50 12
|
2月前
|
监控 安全 API
深入浅出:构建高效RESTful API的最佳实践
在数字化时代,API已成为连接不同软件和服务的桥梁。本文将带你深入了解如何设计和维护一个高效、可扩展且安全的RESTful API。我们将从基础概念出发,逐步深入到高级技巧,让你能够掌握创建优质API的关键要素。无论你是初学者还是有经验的开发者,这篇文章都将为你提供实用的指导和启示。让我们一起探索API设计的奥秘,打造出色的后端服务吧!
|
2月前
|
JSON 缓存 测试技术
构建高效RESTful API的后端实践指南####
本文将深入探讨如何设计并实现一个高效、可扩展且易于维护的RESTful API。不同于传统的摘要概述,本节将直接以行动指南的形式,列出构建RESTful API时必须遵循的核心原则与最佳实践,旨在为开发者提供一套直接可行的实施框架,快速提升API设计与开发能力。 ####
|
2月前
|
安全 测试技术 API
构建高效RESTful API:后端开发的艺术与实践####
在现代软件开发的浩瀚星空中,RESTful API如同一座桥梁,连接着前端世界的绚丽多彩与后端逻辑的深邃复杂。本文旨在探讨如何精心打造一款既高效又易于维护的RESTful API,通过深入浅出的方式,剖析其设计原则、实现技巧及最佳实践,为后端开发者提供一份实用的指南。我们不深入晦涩的理论,只聚焦于那些能够即刻提升API品质与开发效率的关键点,让你的API在众多服务中脱颖而出。 ####
38 0
|
4天前
|
JSON API 数据格式
京东商品SKU价格接口(Jd.item_get)丨京东API接口指南
京东商品SKU价格接口(Jd.item_get)是京东开放平台提供的API,用于获取商品详细信息及价格。开发者需先注册账号、申请权限并获取密钥,随后通过HTTP请求调用API,传入商品ID等参数,返回JSON格式的商品信息,包括价格、原价等。接口支持GET/POST方式,适用于Python等语言的开发环境。
41 11
|
28天前
|
人工智能 自然语言处理 API
Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
谷歌推出的Multimodal Live API是一个支持多模态交互、低延迟实时互动的AI接口,能够处理文本、音频和视频输入,提供自然流畅的对话体验,适用于多种应用场景。
78 3
Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
|
15天前
|
JSON 安全 API
淘宝商品详情API接口(item get pro接口概述)
淘宝商品详情API接口旨在帮助开发者获取淘宝商品的详细信息,包括商品标题、描述、价格、库存、销量、评价等。这些信息对于电商企业而言具有极高的价值,可用于商品信息展示、市场分析、价格比较等多种应用场景。
|
23天前
|
前端开发 API 数据库
Next 编写接口api
Next 编写接口api

热门文章

最新文章