BACK
Featured image of post 【Tampermonkey】輕鬆上手 - 油猴腳本開發

【Tampermonkey】輕鬆上手 - 油猴腳本開發

油猴插件為輕量化腳本提供了一個平台,在線編輯器中編寫油猴腳本即時生效,通過 Github、GreasyFork 快速分發。

參考網站
參考網站

油猴腳本運行於油猴插件之上,油猴插件本質上對瀏覽器能力的再封裝。既然如此,我們先來簡單了解一下瀏覽器插件。


瀏覽器插件(Browser Extension):瀏覽器的擴展應用

說的直白一點,就是拿著瀏覽器開放的能力(插件API),去實現一些小型應用。

瀏覽器插件主要由四部分構成:background scripts、content scripts、全局 UI 元素、options page。

  • background scrips: 後台腳本,一個後台腳本是一個獨立線程,是游離於各個頁面之外的"上帝之眼"。具有訪問各類插件 API 的能力,但同時也喪失了直接操作頁面的能力。
  • content scripts:內容腳本,具有直接操作頁面的能力。其實就是在頁面中運行 js 腳本,可以使用 DOM API。content script 只能直接訪問少量插件 API,但能和 background script 進行雙向通信完成數據交換。
  • 全局 UI 元素:瀏覽器層的UI 交互,包括:
    • 在 Toolbar 顯示 icon,定義點擊 icon 後顯示的 Popup 或其他效果
    • 增加右鍵選項
    • 增加全局快捷鍵
    • 改造新Tab 頁、歷史記錄頁、書籤頁
  • options page:插件配置頁。

瀏覽器插件的核心機制可以用下圖簡單概括:

想必,大家最好奇的還是有哪些 API 以及能用這些 API 做什麼,這裡例舉幾個:

  • contextMenus:增加右鍵選項。
    • 使用選中文本,例如:劃詞翻譯、文本收集
    • 快速調用插件功能,例如:打開 DevTool,頁面剪藏
  • cookies:增刪改查 cookie(任意域名),直接拿著本地 cookie 發送請求,不必再做授權。同時由於後台腳本不是 Web 頁面,在發送請求時沒有跨域限制。
    • 多平台信息聚合
    • 多平台信息分發
  • devtools.panels:增加 Devtool 面板,這個對前端開發者來說應該很熟悉,React Developer Tools、Vue.js devtools。
  • notifications:瀏覽器通知,未打開頁面的情況下進行通知,可以輔助一些工具類應用。
  • storage:全局保存數據,可跟隨瀏覽器帳戶同步。

這裡例舉的只是我常用的一些,只是滄海一粟,更多 API 可以查閱:

瀏覽器插件就簡單介紹到這裡,如果有興趣繼續了解,推薦:ChromeEdgeMozilla三家的文檔。


油猴插件(Tampermonkey)

瀏覽器插件可以實現各式各樣的功能,但有時候開發者只是想對某一個站點加一點點小功能,如果這也要構建環境打包上架分發,未免就太麻煩了一些;從應用市場角度來看,充斥著顆粒化的應用,市場也會擁擠繁雜不堪。

油猴插件為輕量化腳本提供了一個平台,在線編輯器中編寫油猴腳本即時生效,通過 Github、GreasyFork 快速分發。

在油猴插件中,content script 起到非常重要的角色,它將用戶編寫的代碼運行在頁面中,同時提供 GM_xxxx 函數封裝瀏覽器的部分能力。封裝的內部實現是和 background script 通信,驅動 background script 調用插件API。

對油猴插件簡單了解之後,來看看如何編寫油猴腳本。


Tampermonkey API

油猴腳本由頭部核心邏輯兩部分組成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://www.tampermonkey.net/documentation.php?ext=dhdg
// @icon         https://www.google.com/s2/favicons?domain=tampermonkey.net
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // Your code here...
})();

頭部

頭部是腳本的一些元信息、更新方式、指定運行頁面、權限聲明,逐一解釋一下:

配置名作用使用技巧
@name腳本顯示的名稱加後綴實現國際化,例如:
@name:zh-CN 指定在瀏覽器語言為中文時顯示的名稱
@namespace腳本的命名空間,可以理解為腳本的標識為了避免衝突一般使用 Github 倉庫地址
@version與更新相關,當前版本
@updateURL檢查腳本是否更新地址配合 @version 和自動更新使用
@downloadURL檢測到更新時,去哪下載腳本
@supportURL遇到問題時,用戶去哪反饋
@include腳本在哪些頁面運行可使用正則,不支持 hashtag,多個頁面的地址聲明多個 @include 即可
@match@include 類似
@exclude腳本禁止在哪些頁面運行,優先於 @include
@require在腳本運行前引入外部 JavaScript 文件例如:引入 jQuery
@resource聲明外部資源文件,搭配 GM_getResourceText 使用例如:引入 html、icon
@connect聲明 GM_xmlhttpRequest 可訪問的域必須指定才能正常請求
@grant聲明 GM_xxx 函數的使用列表必須先指定權限才能正常使用
@run-at指定腳本運行時機document-start:盡快執行
document-body:當 body 掛載時執行
document-end:DOMContentLoaded 觸發時執行
document-idle:DOMContentLoaded 觸發後執行,也是默認設置項
context-menu: 右鍵菜單項被點擊時執行
@author作者名
@description簡短介紹同樣可以加後綴實現國際化,例如:
@description:zh-CN
@homepage主頁地址如果未設置並且 @namespace 是倉庫地址,默認導向倉庫地址
@icon腳本 icon
@icon6464x64 像素的腳本 icon
@antifeature腳本是否有廣告、挖礦、數據收集等商業行為
@noframes聲明腳本不在 iframe 中運行

核心邏輯

核心邏輯通過一個立即執行函數包裹,避免和全局作用域相互干擾。Tampermonkey 將瀏覽器的部分能力封裝為 GM_XXX 函數以供調用:

API作用使用技巧
unsafeWindow訪問頁面的 Window 對象
GM_addStyle(css)創建全局樣式的快捷方式,向頁面插入 style 元素也可以用 DOM 操作手動創建
GM_addElement(tag_name, attributes)
GM_addElement(parent_node, tag_name, attributes)
向 DOM 新建元素的快捷方式也可以用 DOM 操作手動創建
GM_log(message)在 Console 中打印信息console.log 的快捷方式
GM_getValue(name, defaultValue)從存儲體中獲取數據
GM_deleteValue(name)從存儲體中刪除數據
GM_listValues()列舉存儲體中所有數據項
GM_addValueChangeListener監聽數據更新例如要使 Tab 間數據同步,可以用監聽 value 達成同步
GM_removeValueChangeListener移除監聽
GM_getResourceText(name)獲取 @resource 中已聲明的資源
GM_getResourceURL(name)獲取 @resource 中已聲明的資源(base64 URI 形式)
GM_registerMenuCommand(name, fn, accessKey)在 Tampermonkey 的 popup 中增加選項
GM_unregisterMenuCommand(menuCmdId)移除選項
GM_openInTab(url, options)新開一個 tab 頁
GM_xmlhttpRequest(details)使用後台腳本進行請求,自動帶上 cookie,無跨域問題,目標域需要在 @connect 中提前聲明
GM_download(details)下載資源到本地
GM_getTab(callback)獲取當前 tab 的 object 對象
GM_saveTab(tab)通過 tab 的 object 對象重新打開一個 tab
GM_getTabs(callback)獲取當前存活的所有 tab 的對象,以便和其他腳本實例溝通
GM_notification使用插件 notification API 彈出桌面通知
GM_setClipboard複製內容到剪貼板
GM_info獲取腳本的油猴插件的信息

完整的說明文檔:Tampermonkey documentation

實踐:打印 “Hello, World”

做一個非常簡單的小練習:創建一個名為 “Hello” 的腳本,當進入掘金和知乎頁面時,在 Console 中打印 “Hello, World”。

  1. 新建腳本
  2. 修改腳本名稱
  3. 指定運行地址 @match@include
  4. 直接使用 console.log 或者聲明權限調用 GM_log
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// ==UserScript==
// @name         Hello
// @namespace    http://tampermonkey.net/hello
// @version      0.1
// @description  try to take over the world!
// @author       You Name
// @match        https://zhihu.com/*
// @match        https://juejin.cn/*
// @grant        GM_log
// ==/UserScript==

(function() {
  'use strict';
  GM_log("Hello World");
})();

搭建舒適的開發環境

使用在線編輯器小試牛刀之後,或許你也發現在線編輯器:

  • 缺少語法補全和自動提示
  • 難以格式化代碼

不免懷念起 VSCode。

或許你還會有更深遠的考慮,在線編輯器編輯完成後:

  • 怎麼同步到遠程倉庫,怎麼做代碼分發
  • 如果要用到新語法,怎麼保證跨瀏覽器兼容性
  • 如果代碼越寫越多,沒有模塊化怎麼管理
  • 沒有 TS,很難保證長期維護

這些坑我已經踩過了,並且抽出一個腳手架工具 create-tampermonkey - npm (npmjs.com),一鍵搭建舒適的油猴腳本開發環境。

腳手架集成 rollup + babel + eslint + typescript,支持:

  • 自動生成 UserScript Header
  • 語法和類型系統:ESNext、ES Module、TypeScript
  • 樣式系統:CSS Modules,以及 scss、sass、less、stylus (需安裝對應依賴)
  • 靜態資源:導入圖片、SVG 轉換為 Base64,同時支持 SVG Sprite 多語言
  • 擴展:基於 Rollup,可以按需安裝插件進行擴展

create-tampermonkey 啟動項目

初始化項目

1
2
3
4
5
npx create-tampermonkey demo-userscript
# 或者
npm init tampermonkey demo-userscript
# 或者
yarn create tampermonkey demo-userscript

初始化完畢後,進入目錄安裝依賴。

1
npm run dev # 跑起開發模式

到瀏覽器中打開 dev.user.js,自動進入 Tampermonkey 腳本安裝界面。

最後一步:配置油猴插件

訪問 chrome://extension,找到油猴插件的卡片,點擊 Details 進入配置界面。

勾選 Allow acess to file URLs

刷新頁面,出現彈窗,一切就緒。

用 VSCode 打開項目,這時右下角會推薦一些輔助插件,建議安裝。

代碼中使用到的 GM_xxx 會自動提取到 UserScript Header 中,當然也可以在 src/meta.json 中自定義。

代碼的默認入口是 src/main.js 文件。


實踐:掘金簽到功能

基於上面初始化的項目 demo-userscript 做一個小功能:掘金簽到功能。

定位請求

“掘金簽到"本質是調用接口,我們的實現思路是追踪點擊"立即簽到"按鈕時請求發送情況,定位到

調試接口

打開 Postman 做一下調試,這裡有一個導入小技巧:右鍵拷貝 cURL

到 Postman 中通過 curl 導入整個請求:點擊左側面板中的 import 按鈕,選擇 Raw text 貼上上一步複製的內容即可。

獲取參數

在 Postman 中發現請求需要 aiduuid_signature 三個參數,試試看不帶參數能否請求成功,先確定好必不可少的參數和請求頭。

簡單嘗試後,發現這裡並不需要帶 aiduuid_signature 三個參數,主要是依賴 cookie,使用 GM_xmlhttpRequest 會自動帶上對應的 cookie,事情變得簡單。

修改 src/main.js 的代碼:

src/main.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
GM_xmlhttpRequest({
  url: "https://api.juejin.cn/growth_api/v1/check_in",
  method: "POST",
  headers: {
    "content-type": "application/json",
    "user-agent": navigator.userAgent,
  },
  responseType: "json",
  onload(response) {
    if (response.status === 200) {
      const data = response.response;
      if (data.data === "success") {
        alert("簽到成功");
      } else {
        alert(data.err_msg);
      }
    }
  },
});

刷新頁面測試一下。在其他站點刷新一下居然也可以發送請求,這就是插件沒有跨域限制的優勢了。

再做一下節流優化。利用 GM_setValueGM_getValue 做持續存儲。

src/main.js
 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
const storageKey = "last_sign_timestamp";
// 取得上一次簽到的日子
const lastSignNumberOfDays = GM_getValue(storageKey, 0);
// 計算現在所在的日子
const currentNumberOfDays = Math.floor(
  new Date().valueOf() / 1000 / 60 / 60 / 24
);

// 如果今天已经請求過,不再請求
if (currentNumberOfDays !== lastSignNumberOfDays) {
  GM_xmlhttpRequest({
    url: "https://api.juejin.cn/growth_api/v1/check_in",
    method: "POST",
    headers: {
      "content-type": "application/json",
      "user-agent": navigator.userAgent,
    },
    responseType: "json",
    onload(response) {
      if (response.status === 200) {
        const data = response.response;
        if (data.data === "success") {
          alert("簽到成功");
        } else {
          alert(data.err_msg);
        }
        // 更新最近一次簽到的日子
        GM_setValue(storageKey, currentNumberOfDays);
      }
    },
  });
}

難免會遇到需要獲取數據的情況,可訪問的數據一般有三種:

  1. 頁面中包含數據,通過DOM 獲取
  2. 通過接口請求得到
  3. 存儲在本地存儲中,localStorage 或 cookie 之類

確定方式很粗暴:

  1. 複製參數或參數值到Element 中搜索
  2. 查看前面幾個請求,看看是否有跡可循
  3. 到 localStorage 或 cookie 中搜索

發布腳本

在本地開發完腳本之後,npm run build 構建生產版本並上傳代碼到 Github 或 Gitee。

用 Github/Gitee 上文件的 Raw URL 就能直接實現分發。如果在 package.json 中設置好 repositorycreate-tampermonkey 會自動生成 Raw URL 並賦給 downloadURLupdateURL

但這樣分發存在的問題是無法統計下載量、從網絡訪問的角度考慮同時維護 Github 和 Gitee 兩個倉庫。

另一種分發方式是上傳到腳本平台 greasyfork.org/,登錄後即可發布新腳本,如果代碼託管在 Github 或 GitLab 還可以使用 Webhooks 實現自動更新。


開發技巧

調試油猴腳本

油猴腳本的運行依託於 background scriptcontent script,在調試前需要對運行環境有所區分,例如 GM_xmlhttpRequest 請求是 background script 發出的,DOM 處理和腳本邏輯是 content script 執行的。

確定環境之後,就可以使用對應的調試方式進行調試了。

調試 background script

還是訪問 chrome://extension,找到油猴插件的卡片:

inspect views 後面有個 background.html,點擊一下彈出 background script 的調試彈窗。

調試 content script

在網頁inspect -> Sources -> Page 下找到 Tampermonkey 目錄,頁面中運行的油猴腳本代碼都在這了,選擇目標,斷點調試即可。

獲取 userId 等信息

有時候需要拿一些額外信息做請求,一般有三種方式:

  1. 看看能不能在頁面中搜索到,通過 DOM 獲取
  2. 看看有沒有接口可以調用獲取
  3. 看看本地存儲裡有沒有

目標 DOM 節點未掛載怎麼辦?

如果節點是在首屏加載的,粗暴的方法是使用 setTimeout 做一下延時。

但如果是在交互過程中有DOM 更新,就只能引入監聽機制了,使用 MutationObserver 來實現。

具體的實例可以看【開發記錄】掘金"破圈行動” 輔助腳本- 掘金(juejin.cn)

查看插件源碼

瀏覽器插件安裝之後,插件包被下載到本地目錄中,可通過下述方法訪問。

訪問 chrome://version,找到 Profile path(存放用戶數據的路徑)

訪問 chrome://extensions/,找到目標插件的ID

將 Profile Path 和插件 ID 拼裝在一起 ${Prifile Path}/Extensions/${Extension Id},便是插件包的路徑了。友情提示,通過命令行訪問時需要在空格前加個 \ 轉義一下。


總結

瀏覽器插件利用瀏覽器能力進行功能擴展,具有跨域請求、讀取 cookie、管理歷史記錄、註冊右鍵項等能力。

瀏覽器插件的能力很豐富,能夠實現複雜的功能。但如果只是做一些針對頁面的操作,只需要依賴基礎能力,完全可以使用油猴腳本實現,開發更便捷分發更迅速。

開發油猴腳本,主要是使用 Tampermonkey API 和 JavaScript。

create-tampermonkey 腳手架提供一個全面的油猴腳本開發環境,依托這個環境,可以使用最新的 ES 語法、TypeScript、CSS Modules,在 VSCode 中進行模塊化開發,大大提高開發效率。

開發完畢的油猴腳本可通過 Github/Gitee Raw URL 或 Greasy Fork 平台分發。

瀏覽器插件的主要分工為 background scriptcontent script 兩部分,在調試油猴腳本時需要思考清楚是哪一部分出現的問題,再採用對應的調試方式。

實現了兩個小實踐,走出第一步,接下來盡情發揮創造力吧,玩得開心~

最後分享一下我的 Tampermonkey UserScripts 清單,持續更新,需要的歡迎下載來使用!


comments powered by Disqus