Как предварительно загрузить объекты для ваших обработчиков маршрутов Hapi

Или, как использовать обработчики предварительного маршрута Hapi

Эта проблема

По мере того, как я добавлял новые методы в свой последний проект, я осознавал, что количество потраченных впустую усилий растет.

В каждой конечной точке, имеющей дело с элементом, мы должны были его получить. Это также означало, что каждая конечная точка также должна была иметь дело с проверкой права пользователя на доступ к этому элементу. Когда я начал добавлять вещи, принадлежащие вещам, а затем нам пришлось проверять цепочку владения, это стало утомительно.

Я начал думать - у Express есть промежуточное ПО, интересно, что есть у Hapi? Должно быть что-то, чтобы я мог выполнить работу один раз и сохранить ее в объекте запроса.

В документацию по API!

Решения

Валидации

Это выглядело многообещающе для начала - в конце концов, мы проверяли параметры запроса.

К сожалению, они не помогли - проверки не могут быть добавлены к контексту запроса, поэтому функция проверки получит элементы, а затем функции придется снова получить элемент. (Или мы начинаем кэшировать - возможно, но слишком сложно.)

Плагины

Затем я посмотрел на плагины. Однако для того, что я хотел, они не подходили.

Плагины регистрируются на всем сервере, а не на отдельном маршруте. Но возникает проблема - как узнать, какие запросы должны иметь параметр, а какие нет? Без этого вам все равно придется проверять функции конечных точек, чего я не хотел.

Предмаршрутные функции

Это выглядело намного более многообещающе. Они запускаются после аутентификации, поэтому у вас есть учетные данные пользователя. Они могут добавлять в контекст запроса - возвращаемые ими значения переходят в объект 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.