Rails 为 Rails 所有的异步请求添加加载动画

pinewong · 2018年01月31日 · 最后由 pinewong 回复于 2018年02月03日 · 2371 次阅读

有一个需求,需要为 Rails 上所有的异步请求添加加载动画(其中 Rails: 5.1.4,jQuery: 3.3.1)。具体设计是发出请求后在页面展示正在请求的提示动画,请求结束时隐藏或清除该动画。下面是分情况的实现:

禁用 rails-ujs 的情况

如果没有使用 rails-ujs

// application.js
// 确定项目中下面这行已经被移除
//= require rails-ujs

那么实现只要添加 JS 事件就行了

$(document).on("ajaxSend", function(){
  // 加载异步请求动画
  $("#loading").show();
})
.on("ajaxComplete", function(){
  // 移除异步请求动画
  $("#loading").hide();
})

其中 #loading 是布局层面的 html 加载动画模板,每个页面都需要加载,初始化时先隐藏起来。

使用 rails-ujs 的情况(仅使用于非动态元素)

如果是按默认的 Rails 配置,项目中需要使用 rails-ujs 时,ujs 提供的异步请求(a 或 button 添加 remote: true 这种)绑定的事件不是原生事件,这时候需要绑定另外一种事件实现

$(document).on("ajax:send", function(){
  // 加载异步请求动画
  $("#loading").show();
})
.on("ajax:complete", function(){
  // 移除异步请求动画
  $("#loading").hide();
})

这么处理在一般的情况下没有问题。但是当绑定 send 事件的元素在后端处理中被移除了的时候,后续的事件就无效了。举个例子:

  • tests/index.html.erb
<%= link_to "我会异步删除自己", tests_path, remote: true, method: "delete", id: "test" %>
  • tests/destroy.js.erb
$("#test").remove();

然后点击测试中的链接之后,加载异步请求动画成功,但是请求结束后,动画不会移除。查询资料,说这种异步请求未完成就改变元素的操作不是好实践。但是这种需求在 Rails 还是很普遍啊,例如大家用的 kaminari 分页库的异步加载,就会重新渲染整个列表,也就是触发换页操作的按钮也被移除过了,所以也会出现上述的问题。于是这个问题还是要解决的。

使用 rails-ujs 的情况(完整)

具体要解决 Rails 异步请求加载动画的问题,需要使用到 ajaxSend 事件的 jqXHR 参数,用这个参数值来绑定后续事件,而不再使用 ajaxComplete 事件。

首先,因为当前的 Rails 版本自带的 rails-ujs 不再依赖 jQuery,因此 ajax:send 事件的参数中,不是 jqXHR,所以首先需要替换回原来的 jquery_ujs 库

  • Gemfile
gem 'jquery-rails'
  • application.js
//= require rails-ujs 替换为
//= require jquery_ujs

然后用以下的方式绑定事件

$(function() {
  $(document).on("ajax:send", function(event, jqXHR) {
    // 加载异步请求动画
    $("#loading").show();
    jqXHR.always(function() {
      // 移除异步请求动画
      $("#loading").hide();
    });
  });
});

根据 @n5ken 提示,还有一个多请求干扰的问题。

如:异步删除两个资源,A 的删除动作触发了动画显示,随后 A 的任务完成后动画消失,但 B 如果还在进行中的话,动画实际上已经消失了。

因此还要添加一个全局计数器

$(function() {
  var ajaxSendCount = 0;
  $(document).on('ajax:send', function(event, jqXHR) {
    if (!$("#loading").is(":visible")) {
      ajaxSendCount = 0;
      // 加载异步请求动画
      $("#loading").show();
    }
    ajaxSendCount++;
    jqXHR.always(function() {
      ajaxSendCount--;
      if (count <= 0) {
        // 清除异步请求动画
        $("#loading").hide();
      }
    });
  });
});

现在可以正常的加载异步请求的动画了。

Questions:

  • Loading 是全局的还是控件本身变成 loading 状态?
  • 假如是全局的话,如果有两个异步事件:A、B 同时发出请求,当 A 完成后把 loading 设置为隐藏,但实际上 B 还没完成,所以是否需要考虑加个工作中的任务列表?
n5ken 回复

加载动画是全局的。

一个操作触发请求多个后台?Rails 应用中还没遇到过这个需求。的确,你说的对,这里解决的粒度是单个异步请求,你说的情况处理不了。但这类需求可以放到 js 中使用 ajax 的 Local Events 单独处理,记得处理完后阻止一下事件传播就行(不要让全局事件再处理一遍了)。

你说的任务列表大概是怎么处理的,会不会把事情搞复杂了?

pinewong 回复

如果是全局的话,两个操作两个请求也会出现这种问题,如:异步删除两个资源,A 的删除动作触发了动画显示,随后 A 的任务完成后动画消失,但 B 如果还在进行中的话,动画实际上已经消失了。

你说的对,要添加一个全局计数器,来处理多请求的问题:

$(function() {
  var ajaxSendCount = 0;
  $(document).on('ajax:send', function(event, jqXHR) {
    if (!$("#loading").is(":visible")) {
      ajaxSendCount = 0;
      // 加载异步请求动画
      $("#loading").show();
    }
    ajaxSendCount++;
    jqXHR.always(function() {
      ajaxSendCount--;
      if (count <= 0) {
        // 清除异步请求动画
        $("#loading").hide();
      }
    });
  });
});

举例:删某个资源的动画 不是应该在那个资源的位置有动画么

zj0713001 回复

举例中的不是实际需求。这里的目的就是为整个项目所有的异步请求统一加一个进行中的提示,这个提示可以加在底部或者右上角。你说的特殊情况,特殊处理,不让事件传播到这里处理就行。

加动画的目的是什么?为后台效率低背锅吗?

nouse 回复

就是想在按钮 disabled 期间给用户一个反馈

pinewong 回复

这种按钮 disabled 的情况 直接在按钮上加个转圈的 icon 就可以了 你说不是实际需求 只能说是你们的产品同学有点太绝对的解决问题了 我们的解决办法是这样的:

表单查询、提交的时候 整个表单遮罩 loading 的 icon,按钮中添加 loading 的 icon 同时文字变成 提交中 table 中删除资源,删除的按钮添加 loading 的 icon,这样用户删 3 个 会有 3 个按钮执行动画 各自走各自的

zj0713001 回复

😂 没有,不是产品的事,是项目自己加上的小功能。听你们这么说了,这个东西好像没什么意义。下次解决问题前我会先考虑需求是不是值得的问题😂

pinewong 回复

不会不会,这些细节在不论什么项目中 都是要考虑的

zj0713001 回复

嗯嗯,是要考虑。关于产品体验,还有很多东西要学习,受教了👍

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