前端项目请求层封装过程

调用 ajax 取请求后端数据是项目中最基础的功能。但是如果每次直接调用底层的浏览器 api 去发请求则非常麻烦。现在来分析一下怎么封装这一层,看看有哪些基础问题需要考虑。本文底层使用 fetch ,如果你使用 XMLHttpRequest 甚至第三方库(譬如:axios)封装过程都是大同小异的。

封装重复代码

对于同一个项目通常来说请求参数有很多重复的内容,譬如 url 的拼接,http head 的设置。假设我们调用的是 RESTful 接口,通常我们需要变动的有:1. 请求 url 的 path 部分;2. 参数;3. 请求 method;4. 成功/失败回调函数。我们看下把重复代码封装成一个 ApiSender 的示例代码:

const URL_PREFIX = 'xxx';

let ApiSender = {
  send( options ) {
    let {
      path,
      params,
      method,
      success,
      fail
    } = options;

    let url = URL_PREFIX + path;
    if ( method==='GET' ) {
      url += ('?'+toQueryString( params ));
    }
    let requestBody;
    if ( method==='POST' ) {
      requestBody = params;
    }

    fetch( url, {
      method: method,
      // 这里假设我们项目请求头固定这两个
      headers: {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
      },
      credentials: 'include',
      body: requestBody
    } ).then( function(response){
      let resultJson = response.json();
      if ( /* 判断返回没有错误 */ ) {
        success && success( resultJson );
      } else {
        fail && fail( resultJson.error );
      }
    } );
  }
}

使调用可读性更好

以上封装了一个 ApiSender,调用的时候如下:

ApiSender.send( '/resource', 'GET', {
  pageSize: 10,
  pageNo: 1
}, function( result ){
  // 对结果进行处理
}, function( error ){
  alert( error )
} )

通过传递回调函数的方式,可读性性不是很好(当然这是一个仁者见仁的问题)。我们把返回改成 Promise。因为我们用的是 fetch,它直接返回的就是 Promise,比较好改。如果你底层用的是 XMLHttpRequest,那么可以自行把调用 XMLHttpRequest 的代码封装在一个 Promise 中返回。

let ApiSender = {
  send( options ) {
    let {
      path,
      params,
      method,
      success,
      fail
    } = options;

    let url = URL_PREFIX + path;
    if ( method==='GET' ) {
      url += toQueryString( params );
    }
    let requestBody;
    if ( method==='POST' ) {
      requestBody = params;
    }

    return fetch( url, {
      method: method,
      // 这里假设我们项目请求头固定这两个
      headers: {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
      },
      credentials: 'include',
      body: requestBody
    } ).then( function(response){
      return response.json()
    } );
  }
}

调用的时候代码就变成:

ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function(result){
  if ( /* 判断返回没有错误 */ ) {
    // 处理结果
  } else {
    // 提示错误
  }
} )

从调用者角度抽象返回值

上面代码有一个问题,对于 ApiSend 的调用者来说,他需要直接处理接口返回值,判断是否成功。如果接口返回对象比较简单还好,如果非常复杂,那么调用者就很头疼,举个例子,我碰到过如下的接口返回值:

{
  content: {
    result: {
      errorCode: 1,
      errorMessage: '',
      isSuccess: true
    },
    data: {}|[] // 真正的可用数据
  },
  a: { // 有特征的字段名我做了简化,使用了a,ab这样的字段名。a 这个字段内容是 api 网关层包装的。
    code: 1,
    ab: [ {
      code: 1
    } ]
  }
}

如何判断这个返回值是成功的呢?

let result = { /* 上面那个对象 */ }
if (
  result.a &&
  result.a.code === 0 &&
  result.a.ab &&
  result.a.ab[ 0 ] &&
  result.a.ab[ 0 ].code === 0
) {
  if (
    result.content &&
    result.content.result &&
    result.content.result.isSuccess === true
  ) {
    // 处理结果 result.content.data
  }
}

你想象下,作为 ApiSender 的调用方,会希望得到什么结果?执行正确的时候获得接口返回的数据,执行异常的时候获得错误信息。我不希望调用一个方法,需要通过复杂地解析返回值来判断是否成功。所以最直观的就是把错误封装成一个很直观的返回值:

let ApiSender = {
  send( options ) {

    /* 代码省略掉了 */

    return fetch( /* 参数也省略掉了 */ ).then( function(response){
      let result = response.json();
      if ( isSuccessResult(result) ) {
        return [ null, result.content.data ]
      } else {
        let error = parseError( result );
        return [ error, null ];
      }
    } );
  }
}

那么调用方对结果的判断就非常方便了:

ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function([error,data]){
  if ( !error ) {
    // 处理结果 data
  } else {
    alert( error ); // error 的格式大家可以自行定义,各个项目各有不同
  }
} );

面向切面需要做些什么

以上一个比较基础且简洁的封装就做好了,但是现实中有些基础功能是经常需要的,譬如请求日志,请求错误报错统一处理。如果这些代码需要调用方来做,一来代码重复,二来譬如日志应该是调用方不感知的一个功能。所以我们对代码进一步进行优化,加入这些功能:

let ApiSender = {
  send( options ) {

    /* 代码省略掉了 */

    return fetch( /* 参数也省略掉了 */ ).then( function(response){
      let result = response.json();
      // 记录调用日志
      writeLog( options, result );

      if ( isSuccessResult(result) ) {
        return [ null, result.content.data ]
      } else {
        let error = parseError( result );
        // 界面报错
        MessageComponent.error( `${error.message}(${error.code})` );

        return [ error, null ];
      }
    } );
  }
}

日志你可以上传服务器,也可以就本地 console,日志记录哪些内容,参数如何都按各自的项目需求而定。如此的话,调用方就更简洁了:

ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function([error,data]){
  if ( !error ) {
    // 处理结果 data
  }
} );

绝大多数情况下,调用接口返回错误是需要在页面上提示错误的,但是并不是所有情况都需要。譬如非用户触发的行为,且请求返回的结果并不严重影响页面操作或者流程。那么我们可以在调用 ApiSender 的时候加一个参数,允许调用方跳过全局错误处理:

let ApiSender = {
  send( options ) {

    /* 代码省略掉了 */
    let skipErrorHandler = options.skipErrorHandler;

    return fetch( /* 参数也省略掉了 */ ).then( function(response){
      let result = response.json();
      // 记录调用日志
      writeLog( options, result );

      if ( isSuccessResult(result) ) {
        return [ null, result.content.data ]
      } else {
        let error = parseError( result );

        // 传了这个参数才跳过,不传或者传了非 true 值(当然包括 false),都认为不跳过
        if ( skipErrorHandler===true ) {
          // 界面报错
          MessageComponent.error( `${error.message}(${error.code})` );
        }
        
        return [ error, null ];
      }
    } );
  }
}

所以如果你希望自己处理错误,调用的时候代码就是:

ApiSender.send( '/resource', 'GET', {skipErrorHandler:true/*, 其他参数 */} ).then( function([error,data]){
  if ( !error ) {
    // 处理结果 data
  } else {
    // 自行处理错误
  }
} );

到这里为止,请求层的基本封装算是比较完整了,不过最后有一个小点要考虑下,如果你在 fetch().then 传入的回调函数中因为种种原因而抛出了异常(譬如某个字段没有判空)。那么 ApiSender 的调用方是没法感知的,程序直接就报错了。所以为了程序的健壮性,我们最后再加一个 catch:

let ApiSender = {
  send( options ) {

    /* 代码省略掉了 */
    let skipErrorHandler = options.skipErrorHandler;

    return fetch( /* 参数也省略掉了 */ ).then( function(response){
      let result = response.json();
      // 记录调用日志
      writeLog( options, result );
      if ( isSuccessResult(result) ) {
        return [ null, result.content.data ]
      } else {
        let error = parseError( result );
        // 传了这个参数才跳过,不传或者传了非 true 值(当然包括 false),都认为不跳过
        if ( skipErrorHandler===true ) {
          // 界面报错
          MessageComponent.error( `${error.message}(${error.code})` );
        }
        
        return [ error, null ];
      }
    } ).catch( function(error){
      return [ error, null ];
    } );
  }
}

这样一个对调用方友好,避免代码重复的请求层就封装好了。PS: 如果对 Promise 的 api 不是很熟悉的话,可以先了解下,有助于更好的理解示例代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容