Hapi 嵌套路由

Posted

技术标签:

【中文标题】Hapi 嵌套路由【英文标题】:Hapi nested routing 【发布时间】:2015-05-22 11:38:17 【问题描述】:

假设我想拥有大致如下所示的 REST 端点:

/projects/
/projects/project_id 

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

每个 if 的 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
  )

但我正在寻找一种将项目路由嵌套到项目路由中并进一步传递项目的能力。

有什么建议吗?

【问题讨论】:

我不太明白你的问题是什么。您想要的嵌套路由,并且您已经提供了上面的工作代码。那么你被困在哪里了? 我想知道是否还有其他更方便的方法,因为在这种方法中,第三级路由将变得一团糟。 server.route( method: 'GET', path: '/projects/project_id/items/item_id/results/result_id', handler: getAllItemsForProject )所以我正在寻找解决方案,类似于这个:link 我认为嵌套的问题在于,Hapi 并没有办法让每个路由定义有多个处理程序。您仍然需要显式声明每条路由,并且截至目前,Hapi 并没有真正声明子路由的方法。 @PoMaHTuK 您能否发布一个示例,说明您将如何在另一个框架中解决此问题,或者一些代码说明如果 API 确实存在于 hapi 中,您将如何看待它? @PoMaHTuK 我知道这不是您要寻找的答案,但我强烈建议您扁平化 API。确实每个项目都有其项目,但随着 API 变得更加复杂,这将非常混乱,假设每个项目都有观察者。现在您的 API 端点将是 '/projects/project_id/items/item_id/watchers' 但是每个观察者都有他的项目...什么时候停止?您将使用哪个端点来获取特定用户正在观看的所有项目?您将如何在不同端点之间共享项目获取逻辑?使用 GET 参数过滤数据的 Flat API 会更有用。 【参考方案1】:

虽然 hapi 本身没有“子路由”(我知道)的概念,但基本的实现很容易。

首先,hapi 在路径中提供通配符 变量,使用这些变量,您基本上可以为给定路径创建一个包罗万象的路径。 例如:

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

这些通配符路径有一些规则,最重要的是它只能在最后一段中,如果您将其视为“包罗万象”,这是有道理的。

在上面的示例中,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');
;

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

如果项目处理模块真的很相似,你应该 创建一个用于防止复制同一模块的库 一遍又一遍,因为这使得维护更容易(其中,我 侦察,是拥有子路由的最终目标) 如果你能确定在运行时使用哪些模块,你应该 也能够在服务器进程启动时弄清楚这一点。

创建一个库以防止重复代码是您应该尽早做(学会做)的事情,因为这使维护更容易,您未来的自己会感激不尽。

确定哪些模块可用于处理您在应用程序开始时拥有的各种项目,这将使每个请求都不必一遍又一遍地应用相同的逻辑。 Hapi 可能能够为您缓存它,在这种情况下它并不重要,但如果缓存不是一个选项,您可能最好使用较少的动态路径(我相信这是不提供此功能的主要原因默认情况下是 hapi)。

您可以在应用程序开始时遍历项目文件夹查找所有&lt;project&gt;/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);
            
        );
    );
);

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

【讨论】:

这很快就会变得笨拙。强烈建议不要使用这种方法,因为你失去了 hapi 的有效载荷/参数/等的所有功能。【参考方案2】:

您正在寻找类似于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 路由定义中的每一个都只是一个对象,因此您似乎可以制作一个类似的包装器,如下所示(未经测试):

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 = `$pathPrefixroute.path`;
    server.route(Object.assign(routes, path));
  );

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

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

【讨论】:

是的,这个问题需要一个类似于 Express 嵌套路由的解决方案……但适用于 Hapi 框架!如果问题适用于框架 A,则建议框架 B 的答案是没有建设性的 - 除非它还解释了如何一起使用 A 和 B。 公正的批评。我发布这个是为了回应@Matt Harrison 的评论“你将如何在另一个框架中做到这一点”。我想这清楚地解释了它是如何在 Express 中进行比较的。 Express 和 Hapi 当然可以并排使用(或代理 Hapi 路由到 Express 服务器),但我不知道在 Hapi 服务器中直接使用 Express 路由的方法。 我明白了。我不认为没有很多额外的工作就可以集成 Hapi 和 Express,似乎有一些为 Hapi 提供适配器的举措,但他们很久以前就停止了。感谢hapi-route-builder的主题参考,很有帮助。【参考方案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 功能了。

【讨论】:

不知道sandbox: 'plugin' 存在这样的选项,一直在寻找这样的东西。感谢分享。

以上是关于Hapi 嵌套路由的主要内容,如果未能解决你的问题,请参考以下文章

Vue-Router(三):嵌套路由

Vue 嵌套路由路由守卫

AngularJS“多重路由”嵌套模块——AngularJS“路由”嵌套学习资料教程

vue router 多路由及路由嵌套

VueRouter 嵌套路由

vue路由嵌套,如何子路由也有选中样式