batman
文件大小: unknow
源码售价: 5 个金币 积分规则     积分充值
资源说明:Fighting Crime and Kicking Apps
# batman.js

[batman.js](http://batmanjs.org/) is a framework for building rich single-page browser applications. It is written in [CoffeeScript](http://jashkenas.github.com/coffee-script/) and its API is developed with CoffeeScript in mind, but of course you can use plain old JavaScript too.

#### It's got:

* a stateful MVC architecture
* a powerful binding system
* routable controller actions
* pure HTML views
* toolchain support built on [node.js](http://nodejs.org) and [cake](http://jashkenas.github.com/coffee-script/#cake)

The APIs are heavily inspired by [Rails](http://rubyonrails.org/) and designed to make Rails devs feel right at home.

We're targeting Chrome, Safari 4+, Firefox 3+, and IE 7+ for compatibility, although some of those require you to include [es5shim](https://github.com/kriskowal/es5-shim).

#### Here's some code:

```coffeescript
class Shopify extends Batman.App
  @root 'products#index'

  @resources 'products'

class Shopify.Product extends Batman.Model
  @persist Batman.RestStorage

class Shopify.ProductsController extends Batman.Controller
  index: ->

  show: (params) ->
    Shopify.Product.find params.id, (err, product) =>
      @set('product', product)
```

#### views/products/index.html

```html

```

## Installation

If you haven't already, you'll need to install [node.js](http://nodejs.org) (0.4.x for now, 0.5.x support forthcoming) and [npm](http://npmjs.org/). Then:

```bash
npm install -g batman
```

Generate a new batman.js app somewhere, called my_app:

```bash
cd ~/code
batman new my_app
```

Fire it up:

```bash
cd my_app
batman server # (or just "batman s")
```

Now visit [http://localhost:1047](http://localhost:1047) and start playing around!

## The Basics

Most of the classes you work with in your app code will descend from `Batman.Object`, which gives you some nice things that are used extensively by the rest of the system.

### Events

Events in batman.js are pretty simple. Handling an event is like subscribing to a named channel on a particular object, and firing an event is like publishing an argument list (which might be empty) to all subscribers of that channel.

```coffeescript
class BatBelt.Gadget extends Batman.Object
  constructor: -> @usesRemaining = 5
  use: (howManyTimes) ->
    if howManyTimes <= @usesRemaining
      @usesRemaining -= howManyTimes
      @fire('use', howManyTimes)
```

You can use the `on` method to register handler functions which then get called whenever `fire` is called for the specified event name. The handlers get whichever extra arguments were passed to `fire` after the event name itself:

```coffeescript
gadget.on 'use', (howManyTimes) ->
  console.log "gadget was used #{howManyTimes} times!"
gadget.use(2)
# console output: "gadget was used 2 times!"
```

### Properties

The `observe` function is used to observe changes to properties. This forms the basis of the binding system. Here's a simple example:

```coffeescript
gadget.observe 'name', (newVal, oldVal) ->
  console.log "name changed from #{oldVal} to #{newVal}!"
gadget.set 'name', 'Batarang'
# console output: "name changed from undefined to Batarang!"
```

When you `observe` a property, you are really just handling its "change" event, which happens to get fired with the new and old values.

Of course you can also `get` properties to return their values, and if you want to remove them completely then you can `unset` them:

```coffeescript
gadget.get 'name'
# returns: 'Batarang'
gadget.unset 'name'
# console output: "name changed from Batarang to undefined!"
```

By default, these properties are stored like plain old JavaScript properties: that is, `gadget.name` would return "Batarang" just like you'd expect. But if you set the gadget's name with `gadget.name = 'Shark Spray'`, then the observer function you set on `gadget` will not fire. So when you're working with batman.js properties, use `get`/`set`/`unset` to read/write/delete them.


### Custom Accessors

What's the point of using `gadget.get 'name'` instead of just `gadget.name`? Well, batman.js properties don't always correspond with vanilla JS properties. Let's write a `Box` class with a custom getter for its volume:

```coffeescript
class Box extends Batman.Object
  constructor: (@length, @width, @height) ->
  @accessor 'volume',
    get: (key) -> @get('length') * @get('width') * @get('height')

box = new Box(16,16,12)
box.get 'volume'
# returns 3072
```

The really cool thing about this is that, because we used `@get` to access the component properties of `volume`, batman.js can keep track of those dependencies and let us observe the `volume` directly:

```coffeescript
box.observe 'volume', (newVal, oldVal) ->
  console.log "volume changed from #{oldVal} to #{newVal}!"
box.set 'height', 6
# console output: "volume changed from 3072 to 1536!"
```

The box's `volume` is a read-only attribute here, because we only provided a getter in the accessor we defined. Here's a `Person` class with a (rather naive) read-write accessor for their name:

```coffeescript
class Person extends Batman.Object
  constructor: (name) -> @set 'name', name
  @accessor 'name',
    get: (key) -> [@get('firstName'), @get('lastName')].join(' ')
    set: (key, val) ->
      [first, last] = val.split(' ')
      @set 'firstName', first
      @set 'lastName', last
    unset: (key) ->
      @unset 'firstName'
      @unset 'lastName'
```

### Keypaths

If you want to get at properties of properties, use keypaths:

```coffeescript
employee.get 'team.manager.name'
```

This does what you expect and is pretty much the same as `employee.get('team').get('manager').get('name')`. If you want to observe a deep keypath for changes, go ahead:

```coffeescript
employee.observe 'team.manager.name', (newVal, oldVal) ->
  console.log "you now answer to #{newVal || 'nobody'}!"
manager = employee.get 'team.manager'
manager.set 'name', 'Bill'
# console output: "you now answer to Bill!"
```

If any component of the keypath is set to something that would change the overall value, then observers will fire:

```coffeescript
employee.set 'team', larrysTeam
# console output: "you now answer to Larry!"
employee.team.unset 'manager'
# console output: "you now answer to nobody!"
employee.set 'team', jessicasTeam
# console output: "you now answer to Jessica!"
```

batman.js's dependency tracking system makes sure that no matter how weird your object graph gets, your observers will fire exactly when they should.

## Architecture

The MVC architecture of batman.js fits together like this:

* Controllers are persistent objects which render the views and give them mediated access to the model layer.
* Views are written in pure HTML, and use `data-*` attributes to create bindings with model data and event handlers exposed by the controllers.
* Models have validations, lifecycle events, a built-in identity map, and can use arbitrary storage backends (`Batman.LocalStorage`, `Batman.RestStorage`, and `Batman.RailsStorage` are included).

A batman.js application is served up in one page load, followed by asynchronous requests for various resources as the user interacts with the app. Navigation within the app uses [pushState](https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#Adding_and_modifying_history_entries) where supported, otherwise falling back to [hash-bang fragment identifers](http://www.w3.org/QA/2011/05/hash_uris.html).


### The App Class

Sitting in front of everything else is a subclass of `Batman.App` which represents your application as a whole and acts as a namespace for your other app classes. The app class never gets instantiated; your main interactions with it are using macros in its class definition, and calling `run()` on it when it's time to fire up your app.

Here's a simple app class:

```coffeescript
class BatBelt extends Batman.App
  @controller 'app', 'gadgets'
  @model 'gadget'

  @root 'app#index'
  @route 'faq/:questionID', 'app#faq'
  @resources 'gadgets'
```

The calls to `@controller` and `@model` load external app classes with XHRs. For the controllers, this ends up fetching `/controllers/app_controller.coffee` and `/controllers/gadgets_controller.coffee`. The gadget model gets loaded from `/models/gadget.coffee`.

#### Routes

Routes are defined in a few different ways.

`@route` takes two strings, one representing a path pattern and the other representing a controller action. In the above example, `'faq/:questionID'` matches any path starting with "/faq/" and having one other segment. That segment is then passed as a named param to the controller action function specified by the second string argument.

For the FAQ route, `'app#faq'` specifies the `faq` function on `BatBelt.AppController`, which should take a `params` argument and do something sensible with `params.questionID`.

`@root 'app#index'` is just a shorthand for `@route '/', 'app#index'`.

The `@resources` macro takes a resource name which should ideally be the underscored-pluralized name of one of your models. It sets up three routes, as if you'd used the `@route` macro like so:

```coffeescript
@route 'gadgets', 'gadgets#index'
@route 'gadgets/new', 'gadgets#new'
@route 'gadgets/:id', 'gadgets#show'
@route 'gadgets/:id/edit', 'gadgets#edit'
```

`@resources` can also take an array of labels to set up routing for multiple models at once:

```coffeescript
@resources ['stores', 'products', 'customers']
```

In addition to setting up these routes, the call to `@resources` keeps track of the fact that the `Gadget` model can be accessed in these ways. This lets you load these routes in your controllers or views by using model instances and classes on their own:

```coffeescript
class BatBelt.GadgetsController extends Batman.Controller
  someEventHandler: (node, event) ->
    BatBelt.Gadget.find 1, (err, gadget) =>
      @redirect gadget unless err? # redirects to "/gadgets/1"
  someOtherHandler: (node, event) ->
    @redirect BatBelt.Gadget # redirects to "/gadgets"
```

### Controllers

batman.js controllers are singleton classes with one or more instance methods that can serve as routable actions. Because they're singletons, instance variables persist as long as the app is running.

```coffeescript
class BatBelt.AppController extends Batman.Controller
  index: ->
  faq: (params) ->
    @set('question', @get('questions').get(params.questionID)
```

Now when you navigate to `/#!/faq/what-is-art`, the dispatcher runs this `faq` action with `{questionID: "what-is-art"}`. It also makes an implicit call to `@render`, which by default will look for a view at `/views/app/faq.html`. The view is rendered within the main content container of the page, which is designated by setting `data-yield="main"` on some tag in the layout's HTML. You can prevent this implicit rendering by calling `@render false` in your action.

Controllers are also a fine place to put event handlers used by your views. Here's one that uses [jQuery](http://jquery.com/) to toggle a CSS class on a button:

```coffeescript
class MyApp.BigRedButtonController extends Batman.Controller
  index: ->

  buttonWasClicked: (node, event) ->
    $(node).toggleClass('activated')
```

If you want to redirect to some route, you can use `@redirect`:

```coffeescript
buttonWasClicked: (node, event) ->
  $(node).toggleClass('activated')
  @redirect '/apocalypse/'
```

### Views

You write views in plain HTML. These aren't templates in the usual sense: the HTML is rendered in the page as-is, and you use `data-*` attributes to specify how different parts of the view bind to your app's data. Here's a very small view which displays a user's name and avatar:

```html

``` The `data-bind` attribute on the `

` tag sets up a binding between the user's `name` property and the content of the tag. The `data-bind-src` attribute on the `` tag binds the user's `avatarURL` property to the `src` attribute of the tag. You can do the same thing for arbitrary attribute names, so for example `data-bind-href` would bind to the `href` attribute. batman.js uses a bunch of these data attributes for different things: #### Binding properties * `data-bind="foo.bar"`: for most tags, this defines a one-way binding with the contents of the node: when the given property `foo.bar` changes, the contents of the node are set to that value. When `data-bind` is set on a form input tag, a _two-way_ binding is defined with the _value_ of the node, such that any changes from the user will update the property in realtime. * `data-bind-foo="bar.baz"`: defines a one-way binding from the given property `bar.baz` to any attribute `foo` on the node. * `data-foreach-bar="foo.bars"`: used to render a collection of zero or more items. If the collection descends from `Batman.Set`, then the DOM will be updated when items are added or removed. #### Handling DOM events * `data-event-click="foo.bar"`: when this node is clicked, the function specified by the keypath `foo.bar` is called with the node object as the first argument, and the click event as the second argument. * `data-event-change="foo.bar"`: like `data-event-click`, but fires on change events. * `data-event-submit="foo.bar"`: like `data-event-click`, but fires either when a form is submitted (in the case of `

` nodes) or when a user hits the enter key when an `` or `