Backbone project organization

2019-02-19 22:35发布

问题:

I'm struggling a bit with coming up with a clean, solid way to organize my Backbone application. I'm using Requirejs, Handlebars, and the Requirejs Text plugin to dynamically load HTML views. To simplify things, let's just say the site has the following pages:

Home: displays a collection of products

About: static page

Account: contains account information. products purchased, allows for various updates. Lots of functionality. Has tabs to navigate to different sections.

So I'm going for an SPA that loads new pages into a div ('.backbone-view'). Should I have a general AppView with an el: $('.backbone-view') that is called when the route changes and then loads the appropriate template? Or should I have a view for every page (homeView, aboutView, accountView), all with their el set to backbone-view?

Beyond that...do I need a model for anything except Products? For the static about page, I just load in the html template and that's it. But for products, I need to call the products collection, which renders each product view, each of those being associated with a product model. That's fine...but where do I initialize these product constructs? When I route to the home page, do I do it there? I have this pseudo-code:

  routes: {
        '': 'home',
        'about': 'about',
        'my-account': 'myAccount',
        '*default': 'home'
    },

    'home': function() {
        // Grab template for home page

        // Load up products

        // Replace $('.backbone-view') with home page template populated with products
    },

    'about': function() {
        // Grab about template and replace $('.backbone-view') with its contents
    },

    'myAccount': function() {
        MIND EXPLOSION
    }

I think a big issue is that I'm not clear on the purpose of Views...can they be used simply for page transitions, or should they always have a model attached to them? If the former, I would at least need an AppView and then Views for each page, right? I'm lost as to where I would delegate each step...so any help is appreciated.

Thanks for the help!

回答1:

Here are a few tips after working on very large backbone apps. It's not exhaustive or final..

Divide into two directories

  1. The server directory e.g server/
  2. The publicly accessible directory e.g. www/

Also when you run your build task it would build the app into a distributable version into a build/ or dist/ directory. Probably using Gulp or Grunt.

Extend Backbone

Your entire app will consist of:

  • Views & sub views
  • Routers & sub routers
  • Models
  • Collections

You should extend the Backbone classes even if they are empty at first. The most useful two extensions are:

  • Sub views (a view can have a views object/function with more views, which get cleaned up when you remove the parent view). Models and collections called model or collection get automatically passed down to sub views.
  • Sub routers (it's nice to have the routing logic for each module inside the module folder)

Use pod architecture

As in organise your app around self-contained modules e.g.:

www/app/modules/home/router.js <-- sub router, calls methods in modules.js
www/app/modules/home/module.js <-- prepares endpoints - changing layout, initializing views & models etc
www/app/modules/home/views/... all the views (can have subfolders too)
www/app/modules/home/templates/
www/app/modules/home/models/
www/app/modules/home/collections

Start seeing your app in terms of views and sub views

A page doesn't consist of just one view. It would have perhaps a special "layout" view and inside that would be many views - one which splits the page in half, one which has pagination with more views inside for each page number, a view for a form with lots of sub views inside for each form element and message etc etc

You can start thinking of views as shadowing the DOM tree and divide logically - anything which you think is re-useable on your page make it a package (it's own views and models/collections if it needs them).

Models are for any data and any logic performed on data, if a view was showing anything from the server/api/database it would typically be passed to the view which would pass all or some of the model attributes to the template.

If that item displaying information was in a list, then a collection would manage each model for each item.

Do communication with models

If you find yourself wanting to communicate something from a view to another view, use a shared model. A view should be as decoupled as possible (it shouldn't need to be aware of it's parent).

Have an app state

Create a model called AppState to broadly communicate across the app using triggers and listens.

Have a packages folder (optional)

Whenever you come across stuff in your app which you think could be re-useable, even in other future apps, create a package. These would typically be hosted on their own git repos and you could pull them into projects using package.json or the command line.

Have a folder where you extend inter-app stuff

Have an extensions folder for modules which are consumed by multiple apps - e.g. your backbone extensions could go here. Or, if you created a package for forms but want to do something specifically for this app, then extend it here.

e.g. www/app/extensions/view.js
www/app/extensions/model.js
www/app/extensions/collection.js
www/app/extensions/buttons/link.js // Extending the link view from a "buttons" package.

assets

The reason why I would have an app/ folder in the public www/ folder is so that I could also have an assets folder in there for fonts and images etc:

www/assets/css
www/assets/images

Note: Maybe you want to try and keep assets in the module folders (inline with pod architecture). I haven't done this before but it's worth considering.

index.html

Typically if you are using CommonJS or AMD your index.html would just be boilerplate with no actual DOM elements and you would have one call in there to an entry js file. Since CommonJS has to compile this would just be something like <script src="/app.js"></script> but for AMD it would be more like:

<!--IF NOT BUILD-->
<script data-main="/app/config" src="/packages/require.js"></script>
<!--ELSE
<script src="/app.js"></script>
-->

So when running in dev (non-build) RequireJS will load up app/config.js but in build the whole app will be in app.js. There are various Grunt/Gulp build tasks which will do something like the above for you (obviously that conditional syntax is just made up).

Layouts

I would create a extensions/layout.js which extends extensions/view.js and it would be a simple extension that could have sub views like normal (e.g. header and footer), but also a special subview which I could attach any view to (for the body subview) e.g. a method like setContentView(view).

I would maybe create a module called layouts and in there have a directory modules/layout/default which has a view that has a header and footer subviews. Then reaching the index route would flow something like this:

app/router.js => app/modules/home/router.js => app/modules/home/module.js@index => setContentView(view from app/modules/home/views/index.js)"

Routing

I would have a app router located at e.g. www/app/router.js which could have some special routes but would largely just subroute with an object that pointed at sub routers:

subRouters: {
    'store-locator': StoreLocatorRouter,
    myaccount: MyAccountRouter,
    sitemap: SitemapRouter
}

I would make this possible by extending the normal Backbone router with something like (note in your extension you need to call initSubRouters in initialize) -

define([
    'underscore',
    'backbone'
],
function(_, Backbone) {

    'use strict';

    /**
     * Extended Backbone Boilerplate Router
     * @class extensions/router
     * @extends backbone/view
     */
    var Router = Backbone.Router.extend(
        /** @lends extensions/router.prototype */
        {

        /**
         * Holds reference to sub-routers
         * @type {Object}
         */
        subRouters: {},

        /**
         * Adds sub-routing
         * based on https://gist.github.com/1235317
         * @param {String} prefix The string to be prefixed to the route values
         */
        constructor: function(options) {
            if (!options) {
                options = {};
            }

            var routes = {}, prefix = options.prefix;

            if (prefix) {
                // Ensure prefixes have exactly one trailing slash
                prefix.replace(/\/*$/, '/');
            } else {
                // Prefix is optional, set to empty string if not passed
                prefix = '';
            }

            if (prefix) {
                // Every route needs to be prefixed
                _.each(this.routes, function(callback, path) {
                    if (path) {
                        routes[prefix + '/' + path] = callback;
                    } else {
                        // If the path is "" just set to prefix, this is to comply
                        // with how Backbone expects base paths to look gallery vs gallery/
                        routes[prefix + '(/)'] = callback;
                    }
                });

                // Must override with prefixed routes
                this.routes = routes;
            }

            // .navigate needs subrouter prefix
            this.prefix = prefix;

            // Required to have Backbone set up routes
            Backbone.Router.prototype.constructor.apply(this, arguments);
        },

        /**
         * Sets up 'beforeRoute' event.
         */
        initialize: function() {
            // This is a round about way of adding a beforeRoute event and must
            // happen before any other routes are added.
            Backbone.history.route({
                test: this.beforeRoute
            }, function() {});
        },

        /**
         * Called before routes.
         * @return {Boolean} false This ensures the 'route' is disabled.
         */
        beforeRoute: function() {
            Backbone.history.trigger('beforeRoute');
            return false;
        },

        /**
         * Adds prefix to navigation routes
         * @param  {String} route   Non-prefixed route
         * @param  {Object} options Passed through to Backbone.router.navigate
         */
        navigate: function(route, options) {
            if (route.substr(0, 1) !== '/' && route.indexOf(this.prefix.substr(0,
                this.prefix.length - 1)) !== 0) {
                route = this.prefix + route;
            }
            Backbone.Router.prototype.navigate.call(this, route, options);
        },

        /**
         * Initializes sub-routers defined in `this.subRouters`
         */
        initSubRouters: function() {
            _.each(this.subRouters, function(Router, name) {
                this[name] = new Router({
                    prefix: name
                });
            }, this);
        }

    });

    return Router;
});