Hapi嵌套路由

PoM*_*TuK 18 javascript url-routing node.js hapijs

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

/projects/
/projects/project_id 

/projects/project_id/items/
/projects/project_id/items/item_id
Run Code Online (Sandbox Code Playgroud)

每个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
  })
Run Code Online (Sandbox Code Playgroud)

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

有什么建议?

Rog*_*ker 5

虽然hapi本身没有"subrouting"(我知道)的概念,但基础知识很容易实现.

首先,hapi 在路径中提供通配符变量,使用这些变量基本上可以为给定路径创建一个包含所有路径.例如:

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

这些通配符路径有一些规则,最重要的一个是它只能在最后一个段中,如果你认为它是一个"全能",这是有道理的.

在上面的示例中,{project*}参数将作为request.params.project并且将包含被调用路径的其余部分,例如GET /projects/some/awesome/thing将设置request.params.projectsome/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);
        });
    }
});
Run Code Online (Sandbox Code Playgroud)

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

您甚至不必为每个请求方法指定此方法,因为您可以通过使用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');
};
Run Code Online (Sandbox Code Playgroud)

我认为这说明了如何在运行时在您的应用程序中处理单个项目,尽管它确实引起了您在构建应用程序体系结构时要处理的一些问题:

  • 如果项目处理模块真的非常相似,你应该创建一个库,用来防止一遍又一遍地复制同一个模块,因为这样可以使维护更容易(我认为这是进行子路由的最终目标)
  • 如果你能确定在运行时使用哪些模块,你也应该能够在服务器进程启动时弄清楚这一点.

创建一个库以防止重复代码是你应该尽早(学会做)的事情,因为这样可以使维护更容易,并且你的未来将会感激.

确定哪些模块可用于处理应用程序启动时的各种项目,这样可以避免每次请求不得不一遍又一遍地应用相同的逻辑.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);
            }
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

这有效地取代了在每个请求上查找模块的上述逻辑,并切换到(稍微)更高效的路由,因为您正在准备hapi确切地说您将服务哪些项目,同时仍然将实际处理留给您提供的每个项目模块.(不要忘记实现/projects路由,因为现在需要明确地完成)


tho*_*nic 4

您正在寻找类似于Express 的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;
Run Code Online (Sandbox Code Playgroud)

然后在你的 app.js 或 main paths/index.js 中组装你的路由:

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

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

实际上,我很失望地发现这篇 SO 帖子没有其他回复,所以我假设没有任何现成的东西(甚至第三方模块!)来实现这一点。我想创建一个使用功能组合来消除重复的简单模块可能不会太困难。由于每个 hapi 路由定义只是一个对象,因此您似乎可以制作类似的包装器,如下所示(未经测试):

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}));
  });
}
Run Code Online (Sandbox Code Playgroud)

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

编辑#2为了更直接地回答原来的问题,这里有一个hapi-route-builder有一个setRootPath()方法,可以让您通过一次指定路径的基本部分来实现非常相似的效果。

  • 是的,这个问题需要一个类似于 Express 嵌套路由的解决方案......但是对于 **Hapi** 框架!如果问题适用于框架 A,那么为框架 B 提供答案就没有建设性——除非它还解释了如何一起使用 A 和 B。 (4认同)