Hapi嵌套路由

时间:2015-03-19 11:18:14

标签: javascript node.js url-routing hapijs

假设我希望REST端点看起来像这样:

/projects/
/projects/project_id 

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

每个CRUD都有意义。例如,/ projects 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
  })

但我正在寻找一种方法将物品路线嵌套到项目路线中,以及进一步传递项目的能力。

有什么建议吗?

3 个答案:

答案 0 :(得分:6)

虽然没有" subrouting" (我知道)在hapi本身,基础很容易实现。

首先,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);
        });
    }
});

这样的机制允许您将每个项目作为应用程序中的节点模块,并让您的代码找出用于在运行时处理路径的相应模块。

您甚至不必为每个请求方法指定此项,因为您可以使用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可能能够为你缓存这个,在这种情况下它并不重要,但如果缓存不是一个选项,你可能最好使用不那么动态的路径(我相信 - 这是主要的原因) 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路由,因为现在需要明确地完成这个路径)

答案 1 :(得分:3)

您正在寻找的内容类似于Express's Router。事实上,Express很好地掩盖了这个功能的用处,所以我在这里重新发布一个例子:

// 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或main routes / 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's `server.route
  routes.forEach( route => {
    const path = `${pathPrefix}{route.path}`;
    server.route(Object.assign(routes, {path}));
  });
}

编辑在您的情况下,由于您有多层嵌套,因此类似于Express router.param的功能也非常有用。我对hapi并不十分熟悉,所以我不知道它是否已具备此功能。

编辑#2 要更直接地回答原始问题,hapi-route-builder有一个setRootPath()方法可让您通过指定基础来实现非常相似的功能路径的一部分一次。

答案 2 :(得分:0)

关于这种基本要求的信息并不多。目前,我正在执行以下操作,并且效果很好。

第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功能非常接近。