在谷歌浏览器插件中定义web component

762 阅读4分钟

写在前面:本文并不是浏览器插件或web component的使用教程,而是他们两者的结合使用,所以需要读者对浏览器插件及web component有基本的认识。

零、引言

最近接到了一个需求,是开发一个谷歌浏览器插件(MV3版本)。

此插件需要在用户打开指定页面时,请求后端接口,将后端返回的一些数据展示在页面上。(说白了就是使用插件编写页面UI)。

其实要实现这个需求并不困难,众所周知,插件的contentScripts是可以访问页面的部分属性,如document元素。所以通过document.createElement、Node.appendChild等方法创建元素并插入至页面的目标位置,是完全可以实现需求的。

但如果我们需要编写的UI有多个可复用的部分,使用上述原始方法就显得不那么优雅了。

此时我们自然会想到“组件”这一概念,正如vue组件和react组件一样的可复用组件。那么我们就可联想到web component这一技术,来封装、复用组件。

一、在html中使用

不考虑浏览器插件,只考虑在正常网页HTML(即执行环境包含DOM和BOM)中时,使用web component定义自定义元素是非常方便的,只需一个组件类customElements.defineapi。

下面代码展示了如何使用web components创建自定义元素:

// 封装Card组件类
class Card extends HTMLElement {
  constructor() {
    super();
    // 获取传入的prop
    let text = this.getAttribute("text");

    const shadow = this.attachShadow({ mode: "open" });

    // 创建组件外层元素
    const wrapper = document.createElement("div");
    wrapper.setAttribute("class", "card-wrapper");

    wrapper.innerText = text;

    // 将元素添加到shadow DOM
    shadow.appendChild(wrapper);

    // 添加一些样式
    const style = document.createElement("style");
    style.textContent = `
      .card-wrapper {
        border: 1px solid #ff522f;
        border-radius: 8px;
        padding: 4px;
        font-size: 14px;
        color: #ff522f;
      }
    `;

    shadow.appendChild(style);
  }
}

// 定义自定义元素
customElements.define("message-card", Card);

二、在插件中使用

但非常遗憾的是,在浏览器插件的contentScripts中,是不能访问customElements,那自然就无法调用define方法了。

2-1、思路

既然浏览器插件的contentScripts中无法访问customElements,那么我们希望借助contentScripts可使用的一些api,去使得网页HTML帮我们执行定义自定义元素的相关代码。

所以我们的思路就是,利用插件的contentScripts生成一个script标签,并将其放置在页面的html中,而这个script标签引用的就是上面定义自定义元素的代码。
那么这段代码的运行环境就是网页的html,也就可以访问customElements对象了。

2-2、实践

首先列出本次例子的目录结构:
contentScripts文件夹中的index.js就是普通的contentScripts文件。而card.js就是我们定义自定义元素的代码,它的内容与第一节代码一样。

plugs/
├── src/
│   ├── background/
│   │   └── index.js
│   ├── contentScripts/
│       ├── index.js
│       └── card.js
├── manifest.json
└── index.html

创建好文件之后就要在manifest.json中标明文件该如何使用了。

由下面的代码片段可知,contentScripts/index.js是正常作为contentScripts的一部分被引用的。

contentScripts/card.js就需要配置在web_accessible_resources字段中,这代表了这部分的资源可以被网页访问。 (需要注意的是:resources中的路径是基于插件项目根目录的路径)

{
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": [
        "./src/contentScripts/index.js"
      ],
      "run_at": "document_end",
      "all_frames": true
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["src/contentScripts/card.js"],
      "matches": ["<all_urls>"]
    }
  ]
}

在manifest.json中配置了contentScripts/card.js可被网页访问后,就可以借助chrome.runtime.getURL 这个api,为card.js生成一个url,并作为script元素的src属性值。(需要注意的是chrome.runtime.getURL的参数也是基于插件项目根目录的路径,而非当前文件的相对路径)

然后将此script元素插入至网页html中,那么此script就能加载card.js的内容,并且运行环境就是网页HTML,而非插件的contentScripts了。

// 生成script
const script = document.createElement("script");
// 使用chrome.runtime.getURL生成url
script.src = chrome.runtime.getURL(
  "src/contentScripts/card.js" // 注意:这里的路径也是基于插件项目根目录的路径
);
// 将script插入至页面的html文档中,使其运行环境是网页html
document.documentElement.appendChild(script);

如下截图就是被插进网页的script元素加载的js资源,也就是在插件中定义的自定义元素代码。 image.png

三、总结

总的来说,在浏览器插件中定义自定义元素,就是分三步走:

  1. 正常编写web component代码(但是不要在manifest.json的content_scripts中引入)
  2. 配置manifest.json的web_accessible_resources字段,这代表了这部分的资源可以被网页访问。
  3. 在常规contentScripts文件中,借助chrome.runtime.getURL生成组件文件url,生成script元素,引用此url,并将其插入页面html中。