Chrome插件开发+油猴脚本编写

2023/7/20 chrome插件TypeScript

本文包含油猴插件与chrome插件的简单实现。

起因是项目中需要做一个快捷登录的功能作为内部测试权限使用,因为不能在项目代码中出现,经过思索后我选择使用油猴插件来实现。

在完成后,我对浏览器插件产生了兴趣,打算实践了解下。

# 油猴插件

油猴插件 - 官网 (opens new window)是一个用来增强网页功能的浏览器插件,可以在网页上随意插入js代码并执行。脚本商店greasyfork (opens new window)中有网友上传的针对各个网页的增强功能脚本。

chrome 应用商店安装 (opens new window)

对于我们来说 可以不用了解chrome插件相关的api、逻辑就可以直接使用js去执行操作,低成本上手。

# 新增脚本

打开油猴的管理面板,点击加号新增脚本。

默认打开的代码分两个部分:配置项(==UserScript==部分),执行的代码(普通的js函数+GM函数)

// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://www.numplanet.com/blog/tag/npm/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=numplanet.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Your code here...
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在代码中你可以正常的执行js,可以执行GM_函数(GM API列表 (opens new window))。

对于我的需求来说,只需要在对应页面增加一个按钮 点击后发送一个请求即可。

# chrome插件

如果说不想用油猴,想做一个自己的插件,那么需要学习下chrome插件相关api、执行逻辑。如果想要支持不同的浏览器,需要查询其他浏览器的插件开发文档,不过差别不是很大。

Tips:

插件的发布需要注册谷歌开发者账号,5美金。

如果没有发布的打算,可以使用插件的 “加载已解压的扩展程序” 功能来使用(缺点是对应的代码文件夹不能删除)

# 原理

chrome插件本质上就是几个个独立且可以通信的部分,常用的分别是:popup(点击浏览器插件列表时打开的弹窗)、background(后台运行的js)、content script(在网页中执行的js)、options(扩展程序选项页),还有些不常用的部分,这里就不细说了,可以在这篇chrome 插件开发指南(Manifest V3)- 三、主要构成 (opens new window)文章中了解。

现在举个几例子来帮助理解这几部分的功能:

  • 想要给插件添加配置参数的功能,那应该放到options中;
  • 想要插件在后台继续执行,需要把执行的任务写在background中;
  • 想要在页面中操作dom/css,需要在content script中写需要插入的代码;
  • 想要对页面做设置或者添加快捷入口,可以在content script中写一个插入的弹窗,或者在popup中写一个页面

# 开发环境搭建(vite)

用vite初始化了项目之后,我们需要:

  1. 把浏览器插件需要用的配置文件和配置文件中使用到的静态资源放到 /public 目录下

  2. 配置文件 manifest.json

    1. manifest_version 建议写3,因为2即将要废弃。manifest_version v3 官方文档 (opens new window)
    2. 编译的文件路径写成 ./assets/XXX、静态资源的路径写成 ./img/XXX,这些需要与vite中配置对应
  3. vite.config.js 中,

    1. 需要去掉文件hash,省的每次编译完都要在manifest中修改
    2. 多页面编译,用来编译出多个文件给上文的几个部分使用
    3. 单js文件编译,有个插件很好用:hy-vite-plugin-chrome-ext

# 配置文件

# manifest.json

 {
  "manifest_version": 3,
  "name": "",
  "author": "",  
  "version": "1.0.0",
  "default_locale": "zh_CN",
  "description": "",
  "icons": {
    "16": "img/logo16.png",
    "48": "img/logo48.png",
    "128": "img/logo128.png"
   },
  "homepage_url": "https://www.numplanet.com/",
  
  "action": {
    "default_popup": "action.html",
    "default_title": "Click Me",
    "default_icon": {
      "16": "img/logo16.png",
      "48": "img/logo48.png",
      "128": "img/logo128.png"
    }
  },

  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["assets/content.bundle.js"],
    "css": ["assets/content.bundle.css"]
  }],

  "background": [{
    "matches": ["<all_urls>"],
    "js": ["assets/background.bundle.js"],
    "run_at": "document_idle"
  }]

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { chromeExtension } from './packages/hy-vite-plugin-chrome-ext/src/index';
import path from "path";

export default defineConfig({
  plugins: [
    vue(), 
    chromeExtension({
      singleScripts: ['content', 'sdk'],
    }),
  ],
  build: {
    outDir: "", // 建议改个名,在【加载已解压的扩展程序】中方便查找
    rollupOptions: {
      input: {
        action: path.resolve(__dirname, "action.html"),
        content: path.resolve(__dirname, "content.html"),
        background: path.resolve(__dirname, "background.html"),
      },
      output: {
        entryFileNames: `assets/[name].bundle.js`,
        chunkFileNames: `assets/[name].bundle.js`,
        assetFileNames: `assets/[name].bundle.[ext]`,
      }
    },
    modulePreload: {
      polyfill: false,
    }
  },
  resolve: {
    // 配置路径别名
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 结构间的通信

injected script content script popup script background script
injected script / window.postMessage / /
content script window.postMessage / chrome.runtime.sendNessage
chrome.runtime.connect
chrome.runtime.sendNessage
chrome.runtime.connect
popup script / chrome.tabs.sendMessage
chrome.tabs.connect
/ chrome.extension.getBackgroundPage()
background script / chrome.tabs.sendMessage
chrome.tabs.connect
chrome.extension.getViews /
devtools script chrome.devtools.inspectedwindow.eval / chrome.runtime.sendNessage chrome.runtime.sendNessage

举个例子:

/**
* 情景:
* 每个页面都需要不同的配置,在页面打开但popup没打开时,传消息会报错
* 所以改成当popup打开时,询问页面当前页面参数是什么,再渲染
*/
// content.ts
const Listening = (callback: Function)=>{
    chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
      const {actions, data} = message.data
      switch(actions){
        case "get": 
          sendResponse(configCache);
          break;
        case "set": 
          callback(data as AppConfig)
          break;
      }
    })
  }
// action.ts
const sendToContent = (data: any, callback?: Function) => {
  chrome.tabs.query({
    active: true,
    currentWindow: true,
  }, (tabs: any) => {
    chrome.tabs.sendMessage(tabs[0].id, {
      data,
    }, (res: any) => {
      callback && callback(res);
    });
  });
};
// 这样两部分就可以获取最新的配置参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Tip:

  1. popup / action 是一个东西
  2. 在action没打开时,发送消息是收不到的;打开action之后,右键可以调出DevTools
  3. background.js 是一直在的但是这个部分部包含界面,而且不要滥用,防止内存占用过大
  4. content.js 是不能取到原始页面的全局变量的,但是可以有特殊方法可以跳过使用,理论上是可行的,有待尝试
    1. 插入script 标签,并在innerHTML中写拿原始页面全局变量的代码
    2. 插入dom,并在dom的交互事件中返回某个全局变量
  5. 建议不要看v2的mainfast,省的再升级
  6. 官网说的很清晰 但是没有中文 直接翻译能了解个大概
  7. 打包工具只是为了让编译后的文件格式符合插件的要求,所以框架啥的无所谓,只看结果

# 参考文章

manifest_version v3 官方文档 (opens new window)

chrome 插件开发指南(Manifest V3) (opens new window)

一篇文章教会你如何使用Vue开发Chrome插件 (opens new window)

入门系列3 - background、content、popup的通信 (opens new window)

chrome extension v3 示例(vite) (opens new window)

chrome插件访问原始页面的变量 (opens new window)