【问题标题】:Backbone.js "fat router" design conundrumBackbone.js“胖路由器”设计难题
【发布时间】:2013-01-06 19:35:09
【问题描述】:

过去 2 周我一直在学习主干网和相关工具以及编写应用程序。我遇到了一个设计问题,想知道有哪些可用的解决方案以及 Backbone 专家是否认为这是一个问题。

问题:我最终不得不将我所有的视图依赖项放在我的 router.js 中,并且无法确定它们是否可以解决这个问题。以下是我的 router.js 中的代码:

// router.js
define([
  'jquery',
  'underscore',
  'backbone',
  'text',
  'views/landing',
  'views/dashboard',
],  
    function($, _, Backbone, t,LandingView,DashboardView){
        var AppRouter = Backbone.Router.extend({
        routes: {
          // Define some URL routes
          '': 'showLanding',
          'projects': 'showProjects',
          // Default
          '*actions': 'defaultAction'
        },
        navigate_to: function(model){
                alert("navigate_to");
            },

        showProjects: function() {},
        showLanding: function() {},
    });

    var initialize = function() {
        var app_router = new AppRouter;
        Backbone.View.prototype.event_aggregator = _.extend({}, Backbone.Events);
        // Extend the View class to include a navigation method goTo
        Backbone.View.prototype.goTo = function (loc) {
            app_router.navigate(loc, true);
        };
        app_router.on('route:showLanding', function(){
            var landing = new LandingView();
        });
        app_router.on('route:showProjects', function(){
            var dashboard=new DashboardView();
        });
        app_router.on('defaultAction', function(actions){
            alert("No routes");
            // We have no matching route, lets just log what the URL was
            console.log('No route:', actions);
        });
        Backbone.history.start({pushState: true});
    };
    return {
        initialize: initialize
    };
});

router.js 包括 LandingViewDashboardView 视图,它们依次获取各自的模板。初始路由加载具有登录模板的 LandingView。登录后,它调用 router.js 的 goTo 方法生成一个 DashboardView()。虽然这可行,但我觉得它有点难看。但是我不知道如何从 LandingView 生成一个新的 DashboardView,而不直接从 LandingView() 内部或从路由器引用 DashboardView()。

如果我继续通过 router.js 执行此操作,我最终会直接或间接地从路由器中提取我的所有视图 js 文件。听起来有点丑!

我查看了 Derick Baileys 的事件聚合器模式,但面临的问题是,如果 DashboardView 的实例还不存在,DashboardView 如何订阅由 LandingView 生成的事件?必须有人创建并初始化它才能订阅事件聚合器,对吗?如果那个人是路由器,我是否需要在路由器中预先实例化所有视图?这没有意义。

【问题讨论】:

  • 我想我在你更新的同时更新了这个并且不小心恢复了你的一些更改。
  • 我认为中介者模式可以解决你的问题:pivotallabs.com/users/mrushakoff/blog/articles/…
  • 谢谢 - 现在检查调解员。
  • @nimrod SO Mediator 与 Marionette 的事件聚合器相同,对吗?
  • @Sid 它基于该模式,但据我了解,Marionette 做得更多,并且有很多依赖项。如果您只想专注于调解,我建议您使用该博文。此外,如果您考虑构建一个大型应用程序,请查看提供所有结构的其他框架,这样您就无需过多考虑架构,例如 angular.js 或 ember...

标签: javascript design-patterns backbone.js marionette eventaggregator


【解决方案1】:

我已经解决了这个问题,只在第一次命中路由时导入视图:

define(['backbone'], function(Backbone) {
    var AppRouter = Backbone.Router.extend({
        routes: {
            '':      'home',
            'users': 'users'
        },

        home: function() {
            requirejs(["views/home/mainview"], function(HomeView) {
                //..initialize and render view
            });
        },

        users: function() {
            requirejs(["views/users/mainview"], function(UsersView) {
                //..initialize and render view
            });
        }
    });

    return AppRouter;
});

它并没有解决必须最终将所有视图导入路由器的问题,但是懒惰的requirejs 调用不会强制预先加载和评估所有脚本和模板。

事实上,某个地方的某个人必须导入模块。路由器是一个合理的位置,因为通常它是用户导航到某个页面(视图)时命中的第一段代码。如果您觉得一个路由器负责太多,您应该考虑将您的路由器分成多个路由器,每个路由器负责您应用程序的不同“部分”。作为一个很好的类比,请考虑典型 MVC 场景中的控制器。

多路由器示例

userrouter.js 处理所有与用户相关的视图('users/' 下的路由):

define(['backbone'], function(Backbone) {
    var UserRouter = Backbone.Router.extend({
        routes: {
            'users',        'allUsers',
            'users/:id',    'userById'
        },
        allUsers: function() {
            requirejs(["views/users/listview"], function(UserListView) {
                //..initialize and render view
            });
        },
        userById: function(id) {
            requirejs(["views/users/detailview"], function(UserDetailView) {
                //..initialize and render view
            });
        }
    });
    return UserRouter;
});

postrouter.js 处理所有与 Post 相关的视图(“posts/”下的路由):

define(['backbone'], function(Backbone) {
    var PostRouter = Backbone.Router.extend({
        routes: {
            'posts',        'allPosts',
            'posts/:id',    'postById'
        },
        allPosts: function() {
            requirejs(["views/posts/listview"], function(PostListView) {
                //..initialize and render view
            });
        },
        postById: function(id) {
            requirejs(["views/posts/detailview"], function(PostDetailView) {
                //..initialize and render view
            });
        }
    });
    return PostRouter;
});

approuter.js 是主路由,在应用启动时启动并初始化所有其他路由。

define(['backbone', 'routers/userrouter', 'routers/postrouter'], 
function(Backbone, UserRouter, PostRouter) {

    var AppRouter = Backbone.Router.extend({

        routes: {
            '',        'home',
        },
        initialize: function() {
            //create all other routers
            this._subRouters = {
                'users' : new UserRouter(),
                'posts' : new PostRouter()
            };
        },
        start: function() {
            Backbone.history.start();
        },
        home: function() {
            requirejs(["views/home/mainview"], function(HomeView) {
                //..initialize and render view
            });
        }
    });
    return UserRouter;
});

最后,您的应用程序的 ma​​in.js 会启动应用程序路由器:

new AppRouter().start();

通过这种方式,您可以保持每个单独的路由器精简,并避免在实际需要之前解决依赖关系树。

旁注:如果您使用嵌套的requirejs 调用并且您正在使用r.js 进行构建,请记住设置构建选项findNestedDependencies:true,以便延迟加载的模块包含在构建。

编辑:这是gist that explains lazy vs. immediate module loading in RequireJS

【讨论】:

  • +1 谢谢,延迟加载听起来是个好主意。我会等待更多的答案。
  • 嘿,好主意。但是,最后一个函数不应该返回 AppRouter 而不是 UserRouter 吗?
  • 你能告诉我什么是“_subroutes”吗?它在哪里定义?它是骨干的一部分吗?它是如何工作的?我在任何地方都没有看到任何关于这个的参考,我现在正试图找到它:)
【解决方案2】:

我们为此使用了一个工厂,它只是返回一个视图实例,它也可以缓存实例:

define(function() {
  // Classes are defined like this { key1: Class1, key2: Class2 }
  // not cachedObjects are defined like this { notCached : { key3: Class3 }}
  return function(Classes) {
    var objectCache = {};

    return {
      get: function(key, options) {
        var cachedObject = objectCache[key];
        if (cachedObject){
          return cachedObject;
        }

        var Class = Classes[key];
        if (Class) {
          cachedObject = new Class(options);
          objectCache[key] = cachedObject;
          return cachedObject;
        }

        Class = Classes.notCached[key];
        if (Class) {
          return new Class(options);
        }
      }
    };
  };
});

然后我们有一个创建工厂的模块:

define([
  'common/factory',
  'views/view1',
  'views/view2',
  'views/view3',
  ], function(
    viewCache,
    View1,
    View2,
    View3
  ) {

  var views = {
    route1: View1,
    route2: View2,
    notCached: {
      route3: View3,
    }
  };

  return viewCache(views);
});

在路由器中,您可以通过调用 viewCache.get(route) 轻松获取视图。好处是解耦了视图的创建/缓存,现在可以单独测试。

此外,当我们使用 Marionette 时,我们不在路由器中使用 viewCache,而是在 RegionManager 中使用,这更适合创建视图。我们的路由器只是根据应用的实际状态和路由触发事件。

【讨论】:

  • 我喜欢工厂的想法,但这不是“移动”了将所有视图从路由器预先加载到工厂的问题吗?如,下载所有与视图相关的 .js 文件。
  • 是的,这将加载视图的所有代码,但它将路由器与视图的创建分离。但是您可以重构工厂以使用 requirejs,因此您只存储 require 模块的路径而不是模块本身。
  • 更新以展示这如何与 requirejs 一起工作(未测试)。
  • @AndreasKöberle 您的 requirejs 模式会很棒,但不幸的是我认为它不起作用。一方面,require API 是异步的,因此您也必须使工厂异步。更大的问题是 r.js 编译器只能在 require 调用与 literal 字符串路径一起使用时解析嵌套依赖关系,例如 require(['module/name'], cb)
  • @AndreasKöberle,事实上您可以同步调用require 是对的,但这要归功于RequireJS 为您执行的一些诡计。根据您是使用 AMD 还是 CommonJS 样式的标头,它同步调用 require 可能只是因为运气好,或者可能不会像您想象的那样工作。你让我怀疑自己,所以我不得不去测试。这是它的要点:gist.github.com/4475666
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-01-01
  • 2013-04-14
  • 1970-01-01
  • 2013-08-16
  • 2013-01-12
  • 1970-01-01
  • 2011-10-22
相关资源
最近更新 更多