在 Rails 中使用 javascript


本文主要说几个方面:

1. ajax 介绍
2. unobtrusive js(这个地方不翻译,不知道怎么搞, 它就是一个业界的最佳实践,即能够把js 代码和 html 代码分离)
3. 内置的帮助方法
4. 在服务器端处理 ajax 请求
5. turbolinks 这个 gem



###ajax 介绍

要理解 ajax, 就要知道浏览器是怎么工作的. 当你在浏览器里键入` http://localhost:3000` 然后回车, 浏览器(也就是通常所说的客户端),它发起一个对于服务器的请求, 然后解析响应, 然后再把那些所有用于显示相关的资源给汇聚一起中展现一个完整的页面. 这个也就是他们通常所说的`请求响应圈`.

当然, js 也是可以发起对于服务器的请求的. 然后还能够解析响应. 而且还能够修改页面内容, 把这2个能力搞一起, js 就完全可以处理页面了. 这个也是 ajax 存在的道理.

Rails 默认自带了`CoffeeScript`, 我们接下来的代码中都会用到它来写 js,我们常用的 js 也可以搞的定这些. 看下面使用` CoffeeScript` 的代码:

```
$.ajax(url: "/test").done (html) ->
  $("#results").append html

```

这个代码的意思就是从`/test` 这里获取数据,然后把它添加到` id` 为` result` 的` div` 上.

Rails 提供了很多的内置的帮助方法用来页面上使用, 你基本上不用手写这些代码了, 接下来,我们就要来看看到底 rails 它是以一个什么样的方式来写页面还有 ajax 的.

###unobtrusive js.


其实很久之前,我们对于 js 的使用无非就是下面的这样子:

```
<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>

```

看上去简单,直接在需要的地方添加事件就可以了,但是久而久之,随着逻辑的增加,代码就像一坨屎一样. 怎么办呢, 那就是把 js 代码和 html 分开. 可以这样:

```
paintIt = (element, backgroundColor, textColor) ->
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor

//And then on our page:

<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
```

但是如果有很多类似的行要添加这个事件怎么办呢.

```
<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>
```

我们可以添加一个事件来处理这个情况:

```
paintIt = (element, backgroundColor, textColor) ->
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor
 
$ ->
  $("a[data-background-color]").click (e) ->
    e.preventDefault()
 
    backgroundColor = $(this).data("background-color")
    textColor = $(this).data("text-color")
    paintIt(this, backgroundColor, textColor)

//----

<a href="#" data-background-color="#990000">Paint it red</a>
<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>
```


上面的这个方式我们就称之为` unobtrusive js`, 也就是说我们不在把 js 代码混杂在 html 代码中的.

###内置帮助方法

rails 提供了很多的用 ruby 实现的视图帮助方法,他们可以用来帮助生成 html 代码.如果你要添加 ajax,rails 就可以帮你. 因为` unobtrusive js` 的作用, rails 的 Ajax helpers` 实际分为2部分,一半是 ruby 一半是 js.
`rails.js` 提供了 js 部分.而 ruby 提供了帮助类提供了合适的 html tag 到你的页面 DOM 中. CoffeeScript 就会监听所有的属性,附着合适的处理类.

#### form_for


`form_for` 干什么的呢.它就是最后产生一个 html 的 form 表单. `form_for` 有一个`:remote` 参数. 这样子使用它:

```
<%= form_for(@article, remote: true) do |f| %>
  ...
<% end %>
```
然后它就会搞定这样的 form:

```
<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post">
  ...
</form>

```

**注意** `data-remote="true"` 这个属性会让这个 form 以 ajax 的方式来提交了,而不是平常的正常触发事件.
你可以不想呆坐在那里然后让人家给你一个 form, 你想自己还加点什么东西,所以你可以通过` ajax:success` 这个事件来实现, 当然如果失败了就可以触发这个事件:` ajax:error`.

```
$(document).ready ->
  $("#new_article").on("ajax:success", (e, data, status, xhr) ->
    $("#new_article").append xhr.responseText
  ).on "ajax:error", (e, xhr, status, error) ->
    $("#new_article").append "<p>ERROR</p>"
```

显然,这样的实现太 low 了,你可以实现其他更多的,可以看这:(https://github.com/rails/jquery-ujs/wiki/ajax)

另外一个可能的用法就是直接从 server 端返回 js:


```
# articles_controller
def create
  respond_to do |format|
    if @article.save
      format.html { ... }
      format.js do
        render js: <<-endjs
          alert('Article saved successfully!');
          window.location = '#{article_path(@article)}';
        endjs
      end
    else
      format.html { ... }
      format.js do
        render js: "alert('There are empty fields in the form!');"
      end
    end
  end
end

```
> 这样,如果在终端禁用 js,`format.html { ... }` 就会被调用.

####form_tag

`form_tag` 和` form_for`很相似. 它有一个`: remote` 选项,你可以这样用:

```
<%= form_tag('/articles', remote: true) do %>
  ...
<% end %>

```
它对应生成的 html 代码就是:

```
<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post">
  ...
</form>

```

除了名字,最后都是一样的,至于底层的差别就看文档.

####link_to

`link_to` 它是辅助生成超链接的.他有一个`: remote` 选项,使用方法如下:

```
<%= link_to "an article", @article, remote: true %>
```
它对应的 html 就是这样的:

```
<a href="/articles/1" data-remote="true">an article</a>
```
这样你会发现其实他们都是一样的,相同的事件都可以绑定到这些标签上. 假设,我们假设我们有一个文章的 list, 通过一个 click 就能够删除, 我们这样写:

```
<%= link_to "Delete article", @article, remote: true, method: :delete %>
```
然后写`CoffeeScript`:


```
$ ->
  $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
    alert "The article was deleted."

```
3.4 button_to

`button_to`这个标签用来生成一个 button, 也有`: remote` 选项:

```
<%= button_to "An article", @article, remote: true %>
//this generates

<form action="/articles/1" class="button_to" data-remote="true" method="post">
  <div><input type="submit" value="An article"></div>
</form>
```

###服务器端需要考虑的

ajax 并不是简单的在客户端搞定一切的, 还需要考虑在服务器端处理这些请求. 通常对于 ajax 的请求,服务器端更加倾向于选项` json`而不是` html` 来响应结果.

#### 一个简单的例子

想象你有一系列的用户, 你需要创建一个表单能够创建新的用户.然后你的 `index` 看上去这样的:

```
class UsersController < ApplicationController
  def index
    @users = User.all
    @user = User.new
  end
  # ...
```

index 的视图是这样子的:

```
<b>Users</b>
 
<ul id="users">
<%= render @users %>
</ul>
 
<br>
 
<%= form_for(@user, remote: true) do |f| %>
  <%= f.label :name %><br>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>
```

这里使用到了一个局部模板:

> app/views/users/_user.html.erb

```
<li><%= user.name %></li>
```

这个视图上半部分是现实用户, 然后下半部分是创建新用户. 这个创建动作将会请求` create` ,因为 form 的 remote属性设置为` true`, 所欲它将会以 ajax 的方式来请求. 对应的` create` 动作实现代码如下:

```
# app/controllers/users_controller.rb
# ......
def create
  @user = User.new(params[:user])
 
  respond_to do |format|
    if @user.save
      format.html { redirect_to @user, notice: 'User was successfully created.' }
      format.js   {}
      format.json { render json: @user, status: :created, location: @user }
    else
      format.html { render action: "new" }
      format.json { render json: @user.errors, status: :unprocessable_entity }
    end
  end
end
```

**注意,** `respond_to`的块里的` format.js`! 它允许控制器来响应这个 ajax 请求,之后你需要创建一个对应的`app/views/users/create.js.erb `视图文件, 它会输出对应的 js, 然后发送到客户端.

```
$("<%= escape_javascript(render @user) %>").appendTo("#users");

```
###Turbolinks

Rails4开始内置[这个` Turbolinks` gem.](https://github.com/rails/turbolinks), 它通过 ajax 来尽可能提速页面的渲染.

####它是怎么工作的

Turbolinks 在页面上的所有的`<a>`标签上附着了`click` 事件. 如果你的浏览器支持[` PushState`](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState()_method) 的话, `Turbolinks` 就会自己搞一个 ajax 请求来处理整个页面, 发送一个 ajax 请求来请求数据,渲染这个页面.他会用到` PushState` 来修改` URL` 到一个正确的值.然后语义上刷新页面部分,然后也提供友好的 URLs.

你这里唯一需要的事情就是在` Gemfile` 中添加` Turbolinks`的支持. 然后在`app/assets/javascripts/application.js.`以`CoffeeScript` 的方式启用`//= require turbolinks`.

如果你想针对某一个 tag 禁用它就添加一个` data-no-turbolink` 属性.

####Page Change Events
当你写 CoffeeScript 的时候,你经常需要处理一些页面加载过程中的问题. 通过 jQuery, 你可以这样:

```
$(document).ready ->
  alert "page has loaded!"
```

但是,因为 turbolinks 它重写了页面的正常加载过程,这里需要调整, 你要这样写:

```
$(document).on "page:change", ->
  alert "page has loaded!"
```

##其他资源

- [jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki)
- [jquery-ujs list of external articles
](https://github.com/rails/jquery-ujs/wiki/External-articles)
- [Rails 3 Remote Links and Forms: A Definitive Guide](http://www.alfajango.com/blog/rails-3-remote-links-and-forms/)
- [Railscasts: Unobtrusive JavaScript
](http://railscasts.com/episodes/205-unobtrusive-javascript)
- [Railscasts: Turbolinks](http://railscasts.com/episodes/390-turbolinks)