Ajax应用程式
Ajax 应用程式
It’s not a bug - it’s an undocumented feature. - Unknown
什么是 Ajax?
Ajax 是 Asynchronous JavaScript and XML的缩写,是一种不需要重新整理页面,透过 JavaScript 来与服务器交换资料、更新网页内容的技术。目的在于改善使用者的操作接口,提昇流畅度。它主要是透过浏览器提供的XMLHttpRequestObject来达成,不过因为要支援跨浏览器,大多数人们会选择使用 JavaScript Library 来处理 Ajax,例如最流行的 JQuery。
如果你对 jQuery 和 Ajax 完全陌生的话,推荐 CodeSchool: Try jQuery、Code School: jQuery Ajax 或 Udacity: Intro to AJAX
虽然 Ajax 的缩写中包括 XML ,但是实务上并不一定要用 XML 格式,事实上也已经很少人使用 XML 当作传输的格式了。总归来说,依照 Ajax 使用的格式分类,有三种方式:
- 向服务器请求 HTML 片段,然后客户端浏览器上的 JavaScript 再替换掉页面上的元素
- 向服务器请求 JavaScript 程式脚本,然后客户端浏览器执行它
- 向服务器请求 JSON 或 XML 资料格式,然后客户端浏览器的 JavaScript 解析后再动作。
第一种方式非常简单,但是限制是一次只能更新一小块内容。
Rails 默认使用第二种方式,一方面程式撰写较容易,另一方面也是贯彻 Rails 将 template 统一在 server-side rendering 的设计哲学。
第三种方式则将 JavaScript 程式都放在客户端浏览器上,相较于第二种则多了解析 JSON 或 XML 的部份。以 Web API 的设计角度来看,与表现层无关的 JSON 格式是比较干净的,可以获得比较好的重复使用性。如果团队中有专门的前端工程师,他们会比较喜欢这种方式。
Unobtrusive JavaScript
Rails 使用一种叫做 Unobtrusive JavaScript(UJS) 的方式来挂载内建的 JavaScript 功能,也就是你在 app/assets/javascripts/application.js 里面加载的 //= require jquery_ujs,这些功能包括
- 让超连结可以用
:method参数支援非 GET 方法 - 用超连结、按钮和表单可以用
:remote => true支援 Ajax - 超连结、按钮和表单可以用
"data-confirm"参数可以跳确认对话视窗 - 送出按钮可以用
data-disable-with参数在送出表单时暂时关闭按钮避免重复送出
什么是 Unobtrusive 呢?用个范例来说吧,以下代码会将超连结改成用表单 DELETE 送出,并且用一个提示视窗来作确认:
link_to 'Remove', event_path(1), :method => :delete, :data => { :confirm => "Sure?" }
在 Rails 3 以前的版本,会输出:
<a onclick="if (confirm('Sure?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;" href="/events/1">Remove</a>
在 Rails 3 之后,会输出:
<a rel="nofollow" data-confirm="Sure?" data-method="delete" class="delete" href="/events/1">Remove</a>
Unobtrusive 也就是将 JavaScript 程式与 HTML 完全分开,除了可以让 HTML 码干净之外,也可以支援更换不同的 JavaScript Library ,例如把 Rails 内建的 jQuery 换成 Protytype.js 或 angular.js 等等。
在 Layout 中有输出一段
<%= csrf_meta_tag %>的作用就是搭配给 UJS 使用的,让 JavaScript 可以拿到 CSRF 安全验证码,我们会在 安全 一章讨论到什么是 CSRF 。
第一种方式:替换 HTML 片段
编辑 app/views/events/index.html.erb 最下方加入:
<%= link_to 'Hello!', welcome_say_hello_path, :id => "ajax-load" %>
<div id="content">
</div>
<script>
$('#ajax-load').click( function(e){
e.preventDefault();
var url = $(this).attr("href");
$.ajax(url, {
success: function(response) {
$("#content").html(response);
}
});
</script>
如此点下超连结后,就会把回传的 HTML 置入到<div id="content">里面。
第二种方式:使用 JavaScript 脚本
编辑 app/views/events/index.html.erb,在循环中间加入
<%= link_to 'ajax show', event_path(event), :remote => true %>
在循环外插入一个<div id="event_area"></div>
编辑 app/controllers/events_controller.rb,在 show action 中加入
respond_to do |format|
format.html
format.js
end
新增 app/views/events/_event.html.erb,内容与 show.html.erb 相同
新增 app/views/events/show.js.erb,内容如下
$('#event_area').html("<%= escape_javascript(render :partial => 'event') %>")
.css({ backgroundColor: '#ffff99' });
浏览 <http://localhost:3000/events>
escape_javascript()可以缩写为j()。
上述 :remote => true 背后的原理,如同以下的 jQuery 程式码:
$("#ajaxscript").click(function(e){
e.preventDefault();
var url = $(this).attr("href");
$.ajax(url, {
dataType: "script"
})
})
你会发现到这段程式码其实非常一般化,这也是为什么 Rails 可以将它变成 :remote => true 的原因。
Ajax 按钮
除了超连结 link_to之外,按钮 button_to 加上:remote => true参数也会变成 Ajax。
button_to "Remove",event_path(@event)
除了已经看过的:data => { :confirm => "Are you Sure?" }之外,disable_with可以避免使用者连续按下送出:
button_to "Remove", event_path(@event), :data => { :disable_with => 'Removing' }
Ajax 表单
除了超连结 link_to 加上 :remote 可以变成 Ajax 之外,表单 form_for 也可以加上:remote变成 Ajax。
form_for @event, :remote => true
第三种方式:使用 JSON 资料格式
JavaScript Object Notation(JSON) 是一种源自 JavaScript 的资料格式,是目前 Web 应用程式之间的准标准资料交换格式,在 Rails 之中,每个物件都有to_json方法可以很方便的转换资料格式。
<%= link_to 'ajax show', event_path(event), :remote => true, :data => { :type => :json }, :class => "ajax_update" %>
点击 ajax show 就会送出 Ajax request 了,但接下来要怎么撰写处理 JSON 的程式呢?以下是一个范例:
<script>
$(document).ready(function() {
$('.ajax_update').on("ajax:success", function(event, data) {
var event_area = $('#event_area');
event_area.html( data.name );
});
});
</script>
使用 JSON 通常还会搭配 client-side template 机制,将回传的资料搭配 template 后,再插入到 HTML 之中。
另一种变形的用法则是将 HTML 片段当做 JSON 的资料来传递。
另一种 JSON 的变形是 JSONP(JSON with Padding) ,将 JSON 资料包在一个 JavaScript function 里,这个做的用处是让这个 API 可以跨网域被呼叫。要回传 JSONP 格式,需要用render :js并多一个参数是:callback。例如以下的程式可以搭配 jQuery 的 jsonp 格式:
respond_to do |format|
format.js { render :json => @user.to_json, :callback => params[:callback] }
end
Example code
- 三种用法示范
- <https://github.com/ihower/rails-exercise-ac5/blob/master/app/views/welcome/ajax.html.erb> (ac5)
- <https://github.com/ihower/rails-exercise-ac6/blob/master/app/views/welcome/ajax.html.erb> (ac6)
- <https://github.com/ihower/rails-exercise-ac7/blob/master/app/views/welcome/ajax.html.erb> (ac7)
- <https://github.com/ihower/rails-exercise-ac8/blob/master/app/views/welcome/ajax.html.erb> (ac8)
- Ajax 按钮删除: 删除资料,并在 HTML 上移除该元素
- <https://github.com/ihower/rails-exercise-ac7/blob/master/app/views/comments/_comment.html.erb> (ac7)
- <https://github.com/ihower/rails-exercise-ac8/blob/master/app/views/topics/show.html.erb> (ac8)
- Ajax 按赞: 针对主题按赞或取消赞、订阅或反订阅等等
- <https://github.com/ihower/rails-exercise-ac7/blob/master/app/views/topics/show.html.erb> (ac7)
- <https://github.com/ihower/rails-exercise-ac8/blob/master/app/views/topics/show.html.erb> (ac8)
- Ajax 表单新增: 使用 Ajax 新增,在页面上新增该笔
- <https://github.com/ihower/rails-exercise-ac7/blob/master/app/views/topics/show.html.erb> (ac7)
- <https://github.com/ihower/rails-exercise-ac8/blob/master/app/views/topics/show.html.erb> (ac8)
- Ajax 编辑: 也就是 In-place form editing (点选文字原地编辑)
- <https://github.com/ihower/rails-exercise-ac5/blob/master/app/views/event_attendees/_item.html.erb>
- <https://github.com/ihower/rails-exercise-ac5/blob/master/app/views/events/show.html.erb>
- Ajax 开关 (Toggle Attributes): 设计一个 checkbox 接口的开关
- <https://github.com/ihower/rails-exercise-ac5/blob/master/app/views/events/show.html.erb>
- Ajax 无限捲轴 (Endless Page)
- <https://github.com/ihower/rails-exercise-ac5/blob/master/app/views/events/show.html.erb>
- Ajax 档案上传: HTML 表单没办法用 Ajax 上传档案,需要额外装 gem 做 workaround:
- <https://github.com/JangoSteve/remotipart>
Turbolinks
事实上, Rails 默认让每个换页都用上了 Ajax 技巧,这一招叫做 Turbolinks ,在默认的 Gemfile 中可以看到gem "turbolinks",以及 Layout 中的data-turbolinks-track。
它的作用是让每一个超连结都只用 Ajax 的方式将整个body内容替换掉,这样换页时就不需要重新加载head部份的标籤,包括 JavaScript 和 CSS 等等,只重新入加载 boby 的部分,目的是可以改善换页时的速度。
也因为它没有整页重新加载,所以如果有放在 application.js 里面的 jQuery ready 事件处理,会变成只有第一次加载页面才执行到,换页时就失效了,所以必须改成 Turbolinks 的 turbolinks:load 事件,也就是:
$(document).ready(function(){
//...
}
都要改写成
$(document).on("turbolinks:load", function(){
//...
})
Rails 4 用的旧版turbolinks-classic 是用
page:change事件
另外,它的快取也会影响页面内的 JavaScript,你放在 body 内的 javascript,在浏览回来时会执行两遍,并且官方认为这是 feature 不是 bug,有些 Javascript 重复执行两遍没关系,但是有些就有问题。如果想关掉这个快取功能:放个 <meta name="turbolinks-cache-control" content="no-cache"> 在 layout 的 head 之中可以关掉 Turbolinks 快取功能。
如果要针对特定的超连结关闭 Turbolinks,可以加上 data-turbolinks="false" 的属性来:
<div id="some-div" data-turbolinks="false">
<a href="/">Home (without Turbolinks)</a>
</div>
例如 Facebook Share 的 Javascript code 就被 turbolinks 影响,请参考 turbolinks + facebook share button
因为 Turbolinks 影响了 JavaScript 的 Event Bindings 行为,所以在搭配一些 JavaScript 比较吃重的应用程式,搭配使用 JavaScript 前端框架时,就会尽量移除不要使用,以免互相影响。
⚠ 总之,如果你碰到 js 灵异现象(贴上来的js code 换页回来后不执行,但是重新整理就没问题。或是跳页回来重复执行了两次等等,可以试试看拆掉 Turbolinks:把 Gemfile、applicatio.js 和 layout head 里面相关的 Turbolink 代码拿掉即可,就可以直接绕过这个大坑。
浏览器同源政策和 CORS
跨网域(cross-domain) 的 AJAX 会被 Same-origin policy 浏览器安全政策所限制:
- 浏览器同源政策及其规避方法
- 跨域资源共享CORS 详解
- 实作 Cross-Origin Resource Sharing (CORS) 解决 Ajax 发送跨网域存取 Request
- 同源政策 (Same-origin policy)
解决的方式可以用 CORS 或JSON-P,这要看 server-side 服务器端支援哪一种。CORS 是一个新的网络标准,而 JSON-P 一种向后相同的 hack 方式。
JSON-P
JSONP算是一种 workaround 解,在 jQuery 中可以用以下语法:
$.ajax({
url:,
dataType: "jsonp",
success: function(data) {
//...
clearTimeout(wikiRequestTimeout);
}
})
JSONP 缺点: 1. 只能送 HTTP GET 2. 没有办法 error handling,只能等 timeout,例如设定一个 timeout 显示错误讯息,然后在上述成功时再clearTimeout。
var wikiRequestTimeout = setTimeout(function(){
....append error text
}, 8000);
CORS
CORS 则是目前的新标准解决方案:
Rails 范例 Code: <https://github.com/ihower/rails-exercise-ac5/pull/2>
