理解拦截器模式
发布在子回的前端专栏2014年6月29日view:10497
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

发现问题

在阐述一个设计模式之前,我们必须先提出一些问题,然后应用这个模式去解决这个问题。假设我们在搭建一个Web应用的页面,这个页面上有20处会产生Ajax Call的地方,一些是主动触发的(比如页面加载时),一些是被动触发的(比如用户提交表单)。那么你可能用最简单的想法写出了下面的代码(下面是伪代码)。

// 页面内容初始化
window.addEventListenr('DOMContentLoaded', function() {
  // ajax.get(url) 返回一个promise
  ajax.get(url1).then(callback1, errorCallback1);
  ajax.get(url2).then(callback2, errorCallback2);
});

// 登陆按钮
loginButton.addEventListener('click', function() {
  ajax.get(url3).then(callback3, errorCallback3);
});

// “喜欢”按钮
likeButton.addEventListener('click', function() {
  ajax.get(url3).then(callback3, errorCallback3);
});

这样的代码显然是很难维护的。聪明的朋友们一定已经猜到了,为了提高可维护性,我们可以将代码进行模块化(利用CommonJS之类的方法),或者采用各种各样其他的解耦方法。但是我在这里并不打算说可维护性的事情,而想说代码重用以及用户体验的问题。

假设以下场景:页面需要调用服务器的[GET] /users/:userId这个API获得用户的个人资料,并且也需要调用服务器的[POST] /posts发布文章。在这个过程中,服务器会检查Ajax Call的Request Header里面是否有Token(可能包含在X-CSRF-TOKEN中)。如果Token验证失败,那么服务器会返回HTTP 403错误,表示用户未登陆或者Token失效。

在调用者两个API的时候,我们的页面上需要做以下几件事情:

  1. 在返回结果前,提示用户该请求正在执行(比如显示一个如图1的提示图)
  2. 在返回HTTP 403时,将用户跳转到登陆界面

图1. 请求加载中的提示

如果用上面的方式,然后将代码模块化后,代码可能会是下面的样子(依然是伪代码)。注意,在这里,getUserInformation模块用于调用[GET] /users/:userId这个API,createPost模块用于调用[POST] /posts这个API,loadingIndicator这个模块用于展示提示图。loadingIndicator采用了引用计数的方式来决定是否要展示提示图,即每次调用show方法时将引用计数加1,调用dismiss方法时将引用计数减1,当且仅当引用计数大于0的时候,展示提示图。

get-user-infomation.js
module.register('getUserInformation', function() {
  // 展示等待的图片
  module.use('loadingIndicator').show();
  // 假设获取userId为9的用户信息
  return ajax.get('/users/9').then(function(data) {
    // 使用获取的信息,比如展现在页面上
  }, function(response) {
    if (response.statusCode === 403) {
      // 将用户重定向到登陆页
      redirectUserToLoginPage();
    }
    module.use('loadingIndicator').dismiss();
  });
});
create-post.js
module.register('createPost', function() {
  // 展示等待的图片
  module.use('loadingIndicator').show();

  return ajax.post('/posts').then(function(data) {
    // 使用获取的信息,比如展现在页面上
  }, function(response) {
    if (response.statusCode === 403) {
      // 将用户重定向到登陆页
      redirectUserToLoginPage();
    }
    module.use('loadingIndicator').dismiss();
  });
});

我们可以很明显地看到这两个模块中存在可重用的代码:处理等待图片的部分,以及处理403错误的部分。你可能会说,那就再把它们模块化不就好了吗?的确,处理403错误的部分可以被再度封装到一个模块中(或者一个模块的一部分中),但是这样的做法很容易让代码过度工程化。而且,处理等待图片的代码并无法再被模块化。

解决问题

我们可以注意到,这两个业务逻辑基本上是每一个API调用都会涉及的。我们可以设计一个“拦截器”。这个拦截器可以做以下几件事情:

  1. 当页面上发生Ajax Request的时候,Request首先先经过拦截层处理,再发送到远程服务器。
  2. 当页面上收到Ajax Response的时候,Response首先先经过拦截层处理,再送回给请求Response的代码。

图2. 拦截器图示

下面我们来分步描述,应用拦截器之后,一次Ajax Call中将发生的事情(根据上面的两个业务逻辑)。首先(a)本地发出请求。该请求首先经过拦截器,在(b)时,拦截器调用module.use('loadingIndicator').show()。接下来,(c)拦截器将该请求转发给服务器。服务器处理完毕后,(d)将响应返回,该响应首先经过拦截器。(e)拦截器首先调用module.use('loadingIndicator').dismiss()将提示图的引用计数减1。然后拦截器检查响应的statusCode是否为403,如果是,则跳转到用户登陆页。最后,拦截器将响应返回到本地业务代码中。实际上,一般情况下,我们会把这两个业务分解成两个拦截器,一个专门处理提示图的展示,一个专门处理检查响应头。

用伪代码描述这两个拦截器,就是这个样子的

Interceptor.add('loadingIndicatorDirector', function() {
  return {
    request: function(request) {
      // 在(b)段触发的代码
      module.use('loadingIndicator').show();
    },
    response: function(response) {
      // 在(e)端触发的代码
      module.use('loadingIndicator').dismiss();
    }
  }
});

Interceptor.add('error403Processor', function() {
  return {
    response: function(response) {
      // 在(e)端触发的代码
      if (response.statusCode === 403) {
        // 将用户重定向到登陆页
        redirectUserToLoginPage();
      }
    }
  }
});

我们不仅可以统一处理403错误,还可以处理404错误等其他HTTP错误。这是非常重要的——所有错误的处理都被规范化,这也让我们更易于管理API返回的错误代码及错误行为!

Angular.js拦截器

在一些现有的Web应用框架中提供了拦截器功能,比如Angular.js

// for angular.js >= 1.1.x
myapp.factory('myHttpResponseInterceptor',['$q', '$location', function($q, $location){
  return {
    response: function(response) {
      return promise.then(
        function success(response) {
          return response;
        },
        function error(response) {
          if(response.status === 403){
            $location.path('/signin');
            return $q.reject(response);
          }
          else {
            return $q.reject(response); 
          }
        }
      );
    }
  }
}]);
myapp.config(['$httpProvider',function($httpProvider) {
  // AngularJS将拦截器排入一个队列中,在进行Ajax Call
  // 时,将触发器挨个触发
  $httpProvider.interceptors.push('myHttpResponseInterceptor');
}]);

Angular.js的一个第三方库Restangular提供了更好的拦截器支持,也更适合面向RESTful API的Ajax-heavy应用开发。

总结

拦截器模式处处可见。在一些企业级Java Web应用开发框架(比如Struts)上,拦截器是很重要的一环。在Web应用的开发过程中合理应用拦截器,会有诸多好处。

  • 更易于管理错误
  • 更易于改进用户体验
  • 统一地记录应用的网络请求日志(logging)
  • ……更多

前端开发工程师的职责不仅仅是实现页面,如果仅仅是实现页面,那么还不如把前端开发工程师称作“写网页的”。前端开发工程师应该合理应用工程方法,让用户体验得到优化,让自己更加愉快,也让团队开发更轻松、有效。

订阅

如果你对我的博客的内容感兴趣,欢迎订阅我的小报。我将以电子邮件的方式,每隔一段时间(最多一周一次)向你的邮箱发送一些有趣的文章。这里面,大部分是技术文章,也可能会有一些和生活相关的。毕竟,做技术也是一种生活的方式。小报内不会含有任何的广告,请你放心。

你可以通过我的博客的左侧边栏的订阅按钮订阅我的小报。

评论
发表评论
5年前

return promise.then( 这个promise不知道从哪而来

WRITTEN BY
子回
主攻互联网开发。
TA的新浪微博
PUBLISHED IN
子回的前端专栏

就是一个专栏而已

我的收藏