Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3cc9a14
Showing
3 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,56 @@ | |||
# 客户端如何优化处理因用户误操作导致的多次请求 | |||
|
|||
在互联网应用中,我们经常用到的场景,比如用户点击某个按钮,触发的操作会和后台api进行数据交互,生成一些记录,比如下单购买。如果后台api请求比较慢,而客户端体验又做得不到位,导致用户以为没点击到或者是页面假死,在上次请求还没处理完,就再次点击按钮。这样会导致某个操作生成多次记录,导致一些异常的bug。 | |||
|
|||
很显然,后台的api在这方面是需要做好处理。然而,面对用户,我们需要更好的体验,可以在客户端去避免这些问题,前置地解决问题。 | |||
|
|||
最近听产品经理常说,用户点击某个按钮多次,后台还没处理完导致多笔记录生成,我们需要在用户点击后跳转到一个新的页面,其实这根本不是跳页问题,是程序问题。如果程序员真这么干,是不是要下岗了。 | |||
|
|||
以前偷懒的时候,在前端我们会这么处理: | |||
|
|||
```js | |||
var getUserDataFlag = false; | |||
function getUserData() { | |||
if (getDataFlag) { | |||
return; | |||
} | |||
getDataFlag = true; | |||
$.ajax({ | |||
url: '/xxx/getUser', | |||
success: function () { | |||
getUserData = false; | |||
//todo | |||
}, | |||
error: function () { | |||
getUserData = false; | |||
} | |||
}) | |||
} | |||
//当接口很多的时候,我们的代码就变成这样 | |||
var getUserAssetFlag = true; | |||
function getUserAsset() { | |||
if (getDataFlag) { | |||
return; | |||
} | |||
getDataFlag = true; | |||
$.ajax({ | |||
url: '/xxx/getUserAsset', | |||
success: function () { | |||
getUserAssetFlag = false; | |||
//todo | |||
}, | |||
error: function () { | |||
getUserAssetFlag = false; | |||
} | |||
}) | |||
} | |||
``` | |||
|
|||
上面的例子你会发现,当接口越来越多,维护请求状态的变量将会越来越多,并且当存在氢气依赖时,维护成本更高,也更容易出错。 | |||
|
|||
如何优雅地解决这样的问题,最近在重构angular的项目以及在写微信小程序demo,有一些小实践和总结,例子请参照: | |||
|
|||
[angular的例子](./angular-demo.js) | |||
[微信小程序的例子](./xiaochengxu-demo.js) | |||
|
|||
欢迎大家交流更多好的解决方案 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,87 @@ | |||
;(function () { | |||
//定义api promise service | |||
angular.module('app.apiService', ['']) | |||
.factory('apiService', ['$http', '$q', function($http, $q) { | |||
function isObject (obj) { | |||
return isType(obj, 'Object') | |||
} | |||
function isType (obj, type) { | |||
return Object.prototype.toString.call(obj) === '[object ' + type + ']' | |||
} | |||
//记录所有与后台正在请求的地址和参数 | |||
var requestList = {}; | |||
//将请求记录到requestList中 | |||
function addRequestKey (key) { | |||
requestList[key] = true | |||
} | |||
//将请求从requestList移除 | |||
function removeRequestKey (key) { | |||
delete requestList[key] | |||
} | |||
//请求是否在requestList中 | |||
function hitRequestKey (key) { | |||
return requestList[key] | |||
} | |||
//根据请求的地址,类型以及数据生成唯一请求key | |||
function getRequestKey (data) { | |||
if (!isObject(data)) { | |||
return data | |||
} | |||
var ajaxKey = 'Method: ' + data.method + ',Url: ' + data.url + ',Data: ' | |||
try { | |||
ajaxKey += JSON.stringify(data.data) | |||
} catch (e) { | |||
ajaxKey += data.data | |||
} | |||
return ajaxKey | |||
} | |||
//统一请求入口 | |||
function http (data) { | |||
if (!isObject(data)) { | |||
throw Error('ajax请求参数必须是json对象: ' + data) | |||
} | |||
data.method = (data.method || 'GET').toUpperCase(); | |||
data.headers = data.headers || {'Content-Type': 'application/json'}; | |||
if (data.method === 'GET') { | |||
data.params = data.data; | |||
delete data.data | |||
} | |||
var ajaxKey = getRequestKey(data) | |||
//当key命中且请求未完成,抛出异常 | |||
if (hitRequestKey(ajaxKey)) { | |||
throw Error('重复提交请求:' + ajaxKey) | |||
} | |||
//将当前请求记录起来 | |||
addRequestKey(ajaxKey) | |||
return $http(data) | |||
.then(function (data, status, headers, config) { | |||
//请求完成移除请求记录 | |||
removeRequestKey(ajaxKey) | |||
return data.data | |||
}) | |||
.catch(function (e) { | |||
//请求完成移除请求记录 | |||
removeRequestKey(ajaxKey) | |||
return $q.reject(e); | |||
}) | |||
} | |||
var output = { | |||
// get 请求 | |||
getUserState: function (data) { | |||
return http({ | |||
url: '/xxx/getState', | |||
data: data | |||
}) | |||
}, | |||
// post 请求 | |||
saveUserState: function (data) { | |||
return http({ | |||
url: '/xxx/postData', | |||
method: 'POST', | |||
data: data | |||
}) | |||
} | |||
} | |||
return output; | |||
}]) | |||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,110 @@ | |||
import {isObject} from './util' | |||
|
|||
let Promise = require('../libs/bluebird.min') | |||
let requestList = {} //api请求记录 | |||
|
|||
// 将当前请求的api记录起来 | |||
export function addRequestKey (key) { | |||
requestList[key] = true | |||
} | |||
|
|||
// 将请求完成的api从记录中移除 | |||
export function removeRequestKey (key) { | |||
delete requestList[key] | |||
} | |||
|
|||
//当前请求的api是否已有记录 | |||
export function hitRequestKey (key) { | |||
return requestList[key] | |||
} | |||
|
|||
// 获取串行请求的key,方便记录 | |||
export function getLockRequestKey (data) { | |||
if (!isObject(data)) { | |||
return data | |||
} | |||
let ajaxKey = 'lockRequestKey:' | |||
try { | |||
ajaxKey += JSON.stringify(data) | |||
} catch (e) { | |||
ajaxKey += data | |||
} | |||
return ajaxKey | |||
} | |||
|
|||
//根据请求的地址,请求参数组装成api请求的key,方便记录 | |||
export function getRequestKey (data) { | |||
if (!isObject(data)) { | |||
return data | |||
} | |||
let ajaxKey = 'Method: ' + data.method + ',Url: ' + data.url + ',Data: ' | |||
try { | |||
ajaxKey += JSON.stringify(data.data) | |||
} catch (e) { | |||
ajaxKey += data.data | |||
} | |||
return ajaxKey | |||
} | |||
//所有与服务器进行http请求的出口 | |||
export function http (data) { | |||
if (!isObject(data)) { | |||
throw Error('ajax请求参数必须是json对象: ' + data) | |||
} | |||
data.method = (data.method || 'GET').toUpperCase() | |||
//下面5行是对所有http请求做防重复请求处理,后面单独分享原理 | |||
let ajaxKey = getRequestKey(data) | |||
if (hitRequestKey(ajaxKey)) { | |||
throw Error('重复提交请求:' + ajaxKey) | |||
} | |||
addRequestKey(ajaxKey) | |||
//bluebird.js包装成promisepromise api | |||
return new Promise(function (resolve, reject) { | |||
//通过wx.request api 向服务器端发出http请求 | |||
wx.request({ | |||
url: data.url, | |||
data: data.data, | |||
method: data.method, | |||
header: data.header || {'Content-Type': 'application/json'}, | |||
complete: function (res) { | |||
// 请求完成,释放记录的key,可以发起下次请求了 | |||
removeRequestKey(ajaxKey) | |||
let statusCode = res.statusCode | |||
if (statusCode === 200 || statusCode === 304) { | |||
return resolve(res.data) | |||
} | |||
return reject(res) | |||
} | |||
}) | |||
}) | |||
} | |||
|
|||
//通用get请求方法 | |||
export function httpGet (data) { | |||
return http(data) | |||
} | |||
|
|||
//通用post请求方法 | |||
export function httpPost (data) { | |||
data.method = 'POST' | |||
return http(data) | |||
} | |||
|
|||
// 该方法适用于串行请求的api | |||
export function lockRequest (data, fn) { | |||
let ajaxKey = getLockRequestKey(data) | |||
if (hitRequestKey(ajaxKey)) { | |||
throw Error('重复提交请求:' + ajaxKey) | |||
} | |||
addRequestKey(ajaxKey) | |||
return new Promise(function (resolve, reject) { | |||
fn(data) | |||
.then(function (data) { | |||
removeRequestKey(ajaxKey) | |||
return resolve(data) | |||
}) | |||
.catch(function (error) { | |||
removeRequestKey(ajaxKey) | |||
return reject(error) | |||
}) | |||
}) | |||
} |