油猴腳本入坑指南


希望可以讓開始嘗試自己寫腳本的同學們少走一些彎路吧

Head Pic: #アズールレーン 惡毒 - 小小男爵不要坑的插畫 - pixiv

 

基礎

這部分主要是開始寫油猴腳本前應當有所了解的知識

 

常見的用戶腳本管理器

  • Tampermonkey
    應該是各位見得最多的也是最知名的,好用又穩定,多瀏覽器支持,我很喜歡
  • Greasemonkey
    用戶腳本始祖,我們現在一直習慣說的油猴腳本的“油猴”實際上就是 Greasemonkey,只支持 Firefox
    由於與 Tampermonkey 等其它腳本管理器在 API 的使用上會有些區別,導致某些情況下你很難保持你的腳本同時對 Greasemonkey 兼容,我一般直接放棄兼容
  • Violentmonkey
    由國人開發的一款腳本管理器,界面好看,我很喜歡
 

元數據

即每個油猴腳本都有的,腳本開頭很多行注釋的內容,這是油猴腳本關鍵的基礎部分,剛開始接觸可能會一頭霧水,但你絕不能忽視這部分內容

建議:

  1. 多參考別人的腳本,能對各個字段的意義了解個大概
  2. 閱讀官方 wiki,有每個字段詳細的介紹
    如果你覺得讀鳥語實在是很頭疼,你也可以閱讀由他人維護的中文 GreaseMonkey 用戶腳本開發手冊
  3. 不同的用戶腳本管理器可能會加入自己獨有的 meta,開發時建議以你的腳本打算主要支持的腳本管理器為主,例如這是 Tampermonkey 的文檔
 

GM API

油猴提供了很多強大的 API,它們可以使很操作變得相當簡單

注意每個 API 在使用前需要在元數據中用 @grant 進行聲明,若你不打算使用這些 API,應當聲明 @grant none

以下是一個簡單的表格,幫助你了解油猴的 API 大概能做哪些事情

舊 API 新 API 說明
GM_info GM.info 返回當前腳本的元數據
GM_addStyle   為網頁添加 CSS
GM_setValue GM.setValue 在本地儲存值(只能是字符串),你可以將這個儲存看作是 localStorage 一樣的東西
GM_getValue GM.getValue 獲取使用儲存的值
GM_deleteValue GM.deleteValue 刪除儲存的值
GM_listValues GM.listValues 返回一個由所有儲存值的鍵名組成的數組
GM_getResourceText   獲取元數據中定義的 @resource 的資源內容
GM_getResourceURL GM.getResourceUrl 獲取元數據中定義的 @resource 資源的 URL(base64 編碼后的data:協議地址)
GM_openInTab GM.openInTab 新標簽頁打開指定地址(用來繞過 Chrome 會阻止所有非用戶觸發的window.open的限制)
GM_registerMenuCommand   向油猴插件菜單中添加腳本指令(通常用於打開自己寫的設置界面或者執行代碼之類的)
GM_setClipboard GM.setClipboard 復制指定內容到剪貼板
GM_xmlhttpRequest GM.xmlHttpRequest 發送網絡請求,且允許跨域
  GM.notification 瀏覽器通知
 

新舊 API 的區別

Greasemonkey 從版本 4 開始向性能更高的異步模型發展,舊的 API GM_* 通常是同步的,而新的 API GM.* 是異步的(采用 Promise),在使用時請參考官方 wiki 並多加留意

並且,有些 API 的名稱拼寫也發生了變化,在上面的表格中已經用粗體標識

想了解更多信息可以閱讀官方說明文章 Greasemonkey 4 For Script Authors

 

unsafeWindow

如果你在寫腳本的時候有嘗試直接通過 window 添加或訪問網頁全局變量,你會發現這是沒有效果的

這是因為油猴的沙箱機制,任何人都無法從 window 直接訪問到油猴的 API 或腳本內的變量,保證了安全

如果你確實需要訪問 window,可以使用 unsafeWindow,但在正式發布的腳本中你不應該將任何油猴 API 或者腳本中的變量通過它暴露到 window 中

unsafeWindow 在不同腳本管理器中的表現可能會有所不同,特別是 Violentmonkey,如需考慮兼容性還需要多加測試

 

跨域請求

在油猴腳本中你可以引用網絡腳本來使用 axios 之類的網絡請求模塊,這很方便,但同樣也產生了局限性,例如由於瀏覽器機制的限制,你無法直接在網頁上進行沒有被事先允許的跨域請求

這時建議使用 GM.xmlHttpRequest,同時你應當在元數據用// @connect <value>聲明允許被 GM.xmlHttpRequest 訪問的域名

<value>可以是:

  • 域名,例如example.com,這也將允許所有子域
  • 子域,例如abc.example.com
  • self,即腳本運行的網址
  • localhost
  • IP 地址
  • *

如果你習慣用 axios 之類的用 Promise 封裝的請求模塊,你同樣可以將 GM.xmlHttpRequest 封裝成 Promise 形式

1
2
3
4
5
6
7
const xhr = option => new Promise((resolve, reject) => { GM.xmlHttpRequest({ ...option, onerror: reject, onload: resolve, }); });
 

使用自己的 IDE 編寫油猴腳本

一般腳本管理器自帶的編輯器功能十分單一,全程在里面寫代碼肯定十分不爽,那么如何使用自己的 IDE 編寫腳本並隨時保存隨時生效呢

答案是利用元數據的 @require,它不僅能引用網絡腳本,還可以引用本地腳本,所以我們只要 require 用 IDE 編輯的本地腳本就行了

在這之前我們需要允許油猴插件訪問本地文件,以 Chrome 為例,在擴展程序列表chrome://extensions/進入插件的詳細信息,開啟“允許訪問文件網址”即可,接着就可以// @require file://<本地路徑>的文件網址方式引用本地腳本了

 

引用 CSS

引用 JS 可以采用@require,但 CSS 不行

可行的方法有兩種

  1. 老辦法:用 JS 往<head>插入 CSS 的<link>
  2. API 方法:在元數據中聲明// @resource mycss <地址>,然后GM_addStyle(GM_getResourceText('mycss'));
    別忘了用到的這兩個 API 也要@grant聲明
 

進階

這部分主要是寫腳本的過程中有可能遇到的一些難點的較優解決方法

 

避免將 setInterval 用作動態監聽的解決方案

初學 JS 的新手在遇到監聽動態元素的問題的時候,由於缺乏經驗,通常只能想到用 setInterval 去“每隔一段時間就檢測一下”,當然這也包括我自己,但不管從性能上還是從實現復雜度來說,這都不是一個好選擇,不夠優雅

大部分類似的問題都可以在事件監聽層面運用點技巧來解決

此處會列舉幾個常見的場景來說明一下解決思路

 

1. 監聽動態生成的頁面元素的事件

在有些時候我們可能要去監聽動態生成的頁面元素的事件,例如自動翻頁加載的評論這類

  • 不好的思路
    setInterval 每隔一段時間檢測一下有沒有新生成的頁面元素,然后對這些頁面元素添加事件監聽
  • 好的思路
    由於事件冒泡機制,我們可以監聽其父級元素的點擊事件,然后通過事件信息來確定被點擊的元素currentTarget或其父級元素currentTarget.parentNode

不僅是動態的場景下可以這么做,當你需要針對一個很多元素的靜態列表監聽每個元素的事件時也可以這么做,這種方法最大的優點是你只需要添加一個事件監聽,如果你對列表中的每個元素都添加事件監聽,會增大內存開銷,影響頁面性能

有種比較特殊的情況:

1
2
3
4
5
6
7
<ul class="list"> <li class="item"> <img class="image" /> ... <li> ... </ul>

假設在該場景下,點擊 .image 時它自身會被移除,而你需要得到被點擊的 .image 所在的 .item,由於該 .image 已經被移出頁面的 DOM 樹,因此你無法通過點擊事件的currentTarget.parentNode來得到 .item

最簡單的解決方案是在事件發生時獲取鼠標所在的 .item,例如使用 jQuery:$('.item:hover')

 

2. 對動態生成的頁面元素進行修改

假設一個場景,此處借用一下 vue 的語法來說明頁面元素邏輯:

1
2
3
4
5
6
7
8
9
<!-- Init: showA = true; showB = false; --> <ul class="list"> <li class="item"> <div v-if="showA" class="item-a" @click="showA = false; doSth().then(() => { showB = true });">...</div> <div v-if="showB" class="item-b">...</div> ... <li> ... </ul>

大致就是,當你點擊 .item-a 的時候,.item-a 會被移除,並在一個異步函數doSth()完成后顯示 .item-b

你當前的目標是要在 .item-b 出現的時候修改其內容

  • 不好的思路
    監聽 .item-a 的點擊事件,setInterval 每隔一段時間檢測一下當前 .item 內有沒有 .item-b,有的話就進行修改然后終止該 interval
  • 好的思路
    監聽 .item-a 的點擊事件,當其被點擊后監視 .item 的 DOM 變化,若新增了 .item-b 就對其進行修改

是時候祭出 MutationObserver 了,利用它我們可以監視 DOM 樹的改動,同時它也是過去的 Mutation Events 的替代品

上面所說的場景可以按這個思路來解決

  1. 監聽 .list 的點擊
  2. 當觸發點擊事件時,找到 :hover 狀態的 .item,對其添加 MutationObserver
  3. 當 MutationObserver 監視到 .item-b 被添加時,修改 .item-b,並disconnect()該 MutationObserver

寫成代碼大概像這樣:

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
const findItemB = $item => new Promise((resolve, reject) => { if ($item.length === 0) reject(); // 有可能此時 .item-b 已經出現,所以先檢查下 const $itemB = $item.find('.item-b'); if ($itemB.length > 0) { resolve($itemB); return; } // 監視 .item 的 DOM 樹 childList 變化 new MutationObserver((mutations, self) => { mutations.forEach(({ addedNodes }) => { addedNodes.forEach(node => { if (node.className !== '.item-b') return; self.disconnect(); resolve($(node)); }); }); }).observe($item[0], { childList: true }); }); $('.list').click(async ({ target }) => { if (target.className !== 'item-a') return; const $itemB = await findItemB($('.item:hover')); // do something with $itemB });
 

補充

 

推薦的一些可能會常用的模塊

Github BootCDN 用途
jquery-pjax Link 為頁面添加 pjax 支持
jquery-mousewheel Link 為 jQuery 添加鼠標滾輪事件的支持
FileSaver.js Link 另存為任意 blob 為文件
jszip Link 讀寫創建壓縮文件
gif.js Link 制作 gif,支持 worker 方式
clipboard.js Link 雖然油猴提供剪貼板 API,但該模塊可以提供一些擴展功能,例如 tooltips 反饋等
dragula Link 提供頁面元素的拖拽調序功能
toastr Link 方便的顯示頁內通知


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM