近万字新手 chrome 扩展开发简单入门

15,688 阅读23分钟

前言

本文旨在 chrome 入门开发,如果需要深入开发,可以去 chrome 官网进行学习。 官方提供的了一个修改网页背景颜色的demo,可以点击下载,如果你的下载有问题可以去 github

本文将会列举一些常用的概念说明,包含 5种js 类型对比、 6个核心、消息通信和一些动态化注入、打包发布等。

本文最后会列举一个将页面变为暗黑模式的 chrome demo

demo 演示效果如下:

通过上面的展示效果大家会发现有个小细节,就是在 jd.com tab页面时,插件是会高亮展示,在其他 tab 页面颜色会置灰,这是怎么实现的呢,后面的 demo 会为大家进行解析

什么是 chrome 插件?

本文讲的 chrome 插件真正的叫法应该是 chrome 扩展(Chrome Extension), 真正的插件是指更底层浏览器功能扩展,可能需要对chrome 源码有一定的了解才能去开发。

本文的 chrome 插件是指我们日常使用的一些 chrome 扩展插件。

Extensions are small software programs that customize the browsing experience. They enable users to tailor Chrome functionality and behavior to individual needs or preferences. They are built on web technologies such as HTML, JavaScript, and CSS.

Chrome Extension 是可以定制浏览体验的小型软件程序。它们使用户可以根据个人需要或偏好来定制 Chrome 功能和行为。它基于Web技术(例如HTML,JavaScript和CSS)构建。

为什么要学习 Chrome Extension 开发呢?

会了 Chrome Extension 开发,可以方便我们定制属于自己的浏览器,增强一些浏览器功能,加强自己学习能力,也可以对我们的业务有一些其他方面的思考。

技多不压身,多会点,说不定你再找下一家公司的时候,会多一些技术点。

6 个核心

6 个核心概念包括 Manifest FormatManage EventsDesign User InterfaceContent ScriptsDeclare Permissions and Warn UsersGive Users Options

Manifest Format - 官方文档

每个 Chrome Extension都有一个 JSON 格式的清单文件,名为 manifest.json,提供重要信息。 用来配置所有和插件相关的配置,必须放在项目根目录下面。 简单配置如下所示

{
  "name": "TimeStone",
  "manifest_version": 2,
  "version": "1.0",
  "description": "TimeStone 扩展程序",
  "page_action": {
    "default_icon": "images/icon.png",
    "default_title": "TimeStone 插件",
    "default_popup": "html/home.html"
  },
  "background": {
    "scripts": ["js/background.js"]
  },
  "options_page": "html/options.html",
  "homepage_url": "https://juejin.im/user/4230576473387773",
  "permissions": [
    "tabs",
    "storage",
    "activeTab"
  ]
}

配置介绍如下: 【注意】必须要有加粗标识,推荐使用有高亮

  • manifest_version - 必须要有,还只能是2
  • name - 插件名称
  • version - 版本号
  • description - 描述
  • icons - 图标
  • homepage_url - 插件主页,哈哈,我一般设置为自己掘金主页
  • background - 会一直常驻的后台JS或后台页面,有两种指定方式
    "background":
     {
     	// 2种指定方式,如果指定JS,那么会自动生成一个背景页
     	"page": "background.html"
     	//"scripts": ["js/background.js"]
     }
    
  • browser_action、page_action、app - 浏览器右上角展示,三则只能选其一
    "browser_action": 
     {
     	"default_icon": "img/icon.png",
     	// 图标悬停时的标题,可选
     	"default_title": "这是一个示例Chrome插件",
     	"default_popup": "popup.html"
     }
     "page_action":
     {
     	"default_icon": "img/icon.png",
     	"default_title": "我是pageAction",
     	"default_popup": "popup.html"
     }
    
  • permissions - 插件权限申请设置
    "permissions":
     [
     	"contextMenus", // 右键菜单
     	"tabs", // 标签
     	"notifications", // 通知
     	"webRequest", // web请求
     	"webRequestBlocking",
     	"storage", // 插件本地存储
     ]
    
  • web_accessible_resources - 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
  • chrome_url_overrides - 覆盖浏览器默认页面
  • options_ui - 插件配置页写法
  • omnibox - 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
    "omnibox": { "keyword" : "rick" }
    
  • default_locale - 默认语言
  • devtools_page - devtools页面入口,注意只能指向一个 HTML 文件,不能是 JS 文件

Manage Events with Background Scripts(使用后台脚本管理事件) - 官方文档

Chrome Ex 是基于事件的程序,用于修改或增强 Chrome 浏览器的体验,事件是浏览器触发器,例如,导航到新页面,删除书签或关闭选项卡。扩展程序在其后台脚本中监视这些事件,然后按照指定的指示进行响应。

后台页面在需要时被加载,而在空闲时被卸载。事件的一些示例包括:

  • 该扩展程序首先安装或更新为新版本。
  • 后台页面正在监听事件,并且已调度该事件。
  • 内容脚本或其他扩展 发送消息
  • 程序中的另一个视图(例如弹出窗口)调用 runtime.getBackgroundPage

有效的后台脚本会保持睡眠状态,直到它们正在监听的事件触发,然后按照指定的指令对其进行反应,最后卸载。

注册后台脚本(Register Background Scripts)

manifest.json 中进行配置,如下

{
  "name": "Awesome Test Extension",
  ...
  "background": {
    "scripts": ["js/background.js"],
    "persistent": false
  },
  ...
}

初始化扩展

监听 runtime.onInstalled 事件,使用此事件可以设置状态或进行一次初始化.例如上下文菜单

 chrome.runtime.onInstalled.addListener(function() {
    chrome.contextMenus.create({
      "id": "sampleContextMenu",
      "title": "Sample Context Menu",
      "contexts": ["selection"]
    });
  });

设置监听器

围绕扩展依赖的事件构建背景脚本。定义功能上相关的事件可使后台脚本处于休眠状态,直到这些事件被触发为止,并防止扩展错过重要的触发器。

侦听器必须从页面开始同步注册。

 chrome.runtime.onInstalled.addListener(function() {
    chrome.contextMenus.create({
      "id": "sampleContextMenu",
      "title": "Sample Context Menu",
      "contexts": ["selection"]
    });
  });

  // This will run when a bookmark is created.
  chrome.bookmarks.onCreated.addListener(function() {
    // do something
  });

不要异步注册侦听器,因为它们不会被正确触发。

 chrome.runtime.onInstalled.addListener(function() {
    // ERROR! Events must be registered synchronously from the start of
    // the page.
    chrome.bookmarks.onCreated.addListener(function() {
      // do something
    });
  });

扩展程序可以通过调用将侦听器从其后台脚本中删除 removeListener。如果事件的所有侦听器都被删除,Chrome将不再为该事件加载扩展程序的后台脚本。

  chrome.runtime.onMessage.addListener(function(message, sender, reply) {
      chrome.runtime.onMessage.removeListener(event);
  });

筛选事件

使用支持事件过滤器的 API 将侦听器限制在扩展关心的情况下。如果扩展程序正在监听 tabs.onUpdated 事件,请尝试使用 webNavigation.onCompleted 带有过滤器的事件,因为tabs API不支持过滤器。

chrome.webNavigation.onCompleted.addListener(function() {
      alert("This is my favorite website!");
  }, {url: [{urlMatches : 'https://www.google.com/'}]});

响应事件

事件触发后,存在侦听器可以触发功能。要对事件做出反应,请在侦听器事件内部构造所需的反应。

  chrome.runtime.onMessage.addListener(function(message, callback) {
    if (message.data == “setAlarm”) {
      chrome.alarms.create({delayInMinutes: 5})
    } else if (message.data == “runLogic”) {
      chrome.tabs.executeScript({file: 'logic.js'});
    } else if (message.data == “changeColor”) {
      chrome.tabs.executeScript(
          {code: 'document.body.style.backgroundColor="orange"'});
    };
  });

卸载后台脚本

数据应定期保存,以免重要信息丢失而导致扩展名崩溃而无法接收 onSuspend。使用storage API可以帮助完成此任务。

 chrome.storage.local.set({variable: variableInformation});

如果扩展使用消息传递,请确保所有端口均已关闭。在关闭所有消息端口之前,后台脚本不会卸载。侦听runtime.Port.onDisconnect事件将提供有关打开的端口何时关闭的见解。使用runtime.Port.disconnect手动关闭它们。

 chrome.runtime.onMessage.addListener(function(message, callback) {
    if (message == 'hello') {
      sendResponse({greeting: 'welcome!'})
    } else if (message == 'goodbye') {
      chrome.runtime.Port.disconnect();
    }
  });

通过监视扩展的条目何时从Chrome的任务管理器中出现和消失,可以观察到后台脚本的寿命。

点击Chrome菜单,将鼠标悬停在更多工具上,然后选择“任务管理器”,即可打开任务管理器。

几秒钟不活动后,后台脚本会自行卸载。如果需要最后一分钟的清理,请收听runtime.onSuspend事件。

  chrome.runtime.onSuspend.addListener(function() {
    console.log("Unloading.");
    chrome.browserAction.setBadgeText({text: ""});
  });

但是,与相比,应首选持久数据runtime.onSuspend。它不允许进行尽可能多的清理,并且在崩溃时没有帮助。

Design User Interface - 官方文档

扩展用户界面应有目的且最少。就像扩展本身一样,UI应该自定义或增强浏览体验,而不会分散注意力。

在所有页面上激活扩展名

当扩展程序的功能在大多数情况下可用时, 请使用browser_action

  • manifest.json 注册 browser_action

      {
      "name": "My Awesome browser_action Extension",
      ...
      "browser_action": {
        ...
      }
      ...
    }
    

    声明时"browser_action",图标保持彩色,表示扩展名可供用户使用。

  • Add a badge

    所谓badge就是在图标上显示一些文本,在浏览器图标上方显示一个彩色横幅,最多包含四个字符。它们只能由"browser_action"在其清单中声明的扩展使用 。

    通过调用设置徽章的文本,通过调用 设置横幅的颜色 。 chrome.browserAction.setBadgeText chrome.browserAction.setBadgeBackgroundColor

在所选页面上激活扩展

当扩展程序的功能仅在定义的情况下可用时, 请使用 page_action

  • manifest.json 注册 page_action
      {
      "name": "My Awesome page_action Extension",
      ...
      "page_action": {
        ...
      }
      ...
    }
    
    "page_action" 仅当扩展名对用户可用时, 声明才会使图标变色,否则它将以灰度显示。

  • 定义激活扩展的规则
    通过 在后台脚本chrome.declarativeContentruntime.onInstalled侦听器下调用, 定义何时可以使用扩展的规则 。通过URL 进行的页面操作示例扩展设置了一个条件,即该URL必须包含“ g”。如果满足条件,则分机呼叫。 declarativeContent.ShowPageAction() [注意]: 这里就是回答在 jd.com tab页面时,插件是会高亮展示,在其他 tab 页面颜色会置灰

      chrome.runtime.onInstalled.addListener(function() {
      // Replace all rules ...
      chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
        // With a new rule ...
        chrome.declarativeContent.onPageChanged.addRules([
          {
            // That fires when a page's URL contains a 'g' ...
            conditions: [
              new chrome.declarativeContent.PageStateMatcher({
                pageUrl: { urlContains: 'jd.com' },
              })
            ],
            // And shows the extension's page action.
            actions: [ new chrome.declarativeContent.ShowPageAction() ]
          }
        ]);
      });
    });
    
  • 启用或禁用扩展 扩展程序使用 "page_action" 可以通过调用pageAction.show和来 动态激活和禁用 pageAction.hide

    MAPPY 样本扩展扫描网页的地址,并显示在静态地图上的位置弹出。由于扩展名取决于页面内容,因此无法声明规则来预测哪些页面将是相关的。相反,如果在页面上找到地址,它将调用 pageAction.show 以使图标变色并发出该扩展名在该选项卡上可用的信号。

    chrome.runtime.onMessage.addListener(function(req, sender) {
      chrome.storage.local.set({'address': req.address})
      chrome.pageAction.show(sender.tab.id);
      chrome.pageAction.setTitle({tabId: sender.tab.id, title: req.address});
    });
    

提供扩展图标

扩展程序至少需要一个图标来表示它。尽管可以接受WebKit支持的任何格式,包括BMP,GIF,ICO和JPEG,但以PNG格式提供的图标可以提供最佳的视觉效果。

  • 指定工具栏图标

    工具栏特定的图标注册在manifest.json "default_icon"下 browser_actionpage_action清单中的 字段 中。鼓励包括多个尺寸以适应16倾角空间。建议至少使用16x16和32x32尺寸。

     {
      "name": "My Awesome page_action Extension",
      ...
      "page_action": {
        "default_icon": {
          "16": "extension_toolbar_icon16.png",
          "32": "extension_toolbar_icon32.png"
        }
      }
      ...
    }
    

    所有图标应为正方形,否则可能会变形。如果没有提供图标,则Chrome会将通用图标添加到工具栏。

  • 创建并注册其他图标 - 在工具栏外使用以下尺寸的其他图标。

    图标大小图标使用
    16x16扩展页面上的网站图标
    32x32Windows计算机通常需要此大小。提供此选项将防止尺寸失真缩小48x48选项。
    48x48显示在扩展程序管理页面上
    128x128显示在安装中和Chrome Webstore中
    {
        "name": "My Awesome Extension",
        ...
        "icons": {
          "16": "extension_icon16.png",
          "32": "extension_icon32.png",
          "48": "extension_icon48.png",
          "128": "extension_icon128.png"
        }
        ...
    }
    

其他UI功能

  • Popup

    弹出窗口是一个HTML文件,当用户单击工具栏图标时,它将显示在特殊窗口中。弹出窗口的工作原理与网页非常相似;它可以包含指向样式表和脚本标签的链接,但不允许内联JavaScript。

    Drink Water Event 示例弹出窗口显示可用的计时器选项。用户通过单击提供的按钮之一来设置警报。

<html>
    <head>
      <title>Water Popup</title>
    </head>
    <body>
        <img src='./stay_hydrated.png' id='hydrateImage'>
        <button id='sampleSecond' value='0.1'>Sample Second</button>
        <button id='15min' value='15'>15 Minutes</button>
        <button id='30min' value='30'>30 Minutes</button>
        <button id='cancelAlarm'>Cancel Alarm</button>
     <script src="popup.js"></script>
    </body>
  </html>

上面的设置可以在 browser_action 或者 page_action 中生命

{
    "name": "Drink Water Event",
    ...
    "browser_action": {
      "default_popup": "popup.html"
    }
    ...
  }

也可以通过调用或 来动态设置弹出窗口 。browserAction.setPopuppageAction.setPopup

chrome.storage.local.get('signed_in', function(data) {
    if (data.signed_in) {
      chrome.browserAction.setPopup({popup: 'popup.html'});
    } else {
      chrome.browserAction.setPopup({popup: 'popup_sign_in.html'});
    }
  });
  • Tooltip - 将鼠标悬停在浏览器图标上时,请使用 Tooptip 向用户提供简短说明或说明。

Tooltip 提示已在"default_title"字段 browser_actionpage_action中注册。

  {
  "name": "Tab Flipper",
   ...
    "browser_action": {
      "default_title": "Press Ctrl(Win)/Command(Mac)+Shift+Right/Left to flip tabs"
    }
  ...
 }

Tooltips 的内容也可以通过方法进行更新 browserAction.setTitle and pageAction.setTitle.

 chrome.browserAction.onClicked.addListener(function(tab) {
    chrome.browserAction.setTitle({tabId: tab.id, title: "You are on tab:" + tab.id});
  });
  • Omnibox 用户可以通过多功能框调用扩展功能 。将"omnibox"字段包括在清单中并指定关键字。在 网址列新标签搜索 样本扩展使用“NT”为关键字。
 {
    "name": "Omnibox New Tab Search",\
    ...
    "omnibox": { "keyword" : "nt" },
    "default_icon": {
      "16": "newtab_search16.png",
      "32": "newtab_search32.png"
    }
    ...
  }

当用户在多功能框中输入“ nt”时,它将激活扩展名。为了向用户发出信号,它会对提供的16x16图标进行灰度处理,并将其包含在扩展名旁边的多功能框中。

该扩展程序监听事件。触发后,扩展程序将打开一个新选项卡,其中包含用于用户条目的Google搜索。 omnibox.onInputEntered

chrome.omnibox.onInputEntered.addListener(function(text) {
  // Encode user input for special characters , / ? : @ & = + $ #
  var newURL = 'https://www.google.com/search?q=' + encodeURIComponent(text);
  chrome.tabs.create({ url: newURL });
});

  • Context Menu - 右键菜单

    右键菜单需要申请 contextMenus 权限、

      {
      "name": "Global Google Search",
      ...
      "permissions": ["contextMenus", "storage"],
      "icons": {
        "16": "globalGoogle16.png",
        "48": "globalGoogle48.png",
        "128": "globalGoogle128.png"
     }
     ...
    }
    

    16x16 图标显示在新菜单项的旁边。

通过调用创建上下文菜单 contextMenus.createbackground script。这应该在runtime.onInstalled 监听器事件下完成 。

const kLocales = {
    'com.au': 'Australia',
    'com.br': 'Brazil',
    'ca': 'Canada',
    'cn': 'China',
    'fr': 'France',
    'it': 'Italy',
    'co.in': 'India',
    'co.jp': 'Japan',
    'com.ms': 'Mexico',
    'ru': 'Russia',
    'co.za': 'South Africa',
    'co.uk': 'United Kingdom'
  };
 chrome.runtime.onInstalled.addListener(function() {
    for (let key of Object.keys(kLocales)) {
      chrome.contextMenus.create({
        id: key,
        title: kLocales[key],
        type: 'normal',
        contexts: ['selection'],
      });
    }
  });
  • Commands 扩展程序可以定义特定的 commands并将其绑定到组合键。在该 commands 进行注册需要的 commands
{
    "name": "Tab Flipper",
    ...
    "commands": {
      "flip-tabs-forward": {
        "suggested_key": {
          "default": "Ctrl+Shift+Right",
          "mac": "Command+Shift+Right"
        },
        "description": "Flip tabs forward"
      },
      "flip-tabs-backwards": {
        "suggested_key": {
          "default": "Ctrl+Shift+Left",
          "mac": "Command+Shift+Left"
        },
        "description": "Flip tabs backwards"
      }
    }
    ...
  }

Commands 可用于提供新的或替代的浏览器快捷方式。Tab Flipper 样品扩展监听 commands.onCommand 事件 background script 为每个注册的组合和定义功能。

  chrome.commands.onCommand.addListener(function(command) {
    chrome.tabs.query({currentWindow: true}, function(tabs) {
      // Sort tabs according to their index in the window.
      tabs.sort((a, b) => { return a.index < b.index; });
      let activeIndex = tabs.findIndex((tab) => { return tab.active; });
      let lastTab = tabs.length - 1;
      let newIndex = -1;
      if (command === 'flip-tabs-forward')
        newIndex = activeIndex === 0 ? lastTab : activeIndex - 1;
      else  // 'flip-tabs-backwards'
        newIndex = activeIndex === lastTab ? 0 : activeIndex + 1;
      chrome.tabs.update(tabs[newIndex].id, {active: true, highlighted: true});
    });
  });
  • Override Pages 扩展程序可以使用自己的 html 覆盖浏览器的 新建选项卡书签页面。 它可以包含专门的逻辑和样式,但不允许内联 JavaScript。单个扩展名仅限于覆盖三个可能的页面之一。

注册方式如下

 {
   "name": "Awesome Override Extension",
   ...

   "chrome_url_overrides" : {
     "newtab": "override_page.html",
     "bookmarks": "book.html"
   },
   ...
 }
<html>
  <head>
   <title>New Tab</title>
  </head>
  <body>
     <h1>Hello World</h1>
   <script src="logic.js"></script>
  </body>
 </html>

Content Scripts - 官方文档

内容脚本是在网页上下文中运行的文件。通过使用标准的 文档对象模型(DOM),他们能够读取浏览器访问的网页的详细信息,对其进行更改并将信息传递给其父扩展。

内容脚本的功能

内容脚本可以通过与扩展程序交换消息来访问其父扩展程序使用的Chrome API 。他们还可以使用来访问扩展文件的URL, chrome.runtime.getURL() 并使用与其他URL相同的结果。

  //Code for displaying <extensionDir>/images/myimage.png:
  var imgURL = chrome.runtime.getURL("images/myimage.png");
  document.getElementById("someImage").src = imgURL;

内容脚本还可以访问以下 api

工作方式

内容脚本在一个孤立的世界中运行,允许内容脚本对齐 JavaScript 的环境进行更改,而不会与页面或其他内容脚本发生冲突。

扩展程序可以在网页中运行,其代码类似于以下示例。

  <html>
    <button id="mybutton">click me</button>
    <script>
      var greeting = "hello, ";
      var button = document.getElementById("mybutton");
      button.person_name = "Bob";
      button.addEventListener("click", function() {
        alert(greeting + button.person_name + ".");
      }, false);
    </script>
  </html>

该扩展名可以注入以下内容脚本。

 var greeting = "hola, ";
  var button = document.getElementById("mybutton");
  button.person_name = "Roberto";
  button.addEventListener("click", function() {
    alert(greeting + button.person_name + ".");
  }, false);

如果按下该按钮,将同时出现两个警报。

孤立的世界不允许内容脚本,扩展名和网页访问其他变量或函数创建的任何变量或函数。这还使内容脚本能够启用网页不应该访问的功能。

视频学习链接

Inject Scripts - 注入脚本

内容脚本可以以编程方式声明方式注入。

  • 以编程方式注入

    对需要在特定情况下运行的内容脚本使用程序注入。 要注入程序性内容脚本,请 在清单中提供 activeTab权限。这将授予对活动站点主机的安全访问权限以及对选项卡权限的临时访问 权限,从而使内容脚本可以在当前活动选项卡上运行,而无需指定 跨域权限

     {
      "name": "My extension",
      ...
      "permissions": [
        "activeTab"
      ],
      ...
    }
    
    

    内容脚本可以作为代码注入。

      chrome.runtime.onMessage.addListener(
      function(message, callback) {
        if (message == “changeColor”){
          chrome.tabs.executeScript({
            code: 'document.body.style.backgroundColor="orange"'
          });
        }
     });
    

还可以指定文件

 chrome.runtime.onMessage.addListener(
   function(message, callback) {
     if (message == “runContentScript”){
       chrome.tabs.executeScript({
         file: 'contentScript.js'
       });
     }
  });
  • 声明式注入 对应在指定页面上自动运行的内容脚本使用声明式注入。 声明式注入的脚本在该"content_scripts"字段下的清单中注册。它们可以包括JavaScript文件和/或CSS文件。所有自动运行的内容脚本都必须指定 匹配模式
{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     "matches": ["http://*.nytimes.com/*"],
     "css": ["myStyles.css"],
     "js": ["contentScript.js"]
   }
 ],
 ...
}

名称类型描述
matchesarray of strings需要。 指定此内容脚本将注入到哪些页面。有关 这些字符串的语法的更多详细信息,请参见“ 匹配模式”, 有关如何排除URL的信息,请参见“ 匹配模式和全局 ”。
cssarray of strings可选的。 要插入匹配页面的CSS文件列表。在为页面构造或显示任何DOM之前,将按照它们在此数组中出现的顺序注入它们。
jsarray of strings可选的。 要插入匹配页面的JavaScript文件列表。这些以它们在此数组中出现的顺序注入。
match_about_blankboolean可选的。 脚本是否应注入到about:blank父框架或打开框架与中声明的模式之一匹配的框架中 matches。默认为false。
exclude_matchesarray of strings可选的。 排除本内容脚本将被注入的页面。有关 这些字符串的语法的更多详细信息,请参见匹配模式
include_globsarray of strings可选的。 之后应用matches ,仅包括那些也与此glob匹配的URL。旨在模拟 Greasemonkey关键字。 @include
exclude_globsarray of strings可选的。 在之后应用,matches以排除与该全局匹配的URL。旨在模拟 Greasemonkey关键字。 @exclude
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["http://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

还可以指定 run time、指定框架、与嵌入页面进行通信,更多配置请阅读官方链接 developer.chrome.com/extensions/…

Declare Permissions and Warn Users(声明权限并警告用户)

扩展程序访问网站和大多数Chrome API的能力取决于其声明的 权限。权限应仅限于其功能所需的权限。限制权限可以建立扩展的功能,并减少如果攻击者攻破了扩展而可能导致的数据入侵。通过实现显式,最小和可选权限来保护扩展及其用户。

整理权限

权限是已知的字符串,它们引用Chrome API或 匹配模式 以授予对一个或多个主机的访问权限。它们在清单中列出,并指定为必需权限或 可选权限

 {
    "name": "Permissions Extension",
    ...
    // 必须权限
    "permissions": [
      "activeTab",
      "contextMenus",
      "storage"
    ],
    // 可选权限
    "optional_permissions": [
      "topSites",
      "http://www.developer.chrome.com/*"
    ],
        ...
    "manifest_version": 2
  }

确定所需的权限 - 官方地址

一个简单的扩展可能需要请求多个权限,并且许多权限在安装时显示 警告。用户更有可能信任带有有限警告或在向他们解释权限时扩展。

确定扩展的核心功能及其所需的权限。如果功能需要警告权限,请考虑将其设为可选。

使用事件触发可选权限

可选权限样本扩展的 核心功能是压倒新的标签页。一种功能是显示用户当天的目标。此功能仅需要存储 许可,其中不包含警告。

该扩展程序具有其他功能。显示用户的热门网站。此功能需要topSites权限,并带有警告。

开发依赖于警告(可选)和警告的权限的功能,并有机地引入这些功能,可以使用户无风险地介绍该扩展。此外,这允许用户进一步扩展自己的体验,并创造机会解释警告。

Give Users Options - 官方文档

就是 用户自定义选项页,就是插件的设置页面,有2个入口,一个是右键图标有一个“选项”菜单,还有一个在插件管理页面:

简单配置

  • 整页选项
{
 "name": "My extension",
 ...
 "options_page": "options.html",
 ...
}
  • 嵌入式选项
{
  "name": "My extension",
  ...
  "options_ui": {
    "page": "options.html",
    "open_in_tab": false
  },
  ...
}

5 种 js 类型比较 - 参考链接

Chrome 扩展程序的 JS 主要可以分为这5类:injected scriptcontent-scriptpopup jsbackground jsdevtools js

权限对比

JS种类可访问的APIDOM访问情况JS访问情况直接跨域
injected script和普通JS无任何差别,不能访问任何扩展API可以访问可以访问不可以
content script只能访问 extension、runtime等部分API可以访问不可以不可以
popup js可访问绝大部分API,除了devtools系列不可直接访问不可以可以
background js可访问绝大部分API,除了devtools系列不可直接访问不可以可以
devtools js只能访问 devtools、extension、runtime等部分API可以可以不可以

消息通信 - 官方文档

demo 地址

由于内容脚本(Content Script)是在网页而不是扩展程序的上下文中运行的,因此它们通常需要某种与扩展程序其余部分进行通信的方式。

扩展及其内容脚本之间的通信通过使用消息传递来进行。任何一方都可以侦听从另一端发送的消息,并在同一通道上进行响应。消息可以包含任何有效的JSON对象(空,布尔值,数字,字符串,数组或对象)

  • 有一个简单的 API 可以处理 一次性请求
  • 一个更复杂的 API 可以使您拥有 长期的连接, 以便与共享上下文交换多条消息。
  • 如果你知道另一个扩展名称,也可以跨扩展程序进行传递,交叉扩展消息, 本文将不做讲解,感兴趣可以去官网了解

简单的一次性传递

如果只需要向扩展的另一部分发送一条消息(并有选择地返回响应),则应使用简化的 runtime.sendMessagetabs.sendMessage 。这样,您就可以从内容脚本向扩展名发送一次JSON可序列化的消息,反之亦然。可选的回调参数允许您从另一端处理响应(如果有)。

chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

从扩展名向内容脚本发送请求看起来非常相似,不同之处在于,您需要指定将请求发送至哪个选项卡。本示例演示了将消息发送到所选选项卡中的内容脚本。

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

在接收端,您需要设置 runtime.onMessage 事件侦听器以处理消息。从内容脚本或扩展页面来看,该外观相同。

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting == "hello")
      sendResponse({farewell: "goodbye"});
  });

打开 background 默认背景页就可以看到消息

content-script.js中也会有回应

长连接

有时对话的持续时间长于单个请求和响应,这很有用。在这种情况下,可以分别使用runtime.connecttabs.connect来打开从内容脚本到扩展页面的长寿命通道,反之亦然。通道可以有一个名称,以便您区分不同类型的连接。

建立连接时,会为两端提供一个 runtime.Port 对象,该对象用于通过该连接发送和接收消息。

content-script内容脚本打开频道发送和接收方式如下: 从扩展名向内容脚本发送请求看起来非常相似,不同之处在于,您需要指定要连接到的选项卡。只需将以上示例中的connect连接替换为 tabs.connect即可。

页面与 content-script 建立连接方法如下

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question == "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question == "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

为了处理传入连接,您需要设置 runtime.onConnect 事件侦听器。从内容脚本或扩展页面来看,这看起来是相同的。当扩展的另一部分调用“ connect()”时,将触发此事件,以及 可用于通过连接发送和接收消息的 runtime.Port对象。这是响应传入连接的外观: content-script.js

//监听长连接
chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name == "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke == "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer == "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer == "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

效果如下图所示:

端口寿命

端口被设计为扩展的不同部分之间的双向通信方法,其中(顶层)帧被视为最小部分。

调用tabs.connectruntime.connectruntime.connectNative时,将创建一个Port。此端口可立即用于通过postMessage将消息发送到另一端 。

如果选项卡中有多个框架,则调用tabs.connect会导致对runtime.onConnect事件的多次调用(选项卡中的每个框架一次)。同样,如果使用runtime.connect,则onConnect事件可能会多次触发(扩展过程中的每个帧一次)。

您可能想找出连接何时关闭,例如,您是否为每个打开的端口维护单独的状态。为此,您可以侦听 runtime.Port.onDisconnect 事件。当通道另一侧没有有效端口时,将触发此事件。在以下情况下会发生这种情况:

  • 另一端 没有用于runtime.onConnect的侦听器。
  • 包含端口的选项卡已卸载(例如,如果导航了该选项卡)。
  • connect调用 位置的框架已卸载。
  • 接收端口(通过runtime.onConnect)的所有帧均已卸载。
  • 另一端调用runtime.Port.disconnect。请注意,如果一个connect呼叫在接收方的一端导致多个端口,并且disconnect()在这些端口中的任何一个上都被调用,那么该onDisconnect事件仅在发送方的端口上触发,而不在其他端口上触发。

常用方法标识

动态注入 css 代码

// 动态执行CSS代码,TODO,这里有待验证
chrome.tabs.insertCSS(tabId, {code: 'xxx'});
// 动态执行CSS文件
chrome.tabs.insertCSS(tabId, {file: 'some-style.css'});

注意】 由于通过content_scripts注入的CSS优先级非常高,几乎仅次于浏览器默认样式,稍不注意可能就会影响一些网站的展示效果,所以尽量不要写一些影响全局的样式。 所以注入的时候一定要测试好。

动态执行

官方文档

// 动态执行JS代码
chrome.tabs.executeScript(tabId, {code: 'document.body.style.filter = "grayscale(1)"'});
// 动态执行JS文件
chrome.tabs.executeScript(tabId, {file: 'script.js'});

【注意】background 和 page_action 等页面无法直接访问页面dom,也可以通过 chrome.tabs.executeScript({ code: script, }) 来执行js

获取当前窗口 ID - 文档

chrome.windows.getCurrent(function(currentWindow)
{
	console.log('当前窗口ID:' + currentWindow.id);
});

调试扩展程序

调试程序这块,可以见调试扩展官网讲解的特别详细,还有视频链接

打包与发布

我们可以点击打包扩展程序,然后生成 .rtx 文件,,要发布到Google应用商店的话需要先登录你的Google账号。

给京东页面添加暗黑模式

目录如下

add-dark-to-page
├
├─README.md
├─manifest.json
├─js
| ├─background.js
| └home.js
├─images
|   └icon.png
├─html
|  ├─home.html
|  └options.html
├─css
|  └index.css

首先创建 mainfest.json

{
  "name": "TimeStone1",
  "manifest_version": 2,
  "version": "1.0",
  "description": "TimeStone 扩展程序",
  "page_action": {
    "default_icon": "images/icon.png",
    "default_title": "TimeStone 插件",
    "default_popup": "html/home.html"
  },
  "background": {
    "scripts": ["js/background.js"]
  },
  "options_page": "html/options.html",
  "homepage_url": "https://juejin.im/user/4230576473387773",
  "permissions": ["tabs", "activeTab", "declarativeContent"]
}

暗黑模式思考

我这里的暗黑模式主要是通过 css 的 filter 属性,来控制页面显示。具体可以查看

打开 chrome://extensions/

并加载解压的扩展程序,具体操作如下

创建 home.html和 js 文件下的/background.js

  • home.html
<!DOCTYPE html>
<html>
  <head>
    <title>TimeStone 插件 home 页面</title>
    <meta charset="utf-8" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <link type="text/css" rel="stylesheet" href="../css/index.css" />
  </head>
  <body>
    <div class="time-stone">
      <button class="time-stone_btn" id="ChangeBg">暗黑模式</button>
      <script src="../js/home.js" type="text/javascript"></script>
    </div>
  </body>
</html>

  • home.js
/**
 * 获取URL
 */
function getCurrentTabUrl(callback) {
  let queryInfo = {
    active: true,
    currentWindow: true,
  }

  chrome.tabs.query(queryInfo, (tabs) => {
    let tab = tabs[0]
    let url = tab.url
    console.assert(typeof url === 'string', 'tab.url should be a string')
    callback(url)
  })
}

/**
 *改变当前页面的背景颜色。
 *
 */
function changeBackgroundStyle() {
  const script = 'document.body.style.filter = "grayscale(1)";'
  // See https://developer.chrome.com/extensions/tabs#method-executeScript.
  // 向页面注入JavaScript代码.
  chrome.tabs.executeScript({
    code: script,
  })
}
// 插件会先加载用户上次选择的颜色,如果存在的话。
document.addEventListener('DOMContentLoaded', () => {
  getCurrentTabUrl((url) => {
    // 更改 背景颜色
    ChangeBg.addEventListener('click', function () {
      changeBackgroundStyle()
    })
  })
})

  • background.js
chrome.runtime.onInstalled.addListener(function () {
  // Replace all rules ...
  chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
    // With a new rule ...
    chrome.declarativeContent.onPageChanged.addRules([
      {
        // That fires when a page's URL contains a 'g' ...
        conditions: [
          new chrome.declarativeContent.PageStateMatcher({
            pageUrl: { urlContains: 'jd.com' },
          }),
        ],
        // And shows the extension's page action.
        actions: [new chrome.declarativeContent.ShowPageAction()],
      },
    ])
  })
})

源码地址

上述代码调试有问题,欢迎留言或者提issue,谢谢!

后续会更新一些右键菜单、截取网页请求、右键菜单实例 等demo,谢谢关注。