天涯望帆

乱想者

batman.js 结合rails 入门

| Comments

batman.js 是一个很令人兴奋的前端mvc框架,不过文档是硬伤,外文资料少的可怜,中文资料更是一个悲剧!
这几天因为一个项目需要用到大量ajax,所以有一个想法用前端mvc来做一个整站的ajax,我看了backbone.js spine.js ember.js 等前端mvc,可是他们各自硬伤啊

  1. banbone.js 他的mvc让我感到陌生,从django到rails等web mvc我没看到这么样的mvc,十分复杂,而且让我写出来的代码不可控制
  2. ember.js 他是一个很好的框架,可是依赖让我却步,比如你得使用jquery低版本,这是我没法接受的,而且ember太大了,文档也不是很好
  3. spine.js 是一个很好的框架,例子和文档都很好,可是他却不能实时更新ui

所以我选择baman.js, 不过这东西会让初学者感到沮丧,因为没有一个好的教程,而且官网文档也少得可怜, 有一个和rails结合的例子,可是作者又是haml拥戴者(这~~)硬着头皮看完,并且看了文档,写一下日志,方便自己以后查看

这里使用rails作为后端

install

在Gemfilel里面添加

1
gem 'batman-rails'

安装目录结构

1
rails g batman:install

因为我们要使用mvc,所以我们最好关闭掉rails的自动生成coffeescipt 在application.rb添加

config.generators.javascripts = false

如果你想把自动生成scss的功能也给关掉可以继续添加上

config.generators.stylesheets = false
这样我们的基础工作就算ok了

来一个demo吧!

最简单也是最好用的一个demo就是写一个blog吧,这里我们用一个blog来开始我们的batman之旅,这个blog有这么几个要求:
1. 文章要有标题,内容和图片 2. 无刷新,整站使用ajax

ruby

我们需要使用使用基础的命令来生成一个项目

1
rails new blog

在我们Gemfile里面添加上

1
2
3
gem 'batman-rails'
gem 'carrierwave' #因为我们需要进行图片上传
gem 'bootstrap-sass', '~> 2.2.2.0' # 为了让我们的demo好看点,我们用这个吧

使用bundle进行gem的安装

之后我们先把自动生产javascipt的功能关掉 在application.rb添加
config.generators.javascripts = false

生成我们需要的api,这里因为我们需要快速完成,使用scaffold

1
rails g scaffold post title:string content:text image:string

然后我们运行migrate来生成数据库

1
rake db:migrate

现在生成root首页

1
rails g controller home index

删除public/index.html,并且把app/views/home/index.html.erb 清空

添加root路由 config/routes.rb

1
2
3
4
BatmatBlog::Application.routes.draw do
  root to: "home#index"
  resources :pots
end

安装batman

1
2
3
rails g batman:install
#在javascipt里面创建views目录
mkdir app/assets/javascipts/views

修改application.css 为application.css.scss 添加以下内容

1
2
3
4
5
6
/*
 *= require_self
 *= require_tree .
 */

@import "bootstrap";

在application.js添加以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//= require jquery
//= require jquery_ujs
//= require bootstrap

// Batman.js and its adapters
//= require batman/es5-shim
//= require batman/batman
//= require batman/batman.jquery
//= require batman/batman.rails

//= require batmat_blog

//= require_tree ./models
//= require_tree ./controllers
//= require_tree ./helpers


//= require_tree .
// Run the Batman app
$(document).ready(function(){
  BatmatBlog.run();
});

因为我们需要上传文件,所以我们需要使用carrierwave来做

1
rails generate uploader image

修改model
app/models/post.rb

1
2
3
4
class Post < ActiveRecord::Base
  attr_accessible :content, :image, :title
  mount_uploader :image, ImageUploader
end

好了接下来我们需要进入batman,batman也为了我们准备了很多很有用生成器

1
2
3
4
5
6
Batman:
  batman:controller
  batman:helper
  batman:install
  batman:model
  batman:scaffold

我们使用batman:scaffold来创建我们需要的东西,也方便我们熟悉batman的写法

1
rails g batman:scaffold post

batman

打开app/assets/javascipts/post.js

1
2
3
4
5
class BatmatBlog.Post extends Batman.Model
  @storageKey: 'posts'
  @persist Batman.RailsStorage

  @encode "title", "content", "image"

下面解释一下上面几个东西,因为我的英语翻译确实比较烂,所以还是直接引用英文了

1
2
@storageKey is a class level option which gives the storage adapters something to interpolate into their specific key generation schemes. In the case of LocalStorage or SessionStorage adapters, the storageKey defines what namespace to store this record under in the localStorage or sessionStorage host objects, and with the case of the RestStorage family of adapters, the storageKey assists in URL generation. See the documentation for the storage adapter of your choice for more information.
The default storageKey is null.
1
@persist is how a Model subclass is told to persist itself by means of a StorageAdapter. @persist accepts either a StorageAdapter class or instance and will return either the instantiated class or the instance passed to it for further modification.
1
2
@encode specifies a list of keys a model should expect from and send back to a storage adapter, and any transforms to apply to those attributes as they enter and exit the world of Batman in the optional encoderObject.
The encoderObject should have an encode and/or a decode key which point to functions. The functions accept the "raw" data (the Batman land value in the case of encode, and the backend land value in the case of decode), and should return the data suitable for the other side of the link. The functions should have the following signatures

接下来我们进入controller

我们可以看到batman为我们生成了基本的index show create update

不过我们因为需要有一个get action来生成new和edit 所以我们加上这两个方法,代码就如下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BatmatBlog.PostsController extends Batman.Controller
  index: (params) ->

  show: (params) ->

  new: (params) ->

  create: (params) ->

  edit: (params) ->

  update: (params) ->

  destroy: (params) ->

现在先放着,不写实现先,我们需要写路由了 这个时候,可以说说路由在那里实现了,我们看以我们项目名称命名的coffee文件 app/assets/javascipts/batmat_blog.js.coffee

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
window.BatmatBlog = class BatmatBlog extends Batman.App

  # @root 'controller#all'
  # @route '/controller/:id', 'controller#show', resource: 'model', action: 'show'

  @on 'run', ->
    console?.log "Running ...."

  @on 'ready', ->
    console?.log "BatmatBlog ready for use."

  @flash: Batman()
  @flash.accessor
    get: (key) -> @[key]
    set: (key, value) ->
      @[key] = value
      if value isnt ''
        setTimeout =>
          @set(key, '')
        , 2000
      value

  @flashSuccess: (message) -> @set 'flash.success', message
  @flashError: (message) ->  @set 'flash.error', message

我们需要添加路由,和指定view的存放位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
window.BatmatBlog = class BatmatBlog extends Batman.App

  # @root 'controller#all'
  # @route '/controller/:id', 'controller#show', resource: 'model', action: 'show'
  @root 'posts#index'
  @resources 'posts'

  Batman.ViewStore.prefix = 'assets/views' # 设置view目录

  @on 'run', ->
    console?.log "Running ...."

  @on 'ready', ->
    console?.log "BatmatBlog ready for use."

  @flash: Batman()
  @flash.accessor
    get: (key) -> @[key]
    set: (key, value) ->
      @[key] = value
      if value isnt ''
        setTimeout =>
          @set(key, '')
        , 2000
      value

  @flashSuccess: (message) -> @set 'flash.success', message
  @flashError: (message) ->  @set 'flash.error', message

好了我们来写view, 首先修改appliction.html.erb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
  <title>BatmatBlog</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<div data-yield="main"></div>

</body>
</html>

这时打开主页,并打开console,看到一个错误 Uncaught DevelopmentError: Please define routingKey on the prototype of PostsController in order for your controller to be minification safe. batman.js:750

这是因为rails js会压缩,如果你不压缩js的话,这个无需介意,不过我们的js需要压缩,所以我们必须在controller里面写入这一行代码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BatmatBlog.PostsController extends Batman.Controller

  routingKey: "posts"

  index: (params) ->
    BatmatBlog.Post.load (err, posts) =>
      @set 'posts', posts
  show: (params) ->

  new: (params) ->

  create: (params) ->

  edit: (params) ->

  update: (params) ->

  destroy: (params) ->

现在来写new的view和controller action

新建app/assets/javascipts/views/posts/new.html.erb

1
2
3
4
5
6
<form class="new-post" accept-charset="utf-8" data-event-submit="create" data-formfor-post="post">
  <input type="text" data-bind="post.title" name="post[title]">
  <input type="text" data-bind="post.content" name="post[content]">
  <input type="file" data-bind="post.image" name="post[image]">
  <input type="submit" value="save">
</form>

然后写new和create方法

1
2
3
4
5
6
7
8
9
10
new: (params) ->
    @set "post", new BatmatBlog.Post()

  create: (params) ->
    @get('post').save (err) =>
      if err
        # 做一些错误处理
        throw err
      else
        document.getElementsByName("post[image]")[0].value = "" # 因为图片上传之后,你再次新建的时候记录还是在的,所以我们要手动清楚掉

现在我们写index的view
app/assets/javascipts/posts/index.html.erb

1
2
3
4
5
6
7
8
<div data-foreach-post="posts">
  <div>
    <p data-bind="post.title"></p>
    <a data-route="routes.posts[post]">show</a>
  </div>
</div>

<a data-route="routes.posts.new">new</a>

好了想在写show的view
app/assets/javascipts/posts/show.html.erb

1
2
3
4
<p data-bind="post.title"></p>
<p data-bind="post.content"></p>
<img data-bind-src="post.image.url" alt="">
<a data-route="routes.posts">back</a>

补写controller app/assets/javascipt/controller/posts_controller.js.coffee

1
2
3
show: (params) ->
  @set "post", BatmatBlog.Post.find params.id, (err, post) =>
    throw err if err

现在打开浏览器看看,我们的基本样子就出来了,下一篇将介绍用户登陆,和美化,这篇就到这里

Comments