Permalink
Browse files

add demo

  • Loading branch information...
0 parents commit 3cc9a14665eb80415f48322aac403e7f58679930 @navyxie committed Dec 3, 2016
Showing with 253 additions and 0 deletions.
  1. +56 −0 README.md
  2. +87 −0 angular-demo.js
  3. +110 −0 xiaochengxu-demo.js
@@ -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)
+
+欢迎大家交流更多好的解决方案
@@ -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;
+ }])
+})();
@@ -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)
+ })
+ })
+}

0 comments on commit 3cc9a14

Please sign in to comment.