在 Chrome 扩展程序中,如何确保在使用 chrome-promise 的下一个承诺之前解决上一个承诺?

Posted

技术标签:

【中文标题】在 Chrome 扩展程序中,如何确保在使用 chrome-promise 的下一个承诺之前解决上一个承诺?【英文标题】:In a Chrome extension, how to ensure previous promise resolves before the next one using chrome-promise? 【发布时间】:2017-11-09 04:29:33 【问题描述】:

我一直在使用 chrome-promise 库来包装 Chrome 扩展 API,其外观是返回承诺而不是使用回调。这通常工作得很好,但我似乎遇到了chrome.storage.local API 的问题。

我的扩展程序的事件页面监听 chrome.tabs.onActivatedchrome.tabs.onRemoved 事件。当它收到onActivated 事件时,它将选项卡信息添加到数组并调用chrome.storage.local.set(data) 将更新后的数组存储在本地存储中。

当它收到onRemoved 事件时,它调用chromepromise.storage.local.get(null).then(...) 通过promise 获取选项卡列表,从数组中删除选项卡信息,然后再次调用chrome.storage.local.set() 以保存更新的数组。

问题在于onActivated 事件似乎在来自onRemoved 事件的承诺流解决之前触发。所以onActivated 处理程序检索旧的存储数组,关闭的选项卡仍在其中,然后推送新激活的选项卡。因此,存储的选项卡数据现在包含一个已关闭的选项卡。

我假设这是使用承诺而不是回调的问题,但我想知道是否有其他人遇到过这个库的这个问题并解决了这个问题。

更新

作为 wOxxOm points out,这是“仲裁对单个资源(例如 chrome.storage)的不可预测的异步访问”的一般问题,而不是 chrome-promise 库所独有的。

经过一番研究,我想出了几个解决方案,并在下面添加了答案。一个使用互斥锁来确保(我认为)一个承诺链在chrome.storage 中获取和设置数据在下一个开始之前完成。另一个将根据事件创建的整个承诺链排队,并且在当前一个完全完成之前不会开始下一个。我不确定哪个更好,但我认为锁定更短的时间会更好。

欢迎任何建议或更好的答案。

【问题讨论】:

chrome.storage.local.set()chromepromise.storage.local.get(null).then(...) 的.then 内? 您需要为 chrome.storage 实现自己的基于互斥锁的控制器或使用队列 (example) 和去抖动。 我担心的是,如果扩展程序中没有持久的背景页面,我不确定是否有办法做到这一点。事件页面在停止运行时会被刷新,所以我将状态保持在chrome.storage.local。但是,如果用于设置然后得到它的承诺链可以以不同的顺序发生,我看不出如何持久化一个队列来确保通过该过程的顺序。 排队或使用互斥锁是仲裁对单个资源(如 chrome.storage)的不可预测的异步访问的标准机制。事件页面在 5 秒不活动后被卸载,因此它不应该影响这种情况。 @w0xx0m,我查看了队列示例,但不确定它在此处如何应用。我在问题中添加了我尝试过的方法的简化版本,但它不起作用。任何指针将不胜感激。 【参考方案1】:

队列

此解决方案使用非常简单的排队机制。事件处理程序调用 queue() 并带有一个启动承诺链以处理该事件的函数。如果队列中还没有 promise,则立即调用该函数。否则,它会被推送到队列中,并在当前的 Promise 链完成时触发。这意味着一次只能处理一个事件,效率可能不高。

var taskQueue = [];

function queue(
    fn)

    taskQueue.push(fn);
    processQueue();


function processQueue()

    const nextTask = taskQueue[0];

    if (nextTask && !(nextTask instanceof Promise)) 
        taskQueue[0] = nextTask()
            .then((result) => 
                console.log("RESULT", result);
                taskQueue.shift();
                processQueue();
        );
    



function onActivated(tabID) 
    console.log("EVENT onActivated", tabID);
    queue(() => Promise.resolve(tabID).then(tab => addTab(tab)));


function onRemoved(tabID) 
    console.log("EVENT onRemoved", tabID);
    queue(() => removeTab(tabID));



var localData = 
        tabs: []
    ;

function delay(time) 
   return new Promise(resolve => setTimeout(resolve, time));


function getData()

    return delay(0).then(() => JSON.parse(JSON.stringify(localData)));


function saveData(data, source)

    return delay(0)
        .then(() => 
            localData = data;
            console.log("save from:", source, "localData:", localData);
            return Promise.resolve(localData);
        );


function addTab(tabID)

    return getData().then((data) => 
        console.log("addTab", tabID, "data:", data);
        data.tabs = data.tabs.filter(tab => tab != tabID);
        data.tabs.push(tabID);
        return saveData(data, "addTab");
    );


function removeTab(tabID)

    return getData().then((data) => 
        console.log("removeTab", tabID, "data:", data);
        data.tabs = data.tabs.filter(tab => tab != tabID);
        return saveData(data, "removeTab");
    );



const events = [
    () => onActivated(1),
    () => onActivated(2),
    () => onActivated(3),
    () => onActivated(4),
    () => onActivated(2),
    () =>  onRemoved(2); onActivated(3) 
];

function playNextEvent()

    var event = events.shift();

    if (event) 
        delay(0).then(() =>  event(); delay(0).then(playNextEvent) );
    


playNextEvent();

【讨论】:

【参考方案2】:

互斥体

更新:我最终使用以下方法创建了一个module,它使用互斥锁来确保 Chrome 扩展存储的获取和集合保持其顺序。到目前为止,它似乎运行良好。


此解决方案使用来自this article 的互斥锁实现。 addTab()removeTab() 调用 storageMutex.synchronize() 的函数完成所有存储获取和设置。这应该可以防止后面的事件影响早期事件的存储。

下面的代码是扩展的一个非常简化的版本,但它确实可以运行。底部的 playNextEvent() 调用模拟打开 4 个选项卡,切换回选项卡 2 并关闭它,然后激活选项卡 3。使用setTimeout()s 是为了让所有内容都不会作为一个长调用堆栈运行。

function Mutex() 
    this._busy  = false;
    this._queue = [];


Object.assign(Mutex.prototype, 
    synchronize: function(task) 
        var self = this;

        return new Promise(function(resolve, reject) 
            self._queue.push([task, resolve, reject]);
            if (!self._busy) 
                self._dequeue();
            
        );
    ,

    _dequeue: function() 
        var next = this._queue.shift();

        if (next) 
            this._busy = true;
            this._execute(next);
         else 
            this._busy = false;
        
    ,

    _execute: function(record) 
        var task = record[0],
            resolve = record[1],
            reject = record[2],
            self = this;

        task().then(resolve, reject).then(function() 
            self._dequeue();
        );
    
);


const storageMutex = new Mutex();


function onActivated(tabID) 
    console.log("EVENT onActivated", tabID);
    return Promise.resolve(tabID).then(tab => addTab(tab));


function onRemoved(tabID) 
    console.log("EVENT onRemoved", tabID);
    return removeTab(tabID);



var localData = 
        tabs: []
    ;

function delay(time) 
   return new Promise(resolve => setTimeout(resolve, time));


function getData()

    return delay(0).then(() => JSON.parse(JSON.stringify(localData)));


function saveData(data, source)

    return delay(0)
        .then(() => 
            localData = data;
            console.log("save from:", source, "localData:", localData);
            return Promise.resolve(localData);
        );


function addTab(tabID)

    return storageMutex.synchronize(() => getData().then((data) => 
        console.log("addTab", tabID, "data:", data);
        data.tabs = data.tabs.filter(tab => tab != tabID);
        data.tabs.push(tabID);
        return saveData(data, "addTab");
    ));


function removeTab(tabID)

    return storageMutex.synchronize(() => getData().then((data) => 
        console.log("removeTab", tabID, "data:", data);
        data.tabs = data.tabs.filter(tab => tab != tabID);
        return saveData(data, "removeTab");
    ));



const events = [
    () => onActivated(1),
    () => onActivated(2),
    () => onActivated(3),
    () => onActivated(4),
    () => onActivated(2),
    () =>  onRemoved(2); onActivated(3) 
];

function playNextEvent()

    var event = events.shift();

    if (event) 
        delay(0).then(() =>  event(); delay(0).then(playNextEvent) );
    


playNextEvent();

【讨论】:

以上是关于在 Chrome 扩展程序中,如何确保在使用 chrome-promise 的下一个承诺之前解决上一个承诺?的主要内容,如果未能解决你的问题,请参考以下文章

怎样在Chrome浏览器加入迅雷支持?

在 Chrome Web Store URL 的 Chrome 扩展中绕过 X-Frame-Options

带有 chrome 扩展的 cookie 或 localStorage

带有 chrome 扩展的 cookie 或 localStorage

Chrome 扩展 - 从网页中检索全局变量

如何使用 chrome.tabs.getCurrent 在 Chrome 扩展程序中获取页面对象?