论坛首页 编程语言技术论坛

在Rails3时代js该怎么写?

浏览 16759 次
该帖已经被评为良好帖
作者 正文
   发表时间:2009-09-24   最后修改:2009-09-25
[前注:本篇不是教程,只是一些rails的新技巧、特性的探讨。]

Why?
现在,我们在进行软件、WEB项目开发时都用喜欢用框架,即省时省力,又有规有矩。所谓规矩,最常见的约束就是MVC三层分离,其中V是VIEW(视图),而进行WEB开发时,最常见的VIEW就是HTML页面。HTML到了XHTML(http://en.wikipedia.org/wiki/XHTML)时代,也开始强调了要样式与内容结构分离,“HCJ”三层分离,就是HTML(页面内容)、CSS(页面装饰)、JAVASCRIPT(页面行为)尘归尘土归土,各自归纳到独立的文件中,通过HTML的标签或属性来进行关联,最显而易见的好处是一来方便代码结构化管理、解析,二来方便浏览器缓存。

我们很幸运的,搭上了Rails这俩快车,一路走在流行技术的最前沿,在Rails3时代,Rails秉承“兼容并包”的良好品德和思想,在提供方便默认配置之余,还放开了怀抱,使得更多更方便的框架、类库可以集成进来替换默认配置。在Rails3中,Prototype这个js框架将不会默认绑定(http://rails.uservoice.com/pages/10012-rails/suggestions/99489-unbind-the-framework-from-test-unit-and-prototype)。

[广告:更多Rails3的新特性见:http://www.railsfire.com/article/community-feedback-future-rails]

What?
在Rails3时代还没到临之前,如果不想用Prototype,可以有jRails(http://ennerchi.com/projects/jrails)代替,然后jRails是完全兼容PrototypeHelper(http://api.rubyonrails.org/classes/ActionView/Helpers/PrototypeHelper.html),同样会在HTML中嵌入JS代码片段,从而伤害了HTML的纯洁。为了要维护HTML的纯洁以及JS的主权独立,于是计算机之神说,要Unobtrusive,于是有了Unobtrusive_JavaScript(http://en.wikipedia.org/wiki/Unobtrusive_JavaScript)。

Unobtrusive_JavaScript简单来说,就是把有指定元素行为的JS代码分离出来,如事件描述:
分离前:
<body>
<input type="text" name="date" onchange="validateDate(this);" />
</body>

分离后:
<head>
<script>
window.onload = function(){ //Wait for the page to load.
    var inputs = document.getElementsByTagName('input');
    for(var i=0,l=inputs.length;i<l;i++){ 
        input = inputs[i];
        if(input.name && input.name=='date'){ 
            input.onchange = function(){ 
                validateDate(this);
            }
        }
    }
};
 
function validateDate(){
	//Do something when the content of the 'input' element with the name 'date' is changed.
}
</script>
</head>
<body>
<input type="text" name="date" />
</body>

把所有js绑定操作都统一放到页面加载完再进行。

How?
用一个比较常见的用户信息修改操作为例,修改用户的信息,一般会用如下代码:
<% form_for @user do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :username %><br />
    <%= f.text_field :username %>
  </p>
  <p><%= f.submit "Submit" %></p>
<% end %>

会生成如下HTML代码:
<form action="/users/2" class="edit_user" id="edit_user_2" method="post"><div style="margin:0;padding:0"><input name="_method" type="hidden" value="put" /><input name="authenticity_token" type="hidden" value="ooDWeKPVumeI0r+O4E20g9TjfnxFKHp3ZsnCPCCrSFg=" /></div>
  <p>
    <label for="user_username">Username</label><br />
    <input id="user_username" name="user[username]" size="30" type="text" value="rainchen" />
  </p> 
  <p><input id="user_submit" name="commit" type="submit" value="Submit" /></p>
</form>


如果要改为AJAX提交操作时,可以用remote_form_for helper(http://api.rubyonrails.org/classes/ActionView/Helpers/PrototypeHelper.html#M001649),但这个其实是PrototypeHelper提供的一个辅助方法,把 form_for替换为remote_ form_for 后会在页面中生成如下HTML代码:
<form action="/users/2" class="edit_user" id="edit_user_2" method="post" onsubmit="new Ajax.Request('/users/2', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">

经过之前的一番论述,现在的结论就是<form>标签不纯洁了,被插了一段Prototype的AJAX代码。

在Rails3时代,这种代码是不和谐的,需要批判。

在解决这个问题前,先看一下在Rails3时代,类似AJAX请求场景,是怎样实现的,如
<%= link_to 'Users', users_path %>

会生成HTML:
<a href="/users">Users</a>


用 remote_link_to 替换的话,将会得到:
 <a href="/users" data-remote="true">Users</a>

被加进了一个data-remote属性,data-xxx 形式的属性,在HTML5中是合理又合法的:http://ejohn.org/blog/html-5-data-attributes/

remote_link_to 其实是一个Rails3新的AjaxHelper的方法,实现代码见:
http://github.com/rails/rails/blob/master/actionpack/lib/action_view/helpers/ajax_helper.rb

浏览代码后,不难发现到今天为止,AjaxHelper 中还没发现remote_form_for 的身影,也就是remote_form_for 还只是个传说。

今天我们就是要尝试实现这个传说,让我们就来见证奇迹。

在Rails3时代,没有意外的话,<form>标签也会被插入 data-remote=“true" 这个标记,因此思路很简单,覆盖掉remote_form_for 方法,加入这个标记,然后在页面加载后,用js查找带有这个标记的form,绑上AJAX操作即可。

1. 在application_helper.rb 中加入:
  # unobtrusive javascript helpers
  def remote_form_for(*args, &proc)
      options = args.extract_options!
      options[:html] ||= {}
      options[:html]["data-remote"] = "true"
      options[:html]["data-update"] = options[:update] unless options[:update].blank? # enable update the resposne data to *update* dom
      args << options
      form_for(*args, &proc)
  end

2. 在js框架方面,我选择用jquery
在layout中加入
    <%= javascript_include_tag respond_to?(:local_request?) && local_request? ? 'jquery.min' : 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js' %>
    <%= javascript_include_tag respond_to?(:local_request?) && local_request? ? 'jquery-ui.min' : 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js' %>
    <%= javascript_tag "AUTHENTICITY_TOKEN = '#{protect_against_forgery? ? form_authenticity_token : ""}';" %>
    <%= javascript_include_tag 'application' %>

在application.js 中加入:
$(function(){
  // set authenticity koen for Rails
  if(typeof AUTHENTICITY_TOKEN != 'undefined' && AUTHENTICITY_TOKEN != ''){
    $.ajaxSetup( {data: {authenticity_token: AUTHENTICITY_TOKEN}} );
  }

  // setup app namespace
  var app = {};

  // show the ajax result
   app.ajaxShowResult = function(options){
    options = $.extend({title: '', body: '', width: 200, height: 200}, options);
    if(!$("#app_ajax_result").get(0)){
      $(document.body).append('<div id="app_ajax_result"></div>');
      $("#app_ajax_result").dialog({
        title: '',
        bgiframe: true,
        width: options.width,
        height: options.height,
        modal: true
      });
    }
    $("#app_ajax_result").html(options.body);
    $("#app_ajax_result").dialog('option', 'title', options.title);
    return $("#app_ajax_result").dialog('open');
   };

  // default error handler for ajax request
  app.ajaxError = function(XMLHttpRequest, textStatus, errorThrown){
    return app.ajaxShowResult({title: XMLHttpRequest.statusText, body: XMLHttpRequest.responseText, width: 600, height: 400});
  };

  // default success handler for ajax request
  app.ajaxSuccess = function(data, textStatus){
    if(this.update){
      $("#"+this.update).html(data);
    }else if(this.dataType == 'html'){
      return app.ajaxShowResult({title:textStatus, body: data});
    }
  };

  app.ajax = function(options) {
    $.ajax($.extend({ url : options.url, type : 'get', dataType: 'html', error: app.ajaxError, success: app.ajaxSuccess }, options));
    return false;
  };

  // find all all data-remote tags
  app.setupAjaxHelpers = function(){
    // remote links handler
    $('a[data-remote=true]').live('click', function() {
      return app.ajax({ url : this.href });
    });

    // remote forms handler
    $('form[data-remote=true]').live('submit', function() {
      return app.ajax({ url : this.action, type : this.method, data : $(this).serialize(), update: $(this).attr('data-update') });
    });
  };

    // init
  app.init = function(){
    app.setupAjaxHelpers();
  };

  app.init();

});

关键代码其实只是
    // remote forms handler
    $('form[data-remote=true]').live('submit', function() {
      return app.ajax({ url : this.action, type : this.method, data : $(this).serialize(), update: $(this).attr('data-update') });
    });

默认用jquery-ui来做结果显示。

3.要支持ajax方式获取无layout的action rendered page时,还应该在application_controller.rb里加入:
#  render empty layout for ajax request
  layout proc { |controller| controller.request.xhr? ? nil : 'application' }



后注:
1. 其中jquery的live事件还不是实现得很完整,对IE支持不好:
引用

jQuery is going to be adding support for .live("submit") in the next
release, and it's possible to emulate in rails.jquery.js in the interim.
-- Yehuda
On Tue, May 26, 2009 at 1:07 PM, meefs...@googlemail.com <

- Show quoted text -
--
Yehuda Katz
Developer | Engine Yard
(ph) 718.877.1325


2. 在HTML标签中是约定用css做标记还是用属性做标记,筛选时会性能问题的差异,对我来说不是关注重点。

3. 在现在的Rails中使用Unobtrusive_JavaScript 版本的 remote_link_to 可参考:http://blog.solnic.eu/2009/09/08/unobtrusive-javascript-helpers-in-rails-3


参考:
http://blog.solnic.eu/2009/09/08/unobtrusive-javascript-helpers-in-rails-3
http://groups.google.com/group/rubyonrails-core/browse_thread/thread/3fa1cc2b1979a858
http://nutrun.com/weblog/unobtrusive-ajax-with-jquery-and-rails/
http://docs.jquery.com/Events/live

End?
   发表时间:2009-09-24  
我也觉得应该跟jquery更紧密的结合!
0 请登录后投票
   发表时间:2009-09-24  
我早停止使用所有的 rails js view help 方法,
不过我不是加一个data-remote="true"属性,我是加一个class='ajaxlink'属性。。。

然后使用$('.ajaxlink').ajaxRequest();绑定

ajaxRequest方法实现为:

$.fn.ajaxRequest = function() {
  $(this).unbind('click').click(function(){
    if ($(this).attr('confirm_messae') ? confirm($(this).attr('confirm_messae')) :
      ($(this).attr('ajaxmethod') == 'GET' ? true : confirm(_('are your sure')))) {
        $.cacheAjax({
          url:  $(this).attr('href'),
          type: $(this).attr('ajaxmethod') || "PUT",
          dataType: 'script'
        });
      };
    return false;
  });
};

关于里面的cacheAjax方法是使用了我的一个插件,目的是对 ajax 的请求进行缓存,很好玩、有用哦
插件地址见:http://github.com/jinzhu/cacheAjax

关于里面_('are your sure')的_方法,也是使用了我的一个javascript localize插件:地址见,http://github.com/jinzhu/javascript_localize
0 请登录后投票
   发表时间:2009-09-24  
wosmvp 写道
我早停止使用所有的 rails js view help 方法,
不过我不是加一个data-remote="true"属性,我是加一个class='ajaxlink'属性。。。

然后使用$('.ajaxlink').ajaxRequest();绑定

ajaxRequest方法实现为:

$.fn.ajaxRequest = function() {
  $(this).unbind('click').click(function(){
    if ($(this).attr('confirm_messae') ? confirm($(this).attr('confirm_messae')) :
      ($(this).attr('ajaxmethod') == 'GET' ? true : confirm(_('are your sure')))) {
        $.cacheAjax({
          url:  $(this).attr('href'),
          type: $(this).attr('ajaxmethod') || "PUT",
          dataType: 'script'
        });
      };
    return false;
  });
};

关于里面的cacheAjax方法是使用了我的一个插件,目的是对 ajax 的请求进行缓存,很好玩、有用哦
插件地址见:http://github.com/jinzhu/cacheAjax

关于里面_('are your sure')的_方法,也是使用了我的一个javascript localize插件:地址见,http://github.com/jinzhu/javascript_localize


我看到你的cacheAjax还有js testcases,很好很和谐,对js unit test感兴趣,有经验可分享吗
0 请登录后投票
   发表时间:2009-09-25  
使用 sinatra + QUnit搭建的超轻量级组合,前者作为一个server为ajax提供测试数据,QUnit是JQuery的单元测试框架,这套组合作用也就是用在测试js插件等纯js文件而已。

如果测试大型js应用的话,个人还是感觉还是采用眼见为实的方法比较好,可以选择selenium。? (大家有更好的方案吗?)


别外看了部分代码,感觉你的代码有一点点点问题
# <%= javascript_include_tag respond_to?(:local_request?) && local_request? ? 'jquery.min' : 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js' %> 
# <%= javascript_include_tag respond_to?(:local_request?) && local_request? ? 'jquery-ui.min' : 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js' %> 
# <%= javascript_tag "AUTHENTICITY_TOKEN = '#{protect_against_forgery? ? form_authenticity_token : ""}';" %> 

一个view中多次调用:
respond_to?(:local_request?) && local_request?
AUTHENTICITY_TOKEN?
我的记忆中从 Rails 2.3 (?) 开始不验证ajax请求了?
1 请登录后投票
   发表时间:2009-09-25  
wosmvp 写道
使用 sinatra + QUnit搭建的超轻量级组合,前者作为一个server为ajax提供测试数据,QUnit是JQuery的单元测试框架,这套组合作用也就是用在测试js插件等纯js文件而已。

如果测试大型js应用的话,个人还是感觉还是采用眼见为实的方法比较好,可以选择selenium。? (大家有更好的方案吗?)


别外看了部分代码,感觉你的代码有一点点点问题
# <%= javascript_include_tag respond_to?(:local_request?) && local_request? ? 'jquery.min' : 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js' %> 
# <%= javascript_include_tag respond_to?(:local_request?) && local_request? ? 'jquery-ui.min' : 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js' %> 
# <%= javascript_tag "AUTHENTICITY_TOKEN = '#{protect_against_forgery? ? form_authenticity_token : ""}';" %> 

一个view中多次调用:
respond_to?(:local_request?) && local_request?
AUTHENTICITY_TOKEN?
我的记忆中从 Rails 2.3 (?) 开始不验证ajax请求了?


我在自己的应用中试直接用local_request? 判读的,因为我在application controller中申明了:
helper_method :local_request?
并且要求本地目录中已经下载了对应的jquery代码,否则就直接用google的api。

这里做预判断是为了预防有些同学可能会直接copy代码测试而出错,另外方便生产环境中使用google的js cdn service。


protect_against_forgery? 在Rails 2.3x 默认是开启的(我没记错的话)


0 请登录后投票
   发表时间:2009-09-25  
关于第一点,我们的代码:
<% if RAILS_ENV == 'production' %>
  <script src="http://www.google.com/jsapi"></script>
  <script type='text/javascript'>
    google.load("jquery", "1.3.2");
    google.load("jqueryui", "1.7.1");
  </script>
<% else %>
  <%= javascript_include_merged :jquery %>
<% end %>

(javascript_include_merged是asset_packager的方法)

关于第二点,ajax请求已不在验证protect_against_forgery?,可自行查看Rails源代码确认,记得当时的github吵的很火的,我加个?号只是忘记Rails2.3分支有没有做这个而已,Rails3肯定是的,当然如果没有revert的话。。。
1 请登录后投票
   发表时间:2009-09-25  
cacheAjax 是个很实用的东东,支持一下
0 请登录后投票
   发表时间:2009-09-25  
我用一开始就没碰过rails那些动态插入js的helper方法⋯⋯其实只要具有前端开发经验,不管哪个版本的rails都能做到“无侵入”,遵循web标准。
3 请登录后投票
   发表时间:2009-09-25  
wosmvp 写道
关于第一点,我们的代码:
<% if RAILS_ENV == 'production' %>
  <script src="http://www.google.com/jsapi"></script>
  <script type='text/javascript'>
    google.load("jquery", "1.3.2");
    google.load("jqueryui", "1.7.1");
  </script>
<% else %>
  <%= javascript_include_merged :jquery %>
<% end %>

(javascript_include_merged是asset_packager的方法)

关于第二点,ajax请求已不在验证protect_against_forgery?,可自行查看Rails源代码确认,记得当时的github吵的很火的,我加个?号只是忘记Rails2.3分支有没有做这个而已,Rails3肯定是的,当然如果没有revert的话。。。


不直接判断RAILS_ENV是为了方便本地调试,比如有时你的NB在没有网络的环境,而且也方便在jquery的源码做些logger(有时要跟下jquery源码分析运行流程)

在application_controller.rb 中去掉protect_from_forgery 注释即是开启(今天确认了下,Rails 2.3x 默认是已经去掉注释的了):
protect_from_forgery # See ActionController::RequestForgeryProtection for details

忘了提及一点,要支持ajax方式获取无layout的action rendered page时,还应该在ac里加入:
#  render empty layout for ajax request
  layout proc { |controller| controller.request.xhr? ? nil : 'application' }
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics