谷歌插件开发-Chrome-exts-cli

963 阅读44分钟

前言

什么是Chrome插件

严格来讲,我们正在说的东西应该叫Chrome扩展(Chrome Extension),真正意义上的Chrome插件是更底层的浏览器功能扩展,可能需要对浏览器源码有一定掌握才有能力去开发。

鉴于Chrome插件的叫法已经习惯,本文也全部采用这种叫法,但读者需深知本文所描述的Chrome插件实际上指的是Chrome扩展。

Chrome插件是一个用Web技术开发、用来增强浏览器功能的软件,它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包.

个人猜测crx可能是Chrome Extension如下3个字母的简写

学习Chrome插件开发有什么意义?

增强浏览器功能,轻松实现属于自己的“定制版”浏览器,等等。

Chrome插件提供了很多实用API供我们使用,包括但不限于:

  • 书签控制;
  • 下载控制;
  • 窗口控制;
  • 标签控制;
  • 网络请求控制,各类事件监听;
  • 自定义原生菜单;
  • 完善的通信机制;
  • 等等;

为什么是Chrome插件而不是Firefox插件?

  1. Chrome占有率更高,更多人用;
  2. 开发更简单;
  3. 应用场景更广泛,Firefox插件只能运行在Firefox上,而Chrome除了Chrome浏览器之外,还可以运行在所有
  4. webkit内核的国产浏览器,比如360极速浏览器、360安全浏览器、搜狗浏览器、QQ浏览器等等;
  5. 除此之外,Firefox浏览器也对Chrome插件的运行提供了一定的支持

开发与调试

Chrome插件没有严格的项目结构要求,只要保证本目录有一个manifest.json即可,也不需要专门的IDE,普通的web开发工具即可。
从右上角菜单->更多工具->扩展程序可以进入 插件管理页面,也可以直接在地址栏输入 chrome://extensions 访问

勾选开发者模式即可以文件夹的形式直接加载插件,否则只能安装.crx格式的文件。Chrome要求插件必须从它的Chrome应用商店安装,其它任何网站下载的都无法直接安装,所以,其实我们可以把crx文件解压,然后通过开发者模式直接加载。

开发中,代码有任何改动都必须重新加载插件,只需要在插件管理页按下Ctrl+R即可,以防万一最好还把页面刷新一下。

安装

推荐使用 npm 的方式安装

npm i chrome-exts-cli

可以使用以上命令进行脚手架安装

快速上手

推荐使用 npm 的方式安装

> npm install chrome-exts-cli
> chrome-exts-cli create <你的项目名称>

Snipaste_2022-12-07_16-30-53.png

可以选择上面指定的谷歌插件版本,根据自己需要的版本进行选择创建模板

创建成功后会得到如下项目以及目录结构
Snipaste_2022-12-07_16-30-53.png

快速打包成谷歌插件扩展包

npm install
npm run build-watch

如果npm过程中很慢的话,可以切换成国内镜像进行拉取依赖

npm install --registry=https://registry.npm.taobao.org

这时候项目中会多出一个dist目录,dist就是谷歌浏览器扩展包
打开谷歌浏览器地址栏输入chrome://extensions/打开发者模式选择加载已解压的扩展程序选择打包出来的dist文件夹然后插件就展示在谷歌浏览器上了

显示成功如下
Snipaste_2022-12-07_16-30-53.png

打包与配置

介绍

manifest.json 是 Chrome 插件的核心配置文件,用来配置所有和插件相关的配置,必须放在根目录。其中,manifest_version、name、version3个是必不可少的,开发Chrome插件首先就是配置manifest.json文件了,利用它我们可以定义在什么时机以及在什么网页执行什么脚本,有一些什么行为

本文目标

结合具体应用场景,让读者对manifest.json文件的写法和主要属性拥有初步认识。

目标读者

chrome扩展开发的初学者,想要先从宏观上了解一下chrome扩展能干哪些事情,而不是急于写出一个能运行的demo的人

manifest.json的文件格式

看后缀名就知道,文件格式是json,格式不是开发过程中的一个难点,而是开发者和浏览器之间的一种约定,遵守约定,大家就可以和谐相处。

下面是一些常用配置项的说明

{
    // 扩展名称
    "name": "chrome-extensions-v2",
    // 版本。由1到4个整数构成。多个整数间用"."隔开
    "version": "1.0",
    // manifest文件版本号。Chrome18开始必须为2
    "manifest_version": 2,
    // 描述。132个字符以内
    "description": "用于开发谷歌插件的模板,快速开发上手,对于新手很友好",
    // 扩展图标。推荐大小16,48,128
    "icons": {
        "16": "image/icon-16.png",
        "48": "image/icon-48.png",
        "128": "image/icon-128.png"
    },
    // 语言
    "default_locale": "en",
    // 地址栏右侧图标管理,含图标及弹出页面的设置等
    // 建议至少保留一个设置,不然扩展图标是暗的
    "browser_action": {
        "default_icon": "image/icon-128.png",
        "default_title": "My Message",
        "default_popup": "html/browser.html"
    },
    // 地址栏最后附加图标。含图标及行为等
    "page_action": {
        "default_icon": "image/icon-48.png",
        "default_title": "My Test",
        "default_popup": "html/page.html"
    },
    // 主题,用于更改整个浏览器的外观
    "theme": {},
    // 指定扩展需要跳转到的URL
    "app": {},
    // 指定扩展进程的background运行环境及运行脚本
    "background": {
        "scripts": [
            "lib/jquery-3.3.1.min.js",
            "js/background.js"
        ],
        "page": "html/background.html"
    },
    // 替换页面
    "chrome_url_overrides": {
        "pageToOverride": "html/overrides.html"
    },
    // 指定在web页面运行的脚本/插入的css及运行/插入时机
    "content_scripts": [
        {
            "matches": [
                "https://www.baidu.com/*"
            ],
            "css": [
                "css/mystyles.css"
            ],
            "js": [
                "lib/jquery-3.3.1.min.js",
                "js/content.js"
            ],
            // 注入的时间,可选值: 
            // "document_start" 页面加载开始时 
            // "document_end"  页面加载结束时 
            // "document_idle"  页面空闲时,默认 document_idle
            "run_at": "document_idle"
        }
    ],
    // 安全策略
    "content_security_policy": "",
    // 扩展的官方主页
    "homepage_url": "http://xxx",
    // 插件在隐私模式下的配置
    "incognito": "spanning",
    // 用户操作意图描述
    "intents": {},
    // 扩展唯一标识。不需要人为指定
    "key": "",
    // 扩展所需chrome的最小版本
    "minimum_chrome_version": "1.0",
    // 消息与本地处理模块映射
    "nacl_modules": [],
    // 是否允许脱机运行
    "offline_enabled": true,
    // ominbox即地址栏。用于响应地址栏的输入事件
    "omnibox": {
        "keyword": "myKey"
    },
    // 选项页。用于在扩展管理页面跳转到选项设置
    "options_page": "aFile.html",
    // 申请权限
    "permissions": [
        "https://www.baidu.com/*",
        "background",
        "tabs"
    ],
    // 扩展。可调用第三方扩展
    "plugins": [
        {
            "path": "extension_plugin.dll",
            "public": true
        }
    ],
    // 指定所需要的特殊技术。目前只支持"3D"
    "requirements": {},
    // 自动升级
    "update_url": "http://path/to/updateInfo.xml",
    // 指定资源路径,为String数组
    "web_accessible_resources": []
}

上面代码中,虽然可用的属性有这么多,但是常用的就以下这几个:

  1. name 扩展名称;
  2. version 插件的版本;
  3. manifest_version 配置文件版本;
  4. description 对于插件功能的描述;
  5. icons 插件的图标;
  6. browser_action 定义插件的图标后,点击图标时弹出的页面,以及插件的标题,建议始终保留一个,不设置这个属性图标会是灰色的,设置了后才会亮起来;
  7. background 背景页,扩展进程的背景运行环境,可以拦截修改请求等等;
  8. content_scripts 内容脚本,可以指定在什么时机向什么页面插入什么脚本或者css资源;
  9. permissions 权限申请项,比如存储权限storage,请求拦截权限webRequest, webRequestBlocking等等;

完整的配置文档 :
developer.chrome.com/docs/extens…

vue.config文件打包配置

// 引入webpack插件进行打包
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");

// Generate pages object
const pagesObj = {};
// 这里定义的是你有多个的文件夹就定义多少个文件夹,下面会根据文件夹里面的vue打包成html以及js文件
const chromeName = ["popup", "start","baidu"];

// 进行文件解析输出
chromeName.forEach(name => {
    pagesObj[name] = {
        entry: `src/views/${name}/index.js`,
        template: "public/index.html",
        filename: `${name}.html`
    };
});
const plugins =
    // 生产环境配置
    process.env.NODE_ENV === "production"
        ? [   
            {
                // 将项目目录下的指定文件输出到打包后的指定路径下存放
                from: path.resolve("src/manifest.json"),
                to: `${path.resolve("dist")}/manifest.json`
            },
            {
                from: path.resolve("src/assets/img"),
                to: `${path.resolve("dist")}/img`
            },
            {
                from: path.resolve("src/assets/js"),
                to: `${path.resolve("dist")}/js`
            }
        ]
        : [ // 测试环境配置
            {
                from: path.resolve("src/manifest.json"),
                to: `${path.resolve("dist")}/manifest.json`
            },
            {
                from: path.resolve("src/assets"),
                to: `${path.resolve("dist")}/assets`
            },
            {
                from: path.resolve("src/assets/js"),
                to: `${path.resolve("dist")}/js`
            }
        ];
module.exports = {
    lintOnSave: false,
    // 配置的所有输出的文件对象
    pages: pagesObj,
    // 是否开启打包压缩
    productionSourceMap: false,
    configureWebpack: {
        output: {
            filename: 'js/[name].js'
        },
        plugins: [
            CopyWebpackPlugin(plugins)
        ]
    },
    css: {
        extract: {
            filename: 'css/[name].css'
        }
    },
    chainWebpack: config => {
        // 查看打包组件大小情况
        if (process.env.npm_config_report) {
            // 在运行命令中添加 --report参数运行, 如:npm run build --report
            config
                .plugin('webpack-bundle-analyzer')
                .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
        }
    }
};

vue.config.js配置好以后
可以使用

npm run build-<指定环境>   prod  或者  dev

npm run build:prod    用生产环境的配置进行打包

如果找不到 build:prod 获取build:dev则需要配置

  1. 在项目中的package.json里面配置环境
 "scripts": {
    "build:prod": "vue-cli-service build --mode production",
    "build:dev": "vue-cli-service build --mode development"
  }
  1. 需要定义两个文件
    .env.development
    .env.production

生产环境文件内容如下

NODE_ENV=production

测试环境文件内容如下

NODE_ENV=development

打包成功之后得到如下目录结构


https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b6391a8dba0455ebf1c6c9c6fef385b~tplv-k3u1fbpfcp-zoom-1.image

UI框架引入使用

以ElementUI框架作为示例

  1. 首先打开 ElementUI 的官方文档查看使用步骤
  2. 打开文档可以看到快速上手那栏的文档说明

引用分为两种方式:

  • 完整引入
  • 按需引入

完整引入

npm i element-ui -S

安装完成之后在代码中使用如下

以当前脚手架创建的项目为例

无标题.png

引入后可以在vue中使用elementui框架了

按需引入

借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
首先,安装 babel-plugin-component

npm install babel-plugin-component -D

在项目的根目录创建babel.config.js文件 内容如下

module.exports = {
  presets: [
    '@vue/app'
  ],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

在项目中使用配置如下需要使用那个组件就引入哪个组件就好了

无标题.png

其他的ui框架也是一样的引入方式

Chrome核心概念

Chrome-API

API介绍
accessibilityFeatures使用 chrome.accessibilityFeatures 管理chrome的可访问功能,该API依赖于ChromeSetting prototype of the type API 来读取和设置个人辅助功能,为了读取功能状态,需要请求accessibilityFeatures.read权限。插件需要请求accessibilityFeatures.modify权限来修改功能状态。需要注意的是, accessibilityFeatures.readaccessibilityFeatures.modify两个权限不等价
action可以使用 chrome.action 设置扩展在菜单栏的图标
alarms可以使用chrome.alarms 来设置定期执行或者未来某个时间执行的程序
bookmarks可以使用chrome.bookmarks 进行书签创建、管理或其他方式操作书签。也可以查阅Override Pages,你可以用它来创建一个自定义的书签管理器页面
browserAction(MV2)使图标出现在chrome导航栏右侧,除了它的图标,一个browserAction可以设置一个tooltip、一个badge、或一个popup
browsingDatachrome.browsingData可以从用户的本地配置文件中删除浏览数据
certificateProvider使用此API将证书暴露给需要进行TLS认证的平台
commands可以为自己插件的动作设置快捷键,比如打开浏览器或者向该插件发送命令
contentSettingschrome.contentSettings可以改变控制网站是否可以使用cookies、JavaScript和插件等功能的设置。简单来说,contentSettings可以代替全局在每个网站的基础上定制Chrome的表现
contextMenus可以使用chrome.contextMenus 添加一些选项到chrome的上下文菜单中。你可以选择你的上下文菜单添加适用于哪些类型的对象,如图像、超链接和页面
cookieschrome.cookies 用于查询和修改 cookie 并在更改时收到通知
debuggerchrome.debugger API服务可以代替 remote debugging protocol远程调试协议,使用chrome.debugger 把一个或多个标签与与网络接口关联,调试JavaScript,改变DOM和CSS,等等。使用Debuggee tabId来锁定标签与sendCommand和route事件,再通过回调事件返回tabId
declarativeContentchrome.declarativeContentAPI 可根据页面内容执行操作,无需获得读取页面内容的权限
declarativeNetRequest(Chrome 84)chrome.declarativeNetRequestAPI 用于通过指定声明性规则来阻止或修改网络请求。这允许扩展程序修改网络请求而无需拦截它们并查看其内容,从而提供更多隐私
desktopCapture可用于捕获屏幕、单个窗口或选项卡内容的桌面捕获 API
devtools.inspectedWindowchrome.devtools.inspectedWindow与被检查窗口交互:获得被检查页面的标签ID,在被检查窗口的上下文中评估代码,重新加载页面,或获得页面中的资源列表。
devtools.networkchrome.devtools.network检索有关开发者工具在网络面板中显示的网络请求的信息
devtools.panelschrome.devtools.panels整合你的扩展到开发者面板中:创建自己的面板、访问现有面板、创建侧边栏
documentScanchrome.documentScan发现和检索附属纸质文件扫描仪的图像
downloadschrome.downloads以编程方式启动、监控、操作和搜索下载
enterprise.deviceAttributeschrome.enterprise.deviceAttributes可读取设备属性。注意:此 API 仅适用于由企业策略强制安装的扩展程序
enterprise.hardwarePlatform(Chrome 71)chrome.enterprise.hardwarePlatform用来获取浏览器运行的硬件平台的制造商和型号。注意:该API仅适用于由企业策略安装的扩展
enterprise.networkingAttributes(Chrome 85)chrome.enterprise.networkingAttributes用于读取有关您当前网络的信息的 API。注意:此 API 仅适用于企业策略强制安装的扩展
enterprise.platformKeyschrome.enterprise.platformKeys用于生成硬件支持的密钥并为这些密钥安装证书。证书将由平台管理,可用于 TLS 身份验证、网络访问或通过 chrome.platformKeys 的其他扩展
eventschrome.events命名空间包含 API 调度事件使用的常见类型,以便在发生有趣的事情时通知您
extensionchrome.extensionAPI 具有可供任何扩展页面使用的实用程序。它支持在扩展与其内容脚本之间或扩展之间交换消息,如Message Passing所述。
extensionTypeschrome.extensionTypesAPI 包含 Chrome 扩展的类型声明
fileBrowserHandlerchrome.fileBrowserHandler用于扩展 Chrome 操作系统文件浏览器。例如,您可以使用此 API 使用户能够将文件上传到您的网站
fileSystemProviderchrome.fileSystemProvider用于创建文件系统,可从 Chrome 操作系统上的文件管理器访问
fontSettingschrome.fontSettings用于管理 Chrome 字体设置
gcmchrome.gcm使app和扩展程序能够通过Google Cloud Messaging Service 发送和接收消息
historychrome.history可以用浏览器浏览记录做交互,可以添加、删除和查询浏览器历史记录中的 URL。要使用您自己的版本覆盖历史记录页面,请参阅Override Pages
i18nchrome.i18n在整个app或扩展程序中实现国际化。
identitychrome.identity用于获取OAuth2访问令牌
idlechrome.idle用于检测机器空闲状态的改变
input.imechrome.input.ime 用于为 Chrome 操作系统实现自定义 IME。这允许您的扩展程序处理击键、设置组合和管理候选窗口。
instanceIDchrome.instanceID用于访问实例 ID 服务。
loginState(Chrome 78)chrome.loginState用于读取和监控登录状态
managementchrome.management提供管理已安装及运行中的扩展程序/应用程序列表的方法。它对于覆盖内置新标签页的扩展特别有用
notificationschrome.notifications使用模板创建丰富的通知,并在系统托盘中向用户显示这些通知
omnibox允许您使用 Google Chrome 的地址栏(也称为多功能框)注册关键字。
pageAction(MV2)可以使用 chrome.action 设置扩展在菜单栏的图标
actionchrome.pageAction将图标放置在 Google Chrome 主工具栏中的 API,位于地址栏的右侧。页面操作表示可以在当前页面上执行的操作,但不适用于所有页面。页面操作在不活动时显示为灰色
pageCapturechrome.pageCapture将选项卡保存为 MHTML
permissionschrome.permissions在运行时(不是安装时)请求声明的可选权限,因此用户了解为什么需要这些权限并只授予那些必要的权限
platformKeyschrome.platformKeys用于访问平台管理的客户端证书。如果用户或策略授予权限,扩展可以在其自定义身份验证协议中使用此类证书。例如。这允许在第三方 VPN 中使用平台管理的证书(请参阅 chrome.vpnProvider)
powerchrome.power用来覆盖系统的电源管理功能
printerProviderchrome.printerProvider公开打印管理器使用的事件来查询由扩展控制的打印机,查询它们的功能并将打印作业提交给这些打印机
printing(Chrome 81)chrome.printing将打印作业发送到 Chromebook 上安装的打印机
printingMetrics(Chrome 79)chrome.printingMetrics用于获取有关打印使用情况的数据
privacychrome.privacy用于控制 Chrome 中可能影响用户隐私的功能的使用。这个 API 依赖于ChromeSetting prototype of the type API来获取和设置 Chrome 的配置。
proxychrome.proxy用于管理 Chrome 代理设置。该 API 依赖于ChromeSetting prototype of the type API来获取和设置代理配置。
runtimechrome.runtime用于检索background页、返回manifest的详细信息以及侦听和响应app或扩展生命周期中的事件。您还可以使用此 API 将相对路径转换为绝对路径
scripting(Chrome 88MV3+)chrome.scripting在不同环境中执行脚本
search(Chrome 87)chrome.search通过默认提供方进行搜索
sessionschrome.sessions用来从浏览会话中查询和恢复标签页和窗口
storagechrome.storage存储、检索和跟进用户数据
system.cpusystem.cpu可查询 CPU 元数据
system.displaysystem.display可查询显示元数据
system.memorychrome.system.memory
system.storagechrome.system.storage用来查询存储设备信息,并在可移动存储设备被连接和拆卸时发出通知。
tabCapturechrome.tabCapture用来与标签媒体流交互。
tabGroups(Chrome 89MV3+)chrome.tabGroups用来与浏览器的标签分组系统交互,您可以使用此 API 在浏览器中修改和重新排列选项卡组。要对选项卡进行分组和取消分组,或查询分组中的选项卡,请使用 chrome.tabs
tabschrome.tabs用来与标签系统做交互,可以用来在浏览器中创建、修改和重排标签页
topSiteschrome.topSites用来访问显示在新标签页上的热门站点(即访问量最大的站点),不包括用户自定义的快捷方式。
ttschrome.tts用来播放合成的tts,也可以查看允许扩展实现语音引擎的ttsEngineAPI
ttsEnginechrome.ttsEngine用与扩展实现tts引擎,如果你的扩张注册了这个API,当任何扩展程序或 Chrome 应用程序使用 tts API 生成语音时,它将接收要说的话语和其他参数的事件。然后,您的扩展程序可以使用任何可用的网络技术来合成和输出语音,并把报告状态通过事件发送给回调函数
typeschrome.types 包含Chrome 的类型声明
vpnProviderchrome.vpnProvider用来实现一个 VPN 客户端
wallpaperchrome.wallpaper用来更改 ChromeOS 壁纸
webNavigationchrome.webNavigation以接收关于飞行中的导航请求状态的通知
webRequestchrome.webRequest可以观察和分析流量并拦截、阻止或修改进行中的请求
windowschrome.windows用来与浏览器窗口交互。您可以使用此 API 在浏览器中创建、修改和重新排列窗口。
action可以使用 chrome.action 设置扩展在菜单栏的图标
action可以使用 chrome.action 设置扩展在菜单栏的图标

常用的api

API说明
chrome.runtime.onMessage.addListener接收消息监听事件
chrome.tabs.create创建打开一个新的浏览器窗口
chrome.runtime.sendMessage发送消息
chrome.runtime.getURL会把扩展中的相对路径转为绝对路径,方便正确引入
chrome.action.onClicked.addListener如果manifest.json未配置 action.default_popup,点击扩展按钮会触发此事件
chrome.cookies.getAll获取指定域名下的所有cookie
chrome.tabs.query查询浏览器所有窗口列表

Content-Scripts

所谓content-scripts,其实就是Chrome插件中向页面注入脚本的一种形式(虽然名为script,其实还可以包括css的),借助content-scripts我们可以实现通过配置的方式轻松向指定页面注入JS和CSS(如果需要动态注入,可以参考下文),最常见的比如:广告屏蔽、页面CSS定制,等等。

示例配置:

{
	// 需要直接注入页面的JS
	"content_scripts": 
	[
		{
			//"matches": ["http://*/*", "https://*/*"],
			// "<all_urls>" 表示匹配所有地址
			"matches": ["<all_urls>"],
			// 多个JS按顺序注入
			"js": ["js/jquery-1.8.3.js", "js/content-script.js"],
			// JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式
			"css": ["css/custom.css"],
			// 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
			"run_at": "document_start"
		}
	],
}

特别注意,如果没有主动指定run_atdocument_start(默认为document_idle),下面这种代码是不会生效的:

document.addEventListener('DOMContentLoaded', function(){
	console.log('我被执行了!');
});

content-scripts和原始页面共享DOM,但是不共享JS,如要访问页面JS(例如某个JS变量),只能通过injected js来实现。content-scripts不能访问绝大部分chrome.xxx.api,除了下面这4种:

  1. chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
  2. chrome.i18n
  3. chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
  4. chrome.storage
    其实看到这里不要悲观,这些API绝大部分时候都够用了,非要调用其它API的话,你还可以通过通信来实现让background来帮你调用(关于通信,后文有详细介绍)。

Chrome插件给我们提供了这么强大的JS注入功能,剩下的就是发挥你的想象力去玩弄浏览器了。

Popup

popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。

popup可以包含任意你想要的HTML内容,并且会自适应大小。可以通过default_popup字段来指定popup页面,也可以调用setPopup()方法。

配置方式:

{
	"browser_action":
	{
		"default_icon": "img/icon.png",
		// 图标悬停时的标题,可选
		"default_title": "这是一个示例Chrome插件",
		"default_popup": "popup.html"
	}
}

需要特别注意的是,由于单击图标打开popup,焦点离开又立即关闭,所以popup页面的生命周期一般很短,需要长时间运行的代码千万不要写在popup里面。

在权限上,它和background非常类似,它们之间最大的不同是生命周期的不同,popup中可以直接通过chrome.extension.getBackgroundPage()获取background的window对象。

Background

后台(姑且这么翻译吧),是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。

background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS。

经过测试,其实不止是background,所有的直接通过chrome-extension://id/xx.html这种方式打开的网页都可以无限制跨域。
配置中,background可以通过page指定一张网页,也可以通过scripts直接指定一个JS,Chrome会自动为这个JS生成一个默认的网页:

{
	// 会一直常驻的后台JS或后台页面
	"background":{
		// 2种指定方式,如果指定JS,那么会自动生成一个背景页
		"page": "background.html"
		//"scripts": ["js/background.js"]
	},
}

需要特别说明的是,虽然你可以通过chrome-extension://xxx/background.html直接打开后台页,但是你打开的后台页和真正一直在后台运行的那个页面不是同一个,换句话说,你可以打开无数个background.html,但是真正在后台常驻的只有一个,而且这个你永远看不到它的界面,只能调试它的代码。

background可以包含三种属性,分别是scripts、page和persistent。

如果指定了scripts属性,则Chrome会在扩展启动时自动创建一个包含所有指定脚本的页面;
如果指定了page属性,则Chrome会将指定的HTML文件作为后台页面运行。
通常我们只需要使用scripts属性即可,除非在后台页面中需要构建特殊的HTML但一般情况下后台页面的HTML我们是看不到的。

persistent属性定义了常驻后台的方式 当其值为true时,表示扩展将一直在后台运行,无论其是否正在工作;当其值为false时,表示扩展在后台按需运行,这就是Chrome后来提出的Event Page。Event Page可以有效减小扩展对内存的消耗,如非必要,请将persistent设置为false。persistent的默认值为true。

展示形式

browserAction(浏览器右上角)

通过配置browser_action可以在浏览器的右上角增加一个图标,一个browser_action可以拥有一个图标,一个tooltip,一个badge和一个popup。

示例配置如下:

"browser_action":{
    "default_icon": "img/icon.png",
    "default_title": "这是一个示例Chrome插件",
    "default_popup": "popup.html"
}

图标

browser_action图标推荐使用宽高都为19像素的图片,更大的图标会被缩小,格式随意,一般推荐png,可以通过manifest中default_icon字段配置,也可以调用setIcon()方法。

tooltip

修改browser_action的manifest中default_title字段,或者调用setTitle()方法。

Snipaste_2023-01-04_15-07-54.png

badge

所谓badge就是在图标上显示一些文本,可以用来更新一些小的扩展状态提示信息。因为badge空间有限,所以只支持4个以下的字符(英文4个,中文2个)。badge无法通过配置文件来指定,必须通过代码实现,设置badge文字和颜色可以分别使用setBadgeText()setBadgeBackgroundColor()

在background.js文件中加入如下代码:

chrome.browserAction.setBadgeText({text: 'new'});
chrome.browserAction.setBadgeBackgroundColor({color: [255, 0, 0, 255]});

效果:
Snipaste_2023-01-04_15-13-09.png

pageAction(地址栏右侧)

所谓pageAction,指的是只有当某些特定页面打开才显示的图标,它和browserAction最大的区别是一个始终都显示,一个只在特定情况才显示。

需要特别说明的是早些版本的Chrome是将pageAction放在地址栏的最右边,左键单击弹出popup,右键单击则弹出相关默认的选项菜单:

而新版的Chrome更改了这一策略,pageAction和普通的browserAction一样也是放在浏览器右上角,只不过没有点亮时是灰色的,点亮了才是彩色的,灰色时无论左键还是右键单击都是弹出选项:

具体是从哪一版本开始改的没去仔细考究,反正知道v50.0的时候还是前者,v58.0的时候已改为后者。

调整之后的pageAction我们可以简单地把它看成是可以置灰的browserAction。

  • chrome.pageAction.show(tabId) 显示图标;
  • chrome.pageAction.hide(tabId) 隐藏图标;

示例(只有打开百度才显示图标):

// manifest.json
{
	"page_action":
	{
		"default_icon": "img/icon.png",
		"default_title": "我是pageAction",
		"default_popup": "popup.html"
	},
	"permissions": ["declarativeContent"]
}

// background.js
chrome.runtime.onInstalled.addListener(function(){
	chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){
		chrome.declarativeContent.onPageChanged.addRules([
			{
				conditions: [
					// 只有打开百度才显示pageAction
					new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: 'baidu.com'}})
				],
				actions: [new chrome.declarativeContent.ShowPageAction()]
			}
		]);
	});
});

Snipaste_2023-01-04_17-16-31.pngSnipaste_2023-01-04_17-17-12.png

contextMenu(右键菜单)

通过开发Chrome插件可以自定义浏览器的右键菜单,主要是通过chrome.contextMenusAPI实现,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等,如果有同一个插件里面定义了多个菜单,Chrome会自动组合放到以插件名字命名的二级菜单里

添加右键百度搜索

// manifest.json
{
   "permissions": ["contextMenus","tabs"]
}
// background.js
chrome.contextMenus.create({
	title: '使用度娘搜索:%s', // %s表示选中的文字
	contexts: ['selection'], // 只有当选中文字时才会出现此右键菜单
	onclick: function(params)
	{
		// 注意不能使用location.href,因为location是属于background的window对象
		chrome.tabs.create({url: 'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(params.selectionText)});
	}
});

效果如下:

Snipaste_2023-01-04_17-41-58.png

语法说明

这里只是简单列举一些常用的,完整API参见:developer.chrome.com/extensions/…

chrome.contextMenus.create({
	type: 'normal', // 类型,可选:["normal", "checkbox", "radio", "separator"],默认 normal
	title: '菜单的名字', // 显示的文字,除非为“separator”类型否则此参数必需,如果类型为“selection”,可以使用%s显示选定的文本
	contexts: ['page'], // 上下文环境,可选:["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"],默认page
	onclick: function(){}, // 单击时触发的方法
	parentId: 1, // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	documentUrlPatterns: 'https://*.baidu.com/*' // 只在某些页面显示此右键菜单
});
// 删除某一个菜单项
chrome.contextMenus.remove(menuItemId);
// 删除所有自定义右键菜单
chrome.contextMenus.removeAll();
// 更新某一个菜单项
chrome.contextMenus.update(menuItemId, updateProperties);

override(覆盖特定页面)

使用override页可以将Chrome默认的一些特定页面替换掉,改为使用扩展提供的页面。

扩展可以替代如下页面:

  • 历史记录:从工具菜单上点击历史记录时访问的页面,或者从地址栏直接输入 chrome://history
  • 新标签页:当创建新标签的时候访问的页面,或者从地址栏直接输入 chrome://newtab
  • 书签:浏览器的书签,或者直接输入 chrome://bookmarks

注意:

  • 一个扩展只能替代一个页面;
  • 不能替代隐身窗口的新标签页;
  • 网页必须设置title,否则用户可能会看到网页的URL,造成困扰;

下面的截图是默认的新标签页和被扩展替换掉的新标签页。

无标题3.png

当前页面是在start目录中

代码(注意,一个插件只能替代一个默认页,以下仅为演示):

"chrome_url_overrides":{
   "newtab": "start.html"
}

devtools(开发者工具)

使用过vue的应该见过这种类型的插件:

无标题3.png

是的,Chrome允许插件在开发者工具(devtools)上动手脚,主要表现在:

  • 自定义一个和多个和Elements、Console、Sources等同级别的面板;
  • 自定义侧边栏(sidebar),目前只能自定义Elements面板的侧边栏;

先来看2张简单的demo截图,自定义面板(判断当前页面是否使用了jQuery):

无标题3.png
自定义侧边栏(获取当前页面所有图片):

无标题3.png

devtools扩展介绍

官方:developer.chrome.com/extensions/…
来一张官方图片:

无标题3.png

每打开一个开发者工具窗口,都会创建devtools页面的实例,F12窗口关闭,页面也随着关闭,所以devtools页面的生命周期和devtools窗口是一致的。devtools页面可以访问一组特有的DevTools API以及有限的扩展API,这组特有的DevTools API只有devtools页面才可以访问,background都无权访问,这些API包括:

  • chrome.devtools.panels:面板相关;
  • chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;
  • chrome.devtools.network:获取有关网络请求的信息;

大部分扩展API都无法直接被DevTools页面调用,但它可以像content-script一样直接调用chrome.extensionchrome.runtimeAPI,同时它也可以像content-script一样使用Message交互的方式与background页面进行通信。

实例:创建一个devtools扩展

首先,要针对开发者工具开发插件,需要在清单文件声明如下:

{
	// 只能指向一个HTML文件,不能是JS文件
	"devtools_page": "devtools.html"
}

这个devtools.html里面一般什么都没有,就引入一个js:

无标题2.png

可以看出来,其实真正代码是devtools.js,html文件是“多余”的,所以这里觉得有点坑,devtools_page干嘛不允许直接指定JS呢?

再来看devtools.js的代码:

// 创建自定义面板,同一个插件可以创建多个自定义面板
// 几个参数依次为:panel标题、图标(其实设置了也没地方显示)、要加载的页面、加载成功后的回调
chrome.devtools.panels.create('MyPanel', 'img/icon.png', 'mypanel.html', function(panel){
	console.log('自定义面板创建成功!'); // 注意这个log一般看不到
});

// 创建自定义侧边栏
chrome.devtools.panels.elements.createSidebarPane("Images", function(sidebar){
	// sidebar.setPage('../sidebar.html'); // 指定加载某个页面
	sidebar.setExpression('document.querySelectorAll("img")', 'All Images'); // 通过表达式来指定
	//sidebar.setObject({aaa: 111, bbb: 'Hello World!'}); // 直接设置显示某个对象
});

setPage时的效果:

无标题3.png
以下截图示例的代码:

无标题3.png

// 检测jQuery
document.getElementById('check_jquery').addEventListener('click', function(){
	// 访问被检查的页面DOM需要使用inspectedWindow
	// 简单例子:检测被检查页面是否使用了jQuery
	chrome.devtools.inspectedWindow.eval("jQuery.fn.jquery", function(result, isException){
		var html = '';
		if (isException) html = '当前页面没有使用jQuery。';
		else html = '当前页面使用了jQuery,版本为:'+result;
		alert(html);
	});
});

// 打开某个资源
document.getElementById('open_resource').addEventListener('click', function(){
	chrome.devtools.inspectedWindow.eval("window.location.href", function(result, isException){
		chrome.devtools.panels.openResource(result, 20, function()
		{
			console.log('资源打开成功!');
		});
	});
});

// 审查元素
document.getElementById('test_inspect').addEventListener('click', function(){
	chrome.devtools.inspectedWindow.eval("inspect(document.images[0])", function(result, isException){});
});

// 获取所有资源
document.getElementById('get_all_resources').addEventListener('click', function(){
	chrome.devtools.inspectedWindow.getResources(function(resources)
	{
		alert(JSON.stringify(resources));
	});
});

调试技巧

修改了devtools页面的代码时,需要先在 chrome://extensions 页面按下Ctrl+R重新加载插件,然后关闭再打开开发者工具即可,无需刷新页面(而且只刷新页面不刷新开发者工具的话是不会生效的)。

由于devtools本身就是开发者工具页面,所以几乎没有方法可以直接调试它,直接用 chrome-extension://extid/devtools.html的方式打开页面肯定报错,因为不支持相关特殊API,只能先自己写一些方法屏蔽这些错误,调试通了再放开。

omnibox

所谓options页,就是插件的设置页面,有2个入口,一个是右键图标有一个“选项”菜单,还有一个在插件管理页面:

omnibox是向用户提供搜索建议的一种方式。先来看个gif图以便了解一下这东西到底是个什么鬼:

无标题2.png

注册某个关键字以触发插件自己的搜索建议界面,然后可以任意发挥了。

首先,配置文件如下:

{
	// 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
	"omnibox": { "keyword" : "go" },
}

然后background.js中注册监听事件:

// omnibox 演示
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
	console.log('inputChanged: ' + text);
	if(!text) return;
	if(text == '美女') {
		suggest([
			{content: '中国' + text, description: '你要找“中国美女”吗?'},
			{content: '日本' + text, description: '你要找“日本美女”吗?'},
			{content: '泰国' + text, description: '你要找“泰国美女或人妖”吗?'},
			{content: '韩国' + text, description: '你要找“韩国美女”吗?'}
		]);
	}
	else if(text == '微博') {
		suggest([
			{content: '新浪' + text, description: '新浪' + text},
			{content: '腾讯' + text, description: '腾讯' + text},
			{content: '搜狐' + text, description: '搜索' + text},
		]);
	}
	else {
		suggest([
			{content: '百度搜索 ' + text, description: '百度搜索 ' + text},
			{content: '谷歌搜索 ' + text, description: '谷歌搜索 ' + text},
		]);
	}
});

// 当用户接收关键字建议时触发
chrome.omnibox.onInputEntered.addListener((text) => {
    console.log('inputEntered: ' + text);
	if(!text) return;
	var href = '';
    if(text.endsWith('美女')) href = 'http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=' + text;
	else if(text.startsWith('百度搜索')) href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text.replace('百度搜索 ', '');
	else if(text.startsWith('谷歌搜索')) href = 'https://www.google.com.tw/search?q=' + text.replace('谷歌搜索 ', '');
	else href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text;
	openUrlCurrentTab(href);
});
// 获取当前选项卡ID
function getCurrentTabId(callback){
	chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
	{
		if(callback) callback(tabs.length ? tabs[0].id: null);
	});
}

// 当前标签打开某个链接
function openUrlCurrentTab(url){
	getCurrentTabId(tabId => {
		chrome.tabs.update(tabId, {url: url});
	})
}

消息通信

消息通信介绍

官方文档:developer.chrome.com/extensions/…

Chrome插件中如何互相通信呢?下面先来系统概况一下,然后再分类细说。需要知道的是,popup和background其实几乎可以视为一种东西,因为它们可访问的API都一样、通信机制一样、都可以跨域。

互相通信概览

注:-表示不存在或者无意义,或者待验证

injected-scriptcontent-scriptpopup-jsbackground-js
injected-script-window.postMessage--
content-scriptwindow.postMessage-chrome.runtime.sendMessage chrome.runtime.connectchrome.runtime.sendMessage chrome.runtime.connect
popup-js-chrome.tabs.sendMessage chrome.tabs.connect-chrome.extension. getBackgroundPage()
background-js-chrome.tabs.sendMessage chrome.tabs.connectchrome.extension.getViews-
devtools-jschrome.devtools. inspectedWindow.eval-chrome.runtime.sendMessagechrome.runtime.sendMessage

通信详细介绍

popup和background

popup可以直接调用background中的JS方法,也可以直接访问background的DOM:

// background.js
function test(){
	alert('我是background!');
}

// popup.js
var bg = chrome.extension.getBackgroundPage();
bg.test(); // 访问bg的函数
alert(bg.document.body.innerHTML); // 访问bg的DOM

小插曲,今天碰到一个情况,发现popup无法获取background的任何方法,找了半天才发现是因为background的js报错了,而你如果不主动查看background的js的话,是看不到错误信息的,特此提醒。

至于background访问popup如下(前提是popup已经打开):

var views = chrome.extension.getViews({type:'popup'});
if(views.length > 0) {
	console.log(views[0].location.href);
}

popup或者bg向content主动发送消息

background.js或者popup.js:

function sendMessageToContentScript(message, callback){
	chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
		chrome.tabs.sendMessage(tabs[0].id, message, function(response){
			if(callback) callback(response);
		});
	});
}
sendMessageToContentScript({cmd:'test', value:'你好,我是popup!'}, function(response){
	console.log('来自content的回复:'+response);
});

content-script.js接收:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
	// console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension");
	if(request.cmd == 'test') alert(request.value);
	sendResponse('我收到了你的消息!');
});

双方通信直接发送的都是JSON对象,不是JSON字符串,所以无需解析,很方便(当然也可以直接发送字符串)。

网上有些老代码中用的是chrome.extension.onMessage,没有完全查清二者的区别(貌似是别名),但是建议统一使用chrome.runtime.onMessage。

content-script主动发消息给后台

content-script.js:

chrome.runtime.sendMessage({greeting: '你好,我是content-script呀,我主动发消息给后台!'}, function(response) {
	console.log('收到来自后台的回复:' + response);
});

background.js 或者 popup.js:

// 监听来自content-script的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
	console.log('收到来自content-script的消息:');
	console.log(request, sender, sendResponse);
	sendResponse('我是后台,我已收到你的消息:' + JSON.stringify(request));
});

注意事项:

  • content_scripts向popup主动发消息的前提是popup必须打开!否则需要利用background作中转;
  • 如果background和popup同时监听,那么它们都可以同时收到消息,但是只有一个可以sendResponse,一个先发送了,那么另外一个再发送就无效;

injected script和content-script

content-script和页面内的脚本(injected-script自然也属于页面内的脚本)之间唯一共享的东西就是页面的DOM元素,有2种方法可以实现二者通讯:

  1. 可以通过window.postMessagewindow.addEventListener来实现二者消息通讯;
  2. 通过自定义DOM事件来实现;
    第一种方法(推荐):

injected-script中:

window.postMessage({"test": '你好!'}, '*');

content script中:

window.addEventListener("message", function(e){
	console.log(e.data);
}, false);

第二种方法:

injected-script中:

var customEvent = document.createEvent('Event');
customEvent.initEvent('myCustomEvent', true, true);
function fireCustomEvent(data) {
	hiddenDiv = document.getElementById('myCustomEventDiv');
	hiddenDiv.innerText = data
	hiddenDiv.dispatchEvent(customEvent);
}
fireCustomEvent('你好,我是普通JS!');

content-script.js中:

var hiddenDiv = document.getElementById('myCustomEventDiv');
if(!hiddenDiv) {
	hiddenDiv = document.createElement('div');
	hiddenDiv.style.display = 'none';
	document.body.appendChild(hiddenDiv);
}
hiddenDiv.addEventListener('myCustomEvent', function() {
	var eventData = document.getElementById('myCustomEventDiv').innerText;
	console.log('收到自定义事件消息:' + eventData);
});

长连接和短连接

其实上面已经涉及到了,这里再单独说明一下。Chrome插件中有2种通信方式,一个是短连接(chrome.tabs.sendMessagechrome.runtime.sendMessage),一个是长连接(chrome.tabs.connectchrome.runtime.connect)。

短连接的话就是挤牙膏一样,我发送一下,你收到了再回复一下,如果对方不回复,你只能重新发,而长连接类似WebSocket会一直建立连接,双方可以随时互发消息。

短连接上面已经有代码示例了,这里只讲一下长连接。
popup.js:

getCurrentTabId((tabId) => {
	var port = chrome.tabs.connect(tabId, {name: 'test-connect'});
	port.postMessage({question: '你是谁啊?'});
	port.onMessage.addListener(function(msg) {
		alert('收到消息:'+msg.answer);
		if(msg.answer && msg.answer.startsWith('我是'))
		{
			port.postMessage({question: '哦,原来是你啊!'});
		}
	});
});

content-script.js:

// 监听长连接
chrome.runtime.onConnect.addListener(function(port) {
	console.log(port);
	if(port.name == 'test-connect') {
		port.onMessage.addListener(function(msg) {
			console.log('收到长连接消息:', msg);
			if(msg.question == '你是谁啊?') port.postMessage({answer: '我是你爸!'});
		});
	}
});

动态注入或执行JS

虽然在backgroundpopup中无法直接访问页面DOM,但是可以通过chrome.tabs.executeScript来执行脚本,从而实现访问web页面的DOM(注意,这种方式也不能直接访问页面JS)。

示例manifest.json配置

{
	"name": "动态JS注入演示",
	...
	"permissions": [
		"tabs", "http://*/*", "https://*/*"
	],
	...
}
// 动态执行JS代码
chrome.tabs.executeScript(tabId, {code: 'document.body.style.backgroundColor="red"'});
// 动态执行JS文件
chrome.tabs.executeScript(tabId, {file: 'some-script.js'});

动态注入CSS

// 动态执行CSS文件
chrome.tabs.insertCSS(tabId, {file: 'some-style.css'});

获取当前窗口ID

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

获取当前标签页ID

一般有2种方法:

// 获取当前选项卡ID
function getCurrentTabId(callback){
	chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
	{
		if(callback) callback(tabs.length ? tabs[0].id: null);
	});
}

获取当前选项卡id的另一种方法,大部分时候都类似,只有少部分时候会不一样(例如当窗口最小化时)

// 获取当前选项卡ID
function getCurrentTabId2(){
	chrome.windows.getCurrent(function(currentWindow){
		chrome.tabs.query({active: true, windowId: currentWindow.id}, function(tabs){
			if(callback) callback(tabs.length ? tabs[0].id: null);
		});
	});
}

本地存储

本地存储建议用chrome.storage而不是普通的localStorage,区别有好几点,个人认为最重要的2点区别是:

  • chrome.storage是针对插件全局的,即使你在background中保存的数据,在content-script也能获取到;
  • chrome.storage.sync可以跟随当前登录用户自动同步,这台电脑修改的设置会自动同步到其它电脑,很方便,如果没有登录或者未联网则先保存到本地,等登录了再同步至网络;

需要声明storage权限,有chrome.storage.syncchrome.storage.local2种方式可供选择,使用示例如下:

// 读取数据,第一个参数是指定要读取的key以及设置默认值
chrome.storage.sync.get({color: 'red', age: 18}, function(items) {
	console.log(items.color, items.age);
});
// 保存数据
chrome.storage.sync.set({color: 'blue'}, function() {
	console.log('保存成功!');
});

API总结

比较常用用的一些API系列:

  • chrome.tabs
  • chrome.runtime
  • chrome.webRequest
  • chrome.window
  • chrome.storage
  • chrome.contextMenus
  • chrome.devtools
  • chrome.extension

查看已安装插件路径

已安装的插件源码路径:C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions,每一个插件被放在以插件ID为名的文件夹里面

无标题2.png
如何查看某个插件的ID?进入 chrome://extensions ,然后勾线开发者模式即可看到了

无标题2.png

特别注意background的报错

很多时候你发现你的代码会莫名其妙的失效,找来找去又找不到原因,这时打开background的控制台才发现原来某个地方写错了导致代码没生效,正式由于background报错的隐蔽性(需要主动打开对应的控制台才能看到错误),所以特别注意这点。

注入CSS的时候必须小心

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

之所以强调这个,是因为这个带来的问题非常隐蔽,不太容易找到,可能你正在写某个网页,昨天样式还是好好的,怎么今天就突然不行了?然后你辛辛苦苦找来找去,找了半天才发现竟然是因为插件里面的一个样式影响的!

无标题2.png

接口拦截

想要某个页面所有发出的请求进行响应拦截首先想要在核心配置文件中加入要拦截接口的域名

文中以虾皮购物网站为例子进行操作代码如下:

创建一个utils.js文件

var script = document.createElement('script');
script.type = 'text/javascript';
script.text = "let reg = new RegExp('^(http|https)://.*?\.xiapibuy.com/api/v[0-9]/*');let regV2 = new RegExp('^(http|https)://shopee\..*?/api/(v[0-9])/*');" +
    "function sendData(data) {var requsetData = JSON.stringify({code:'DCODE_01',data:data}); window.postMessage(requsetData, '*')};" +
    "try { let fetch_interceptor={" +
    "originalFetch: window.fetch.bind(window), " +
    "myFetch: function (...args) {" +
    "return fetch_interceptor.originalFetch(...args).then((response) => {" +
    "if (response.ok) {" +
    "if (reg.test(response.url) || regV2.test(response.url)) {" +
    "let fetchResponse = response.clone();" +
    "fetchResponse.json().then(function (fetchData) {" +
    "fetchData.request = args;" +
    "sendData(fetchData) " +
    "})" +
    "}" +
    "} return response; " + "})}}; " +
    "window.fetch = fetch_interceptor.myFetch;" +
    "(function (open) {XMLHttpRequest.prototype.open = function (method, url, async, user, pass) {" +
    "this.addEventListener('readystatechange', function () {" +
    "if (this.readyState == 4 && (reg.test(url) || regV2.test(url)) && this.response) {" +
    "let ajaxData = JSON.parse(this.response);ajaxData.request = {url: url, method: method};" +
    "sendData(ajaxData)}}, false);open.call(this, method, url, async, user, pass)}})(XMLHttpRequest.prototype.open); }catch (e) {}"


document.documentElement.appendChild(script);

reg 是需要拦截的接口地址,可以用正则去匹配
window.postMessage是发送消息,监听到接口请求响应,就发送消息 code为DCODE_01

mainifest.json内容:

"content_scripts": [
        {
            "matches": [
                "*://*.xiapibuy.com/*"
            ],
            "js": [
                "js/utils.js"
            ],
            "run_at": "document_end"
        }
    ],

监听window.postMessage发送过来的消息

window.addEventListener('message', function (event) {
      try {
        if (JSON.parse(event.data).code == 'DCODE_01') {
          console.log(JSON.parse(event.data))
        }
      } catch (e) {
        e.toString()
      }
    }, false);

无标题2.png

无标题2.png

网络请求

Axios请求封装

为了方便的使用网络请求,可以引入axios进行使用

npm install axios

创建一个request.js文件

import axios from "axios";
// 创建axios实例
const instance = axios.create({
    timeout: 30000 // request timeout*/
})

instance.interceptors.request.use(
    async config => {
        config.timeout = 80000
        if (config.requestJson) {
            config.headers['Content-Type'] = 'application/json;charset=UTF-8'
        }
        if (config.formDate) {
            config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
        }
        if (config.upload) {
            config.headers['Content-Type'] = 'multipart/form-data'
        }
        return config
    },
    error => {
        return Promise.reject(error)
    }
)
export default instance

使用方式

import request from '../js/request'

// get请求 
export function getDemo(query) {
    return request({
        url: `http://localhost/demo`,
        method: 'get',
        params: query
    })
}

// 表单请求
export function getDemo(data) {
    return request({
        url: `http://localhost/demo`,
        method: 'post',
        formDate: true,
        data
    })
}
//json格式传参
export function getDemo(query) {
    return request({
        url: `http://localhost/demo`,
        method: 'get',
        requestJson: true,
        params: query
    })
}
//文件上传
export function getDemo(file) {
    return request({
        url: `http://localhost/demo`,
        method: 'post',
        upload: true,
        file
    })
}

Axios请求封装

上篇说了axios封装进行请求,这次用的是自带的fetch封装请求 为了方便的使用网络请求,可以引入fetch进行使用

npm install axios

创建一个request.js文件

export default async(url = '', data = {}, type = 'GET',token= '') => {
    type = type.toUpperCase(); // 请求方式小写转换成大写

    if (type == 'GET') {
        let dataStr = ''; //数据拼接字符串
        Object.keys(data).forEach(key => {
            dataStr += key + '=' + data[key] + '&';
        })
        if (dataStr !== '') {
            dataStr = dataStr.substr(0, dataStr.lastIndexOf('&'));
            url = url + '?' + dataStr;
        }
    }
    let requestConfig = {
        credentials: 'same-origin',
        method: type,
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            "Authorization": "Bearer " + token,
            "small-program": "plugin"
        },
        mode: "cors"// 用来决定是否允许跨域请求  值有 三个 same-origin,no-cors(默认)以及 cores;
    }

    if (type == 'POST') {
        Object.defineProperty(requestConfig, 'body', {
            value: JSON.stringify(data)
        })
    }
    try {
        const response = await fetch(url, requestConfig);
        const responseJson = await response.json();
        return responseJson
    } catch (error) {
        throw new Error(error)
    }
}

使用方式

// get请求
export const getDemo = (params) => api(`http://localhost/demo`,params,"GET")
// post请求
export const getDemo = (data) => api(`http://localhost/demo`,data,"POST")
// post请求 带token
export const getDemo = (data,token) => api(`http://localhost/demo`,data,"POST",token)

区分生产与测试请求地址

在项目根目录下创建.env.development(用于测试环境)与.env.production(用于生产环境)文件

.env.development:

# 测试环境变量配置
NODE_ENV = 'development'
VUE_APP_BASE_URL = http://localhost

.env.production

# 生产环境变量配置
NODE_ENV = 'production'
VUE_APP_BASE_URL = http://192.168.100.100

首先可以创建一个config.js的文件,用于请求地址配置获取

let url = process.env["VUE_APP_BASE_URL"]
export{
   url
}

修改 请求api

import {
    url
} from '../js/config'

// axios方式
export function getDemo(data) {
    return request({
        url: `${url}/demo`,
        method: 'post',
        formDate: true,
        data
    })
}
// fetch方式
export const getDemo = (params) => api(`${url}/demo`,params,"GET")

打包时进行区分 用测试环境打包还是生产环境打包

package.json:

"scripts": {
    "build": "vue-cli-service build --mode development",
    "build:prod": "vue-cli-service build --mode production"
  },

打包使用如下命令

测试环境打包

npm run build

生产环境打包

npm run build:prod

V3版本改动

manifest_version代表此扩展程序使用的 manifest.json 版本,目前主流版本为2,最新版本为3

为什么要升级MV3?

自2023年起,Chrome应用商店将不再接受Manifest V2扩展,构建新扩展需要升级到Manifest V3。以下为Manifest V2支持时间线:

无标题2.png

Manifest V2 to V3有哪些变化

参考:
Chrome Extension Tutorial: Migrating to Manifest V3 from V2
How to migrate manifest version 2 to v3 for chrome extension?

manifest.json配置

  1. page_action和browser_action在MV3中统一到action下
// Manifest v2
"browser_action": {...}
"page_action": {
    "default_icon": "images/icon.png",
    "default_title": "这是一个示例Chrome插件",
    "default_popup": "popup.html"
},

// Manifest v3
"action": {...}
  1. 在MV3中,background persistent被弃用,background scripts被替换为background service_worker
// Manifest v2
"background": {
    "scripts": ["static/js/background.js"],
    "persistent": true
},

// Manifest v3
"background": {
    "service_worker": "static/js/background.js"
}
  1. web_accessible_resources类型由字符串数组变更为对象
//v2配置,数组格式,多个资源逗号添加即可,v2好像不限制外部访问,这里添加了一个图片地址,那么所有网站都可以通过"chrome-extension://chrome扩展程序ID/images/logo.png"绝对地址访问到这张图片
"web_accessible_resources": [
  "images/logo.png"
]

//v3配置,数组对象格式,对象中分了可外部访问的资源以及允许哪些外部网站可以访问该资源,下面这里的配置说明只有在csdn域名下才可以通过"chrome-extension://Chrome扩展程序ID/images/logo.png"绝对地址访问到这张图片,其它域名则无法访问
"web_accessible_resources": [{
  "resources": ["*/images/logo.png"], //resources添加可外部访问的资源
  "matches": [ //matches添加允许访问资源的网站域名
    "https://*.csdn.net/*"
  ]
}]
  1. 内容安全政策参数:V2的value是字符串,V3是对象
//v2配置,v2版本中可以配置script等通过外部引入,这个content_security_policy配置参数不加或者加上之后相应的值填none
"content_security_policy": "script-src 'none'; object-src 'none'",

//v3配置,v3版本中安全政策配置script引入等信息,都必须填写self,即只允许script标签引用当前插件内部文件,不允许引用外部链接,如果不填写self的话,插件添加到扩展程序时会报错
"content_security_policy": {
   //原文:此政策涵盖您的扩展程序中的页面,包括 html 文件和服务人员;
   "extension_pages": "script-src 'self'; object-src 'self'",

//原文:此政策涵盖您的扩展程序使用的任何[沙盒扩展程序页面](https://developer.chrome.com/docs/extensions/mv3/manifest/sandbox/)。;
   "sandbox": "sandbox allow-scripts; script-src 'self'; object-src 'self'"
}
  1. permissions参数:权限配置参数
//v2配置权限,在v2中的权限配置中,ChromeAPI权限和主机权限是一起配置的。
//ChromeAPI权限:需要使用Chrome的一些API的话需要配置对应的API权限,否则会报错未添加权限而无法使用,如tags标签页,contextMenus添加自定义右键菜单项)
//主机权限:主机权限也可以称为请求白名单权限,在背景页backgroud.js里面或者popup页面调用某个网站请求时,增加该网站的白名单权限,如果没添加的则调用请求会报跨域)
"permissions": [
  "tabs",
  "contextMenus",
  "http://*.xxx.com/" //添加xxx域名,则在插件中可以请求xxx网站的接口,如果没加的话请求接口会跨域
]

//v3配置,在v3版本中API权限和主机权限的配置分开了
//ChromeAPI权限
"permissions": ["tabs", "contextMenus", ...],
//主机权限
"host_permissions": ["http://*.xxx.com/", ...]

消息通信

在MV2中,chrome.extension.onMessage.addListener()是很常用的content向background通信的手段,但在MV3中,需要改写为chrome.runtime.onMessage.addListener()

升级V3时遇到的问题

添加插件到扩展程序时报错安全策略不允许不安全的策略

造成原因:原因就是上面的安全策略参数没有把script-src设为self
解决方案:把script-src值设置为self即可

复制v2版本的permissions配置项到v3时,把配置项改名为host_permissions,添加到扩展程序时报错权限清单配置错误

造成原因:因为V3的API权限配置和主机权限配置是两个参数,host_permissions里面不能添加API权限
解决方案:区分出API权限和主机权限的可配置值,把API权限的信息添加到permissions配置项中,host_permissions主机权限只保留白名单域名信息

需要在插件中监听某个页面的请求,使用chrome.webRequest.xxx.addListener方法报错无效

造成原因:因为上面第二条的问题原因,当时直接把webRequestAPI权限删掉了,没有把webRequestAPI权限添加到permissions配置项中
解决方案:添加permissions配置项,并把webRequest添加进去即可使用chrome.webRequest的监听事件

使用chrome.webRequest.xxx.addListener方法监听时,第三个参数传blocking报错,在API权限中配置webRequestBlocking了也不行

造成原因:不详!不清楚是不是V3不能用blocking了
解决方案:把blocking改成responseHeaders或者requestBody,如果是要监听请求获取请求头参数则改成responseHeaders,如果是要监听请求获取请求体参数则改成requestBody

在外部网站直接a标签href="chrome-extension://Chrome扩展程序ID/main.html"跳转到插件popup页,页面报错无法访问

造成原因:V3不能在外部网站通过这种绝对地址跳转的方式访问插件popup页了
解决方案:需要在外部网站通过chrome.runtime.sendMessage给插件发送消息,然后在插件background.js里面监听消息后用chrome.tabs.create({url: url}, function (tabs){})创建一个标签页打开,url是通过chrome.runtime.getURL('main.htm')来拼接的,getURL方法可以获取到当前插件的地址前缀’chrome-extension://chrome扩展程序ID/',然后拼接传进去的参数地址

右侧插件图标点击监听事件chrome.browserAction.onClicked.addListener方法失效

造成原因:V3废除了browserAction配置,改为了action,前面的配置文件参数的区别说明中第一点有提到
解决方案:需要在配置文件中添加action配置项,且监听事件改成使用chrome.action.onClicked.addListener方法来监听

添加自定义鼠标右键菜单选项chrome.contextMenus.create({})方法报错参数不支持onclick回调函数字段

造成原因:V3中右键菜单的配置项去除了onclick回调函数参数,改为了另一种方式进行监听右键自定义菜单选项点击事件
解决方案:添加右键菜单的点击监听事件函数chrome.contextMenus.onClicked.addListener(function(){})来进行监听

V3版本的service worker(原V2的背景页,即background)中不支持window和document对象

造成原因:V3最大的改动就是这一点,原来V2的背景页和浏览器是有关联的,service worker直接和浏览器进行了隔离,导致window对象和document对象都无法使用,比如我们用到了浏览器数据库window.IndexdDB,以及因为没有了document对象,导致在background中无法引用jQuery去使用$.ajax,且原生的XmlHttpRequest请求也无法使用,vue的axios请求也不行
解决方案:V3版本中直接给了indexedDB这么一个参数对象,无需window.indexedDB调用,直接indexedDB调用即可使用浏览器数据库,可以一切照旧,ajax请求则需要换成Fetch请求才行

可以看这两篇介绍background的改动:

  1. developer.chrome.com/docs/extens…
  2. developer.chrome.com/docs/extens…

因为不能用ajax请求,换成fetch请求后,有些请求报错501、500、401等

造成原因:fetch请求头默认参数和ajax请求头默认参数不一致,以及请求参数的格式也会发生变化
解决方案:先找之前的ajax请求,右键可以复制成fetch请求方式查看区别

发布插件到谷歌市场

第一步,进入 Google 开发者中心注册开发者账户。

如果程序员连墙都翻不出去话,当不了好厨子。点击链接chrome.google.com/webstore/de…

进入google开发者控制台,你会看到下图,提示交5美元注册开发者。 注册google开发者账户,提交5美元注册谷歌开发者账户:

第二步,提交chrome扩展程序或者chrome插件,应用。

注册开发者身份成功后,我们就可以将CRX文件打包提交了。在开发者信息中心,选择添加新项。首次发布项目之前,您必须支付 US5.00的一次性开发者注册费。谷歌收取此费用的目的是对开发者帐户进行验证,并为用户提供更好的保护,以防他们受到欺骗性活动的侵害。支付注册费后,您最多可以发布 20 项内容。谷歌使用 Google 电子钱包来处理US5.00的一次性开发者注册费。谷歌收取此费用的目的是对开发者帐户进行验证,并为用户提供更好的保护,以防他们受到欺骗性活动的侵害。支付注册费后,您最多可以发布20项内容。谷歌使用Google电子钱包来处理US5.00付款。如果您之前未使用过 Google 电子钱包,我们会要求您提供结算信息。

无标题3.png

无标题3.png
注意事项:
注意上传的文件格式需要时zip格式,而不是crx格式。

第三步,编辑chrome插件信息。

我们在访问谷歌应用商店的插件chrome扩展程序上有一些文字的介绍信息,我们开发人员在上传成功后,需要在开发者信息中心中编辑插件的基本信息。

无标题3.png

填写完信息,点击提交审核即可。

捐赠

如果你觉得文章不错,可以捐赠请作者吃包辣条~,在此表示感谢^_^

微信图片_20230104230002.png