无法将参数传递给 chrome.declarativeContent.SetIcon

Can't pass arguments to chrome.declarativeContent.SetIcon

我正在尝试开发一个简单的 chrome 扩展。有一个 pageAction 的默认图标应该出现在具有特定 URL (http://www.example.com/*) 的页面上。

有两个文件

manifest.json

{
  "manifest_version": 2,
  "name": "name",
  "description": "description",
  "version": "1.0",
  "background": {
    "scripts": [
      "background.js"
    ],
    "persistent": false
  },
  "page_action": {
    "default_icon" : "images/icons/19.png"
  },
  "permissions": [
    "declarativeContent"
  ]
}

background.js

chrome.runtime.onInstalled.addListener(function () {
    chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
        chrome.declarativeContent.onPageChanged.addRules([
            {
                // rule1
                conditions : [
                    new chrome.declarativeContent.PageStateMatcher({
                        pageUrl : {urlPrefix : 'http://www.example.com/'}
                    })
                ],
                actions    : [
                    new chrome.declarativeContent.ShowPageAction()
                ]
            },
            {
                // rule2
                conditions : [
                    new chrome.declarativeContent.PageStateMatcher({
                        pageUrl : {queryContains : 'q1=green'}
                    })
                ],
                actions    : [
                    new chrome.declarativeContent.SetIcon({
                        path : {"19" : "images/icons/green.png"}
                    })
                ]
            }
        ]);
    });
});

rule1 应该显示 pageAction 的图标,rule2 应该将带有 URL 的页面上的图标更改为绿色版本,看起来像 http://www.example.com/?q1=green

但是在安装扩展的过程中出现了以下情况:

Error in response to events.removeRules: Error: Invalid value for argument 1. Property '.0': Value does not match any valid type choices.

我深入研究了这个错误,文档似乎没有很好地反映未实现使用 path 参数这一事实。这肯定是一个错误,已跟踪 here.

现在,要解决此问题,您需要在调用 SetIcon 之前加载图像并将其转换为 ImageData 格式。

// Takes a local path to intended 19x19 icon
//   and passes a correct SetIcon action to the callback
function createSetIconAction(path, callback) {
  var canvas = document.createElement("canvas");
  var ctx = canvas.getContext("2d");
  var image = new Image();
  image.onload = function() {
    ctx.drawImage(image,0,0,19,19);
    var imageData = ctx.getImageData(0,0,19,19);
    var action = new chrome.declarativeContent.SetIcon({imageData: imageData});
    callback(action);
  }
  image.src = chrome.runtime.getURL(path);
}

chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
  createSetIconAction("images/icons/green.png", function(setIconAction) {
    chrome.declarativeContent.onPageChanged.addRules([
      /* rule1, */
      {
        conditions : [
          new chrome.declarativeContent.PageStateMatcher({
            pageUrl : {queryContains : 'q1=green'}
          })
        ],
        actions    : [ setIconAction ]
      }
    ]);        
  });
});

如果需要,这可以推广以支持高 DPI 图标 (19 + 38):

function createSetIconAction(path19, path38, callback) {
  var canvas = document.createElement("canvas");
  var ctx = canvas.getContext("2d");
  var image19 = new Image();
  image19.onload = function() {
    ctx.drawImage(image19,0,0,19,19); // fixed
    var imageData19 = ctx.getImageData(0,0,19,19);
    var image38 = new Image();
    image38.onload = function() {
      ctx.drawImage(image38,0,0,38,38);
      var imageData38 = ctx.getImageData(0,0,38,38);      
      var action = new chrome.declarativeContent.SetIcon({
        imageData: {19: imageData19, 38: imageData38}
      });
      callback(action);
    }
    image38.src = chrome.runtime.getURL(path38);
  }
  image19.src = chrome.runtime.getURL(path19);
}

其实可以用new chrome.declarativeContent.SetIcon({ path:'yourPath.png' }), 无需指定大小path: {"19": "images/icons/green.png"},其默认值为:16 使用declarativeContent.SetIcon需要注意一个问题,其实是个bug。

实际使用path最终会自动转换为ImageData

看截图:

declarativeContent.SetIcon错误的根本原因是:它是异步的API,但同时又没有异步回调。你唯一能做的就是等待。

const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
console.log(action.imageData); // =>  undefined

看截图:

// invalid
new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' }, action => console.log(action));  

需要等待一段时间:

const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
setTimeout(() => {
  console.log(action.imageData); // {16: ArrayBuffer(1060)}
}, 5);

看截图:

当你明白了SetIcon错误的原因后,问题就迎刃而解了。 你只需要将addRules的操作放在事件中即可。

onInstalled 事件

const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};

chrome.runtime.onInstalled.addListener(() => {
  chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
    chrome.declarativeContent.onPageChanged.addRules([rule2]);
  });
});

pageAction.onClicked

const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};

chrome.pageAction.onClicked.addListener(() => {
  if (condition) {
    chrome.declarativeContent.onPageChanged.removeRules(['hideAction']);
  } else {
    chrome.declarativeContent.onPageChanged.addRules([rule2]);
  }
});

还有一些相关资料:

SetIcon源代码

declarativeContent.SetIcon = function (parameters) {
  // TODO(devlin): This is very, very wrong. setIcon() is potentially
  // asynchronous (in the case of a path being specified), which means this
  // becomes an "asynchronous constructor". Errors can be thrown *after* the
  // `new declarativeContent.SetIcon(...)` call, and in the async cases,
  // this wouldn't work when we immediately add the action via an API call
  // (e.g.,
  //   chrome.declarativeContent.onPageChange.addRules(
  //       [{conditions: ..., actions: [ new SetIcon(...) ]}]);
  // ). Some of this is tracked in http://crbug.com/415315.
  setIcon(
    parameters,
    $Function.bind(function (data) {
      // Fake calling the original function as a constructor.
      $Object.setPrototypeOf(this, nativeSetIcon.prototype);
      $Function.apply(nativeSetIcon, this, [data]);
    }, this)
  );
};

相关问题讨论: http://crbug.com/415315

无解

正如我之前提到的,这是一个错误。没有解决方案,只有变通办法。

解决方法

#1:使用canvas

绘制图标

已经Xan described in

#2 等待图标加载(超时 hack)

感谢 weiya-ou's 我意识到我可以等待异步图标数据转换完成。

// Make your handler `async`
chrome.runtime.onInstalled.addListener(async () => {
  const action = await new chrome.declarativeContent.SetIcon({
    path: {
      19: 'images/19.png',
      38: 'images/38.png',
    },
  })

  // THE WAIT STARTS

  // Wait max. 10 loops
  for (let i = 0; i < 10; i++) {
    // Create a promise
    const checkAvailability = new Promise((resolve) => {
      // Resolve promise after 100ms
      setTimeout(() => resolve(!!action.imageData), 100)
    })

    // Wait for the promise resolution
    const isAvailable = await checkAvailability

    // When image available, we are done here
    if (isAvailable) break
  }

  // THE WAIT ENDS

  const condition = new chrome.declarativeContent.PageStateMatcher({
    pageUrl: { hostEquals: 'my.page.net' },
  })

  chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
    chrome.declarativeContent.onPageChanged.addRules([
      {
        conditions: [condition],
        actions: [action],
      },
    ]);
  });
});

#3 使用 chrome.tabs

您需要 tabs 权限(如 here 所述)。

chrome.tabs.onUpdated.addListener((tabId, { status }, { url }) => {
  // Only check when URL is resolved
  if (status !== 'complete') return

  // What is our target page?
  const isOurPage = url?.match(/my\.page\.net/)

  if (isOurPage) {
    // Show active icon
    chrome.pageAction.setIcon({
      path: {
        19: 'images/19.png',
        38: 'images/38.png',
      },
      tabId,
    })
  } else {
    // Show inactive icon
    chrome.pageAction.setIcon({
      path: {
        19: 'images/19-inactive.png',
        38: 'images/38-inactive.png',
      },
      tabId,
    })
  }
})