Chrome Extension 製作笔记分享-提升你使用 Udemy 网站体验的 Udemy Enhancer 套件

前言

前阵子研究了 Chrome Extension(Chrome 扩充套件),并且还製作了一个和 Udemy 相关的扩充套件 Udemy Enhancer,透过这个 Chrome Extension 增加了几个功能,包括常见的 Picture-in-Picture、萤幕截图、控制影片播放速度、影片佔满网页等功能,并将原始码开源于这个 Github Repository。

在 Chrome Web Store 查看 Udemy Enhancer 套件可以点我

http://img2.58codes.com/2024/20116883edAgBICUDJ.jpg

而在製作过程中我也蒐集了不少的资料和做纪录,所以这篇主要是分享一些我觉得比较重要和通用的资讯,分享给也想自己製作一个扩充套件的读者,内容包括:

一些 Chrome API 的介绍,在製作 Chrome extension 的功能常会和 Chrome 的 API 做搭配,例如你可能会读取一个 http request 的资讯、网页页籤的资讯,或是把一些少量的资讯存在浏览器里。一些 Chrome extension 範例的简单介绍,因为有时光看 Chrome API 的文件还是不一定知道该 API 要怎么使用,所以 GoogleChrome 有推出一些範例套件给开发者做参考,我也将几个範例套件整理下做个简单纪录。manifest.json 简单介绍,此档案可以说是扩充套件的心脏,很多重要的套件资讯都整理于此。其余补充资料。

常用 Chrome API 介绍

chrome.runtime.onInstalled

让 extension 设定一些预设的状态,在套件安装完成时自动执行。

範例

安装时用 chrome.storage 储存色码。

let color = '#3aa757';chrome.runtime.onInstalled.addListener(() => {  chrome.storage.sync.set({ color });});

chrome.runtime.onMessage

主要在 runtime.sendMessage()tabs.sendMessage() 这两个 api 呼叫时触发。

runtime.sendMessage(): 向 extension 内的其他页面发送讯息,但不包括 content scripts 内的程式码。

content scripts 指的是例如 manifest.json 设定的资讯,这边的程式码会注入到指定的网页中执行

"content_scripts": [  {    "matches": ["https://*.udemy.com/*"],    "js": ["dist/js/injector.js"],    "run_at": "document_end"  }]

tabs.sendMessage(): 把讯息传送给 content script 内的专属 api。

範例

index.js 点击某个按钮触发 chrome.runtime.sendMessage,名称是 'inject-programmatic',所以 sw.js 会触发 chrome.runtime.onMessage 并根据 message 的 name 去最对应的事情。

document  .querySelector('#inject-programmatic')  .addEventListener('click', async () => {    const world = document.querySelector("[name='world']").value;    chrome.runtime.sendMessage({      name: 'inject-programmatic',      options: { world }    });  });

service worker 的 sw.js:

chrome.runtime.onMessage.addListener(async ({ name, options }) => {  if (name === 'inject-programmatic') {    await chrome.storage.local.set({ options });    await chrome.tabs.create({      url: 'https://example.com/#inject-programmatic'    });  }});

chrome.action.onClicked

chrome.action 可以用来控制 extension icon 的行为,而 onClicked 便是点击 icon 触发的事件,但有 popup 视窗时不会触发。

chrome.action.onClicked.addListener((tab) => {  chrome.scripting.executeScript({    target: {tabId: tab.id},    files: ['content.js']  });});

chrome.webRequest.onBeforeRequest

chrome.webRequest 可以用来观察和分析请求,并做出修改、阻挡、拦截,整个请求的生命週期可以参考文件: Life cycle of requests。

chrome.webRequest.onBeforeRequest api 是一个 http 请求生命週期的一个部分(看上图),是在当一个请求发生前触发,可以用来修改(取消、重导向)请求。

要使用这个 api 必须在 manifest.json 加上 permissions: webRequest,并且 host_permissions 加上指定的网址。

"permissions": ["webRequest", "storage", "declarativeNetRequest", "declarativeNetRequestFeedback"],"host_permissions": ["https://*.udemy.com/*"],

语法:

chrome.webRequest.onBeforeRequest.addListener(    callback, filter, opt_extraInfoSpec);
callback: 强制带入的参数,会包括当前请求 url 的资讯filter: 设定只在特定条件触发事件,ex: 特定 url 触发opt_extraInfoSpec: 如果包括 blocking,请求会等待 callback 回传东西后才继续,是调整请求的关键属性。透过让 callback 函式回传属于 BlockingResponse 的属性去修改请求,例如能在一个请求的各生命週期进行取消、重导向,或是在 onBeforeSendHeaders, onHeadersReceived 修改 headers

chrome 官方文件有提出此 api 在 manifest v3 使用中,不支援 "webRequestBlocking",也就是加上 blocking 会跳出错误讯息。

As of Manifest V3, the "webRequestBlocking" permission is no longer available for most extensions. Consider "declarativeNetRequest", which enables use of the declarativeNetRequest API. Aside from "webRequestBlocking", the webRequest API will be unchanged and available for normal use.

範例

示範如何去阻挡请求到 www.evil.com:

chrome.webRequest.onBeforeRequest.addListener(  function(details) { return {cancel: true}; },  {urls: ["*://www.evil.com/*"]},  ["blocking"]);

chrome.declarativeNetRequest

可以用来观察和分析请求,并做出修改、阻挡、拦截。和 chrome.webRequest 作用相当像,但也还是有些许差异:

declarativeNetRequest

Chrome 的 Manifest v2、v3 规範都可以使用Firefox 的 Manifest v2 不可以使用,但 Manifest v3 有支援

webRequest

Chrome 的 Manifest v2 可以使用,但在 Manifest v3 也可以使用,但有些功能不会正常运作Firefox 的 Manifest v2 可以使用

chrome.storage

作用和 localStorage 类似,可以用来储存资料、状态,允许你使用脚本在地端用资料库的形式存取资料,但针对扩充功功能的开发特别优化。

储存的资料地方常见有两个: local、sync,差别在于 sync 会根据 Chrome 上登入的 google 帐户同步资料,而 local 只储存资料在本机。

使用範例

sync 的文字部分可以换成 local,只是储存的位置不同。

chrome.storage.sync.set({ key: value }).then(() => {  console.log("Value is set to " + value);});chrome.storage.sync.get(["key"]).then((result) => {  console.log("Value currently is " + result.key);});// 清除chrome.storage.local.clear(function() {  var error = chrome.runtime.lastError;  if (error) {    console.error(error);  }  // do something more});chrome.storage.sync.clear(); // callback is optionalchrome.storage.local.remove(keyName,function() { // Your code // This is an asyn function});

chrome.tabs.query

chrome.tabs 是可以用来操作浏览器页籤的 api,而 chrome.tabs.query 可以抓出当前浏览器所有的页籤和它们的属性。

ex:

changeColor.addEventListener("click", async () => {  // 捞出指定属性的 tabs  let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });  chrome.scripting.executeScript({    target: { tabId: tab.id },    func: () => console.log('do something...'),  });});

不过记得 manifest.json 先设定开放权限才取得到。

{  "permissions": [    "activeTab",    "tabs"  // 可以 query 到所有 tab 的 url  ]}

chrome.scripting.executeScript

chrome.scripting 可以将 JS/CSS 注入到网站,和 content scripts 有点像,但 chrome.scripting 可以决定执行时间点。

chrome.scripting.executeScript 可以注入指定的程式码到指定的地方,并记得要设定 scripting: "permissions": ["scripting", "activeTab"]

以下範例是将 javascript 的程式码档案注入到指定的 tabId 中:

let changeColor = document.getElementById("changeColor");changeColor.addEventListener("click", async () => {  let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });  chrome.scripting.executeScript({    target: { tabId: tab.id },    func: setPageBackgroundColor,  });});

chrome.tabs.captureVisibleTab

此 api 可以用来撷取当前视窗的可视範围

要在 manifest.json 的 permissions 属性加上 activeTab,host_permissions 加上 <all_urls> 才可以使用

  "permissions": ["activeTab"],  "host_permissions": ["<all_urls>"],

语法

chrome.tabs.captureVisibleTab(  windowId?: number,  options?: ImageDetails,  callback?: function,)

传入的参数有三个:

windowId: 当前 tab 的 windowId 属性,可以从 chrome.tabs.query 去取得options:一个物件,设定撷取图片的一些格式format: 预设 jpegquality: 控制图片的储存品质callback: 回呼函式,将图片的 base64 字串当作参数,(dataUrl: string) => void

manifest.json

manifest 档用来设定套件资讯、需要的 Chrome 权限等,其中 manifest_version 版本的不同会影响撰写的属性设定。

官方撰写 2 vs. 3 的版本差异文件

action

设定有关 extension icon 的一些资讯、触发行为

ex1: 点击 icon 触发事件

// background.jschrome.action.onClicked.addListener((tab) => {  chrome.scripting.executeScript({    target: {tabId: tab.id},    files: ['content.js']  });});

ex2: 可以设定 icon、点击后的弹出网页

{  "name": "Action Extension",  ...  "action": {    "default_icon": {              // optional      "16": "images/icon16.png",   // optional    },    "default_title": "Click Me",   // optional, shown in tooltip    "default_popup": "popup.html"  // optional  },  ...}

background

运用 service_worker,设定会在网页背景运作的程式码档案。

"background": {  "service_worker": "background.js"  // "scripts": ["jquery.js", "my-background.js"]},

content_script

用来注入网页页面的 script,matches 用来设定哪些网域要注入 content.js 的程式码

{  ...  "content_scripts": [    {      "js": ["scripts/content.js"],      "matches": [        "https://developer.chrome.com/docs/extensions/*",        "https://developer.chrome.com/docs/webstore/*"      ]    }  ]}

options_page/options_ui

设定右键点击 extension 时,会出现选项名称的这个选项,再点击后会出现的页面

permission

设定这个 extension 能使用哪些浏览器功能的权限

ex:

"permissions": ["scripting", "activeTab"],

各属性可参考此官方文件

host_permissions

设定一些网域的权限,例如可以取得网站的 cookies,解除跨域限制

{  ...  "host_permissions": [    "https://developer.chrome.com/*"  ],  ...}

chrome-extensions-samples 功能笔记

纪录一下各个範例的功能,也许有用到就能参考。

api-samples/action

介绍 chrome.action 的一些 api 使用範例

安装后会弹出一个页面的功能enable、disable popup 视窗切换 popup html点击 extension icon 后弹出一个页面调整 icon 图、badge 文字

api-samples/alarms

介绍和一些 chrome.alarms 相关的 api

api-samples/declarativeNetRequest

此资料夹底下还分成三个 extension

no-cookies

使用 chrome.declarativeNetRequest API 从 http 请求移除 "Cookie" header

url-blocker

使用 chrome.declarativeNetRequest API 阻挡 http 请求

url-redirect

将指定的 url 重新导向到指定 url

api-samples/default_command_override

增加透过一些快捷键就能切换网页 tab 的功能

Press Ctrl+Shift+Right or Ctrl+Shift+Left (Command+Shift+Right or Command+Shift+Left on a Mac) to flip through window tabs

使用了 chrome.commands api

api-samples/scripting

使用 chrome.scripting api,注入 js 到网页内

使用 chrome.webNavigation.onDOMContentLoaded 去设定网页载入时做的事情使用了 chrome.runtime.onMessage 透过 message 触发注入的 scriptDynamic 的 tab 有很多选项,那些是设定 script 的一些参数

api-samples/web-accessible-resources

使用了一些关于 chrome.runtime 的 api 去读取外部资源(以图片为例)

functional-samples/reference.mv3-content-scripts

介绍使用 chrome.scripting


开发碰到的难题

1. 相关资源相较于常见的开发功能偏少

在製作 extension 时,考量到 Chrome 预计会逐渐将 Manifest V2 开发出的套件做淘汰,所以我直接决定使用 Manifest V3 版本做开发,但常常会查到 Manifest V2 版本的资料,以开发功能来说,扩充套件可以参考的资源就比开发一般网站少了,而不同版本的资料导致过往的资料不好做参考,更是增加了开发的难度。

而新旧版本的 api 也是个问题,Manifest V2 有些 api 都没有开出对应的新版 api,ex: chrome.webRequest.onBeforeRequest。

2. 截图功能碰到的困难

这边整理的是在开发截图功能碰到的困难。

1. 使用第三方套件出现 Content Security Policy 的问题

原本想参考一些网路文章所介绍的截图套件 html2canvas、Canvas2image 去实作截图功能,但没想到安装套件之后,会发生类似于以下的错误讯息:

Refused to load the script … because it violates the following Content Security Policy directive: …, so 'script-src' is used as a fallback.

看到 Content Security Policy 可以知道是和安全性有关,自己评估是因为第三方套件的引入,其程式码不符合网站设定的政策,所以就会被阻挡下来,这样的设计可以避免 XSS 攻击,让未知的第三方 script 程式码不能执行,因此经过考量后,决定尝试使用原生 API 去做截图的功能。

2. udemy screenshot black screen,Udemy 设定不给使用者截图

因为决定不使用套件的关係,所以经过一番搜寻资料后,使用到了 CanvasRenderingContext2D.drawImage() API,它可以将截取到的 Video、Image DOM 元素加上指定的位置转换成 Canvas 然后绘製在 Canvas 画布上,例如这个範例按下按钮就可以截取当前影片播放的画面,How to capture image from video?。

但是在 Udemy 网站上要去截图时,Udemy 的网站会将影片画面变黑掉,只要 google 搜寻 "udemy screenshot black screen" 就可以看到相当多的讨论,所以我在做功能时不会直接抓取 Udemy 的 Video DOM 元素当作 CanvasRenderingContext2D.drawImage() 的第一个参数,而是透过
chrome.tabs.captureVisibleTab 先去截取整个萤幕画面,会产生图片 base64 的字串,并使用 JS 去产生一个 Image DOM 元素。

产生的 Image DOM 元素,再搭配使用者拖曳选取区域的功能去计算出要提供给 CanvasRenderingContext2D.drawImage() 的参数,成功截取之后就能取得图片 base64 的字串。

3. 处理 drawImage 撷取图片位置、大小不正确的问题

由于使用者装置不同的关係,有涉及到 devicePixelRatio 的处理,以下整理一些参考资料给需要开发类似功能的读者。

一个萤幕是由很多个点所构成一整个画面,而单个点的单位叫做 px(pixel),中文可以称为画素、像素,具有相对单位的性质,也就是有可能在固定的长度下(ex: 1 英吋),其长乘上宽的 px 数,也可称为解析度,会有所不同,所以产生一个单位 PPI(pixels per inch),用来表示 1 英吋内有多少 pixel 的像素密度。

DPI(dots per inch) 名称和 PPI 相当接近,对于印表机来说,dots 为墨点,DPI 表示 1 英吋内有多少 dots。

CSS 也有 px,常见用来表示 DOM 元素长宽高、边距等的单位,和萤幕的 px 有一个倍率关係,常称为 DPR(device pixel ratio),指 1 个 CSS px 佔用多少萤幕设备 px,而平常在用 CSS 写网页样式时,浏览器 render 引擎都会帮我们做好 DPR 换算的工作,所以我们可以不用另外计算 CSS px,但是要加上 <meta name="viewport" content="width=device-width, initial-scale=1">,让浏览器 render 出符合装置宽度,且未缩放的页面。

但在使用 CanvasRenderingContext2D.drawImage() 绘製截图时,因为绘製的是 canvas 画布等向量元素,所以就必须将 DPR 考虑进去计算,才不会截取到位置、大小不正确的图片。

参考资料:
重新认识 Pixel、DPI / PPI 以及像素密度

Window.devicePixelRatio

4. 将 canvas 图片转为 base64 跳出的错误

Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

解决: 加上 img.setAttribute("crossOrigin",'Anonymous');,开放跨域的限制即可。

参考文章:
https://www.jianshu.com/p/6fe06667b748


参考文件

chrome extension 官方文件

Extension development overview

这页蛮重要的,列出製作 extension 各功能会用到的 api

[Chrome Extension] API 笔记

铁人赛-你知道这是什么吗? Chrome Extension MV3 With Vite系列

铁人赛-Chrome extension 学习手札

铁人赛-只不过是想强迫自己定时喝个水有必要那么麻烦吗之我想写一个 Chrome Extension 强迫我喝水


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章