Rails Rails 用 RJS 简单有效的实现页面局部刷新

huacnlee · 发布于 2016年02月22日 · 最后由 miserytan 回复于 2017年07月25日 · 10638 次阅读
De6df3
本帖已被设为精华帖!

我们时常在做项目的时候会遇到这样的需求,页面上有个基本数据的下拉框,用于选择一些数据,如:项目版本号、联系人、收件地址、银行账号列表什么的。

而需求可能会要求说可以在这个页面随时增加那些下拉框的选择值,而不是另开窗口。

于是我们可能需要实现一个浮动窗口,里面包含新增表单,实现表单 AJAX 提交,最后在刷新主窗口的下拉菜单控件。

这里介绍一下我最近实现的一个简单方式:

主表单页面:

link_to + remote 的方式实现一个 Ajax 请求的点击

<div class="form-field-user-address">
  <%= f.select :address, collection: current_user.addresses %>
</div>
<%= link_to '新增联系地址', new_address_path, remote: true %>

AddressesController 保持标准的 Rails new 函数结构

class AddressesController < ApplicationController
  def new
    @address = Address.new
  end

  def create
    @address = Address.new(address_params)
    respond_to do |format|
      if @address.save
        format.html { redirect_to addresses_path, notice: '地址新增成功。' }
        format.js
      else
        format.html { render :new }
        format.js
      end
    end
  end
end

新增 new.js.erb (以后会大量用 js.erb 的做法),由于前面是 remote 请求,HTTP Header 里面带的信息会让 Rails 渲染 js.erb 的模板 这里渲染 _form.html.erb 生成地址表单的 HTML:

var html = "<%= j(render('form', remote: true)) %>";
// bootbox 弹窗插件 http://bootboxjs.com/
bootbox.dialog({
  title: "新增联系地址",
  message: html
});

然后我们就得到了这样的界面:

然后正题来了,我们怎样才能在地址新增成功以后,以无刷新的方式更新主表单上面那个下拉框呢?

这就是本帖要将的技巧,非常简单,而且适用于任何地方!

我们新增一个 create.js.erb,前面说过 remote 的请求,Rails 会选择 js.erb 的模板来渲染

<% if @address.errors.blank? %>
  // 用 Ajax 载入当前页面,找到下拉框的 Dom 的新 HTML,再替换目前页面的下拉框 Dom
  var selector = '.form-field-user-address';
  $.get(location.href, function(html) {
    var doc = $(html).find(selector);
    $(selector).replaceWith(doc);
  });
  bootbox.hideAll();
<% else %>
  var html = "<%= j(render('form', remote: true)) %>";
  // 保存失败,直接替换浮动窗口上面的 HTML 为新的 form render 结果
  // 因为 @address 包含验证错误信息,所以 render 出来的 HTML 也包含验证信息
  $('form.new_address').replaceWith(html);
<% end %>

为了很多地方都能用到,于是我把上面的变成一个公共函数:

app.coffee

window.App =
  refreshDom: (selector) ->
    $.get location.href, (html) ->
        doc = $(html).find(selector)
        $(selector).replaceWith(doc)

create.js.erb 就可以简化了:

<% if @address.errors.blank? %>
  App.refreshDom('.form-field-user-address');
  bootbox.hideAll();
<% else %>
  var html = "<%= j(render('form', remote: true)) %>";
  $('form.new_address').replaceWith(html);
<% end %>

扩充阅读

共收到 36 条回复
9695

只是简单提一下,最近在看React,感觉React也可以做类似的事情。

A908ae

很实用,赞。

Jquery ujs里是不是已经定义了页面收到js代码后自动执行的过程了?

De6df3

其实一开始我是在找 Turbolinks 里面是否有类似的方法,尝试了

Turbolinks.visit(location.href, { change: ['form-field-user-address'] });

但结果失败了,首先浏览器会滚动到顶部页面动了...

De6df3

#2楼 @adamshen 对,以上的动作和 jquery_ujs 有非常大的关系

8549

很实用啊

2973

#3楼 @huacnlee 以前项目就是这么干的,但是只是有时有些 js 需要重新 bind 到新的 dom 上.

12381

现在已经不支持手动点赞了嘛

3790

@huacnlee 以前这样用过,有点小问题是若 xxx.js.erb 有 js 错误,在浏览器里相当不好定位(报错很奇怪,位置也不好定义);所以感觉这比较适合 xxx.js.erb 里 js 逻辑不复杂的情况。

De6df3

#8楼 @qinfanpeng 复杂的都要放到 JS 函数里面,避免服务端返回过多多余代码

3790

确实,有次这样弄,把我坑惨了,后面就把复杂js逻辑放到纯 js 里去了。 感觉 RailsRequest Header 设置成 script,然后浏览器加载完后就自动执行这段逻辑了。

2575

@huacnlee js.erb 里的脚本是直接 eval 跑的,所以里面声明的变量会变成 全局变量 ,所以任何 js.erb 最好加上闭包。CoffeeScript 貌似不用管这个问题,因为默认生成的 JavaScript 就是有闭包的。

96

@darkbaby123 你说的闭包指的是命名空间吗

2575

@optionsource 也许没表达清楚,就是建一个匿名函数表达式去封装一下:

(function() {
// Your code
})();
2852

这样写新dom要重新绑定事件,有时候会很麻烦。

Ab72dc

RJS 对于独立的小功能点 非常适用,谁用谁知道。

14293

当做到局部刷新页面之后,华顺你有没有尝试做过“页面后退”操作。之前我这样实现过然后想返回前一个页面,但是在浏览器中显示了你所说的 .js.erb 中 的 js 代码。
补充:项目禁用了 Turbolinks

1249

虽然具有通用性,但要重新获取一次当前页面,然后获取里面的最新dom来更新旧页面的数据,显得有点浪费带宽:),可以用通用的url获取JSON数据来更新;而且用replace的方式,还会造成事件丢失。

96

不喜欢这种到处都是碎片的感觉, 还有怎么测试.

2358

使用angular会不会更简单一些?

4594

#19楼 @alvin2ye 单元测试就只能测js函数,功能测试用capybara测一下

96

最近因为交接同事的项目,有用到vue.js,感觉用vue.js处理这些会更简单

9162

refresh UI 的时候其实会有DOM上绑定的JS事件需要重新绑定的问题…… 我和华顺的场景一样: 新建地址后,增加新的地址,并可以点击编辑再次弹出模态框保存地址,但由于是新出现的DOM,所以需要重新绑定模态框弹出的事件。我也试过直接用Turbolink带的API,没有合适的,问题也很多,也试过楼主这样抽出来替换方法然后用的,但会出现上述问题,最后都是手写……不知道 @huacnlee 有没有解决类似的问题……

96

感觉现在技术栈都落伍了,我还停留在ajax的时代,看来要被抛弃了

14534

这种做法实际上是 SJR-Server-generated JavaScript Responses(by DHH) ;Basecamp 中的大部分 Ajax 操作都是这样玩的;RJS was a "ruby-to-js" template system,这玩意只能算是 coffee 的一种粗糙形式。

另外我一直有个疑惑是,返回的 javascript 在哪里由谁执行的,看了 jquery-ujs 源码并没有执行的语句,jquery-ujs 中 ajax 的回调是这样:

success: function(data, status, xhr) {
  element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
  element.trigger('ajax:complete', [xhr, status]);
},
De6df3

#23楼 @cassiuschen 事件绑定的处理,你用 Backbone Events 就简单了。这也是我为什么会在 Ruby China 用 Backbone 的原因

27楼 已删除
15420

#26楼 @huacnlee 没用Backbone。。。现在DOM没法重新绑定事件了。还没解决思路

15420

#26楼 @huacnlee 上个厕所,想到个解决方法,解决了。。。

96

果然没有什么是上一次厕所不能解决的,如果不行,那就两次。

26044

用的Bootstrap v4?

5178

#25楼 @holysoros jQuery.ajax() dataType设为“script” , 就会把response 当成 js来执行,严格来说这是 jQuery 实现的 ,rails的jquery-ujs 只是定义了一些全局事件和事件代理 detail: http://api.jquery.com/jquery.ajax/ @cassiuschen 你应该用事件代理,参考jQuery的on(http://api.jquery.com/on/)方法,把事件代理到父级元素上,不要把事件直接绑到操作的元素。 backbone 也是这种原理。

$( "body" ).on( "click", "p", function() {
  alert( $( this ).text() );
});
26232

这种写法在slim模板下能够实现吗?我在一个级联选择框的样例代码中看到这种做法,但是我的项目里面用的是slim模板,没找到正确编写xxx.js.slim的方法,最后只能用了最原始的ajax实现的。

6480

@huacnlee

谢谢你的分享,很好的想法! 关于下面的代码有个问题:

window.App =
  refreshDom: (selector) ->
    $.get location.href, (html) ->
        doc = $(html).find(selector)
        $(selector).replaceWith(doc)

如果location.href的页面本来就非常复杂,数据量大的话,那么不是很慢呢?有没有更好的方法呢?

5c8fb2

@huacnlee 能看下您的github源码吗?同样的需求,尝试着照你说的写着,没写出来

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册