- Оригинальный адрес:How to build an RPC based API with node.js
- Оригинальный автор:Alloys Mila
- Перевод с:Команда облачных переводчиков Alibaba
- Ссылка на перевод:GitHub.com/рассветные команды/…
- Переводчик:пасти
- Корректор:также дерево,Духовное болото
Как создать систему API на основе RPC с помощью NodeJS
API съедал наши усилия по разработке в течение долгого времени, когда он существовал. Независимо от того, создаете ли вы микрослужбу, доступную только другим микрослужбам, или службу, открытую для внешнего мира, вам необходимо разработать API.
В настоящее время большинство API основаны на спецификации REST, которую легко понять и которая построена на основе протокола HTTP. Но по большей части REST может быть не для вас. Многие компании, такие как Uber, facebook, Google, netflix и т. д., создали свои собственные протоколы межсервисного взаимодействия, и ключевой вопрос здесь — когда это делать, а не нужно ли это делать.
Предположим, вы хотите использовать традиционный метод RPC, но при этом хотите передавать json-данные в формате http, как это сделать через node.js? Читайте дальше для этой статьи.
Прежде чем читать этот учебник, вы должны убедиться в следующих двух пунктах
- У вас должен быть как минимум реальный опыт работы с Node.js.
- Для поддержки ES6 необходимо установить Node.js.
v4.0.0
версия выше.
Принципы дизайна
В этом руководстве мы установим следующие два ограничения для API:
- Будьте проще (без внешней упаковки и сложных операций)
- Документация по API и интерфейсу, которую следует писать вместе
Начать сейчас
Полный исходный код этого руководства можно найти по адресуGithub, так что вы можете клонировать его для удобного просмотра. Во-первых, нам нужно сначала определить типы и методы, которые будут с ними работать (это будут те же самые методы, которые вызываются через 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
object для хранения нашего типа,key
это имя типа,value
является его определением. Определение включает в себя описание (фрагмент читаемого текста, в основном используемый для создания документов) и описывает каждое свойство в свойствах, поэтому дизайн в основном используется для создания и проверки документов и, наконец, передается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;
Как видите, по своей структуре он очень похож на модуль type, но главное отличие состоит в том, что определение каждого метода содержит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.
Именно здесь большинство из нас хочет использовать экспресс, но мы этого не делаем, поэтому мы будем использовать модуль 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
});
});
}
};
Это очень важная часть всей программы, потому что она обеспечивает фактический интерфейс. У нас есть набор конечных точек, каждая конечная точка соответствует функции-обработчику, которая вызывается при совпадении пути. По замыслу каждая функция-обработчик должна возвращать обещание.
Конечная точка RPC получает объект json, содержащий содержимое запроса, а затем анализирует каждый запрос какmethods.js
Соответствующий метод в файле, который вызывает методexec
функцию и вернуть результат или выдать ошибку.
description просматривает конечные точки для описания методов и типов и возвращает эту информацию вызывающей стороне. Упростите для разработчиков, использующих 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
}
}
Сервер создаст нового пользователя на основе отправленного вами запроса и вернет ответ. Построена хорошо документированная система API на основе RPC.
Обратите внимание, что мы не выполняли проверку параметров в интерфейсе этого руководства, вы должны вручную убедиться в правильности данных при вызове теста.