Как предварительно загрузить объекты для ваших обработчиков маршрутов Hapi
Или, как использовать обработчики предварительного маршрута Hapi
Эта проблема
По мере того, как я добавлял новые методы в свой последний проект, я осознавал, что количество потраченных впустую усилий растет.
В каждой конечной точке, имеющей дело с элементом, мы должны были его получить. Это также означало, что каждая конечная точка также должна была иметь дело с проверкой права пользователя на доступ к этому элементу. Когда я начал добавлять вещи, принадлежащие вещам, а затем нам пришлось проверять цепочку владения, это стало утомительно.
Я начал думать - у Express есть промежуточное ПО, интересно, что есть у Hapi? Должно быть что-то, чтобы я мог выполнить работу один раз и сохранить ее в объекте запроса.
Решения
Валидации
Это выглядело многообещающе для начала - в конце концов, мы проверяли параметры запроса.
К сожалению, они не помогли - проверки не могут быть добавлены к контексту запроса, поэтому функция проверки получит элементы, а затем функции придется снова получить элемент. (Или мы начинаем кэшировать - возможно, но слишком сложно.)
Плагины
Затем я посмотрел на плагины. Однако для того, что я хотел, они не подходили.
Плагины регистрируются на всем сервере, а не на отдельном маршруте. Но возникает проблема - как узнать, какие запросы должны иметь параметр, а какие нет? Без этого вам все равно придется проверять функции конечных точек, чего я не хотел.
Предмаршрутные функции
Это выглядело намного более многообещающе. Они запускаются после аутентификации, поэтому у вас есть учетные данные пользователя. Они могут добавлять в контекст запроса - возвращаемые ими значения переходят в объект request.pre
. И вы можете добавлять их в отдельные маршруты.
Похоже, у нас есть победитель!
Пробовать это
Нам нужно с чего начать. Давайте расширим сервер людей из сообщения на использование шаблонов и проверки.
Мы также сделаем первую попытку без использования функции предварительного маршрута. Это позволяет нам проверить, работает ли основной поток, поскольку мы не использовали его раньше, и мы можем увидеть, какие изменения он вносит в код.
У нас есть маршрут /people
, по которому можно получить список всех людей, которых мы сохранили. Давайте добавим новый маршрут, чтобы найти человека. /people/{personId}
было бы неплохо RESTful.
Тестовое задание
Сначала - как всегда - добавляем тест.
. it("can get an individual person", async () => {
const res = await server.inject({
method: "get",
url: "/people/1"
});
expect(res.statusCode).to.equal(200);
expect(res.payload).to.not.be.null;
});
Конечно, это не удается, поскольку сервер еще не знает об этом маршруте.
Шаблон
Затем мы добавим шаблон, который будет использоваться. Мы сохраняем простоту - речь идет не о том, чтобы что-то выглядело красиво, а просто о тестировании концепции.
<html>
<head>
<title>Purple People Eaters</title>
</head>
<body>
<p><%= person.name %> - <%= person.age %></p>
<a href="/people">Go back to people</a>
</body>
</html>
Код
Теперь мы начинаем добавлять собственно код. Первое, что нам нужно сделать, это расширить таблицу маршрутов:
export const peopleRoutes: ServerRoute[] = [
{ method: "GET", path: "/people", handler: showPeople },
{ method: "GET", path: "/people/{personId}", handler: showPerson },
{ method: "GET", path: "/people/add", handler: addPersonGet },
{ method: "POST", path: "/people/add", handler: addPersonPost }
];
Затем функция-обработчик. Поскольку в этом проекте мы не занимаемся аутентификацией, это уже довольно просто.
async function showPerson(request: Request, h: ResponseToolkit): Promise<ResponseObject> {
const person = people.find(person =>
person.id == parseInt(request.params.personId)
);
return h.view("person", { person: person });
}
Обратите внимание, что здесь мы пропускаем проверку ошибок, чтобы что-то заработало. И это работает!
server handles people - positive tests
✓ can see existing people
✓ can show 'add person' page
✓ can add a person and they show in the list
✓ can get an individual person
Использование pre
Первым делом необходимо проверить подпись функции, необходимую для обработчиков предварительного маршрута. Похоже, он очень похож на стандартный обработчик запросов, но с другим типом возвращаемого значения.
Это имеет смысл - обработчики запросов возвращают HTTP-ответы, в то время как обработчики предварительного маршрута потенциально возвращают объекты.
Он должен быть надежным - это функция, которая проверяет правильность входящих данных - поэтому мы добавляем все проверки на ошибки, которые обычно присутствуют в маршрутах HTTP. Наш дизайн для этого состоит в том, чтобы либо вернуть действительный объект, либо выбросить исключение, поэтому мы делаем наш возвращаемый тип Person
.
async function checkPerson(request: Request, h: ResponseToolkit): Promise<Person> {
// Did the user actually give us a person ID?
if (!request.params.personId) {
throw Boom.badRequest("No personId found");
}
try {
const person = people.find(person => person.id == parseInt(request.params.personId));
if (!person) {
throw Boom.notFound("Person not found");
}
return person;
} catch (err) {
console.error("Error", err, "finding person");
throw Boom.badImplementation("Error finding person");
}
}
const checkPersonPre = { method: checkPerson, assign: "person" };
Нам нужно изменить таблицу маршрутизации, чтобы добавить новую опцию:
{ method: "GET", path: "/people/{personId}", handler: showPerson, options: { pre: [checkPersonPre] } },
А затем обновите функцию showPerson
:
async function showPerson(request: Request, h: ResponseToolkit): Promise<ResponseObject> {
return h.view("person", { person: request.pre.person });
}
Даже в нашем игрушечном проекте наш HTTP-обработчик теперь выглядит намного чище.
Использование в реальном проекте
Приведя пример из проекта, который я разрабатываю, вы увидите, что это имеет еще большее значение.
До изменений каждый маршрут должен был:
- получить сайт, проверяя, разрешено ли пользователю ссылаться на сайт
- получить событие, проверяя, что оно было подключено к этому сайту
- обрабатывать отсутствующие / неверные значения
Это выглядело примерно так:
async function deleteEventPost(request: Request, h: ResponseToolkit): Promise<ResponseObject> {
try {
if (!request.params.siteId) {
throw Boom.badRequest("No site ID");
}
if (!request.params.eventId) {
throw Boom.badRequest("No event ID");
}
// We don’t actually want the site or event, we just
// want to confirm ownership.
const site = await getSite(request.auth.credentials.id, request.params.siteId);
if (!site) {
throw Boom.notFound();
}
const event = await getEvent(site.id, request.params.eventId);
if (!event) {
throw Boom.notFound();
}
await deleteEvent(event.id);
return h.redirect(`/sites/${site.id}/events`);
} catch (err) {
console.error("Error", err);
throw Boom.badImplementation("error deleting event");
}
}
После добавления обработчиков предварительного маршрута это немного уменьшилось:
async function deleteEventPost(request: Request, h: ResponseToolkit): Promise<ResponseObject> {
try {
await deleteEvent(request.pre.event.id);
return h.redirect(`/sites/${request.pre.site.id}/events`);
} catch (err) {
console.error("Error", err);
throw Boom.badImplementation("error deleting event");
}
}
Повторите это почти для каждой функции, и вы поймете, почему это победа!
Вся работа выполняется в одном месте - фактические функции просмотра могут просто предполагать, что данные есть и действительны, поскольку, если это не так, они не будут работать, и они могут продолжить с тем, чем они должны быть на самом деле. делает.
Конец
Ну вот и все. Сообщите мне, было ли это полезно. Как обычно, код из поста можно найти в моем репозитории на Github.