The Kai Way

Pragmaticly hacking

Rails Ujs

| Comments

UJS是Rails 3引入的JavaScript框架与Rails的抽象层。我们知道Rails一些Helper是依赖于JavaScript框架的,比如Ajax Form,Ajax Link等,并且在Rails 3之前默认集成的JavaScript框架是Prototype,再这之后才换成了社区呼声很高的jQuery

如前面所说UJS是个抽象层,它需要在每个框架上实现对应的接口,比如官方实现了jquery-ujsprototype-ujs。本篇主要以jquery-ujs为例来讲解UJS。jquery-ujs代码很短,只有500行不到,想先浏览一下整个代码可访问Github的jquery-ujs repo

data-confirm

先从最简单confirm例子入手。比如,在某些链接上让用户在点击链接后再次确认一次,我们一般会这么写

1
link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }

这里和普通的链接不同的地方只是在于多加了一个data-confirm属性,然后UJS帮你实现了弹出确认框。这其中的实现方法很简单,写过jQuery的同学都会,就是监听所有链接的click事件,当这个被点击链接上有data-confirm属性时,取出data-confirm中的文本,弹出确认框,并根据用户的操作选择是否中断这个点击事件的处理。

data-method

然后我们来看看,另外一种比较常见的链接用法,让链接点击时使用非GET方法请求对应的URL,代码如下

1
link_to("Sign Out", sign_out_path, method: :delete)

这里传入的method参数,生成HTML时会被转换为data-method="delete"。与前面一个例子一样,UJS在这个链接的click事件上监听,当这个链接有data-method属性时,创建一个隐藏的form标签,并附带上名为_method参数,值为data-method属性值的input标签,最后将这个构造的表单提交。

通过这样的小技巧,Rails开发者就能通过<a>标签以任何想要的HTTP Method请求对应的链接。

Ajax Form

在Rails 3之后的form_tagform_for上传入remote: true就能实现表单的Ajax提交,同样这个事情,UJS也是通过监听所有的Form标签的submit事件,然后检测标签上的data-remote属性来实现的。

对于开发者,在传入了remote: true之后要怎样去插入对应的Ajax处理器呢?UJS在对应Ajax提交阶段上触发了Rails自定义的ajax:xxx事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
beforeSend: function(xhr, settings) {
  if (settings.dataType === undefined) {
    xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
  }
  return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
},
success: function(data, status, xhr) {
  element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
  element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
  element.trigger('ajax:error', [xhr, status, error]);
}

所以基于UJS我们可以这样直接在form元素上绑定上对应的Ajax提交处理代码

1
2
3
4
5
$("#myform").on("ajax:success", function () {
  alert("Post successfully:)")
}).on("ajax:error", function () {
  alert("Post fail:(")
})

UJS在Ajax表单提交了之后,还会将该表单中的buttoninput[type='submit']都加上disable属性,防止用户多次点击引发多次提交。

此外,UJS实现的Ajax Form上还有两个特殊的事件

  • ajax:aborted:required 当表单提交的时候,有未填的input标签时会触发这个事件,你可以选择去处理
  • ajax:aborted:file 我们知道通过正常的方式是无法通过AJAX来提交文件的,当表单里包含了文件字段的时候,这个事件会被触发,在这里可以去实现自己的文件提交逻辑。比如remotipart通过这个事件实现了Ajax Form的文件提交。

选择器

UJS的所有功能都通过预设的选择器,绑定事件处理逻辑到对应元素上,以下是选择器的定义

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
29
30
31
$.rails = {
    // Link elements bound by jquery-ujs
    linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',

    // Button elements boud jquery-ujs
    buttonClickSelector: 'button[data-remote]',

    // Select elements bound by jquery-ujs
    inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',

    // Form elements bound by jquery-ujs
    formSubmitSelector: 'form',

    // Form input elements bound by jquery-ujs
    formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])',

    // Form input elements disabled during form submission
    disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]',

    // Form input elements re-enabled after form submission
    enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled',

    // Form required input elements
    requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',

    // Form file input elements
    fileInputSelector: 'input[type=file]',

    // Link onClick disable selector with possible reenable after remote submission
    linkDisableSelector: 'a[data-disable-with]'
}

这提供可配置对应选择器的机会,比方说使用某个jQuery插件,它通过data-remote去标记别的事情。那么在不修改这个插件的情况下想让UJS继续工作,我们可以重新配置UJS的选择器

1
$.rails.formSubmitSelector = 'form([data-ajax-form])';

CSRF Token

UJS还会在每次表单提交上自动附带上CSRF Token。Rails 3之后强制所有的非幂等HTTP请求需要带上CSRF token作安全校验,用来防止XSS攻击。这就要求开发者在每次写Ajax请求的时候,都需要手动把这部分的token带上,UJS也通过jQuery的ajaxPrefilter接口,让每次的Ajax请求都自动附带上CSRF token。

另外,每次UJS初始化时,会为页面上所有表单都加上带有CSRF Token的隐藏input标签,让表单在提交时都能自动带上CSRF Token。

从UJS学到了什么

  • jquery-ujs的所有事件绑定都是绑定在document,等到事件触发后再分发到对应的事件处理逻辑里,不需要在初始化时查找对应的元素并绑定事件
  • 充分利用HTML5的data属性来解耦事件处理逻辑,将各种参数序列化到data属性
  • 利用jQuery的自定义事件更好地定制自己的事件处理流程

参考

Comments