本文已參與「掘力星計(jì)劃」,,贏取創(chuàng)作大禮包,挑戰(zhàn)創(chuàng)作激勵(lì)金,。
注:本文的油猴特指 Tampermonkey,。
油猴腳本運(yùn)行于油猴插件之上,油猴插件本質(zhì)上對(duì)瀏覽器能力的再封裝,。既然如此,,我們先來(lái)簡(jiǎn)單了解一下瀏覽器插件。
瀏覽器插件(Browser Extension):瀏覽器的擴(kuò)展應(yīng)用
說(shuō)的直白一點(diǎn),就是拿著瀏覽器開(kāi)放的能力(插件 API),,去實(shí)現(xiàn)一些小型應(yīng)用,。
瀏覽器插件主要由四部分構(gòu)成:background scripts、content scripts,、全局 UI 元素,、options page。
- background scrips: 后臺(tái)腳本,,一個(gè)后臺(tái)腳本是一個(gè)獨(dú)立線程,,是游離于各個(gè)頁(yè)面之外的“上帝之眼”。具有訪問(wèn)各類插件 API 的能力,,但同時(shí)也喪失了直接操作頁(yè)面的能力,;
- content scripts:內(nèi)容腳本,具有直接操作頁(yè)面的能力,。其實(shí)就是在頁(yè)面中運(yùn)行 js 腳本,,可以使用 DOM API。
content script 只能直接訪問(wèn)少量插件 API,,但能和 background script 進(jìn)行雙向通信完成數(shù)據(jù)交換,;
- 全局 UI 元素:瀏覽器層的 UI 交互,包括
- 在 Toolbar 顯示 icon,,定義點(diǎn)擊 icon 后顯示的 Popup 或其他效果
- 增加右鍵選項(xiàng)
- 增加全局快捷鍵
- 改造新 Tab 頁(yè),、歷史記錄頁(yè)、書(shū)簽頁(yè)
- options page:插件配置頁(yè)
瀏覽器插件的核心機(jī)制可以用下圖簡(jiǎn)單概括
想必,,大家最好奇的還是有哪些 API 以及能用這些 API 做什么,,這里例舉幾個(gè):
contextMenus :增加右鍵選項(xiàng)
- 使用選中文本,例如:劃詞翻譯,、文本收集
- 快速調(diào)用插件功能,,例如:打開(kāi) DevTool,頁(yè)面剪藏
cookies :增刪改查 cookie(任意域名),,直接拿著本地 cookie 發(fā)送請(qǐng)求,,不必再做授權(quán)。同時(shí)由于后臺(tái)腳本不是 Web 頁(yè)面,,在發(fā)送請(qǐng)求時(shí)沒(méi)有跨域限制,。:
- 多平臺(tái)信息聚合
- 多平臺(tái)信息分發(fā)
devtools.panels :增加 Devtool 面板,這個(gè)對(duì)前端開(kāi)發(fā)者來(lái)說(shuō)應(yīng)該很熟悉,,React Developer Tools,、Vue.js devtools
notifications :瀏覽器通知,未打開(kāi)頁(yè)面的情況下進(jìn)行通知,,可以輔助一些工具類應(yīng)用
storage :全局保存數(shù)據(jù),,可跟隨瀏覽器賬戶同步
這里例舉的只是我常用的一些,,只是滄海一粟,更多 API 可以查閱
瀏覽器插件就簡(jiǎn)單介紹到這里,,如果有興趣繼續(xù)了解,,推薦:Chrome,Edge,,Mozilla 三家的文檔,。
油猴插件(Tampermonkey)
瀏覽器插件可以實(shí)現(xiàn)各式各樣的功能,但有時(shí)候開(kāi)發(fā)者只是想對(duì)某一個(gè)站點(diǎn)加一點(diǎn)點(diǎn)小功能,,如果這也要構(gòu)建環(huán)境打包上架分發(fā),,未免就太麻煩了一些;從應(yīng)用市場(chǎng)角度來(lái)看,,充斥著顆?;膽?yīng)用,市場(chǎng)也會(huì)擁擠繁雜不堪,。
油猴插件為輕量化腳本提供了一個(gè)平臺(tái),,在線編輯器中編寫油猴腳本即時(shí)生效,通過(guò) Github,、GreasyFork 快速分發(fā),。
在油猴插件中, content script 起到非常重要的角色,,它將用戶編寫的代碼運(yùn)行在頁(yè)面中,,同時(shí)提供 GM_xxxx 函數(shù)封裝瀏覽器的部分能力。封裝的內(nèi)部實(shí)現(xiàn)是和 background script 通信,,驅(qū)動(dòng) background script 調(diào)用插件 API,。
對(duì)油猴插件簡(jiǎn)單了解之后,來(lái)看看如何編寫油猴腳本,。
Tampermonkey API
油猴腳本由頭部和核心邏輯兩部分組成
// ==UserScript==
// @name New Userscript
// @namespace http:///
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://www./documentation.php?ext=dhdg
// @icon https://www.google.com/s2/favicons?domain=
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Your code here...
})();
頭部是腳本的一些元信息,、更新方式、指定運(yùn)行頁(yè)面,、權(quán)限聲明,,逐一解釋一下
配置名 | 作用 | 使用技巧 |
---|
@name | 腳本的顯示名稱 | 加后綴實(shí)現(xiàn)國(guó)際化,例如,,@name:zh-CN 指定在瀏覽器語(yǔ)言為中文時(shí)顯示的名稱 | @namespace | 腳本的命名空間,可以理解為腳本的標(biāo)識(shí) | 為了避免沖突一般使用 github 倉(cāng)庫(kù)地址 | @version | 與更新相關(guān),,當(dāng)前版本 | | @updateURL | 檢查腳本是否更新地址 | 配合 @version 和自動(dòng)更新使用 | @downloadURL | 檢測(cè)到更新時(shí),,去哪下載腳本 | | @supportURL | 遇到問(wèn)題時(shí),用戶去哪反饋 | | @include | 腳本在哪些頁(yè)面運(yùn)行 | 可使用正則,,不支持 hashtag,,多個(gè)頁(yè)面的地址聲明多個(gè) @include 即可 | @match | 與 @include 類似 | | @exclude | 腳本禁止在哪些頁(yè)面運(yùn)行,,優(yōu)先于 @include | | @require | 在腳本運(yùn)行前引入外部 JavaScript 文件 | 例如,引入 jQuery | @resource | 聲明外部資源文件,,搭配 GM_getResourceText 使用 | 例如引入 html,、icon | @connect | 聲明 GM_xmlhttpRequest 可訪問(wèn)的域 | 必須指定才能正常請(qǐng)求 | @grant | 聲明 GM_xxx 函數(shù)的使用列表 | 必須先指定權(quán)限才能正常使用 | @run-at | 指定腳本運(yùn)行時(shí)機(jī) | document-start: 盡快執(zhí)行 document-body: 當(dāng) body 掛載時(shí)執(zhí)行 document-end: DOMContentLoaded 觸發(fā)時(shí)執(zhí)行 document-idle: DOMContentLoaded 觸發(fā)后執(zhí)行,也是默認(rèn)設(shè)置項(xiàng) context-menu: 右鍵菜單項(xiàng)被點(diǎn)擊時(shí)執(zhí)行 | @author | 作者名 | | @description | 簡(jiǎn)短介紹 | 同樣可以加后綴實(shí)現(xiàn)國(guó)際化 | @homepage | 主頁(yè)地址 | 如果未設(shè)置并且 @namespace 是倉(cāng)庫(kù)地址,,默認(rèn)導(dǎo)向倉(cāng)庫(kù)地址 | @icon | 腳本 icon | | @icon64 | 64x64像素的腳本 icon | | @antifeature | 腳本是否有廣告,、挖礦、數(shù)據(jù)收集等商業(yè)行為 | | @noframes | 聲明腳本不在 iframe 中運(yùn)行 | |
核心邏輯通過(guò)一個(gè)立即執(zhí)行函數(shù)包裹,,避免和全局作用域相互干擾,。Tampermonkey 將瀏覽器的部分能力封裝為 GM_XXX 函數(shù)以供調(diào)用。
API | 作用 | 使用技巧 |
---|
unsafeWindow | 訪問(wèn)頁(yè)面的 Window 對(duì)象 | | GM_addStyle(css) | 創(chuàng)建全局樣式的快捷方式,,向頁(yè)面插入 style 元素 | 也可以用 DOM 操作手動(dòng)創(chuàng)建 | GM_addElement(tag_name, attributes) GM_addElement(parent_node, tag_name, attributes) | 向 DOM 新建元素的快捷方式 | 也可以用 DOM 操作手動(dòng)創(chuàng)建 | GM_log(message) | 在 console 中打印信息 | console.log 的快捷方式 | GM_setValue(name, value) | 持續(xù)化存儲(chǔ)數(shù)據(jù) | | GM_getValue(name, defaultValue) | 從存儲(chǔ)體中獲取數(shù)據(jù) | | GM_deleteValue(name) | 從存儲(chǔ)體中刪除數(shù)據(jù) | | GM_listValues() | 列舉存儲(chǔ)體中所有數(shù)據(jù)項(xiàng) | | GM_addValueChangeListener | 監(jiān)聽(tīng)數(shù)據(jù)更新 | 例如要使 Tab 間數(shù)據(jù)同步,,可以用監(jiān)聽(tīng) value 達(dá)成同步 | GM_removeValueChangeListener | 移除監(jiān)聽(tīng) | | GM_getResourceText(name) | 獲取 @resource 中已聲明的資源 | | GM_getResourceURL(name) | 獲取 @resource 中已聲明的資源(base64 URI 形式) | | GM_registerMenuCommand(name, fn, accessKey) | 在 Tampermonkey 的 popup 中增加選項(xiàng) | | GM_unregisterMenuCommand(menuCmdId) | 移除選項(xiàng) | | GM_openInTab(url, options) | 新開(kāi)一個(gè) tab 頁(yè) | | GM_xmlhttpRequest(details) | 使用后臺(tái)腳本進(jìn)行請(qǐng)求,自動(dòng)帶上 cookie,,無(wú)跨域問(wèn)題,,目標(biāo)域需要在 @connect 中提前聲明 | | GM_download(details) | 下載資源到本地 | | GM_getTab(callback) | 獲取當(dāng)前 tab 的 object 對(duì)象 | | GM_saveTab(tab) | 通過(guò) tab 的 object 對(duì)象重新打開(kāi)一個(gè) tab | | GM_getTabs(callback) | 獲取當(dāng)前存活的所有 tab 的對(duì)象,以便和其他腳本實(shí)例偶同學(xué) | | GM_notification | 使用插件 notification API 彈出桌面通知 | | GM_setClipboard | 復(fù)制內(nèi)容到剪貼板 | | GM_info | 獲取腳本的油猴插件的信息 | |
完整的說(shuō)明文檔:Tampermonkey documentation
實(shí)踐:打印 Hello, World
做一個(gè)非常簡(jiǎn)單的小練習(xí):創(chuàng)建一個(gè)名為 'Hello' 的腳本,,當(dāng)進(jìn)入掘金和知乎頁(yè)面時(shí),,在 Console 中打印 'Hello, World'。
- 新建腳本
- 修改腳本名稱
- 指定運(yùn)行地址
@match 或 @include
- 直接使用
console.log 或者聲明權(quán)限調(diào)用 GM_log
// ==UserScript==
// @name Hello
// @namespace http:///hello
// @version 0.1
// @description try to take over the world!
// @author You Name
// @match https://zhihu.com/*
// @match https:///*
// @grant GM_log
// ==/UserScript==
(function() {
'use strict';
GM_log('Hello World');
})();
搭建舒適的開(kāi)發(fā)環(huán)境
使用在線編輯器小試牛刀之后,,或許你也發(fā)現(xiàn)在線編輯器
- 缺少語(yǔ)法補(bǔ)全和自動(dòng)提示
- 難以格式化代碼
不免懷念起 VSCode,。
或許你還會(huì)有更深遠(yuǎn)的考慮,在線編輯器編輯完成后:
- 怎么同步到遠(yuǎn)程倉(cāng)庫(kù),,怎么做代碼分發(fā)
- 如果要用到新語(yǔ)法,,怎么保證跨瀏覽器兼容性
- 如果代碼越寫越多,沒(méi)有模塊化怎么管理
- 沒(méi)有 TS,,很難保證長(zhǎng)期維護(hù)
這些坑我已經(jīng)踩過(guò)了,,并且抽出一個(gè)腳手架工具 create-tampermonkey - npm (),一鍵搭建舒適的油猴腳本開(kāi)發(fā)環(huán)境,。
腳手架集成 rollup + babel + eslint + typescript,,支持:
- 自動(dòng)生成 UserScript Header
- 語(yǔ)法和類型系統(tǒng):ESNext、ES Module,、TypeScript
- 樣式系統(tǒng):CSS Modules,,以及 scss、sass,、less,、stylus(需安裝對(duì)應(yīng)依賴)
- 靜態(tài)資源:導(dǎo)入圖片、SVG 轉(zhuǎn)換為 Base64,,同時(shí)支持 SVG Sprite
- 多語(yǔ)言
- 擴(kuò)展:基于 Rollup,,可以按需安裝插件進(jìn)行擴(kuò)展
create-tampermonkey 啟動(dòng)項(xiàng)目
初始化項(xiàng)目
npx create-tampermonkey demo-userscript
或者
npm init tampermonkey demo-userscript
或者
yarn create tampermonkey demo-userscript
初始化完畢后,,進(jìn)入目錄安裝依賴
npm run dev 跑起開(kāi)發(fā)模式
到瀏覽器中打開(kāi) dev.user.js ,自動(dòng)進(jìn)入 Tampermonkey 腳本安裝界面
最后一步:訪問(wèn) chrome://extension ,,找到油猴插件的卡片,,點(diǎn)擊 Details 進(jìn)入配置界面
勾選 Allow acess to file URLs
刷新頁(yè)面,出現(xiàn)彈窗,,一切就緒,。
用 VSCode 打開(kāi)項(xiàng)目,這時(shí)右下角會(huì)推薦一些輔助插件,,建議安裝,。
代碼中使用到的 GM_xxx 會(huì)自動(dòng)提取到 UserScript Header 中,當(dāng)然也可以在 src/meta.json 中自定義,。
代碼的默認(rèn)入口是 src/main.js 文件,。
實(shí)踐:掘金簽到功能
基于上面初始化的項(xiàng)目 demo-userscript 做一個(gè)小功能:掘金簽到功能。
1. 定位請(qǐng)求
“掘金簽到”本質(zhì)是調(diào)用接口,,我們的實(shí)現(xiàn)思路是追蹤點(diǎn)擊“立即簽到”按鈕時(shí)請(qǐng)求發(fā)送情況,,定位到
2. 調(diào)試接口
打開(kāi) Postman 做一下調(diào)試,這里有一個(gè)導(dǎo)入小技巧,。
右鍵拷貝 cURL
到 Postman 中通過(guò) curl 導(dǎo)入整個(gè)請(qǐng)求:點(diǎn)擊左側(cè)面板中的 import 按鈕,,選擇 Raw text 粘貼上一步復(fù)制的內(nèi)容即可。
3. 獲取參數(shù)
在 Postman 中發(fā)現(xiàn)請(qǐng)求需要 aid ,、uuid ,、_signature 三個(gè)參數(shù),試試看不帶參數(shù)能否請(qǐng)求成功,,先確定好必不可少的參數(shù)和請(qǐng)求頭,。
簡(jiǎn)單嘗試后,發(fā)現(xiàn)這里并不需要帶 aid ,、uuid ,、_signature 三個(gè)參數(shù),主要是依賴 cookie ,,使用 GM_xmlhttpRequest 會(huì)自動(dòng)帶上對(duì)應(yīng)的 cookie ,,事情變得簡(jiǎn)單。
修改 src/main.js 的代碼
GM_xmlhttpRequest({
url: 'https://api./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);
}
}
},
});
刷新頁(yè)面測(cè)試一下,。在其他站點(diǎn)刷新一下居然也可以發(fā)送請(qǐng)求,,這就是插件沒(méi)有跨域限制的優(yōu)勢(shì)了。
再做一下節(jié)流優(yōu)化,。利用 GM_setValue 和 GM_getValue 做持續(xù)存儲(chǔ),。
const storageKey = 'last_sign_timestamp';
// 獲取上一次簽到的日子
const lastSignNumberOfDays = GM_getValue(storageKey, 0);
// 計(jì)算現(xiàn)在所在的日子
const currentNumberOfDays = Math.floor(
new Date().valueOf() / 1000 / 60 / 60 / 24
);
// 如果今天已經(jīng)請(qǐng)求過(guò),不再請(qǐng)求
if (currentNumberOfDays !== lastSignNumberOfDays) {
GM_xmlhttpRequest({
url: 'https://api./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);
}
},
});
}
難免會(huì)遇到需要獲取數(shù)據(jù)的情況,可訪問(wèn)的數(shù)據(jù)一般有三種:
- 頁(yè)面中包含數(shù)據(jù),,通過(guò) DOM 獲取
- 通過(guò)接口請(qǐng)求得到
- 存儲(chǔ)在本地存儲(chǔ)中,localStorage 或 cookie 之類
確定方式很粗暴:
- 復(fù)制參數(shù)或參數(shù)值到 Element 中搜索,;
- 查看前面幾個(gè)請(qǐng)求,,看看是否有跡可循;
- 到 localStorage 或 cookie 中搜索,。
分發(fā)腳本
在本地開(kāi)發(fā)完腳本之后,,npm run build 構(gòu)建生產(chǎn)版本并上傳代碼到 Github 或 Gitee。
用 Github/Gitee 上文件的 Raw URL 就能直接實(shí)現(xiàn)分發(fā),。如果在 package.json 中設(shè)置好 repository ,,create-tampermonkey 會(huì)自動(dòng)生成 Raw URL 并賦給downloadURL 、updateURL ,。
但這樣分發(fā)存在的問(wèn)題是無(wú)法統(tǒng)計(jì)下載量,、從網(wǎng)絡(luò)訪問(wèn)的角度考慮同時(shí)維護(hù) Github 和 Gitee 兩個(gè)倉(cāng)庫(kù)。另一種分發(fā)方式是上傳到腳本平臺(tái) / ,, 登錄后即可發(fā)布新腳本,,如果代碼托管在 Github 或 GitLab 還可以使用 Webhooks 實(shí)現(xiàn)自動(dòng)更新。
開(kāi)發(fā)技巧
調(diào)試油猴腳本
油猴腳本的運(yùn)行依托于 background script 和 content script ,,在調(diào)試前需要對(duì)運(yùn)行環(huán)境有所區(qū)分,,例如 GM_xmlhttpRequest 請(qǐng)求是 background script 發(fā)出的,DOM 處理和腳本邏輯是 content script 執(zhí)行的,。
確定環(huán)境之后,,就可以使用對(duì)應(yīng)的調(diào)試方式進(jìn)行調(diào)試了。
調(diào)試 background script
還是訪問(wèn) chrome://extension ,,找到油猴插件的卡片
inspect views 后面有個(gè) background.html,,點(diǎn)擊一下彈出 background script 的調(diào)試彈窗。
調(diào)試 content script
在網(wǎng)頁(yè) inspect -> Sources -> Page 下找到 Tampermonkey 目錄,,頁(yè)面中運(yùn)行的油猴腳本代碼都在這了,,選擇目標(biāo),斷點(diǎn)調(diào)試即可,。
獲取 userId 等信息
有時(shí)候需要拿一些額外信息做請(qǐng)求,,一般有三種方式:
- 看看能不能在頁(yè)面中搜索到,通過(guò) DOM 獲取
- 看看有沒(méi)有接口可以調(diào)用獲取
- 看看本地存儲(chǔ)里有沒(méi)有
目標(biāo) DOM 節(jié)點(diǎn)未掛載怎么辦,?
如果節(jié)點(diǎn)是在首屏加載的,,粗暴的方法是使用 setTimeout 做一下延時(shí)。
但如果是在交互過(guò)程中有 DOM 更新,,就只能引入監(jiān)聽(tīng)機(jī)制了,,使用 MutationObserver 來(lái)實(shí)現(xiàn)。
具體的實(shí)例可以看 【開(kāi)發(fā)記錄】掘金 “破圈行動(dòng)” 輔助腳本 - 掘金 ()
查看插件源碼
瀏覽器插件安裝之后,,插件包被下載到本地目錄中,,可通過(guò)下述方法訪問(wèn),。
訪問(wèn) chrome://version ,找到 Profile path(存放用戶數(shù)據(jù)的路徑)
訪問(wèn) chrome://extensions/ ,,找到目標(biāo)插件的 ID
將 Profile Path 和插件 ID 拼裝在一起 ${Prifile Path}/Extensions/${Extension Id} ,,便是插件包的路徑了。友情提示,,通過(guò)命令行訪問(wèn)時(shí)需要在空格前加個(gè) \ 轉(zhuǎn)義一下,。
總結(jié)
瀏覽器插件利用瀏覽器能力進(jìn)行功能擴(kuò)展,具有跨域請(qǐng)求,、讀取 cookie,、管理歷史記錄、注冊(cè)右鍵項(xiàng)等能力,。
瀏覽器插件的能力很豐富,,能夠?qū)崿F(xiàn)復(fù)雜的功能。但如果只是做一些針對(duì)頁(yè)面的操作,,只需要依賴基礎(chǔ)能力,,完全可以使用油猴腳本實(shí)現(xiàn),開(kāi)發(fā)更便捷分發(fā)更迅速,。
開(kāi)發(fā)油猴腳本,,主要是使用 Tampermonkey API 和 JavaScript。
create-tampermonkey 腳手架提供一個(gè)全面的油猴腳本開(kāi)發(fā)環(huán)境,,依托這個(gè)環(huán)境,,可以使用最新的 ES 語(yǔ)法、TypeScript,、CSS Modules,,在 VSCode 中進(jìn)行模塊化開(kāi)發(fā),大大提高開(kāi)發(fā)效率,。
開(kāi)發(fā)完畢的油猴腳本可通過(guò) Github/Gitee Raw URL 或 Greasy Fork 平臺(tái)分發(fā),。
瀏覽器插件的主要分工為 background script 和 content script 兩部分,在調(diào)試油猴腳本時(shí)需要思考清楚是哪一部分出現(xiàn)的問(wèn)題,,再采用對(duì)應(yīng)的調(diào)試方式,。
實(shí)現(xiàn)了兩個(gè)小實(shí)踐,走出第一步,,接下來(lái)盡情發(fā)揮創(chuàng)造力吧,,玩得開(kāi)心~
|