Вложенная маршрутизация Hapi

Предположим, что я хочу иметь конечные точки REST, которые выглядят примерно так:

/projects/
/projects/project_id 

/projects/project_id/items/
/projects/project_id/items/item_id

CRUD на каждом, если имеет смысл. Например, /project POST создает новый проект, GET извлекает все проекты. /projects/project _id GET извлекает только один проект.

Элементы являются специфичными для проекта, поэтому я помещаю их в project_id, который является конкретным проектом.

Есть ли способ создания такого рода вложенных маршрутов?

Сейчас у меня есть что-то вроде этого:

  server.route({
    method: 'GET',
    path: '/projects',
    handler: getAllProjects
  });

  server.route({
    method: 'GET',
    path: '/projects/{project_id}',
    handler: getOneProject
  });

  server.route({
    method: 'GET',
    path: '/projects/{project_id}/items/{item_id}',
    handler: getOneItemForProject
  });

  server.route({
    method: 'GET',
    path: '/projects/{project_id}/items',
    handler: getAllItemsForProject
  })

Но я ищу способ вставить маршруты элементов в маршруты проектов и возможность дальнейшего прохождения проекта.

Любые рекомендации?

Ответы

Ответ 1

Пока нет понятия "подпрограммирование" (что я знаю) в самом хапи, основы достаточно легки для реализации.

Во-первых, hapi предлагает подстановочные переменные в путях, используя их, вы в основном создаете маршрут для всего заданного пути. Например:

server.route({
  method: 'GET',
  path: '/projects/{project*}',
  handler: (request, reply) => {
    reply('in /projects, re-dispatch ' + request.params.project);
  }
});

Есть несколько правил для этих шаблонов подстановочных знаков, наиболее важным из которых является то, что он может быть только в последнем сегменте, что имеет смысл, если вы думаете об этом как о "catch-all".

В приведенном выше примере параметр {project*} будет доступен как request.params.project и будет содержать оставшуюся часть вызываемого пути, например. GET /projects/some/awesome/thing установит request.params.project в some/awesome/project.

Следующий шаг - обработать этот "подпуть" (ваш реальный вопрос), который в основном зависит от вкуса и того, как вы хотели бы работать. Ваш вопрос, похоже, подразумевает, что вы не хотите создавать бесконечный повторяющийся список почти подобных вещей, но в то же время иметь очень конкретные маршруты проекта.

Один из способов заключается в разделении параметра request.params.project на куски и поиск папок с соответствующими именами, которые могут содержать логику для дальнейшей обработки запроса.

Давайте рассмотрим это понятие, предположив структуру папок (относительно файла, содержащего маршрут, например index.js), который может быть легко использован для включения обработчиков для определенных маршрутов.

const fs = require('fs'); // require the built-in fs (filesystem) module

server.route({
    method: 'GET',
    path: '/projects/{project*}',
    handler: (request, reply) => {
        const segment = 'project' in request.params ? request.params.project.split('/') : [];
        const name = segment.length ? segment.shift() : null;

        if (!name) {
            //  given the samples in the question, this should provide a list of all projects,
            //  which would be easily be done with fs.readdir or glob.
            return reply('getAllProjects');
        }

        let projectHandler = [__dirname, 'projects', name, 'index.js'].join('/');

        fs.stat(projectHandler, (error, stat) => {
            if (error) {
                return reply('Not found').code(404);
            }

            if (!stat.isFile()) {
                return reply(projectHandler + ' is not a file..').code(500);
            }

            const module = require(projectHandler);

             module(segment, request, reply);
        });
    }
});

Механизм, подобный этому, позволит вам иметь каждый проект в качестве модуля node в вашем приложении, а ваш код вычисляет соответствующий модуль, который будет использоваться для обработки пути во время выполнения.

Вам даже не нужно указывать это для каждого метода запросов, так как вы можете просто маршрутизировать несколько методов, используя method: ['GET', 'POST', 'PUT', 'DELETE'] вместо method: 'GET'.

Однако он не имеет дело с повторяющимся объявлением о том, как обрабатывать маршруты, так как вам потребуется довольно похожая настройка модуля для каждого проекта.

В способе, в котором приведенный выше пример включает и вызывает "обработчики подпрограммы", примерная реализация:

//  <app>/projects/<projectname>/index.js
module.exports = (segments, request, reply) => {
    //  segments contains the remainder of the called project path
    //  e.g. /projects/some/awesome/project
    //       would become ['some', 'awesome', 'project'] inside the hapi route itself
    //       which in turn removes the first part (the project: 'some'), which is were we are now
    //       <app>/projects/some/index.js
    //       leaving the remainder to be ['awesome', 'project']
    //  request and reply are the very same ones the hapi route has received

    const action = segments.length ? segments.shift() : null;
    const item   = segments.length ? segments.shift() : null;

    //  if an action was specified, handle it.
    if (action) {
        //  if an item was specified, handle it.
        if (item) {
            return reply('getOneItemForProject:' + item);
        }

        //  if action is 'items', the reply will become: getAllItemsForProject
        //  given the example, the reply becomes: getAllAwesomeForProject
        return reply('getAll' + action[0].toUpperCase() + action.substring(1) + 'ForProject');
    }

    //  no specific action, so reply with the entire project
    reply('getOneProject');
};

Я думаю, это иллюстрирует, как отдельные проекты могут обрабатываться внутри вашего приложения во время выполнения, хотя это вызывает несколько проблем, которые вы хотите решить при построении архитектуры приложений:

  • Если модуль обработки проекта действительно очень похож, вы должны создайте библиотеку, которую вы используете для предотвращения копирования одного и того же модуля снова и снова, поскольку это облегчает обслуживание (что, я recon, была конечной целью иметь под-маршрутизацию)
  • если вы можете выяснить, какие модули использовать во время выполнения, вы должны также сможете понять это, когда начинается процесс сервера.

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

Выяснив, какие модули будут доступны для обработки различных проектов, которые у вас есть в начале приложения, будет сохраняться каждый запрос от необходимости повторять одну и ту же логику снова и снова. Хапи может кэшировать это для вас, и в этом случае это не имеет особого значения, но если кэширование не является вариантом, вам может быть лучше использовать менее динамические пути (что, я считаю, является основной причиной, по которой это не предлагается по умолчанию hapi).

Вы можете перемещать папку проектов в поисках всех <project>/index.js в начале приложения и регистрировать более конкретный маршрут, используя glob вот так:

const glob = require('glob');

glob('projects/*', (error, projects) => {
    projects.forEach((project) => {
        const name = project.replace('projects/', '');
        const module = require(project);

        server.route({
            method: 'GET',
            path: '/projects/' + name + '/{remainder*}',
            handler: (request, reply) => {
                const segment = 'remainder' in request.params ? request.params.remainder.split('/') : [];

                module(segment, request, reply);
            }
        });
    });
});

Это эффективно заменяет приведенную выше логику поиска модуля на каждый запрос и переключается на (немного) более эффективную маршрутизацию, поскольку вы высоко цените hapi, какие проекты вы будете обслуживать, оставив при этом фактическую обработку для каждого модуля проекта Вы предоставляете. (Не забудьте реализовать маршрут /projects, так как это теперь нужно сделать явно)

Ответ 2

То, что вы ищете, похоже на Express Router. Фактически, Экспресс неплохо похоронил полезность этой функции, поэтому я снова отправлю пример здесь:

// routes/users.js:
// Note we are not specifying the '/users' portion of the path here...

const router = express.Router();

// index route
router.get('/', (req, res) => {... });

// item route
router.get('/:id', (req, res) => { ... });

// create route
router.post('/', (req,res) => { ... });

// update route
router.put('/:id', (req,res) => { ... });

// Note also you should be using router.param to consolidate lookup logic:
router.param('id', (req, res, next) => {
  const id = req.params.id;
  User.findById(id).then( user => {
    if ( ! user ) return next(Boom.notFound(`User [${id}] does not exist`));
    req.user = user;
    next();
  }).catch(next);
});

module.exports = router;

Затем в ваших app.js или основных маршрутах /index.js, где вы собираете маршруты:

const userRoutes = require('./routes/users')

// now we say to mount those routes at /users!  Yay DRY!
server.use('/users', userRoutes)

Я действительно разочарован тем, что нашел это сообщение SO без каких-либо других ответов, поэтому я не допущу ничего из коробки (или даже стороннего модуля!) для достижения этого. Я полагаю, что создать простой модуль, который использует функциональную композицию для удаления дублирования, может быть не слишком сложно. Поскольку каждый из этих hapi-маршрутов defs - это всего лишь объект, кажется, что вы можете создать аналогичную оболочку, такую ​​как следующая (непроверенная):

function mountRoutes(pathPrefix, server, routes) {
  // for the sake of argument assume routes is an array and each item is 
  // what you'd normally pass to hapi `server.route
  routes.forEach( route => {
    const path = `${pathPrefix}{route.path}`;
    server.route(Object.assign(routes, {path}));
  });
}

EDIT В вашем случае, поскольку у вас несколько слоев вложенности, функция, похожая на Express router.param также будет чрезвычайно полезна. Я не очень хорошо знаком с hapi, поэтому я не знаю, имеет ли он эту способность.

EDIT # 2 Чтобы более точно ответить на исходный вопрос, вот hapi-route-builder имеет setRootPath(), который позволяет достичь чего-то очень похожего, позволяя вам указать базовую часть пути один раз.

Ответ 3

Там нет большой информации о таком базовом требовании. В настоящее время я делаю следующее, и это работает хорошо.

Шаг 1: Содержите маршруты в плагине, как показано ниже:

// server.js
const server = Hapi.server({ ... })
await server.register(require('./routes/projects'), { routes: { prefix: '/projects' } })

Шаг 2: Зарегистрируйте ext в рамках этого плагина.

// routes/projects/index.js
module.exports = {

    name: 'projects',

    async register(server) {

        server.route({
            method: 'get',
            path: '/', // note: you don't need to prefix with 'projects'
            async handler(request, h) {
                return [ ... ]
            }
        })

        server.route({
            method: 'get',
            path: '/{projectId}', // note: you don't need to prefix with 'projects'
            async handler(request, h) {
                return { ... }
            }
        })

        server.ext({
            // https://hapijs.com/api#request-lifecycle
            type: 'onPostAuth',
            options: {
                // 'sandbox: plugin' will scope this ext to this plugin
                sandbox: 'plugin'
            },
            async method (request, h) {
                // here you can do things as 'pre' steps to all routes, for example:
                // verify that the project 'id' exists
                if(request.params.projectId) {
                    const project = await getProjectById(request.params.projectId)
                    if(!project) {
                        throw Boom.notFound()
                    }
                    // Now request.params.project can be available to all sub routes
                    request.params.project = project
                }
                return h.continue
            }
        })

    }

}

Это было настолько близко к тому, что я смог восстановить функциональность Express Router.