如何处理对后端服务进行相同调用的多个浏览器脚本

How do I handle multiple browser scripts making the same calls to the back-end service

我有一个网页,其中的不同部分都需要相同的后端数据。每个都是孤立的,因此它们最终都会对后端进行相同的调用。

当调用已在进行中并由同一网页上的不同代码段启动时,避免调用 Web 服务器的最佳方法是什么?

这是一个例子。我将使用 setTimeout 来模拟异步调用。

假设有一个异步函数 returns 联系人列表,在此示例中它基本上是一个简单的字符串数组:

var getContacts = function() {
  log('Calling back-end to get contact list.');
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      log('New data received from back-end.');
      resolve(["Mary","Frank","Klaus"]);
    }, 3000);
  });
};

现在,让我们创建三个独立的函数,每个函数出于不同的目的调用上述函数。

转出联系人​​列表:

var dumpContacts = function() {
  getContacts().then(function(contacts) {
    for( var i = 0; i < contacts.length; i++ ) {
      log( "Contact " + (i + 1) + ": " + contacts[i] );
    }
  });
};

确定特定联系人是否在列表中:

var contactExists = function(contactName) {
  return getContacts().then(function(contacts) {
    return contacts.indexOf(contactName) >= 0 ? true : false;
  });
};

获取第一个联系人的姓名:

var getFirstContact = function() {
  return getContacts().then(function(contacts) {
    if ( contacts.length > 0 ) {
      return contacts[0];
    }
  });
};

下面是一些使用这三个函数的示例代码:

// Show all contacts
dumpContacts();

// Does contact 'Jane' exist?
contactExists("Jane").then(function(exists){
  log("Contact 'Jane' exist: " + exists);
});

getFirstContact().then(function(firstContact){
  log("first contact: " + firstContact);
});

以上例程使用全局 log() 函数。 console.log() 可以改用。上面的 log() 函数记录到浏览器 window 并且实现如下:

function log() {
  var args = Array.prototype.slice.call(arguments).join(", ");
  console.log(args);
  var output = document.getElementById('output');
  output.innerHTML += args + "<br/>";
}

并在 html 中要求以下内容:

<div id='output'><br/></div>

当上面的代码为运行时,你会看到:

Calling back-end to get contact list.

New data received from back-end.

三遍,没必要

如何解决?

这个样本在Plunker上可以执行: http://plnkr.co/edit/6ysbNTf1lSf5b7L3sJxQ?p=preview

如果希望减少对后端的不必要调用的次数,请坚持承诺,在它仍未解决时,return它用于新调用而不是发出另一个调用后端。

这是一个将异步函数(return承诺)转换为仅在承诺尚未解决时才调用的函数的例程。

var makeThrottleFunction = function (asyncFunction) {
  var currentPromiser = getPromise = function() {
    var promise = new Promise(function(resolve, reject) {
      asyncFunction().then(function(value) {
        resolve(value);
        currentPromiser = getPromise;
      }).catch(function(e) {
        reject(e);
        currentPromiser = getPromise;
      });
    });

    currentPromiser = function() {
      return promise;
    };

    return promise;
  }

  return function () {
    return currentPromiser();
  };
};

在您的例程中,您可以像这样转换 getContacts

var getContacts = makeThrottleFunction(getContacts);

或者直接传递整个函数体。

请记住,这仅适用于对后端的无参数调用。

示例 plunker 代码:http://plnkr.co/edit/4JTtHmFTZmiHugWNnlo9?p=preview

编辑、更新

删除了"nested"ternary模式;添加

  • a) dfd.err(), .catch()处理Promise.reject(/* reason ? */) arguments传递给dfd.fn()
  • b) dfd.process() 内的 args === "" 处理 "":空 String 作为 argument 传递给 dfd.fn()
  • c) 替换 "chaining" .then() 调用 then.apply(dfd.promise, [contactExists, getFirstContact])

本机 Error() 作为 argument 传递:dfd.fn(new Error("error"))global 范围内处理; dfd.fn() 仍然 returns dfd.promise。可能在 dfd.process() 之前或之时调整为 return "early" 在 Error 或将 Error 传递给 dfd.err() ;根据要求。未在下面的 js 中解决。

尝试

var dfd = {
  // set `active` : `false`
  "active": false,
  // set `promise`: `undefined`
  "promise": void 0,
  // process `arguments`, if any, passed to `dfd.fn`
  "process": function process(args) {
    // return `Function` call, `arguments`, 
    // or "current" `dfd.promise`;
    // were `args`:`arguments` passed ?
    // handle `""` empty `String` passed as `args`
    return args === "" || !!args
             // if `args`:`Function`, call `args` with `this`:`dfd`,
             // or, set `args` as `value`, `reason`
             // of "next" `dfd.promise`
             // return "next" `dfd.promise` 
           ? args instanceof Function && args.call(this) || args 
             // set `dfd.active`:`false`
             // when "current" `dfd.promise`:`Promise` `fulfilled`,
             // return "current" `dfd.promise`
           : this.active = true && this.promise
  },
  // handle `fulfilled` `Promise.reject(/* `reason` ? */)`,
  // passed as `args` to `dfd.fn`
  "err": function err(e) {
    // notify , log `reason`:`Promise.reject(/* `reason` ? */)`, if any,
    // or, log `undefined` , if no `reason` passed: `Promise.reject()` 
    console.log("rejected `Promise` reason:", e || void 0);
  },
  // do stuff
  "fn": function fn(args /* , do other stuff */) {
    // set `_dfd` : `this` : `dfd` object
    var _dfd = this;
    // if "current" `dfd.promise`:`Promise` processing,
    // wait for `fulfilled` `dfd.promise`;
    // return `dfd.promise`
    _dfd.promise = !_dfd.active
                     // set, reset `dfd.promise`
                     // process call to `dfd.async`;
                     // `args`:`arguments` passed to `dfd.fn` ?,
                     // if `args` passed, are `args` `function` ?,
                     // if `args` `function`, call `args` with
                     // `this`:`dfd`; 
                     // or, return `args`
                   ? _dfd.process(args)
                     // if `_dfd.active`, `_dfd.promise` defined,
                     // return "current" `_dfd.promise`
                   : _dfd.promise.then(function(deferred) {
                        // `deferred`:`_dfd.promise`
                        // do stuff with `deferred`,
                        // do other stuff,
                        // return "current", "next" `deferred`
                        return deferred
                      })
                      // handle `args`:`fulfilled`,
                      // `Promise.reject(/* `reason` ? */)`
                      .catch(_dfd.err);
    return Promise.resolve(_dfd.promise).then(function(data) {
        // `data`:`undefined`, `_dfd.promise`
        // set `_dfd.active`:`false`,
        // return `value` of "current", "next" `_dfd.promise`
        _dfd.active = false;
        return data
      })
      // handle `fulfilled` `Promise.reject(/* `reason` ? */), 
      // if reaches here ?
      .catch(_dfd.err)
  }
};

    function log() {
        var args = Array.prototype.slice.call(arguments).join(", ");
        console.log(args);
        var output = document.getElementById('output');
        output.innerHTML += args + "<br/>";
    };

    var dumpContacts = function () {
        log('Calling back-end to get contact list.');
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                log('New data received from back-end.');
                resolve(["Mary", "Frank", "Klaus"]);
            }, 3000);
        });
    };

    var getContacts = function () {
        return dfd.async().then(function (contacts) {
            for (var i = 0; i < contacts.length; i++) {
                log("Contact " + (i + 1) + ": " + contacts[i]);
            }
        });
    };

    var contactExists = function (contactName) {
        return dfd.async().then(function (contacts) {
            return contacts.indexOf(contactName) >= 0 ? true : false;
        });
    };

    var getFirstContact = function () {
        return dfd.async().then(function (contacts) {
            if (contacts.length > 0) {
                return contacts[0];
            }
        return contacts
    });
    };


    // Test:

// Show all contacts
dfd.async(dumpContacts)
.then(getContacts)
.then.apply(dfd.promise, [
  // Does contact 'Jane' exist?
  contactExists("Jane").then(function (exists) {
    log("Contact 'Jane' exist: " + exists);
  })
  , getFirstContact().then(function (firstContact) {
    log("first contact: " + firstContact);
  })
]);

function log() {
  var args = Array.prototype.slice.call(arguments).join(", ");
  console.log(args);
  var output = document.getElementById('output');
  output.innerHTML += args + "<br/>";
  return output
};

var dfd = {
  "active": false,
  "promise": void 0,
  "process": function process(args) {
    return args === "" || !!args
           ? args instanceof Function && args.call(this) || args 
           : this.active = true && this.promise
  },
  "err": function err(e) {
    console.log("rejected `Promise` reason:", e || void 0);
  },
  "fn": function fn(args) {
    var _dfd = this;
    _dfd.promise = !_dfd.active
                   ? _dfd.process(args)
                   : _dfd.promise.then(function(deferred) {
                       return deferred
                     })
                     .catch(_dfd.err);
    return Promise.resolve(_dfd.promise).then(function(data) {
        _dfd.active = false;
        return data
      })
      .catch(_dfd.err)
  }
};

var dumpContacts = function() {
  log('Calling back-end to get contact list.');
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      log('New data received from back-end.');
      resolve(["Mary", "Frank", "Klaus"]);
    }, 3000);
  });
};

var getContacts = function() {
  return dfd.fn().then(function(contacts) {
    for (var i = 0; i < contacts.length; i++) {
      log("Contact " + (i + 1) + ": " + contacts[i]);
    }
  });
};

var contactExists = function(contactName) {
  return dfd.fn().then(function(contacts) {
    return contacts.indexOf(contactName) >= 0 ? true : false;
  });
};

var getFirstContact = function() {
  return dfd.fn().then(function(contacts) {
    if (contacts.length > 0) {
      return contacts[0];
    }
    return contacts
  });
};


// Test:

// Show all contacts
dfd.fn(dumpContacts)
  .then(getContacts)
  .then(function() {
    // Does contact 'Jane' exist?
    return contactExists("Jane").then(function(exists) {
      log("Contact 'Jane' exist: " + exists);
    })
  })
  .then(function() {
    return getFirstContact().then(function(firstContact) {
      log("first contact: " + firstContact);
    })
  });
<body>
  Must use browser that supportes the Promises API, such as Chrome

  <div id='output'>
    <br/>
  </div>

  <hr>
</body>

只需将结果缓存在进行调用的函数中:

function cache(promiseReturningFn){
    var cachedVal = null;  // start without cached value
    function cached(){
        if(cachedVal) return cachedVal; // prefer cached result
        cachedVal = promiseReturningFn.apply(this, arguments); // delegate
        return cachedVal; // after we saved it, return it
    }
    cached.flush = function(){ cachedVal = undefined; };
    return cached;
}

这有一个警告,即实际结果为空时会失败,但除此之外它可以很好地完成工作。

您现在可以缓存任何 promise 返回函数 - 上面的版本只缓存忽略参数 - 但您可以构建一个类似的函数,它也有一个 Map 和基于不同参数的缓存 - 但让我们专注于您的用例。

var getContactsCached = cache(getContacts);

getContactsCached();
getContactsCached();
getContactsCached(); // only one async call ever made

缓存方法实际上什至与 promises 无关——它所做的只是接受一个函数并缓存它的结果——你可以将它用于任何事情。事实上,如果您正在使用像 underscore 这样的库,您已经可以使用 _.memoize 为您完成它了。