本帖最后由 溯水流光 于 2025-5-28 18:41 编辑
库地址: https://scriptcat.org/zh-CN/script-show-page/3508
// @require https://scriptcat.org/lib/3508/1.0.2/MenuManager.js
本库的功能
油猴菜单库,支持开关菜单,支持批量添加,为您解决批量添加和开关菜单的烦恼
- 支持批量添加菜单
- 支持快速实现开关菜单
- 支持开关状态自动存储并持久化
实际应用的案例, 油管自动跳转: https://bbs.tampermonkey.net.cn/thread-8787-1-1.html
油管播放短视频时, 自动跳转为长视频播放页面
文末通过一个简单的小案例,带你了解 MenuManager 的使用。
灵感来源
https://greasyfork.org/zh-CN/scripts/411512-gm-createmenu
5年前的库了, 很感谢作者的开源和分享, 但是其代码质量实在无法恭维, 我重构和整理了 2 个多小时, 最后还是选择直接推倒重来
但本库的 api 设计上是有参考这个库的
使用方式
本库是对 GM_registerMenuCommand 和 GM_unregisterMenuCommand 的封装
所以使用前, 请先参考 GM_registerMenuCommand 的使用教程:
https://bbs.tampermonkey.net.cn/thread-271-1-1.html
使用前的准备
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
我们需要为脚本申请这两个权限, 才能成功通过 menuManager 创建菜单项
去除 grant none, 这会导致我们无法申请 GM 函数, 直接去除就好了:
// @grant none
或着可以修改为:
// @grant unsafeWindow
创建一个开关菜单
menuManager.addAndCreate({
// 开关默认状态, 不写 default 的话, 默认为 true
default : true,
on : {
// 开关 开启状态的提示信息
name : "自动跳转状态: 开启✅ (点我关闭)",
},
off : {
// 开关 关闭状态的提示信息
name : "自动跳转状态: 关闭❎ (点我开启)",
},
accessKey: 'E', // 快捷键
callback(state, isInit){
// state 为当前开关状态, isInit 为是否为初始化
console.log(state, isInit)
}
});
大功告成了:
可以自己点击菜单, 观察控制台输出
menuManager 创建开关菜单的内部逻辑:
menuManager addAndCreate 时
会通过 on 和 off 的 name, 获取唯一 name, 称之为 storeKey
menuManager 会通过这个 storeKey 去 localStorage 获取存储的 开关状态的 boolean 值
如果为 null, 则使用 default 指定的值为 开关状态的 boolean 值
如果不为 null, 则转换 "true" "false" 为 true false, 并作为 开关状态的 boolean 值
开关状态的 boolean, 我们用 currState 存储
然后会调用 callback(currState, true) 这里便是初始化
然后会通过 GM_registerMenuCommand 注册菜单, 来显示对应的 on 或 off 的 name
当菜单点击的时候, 会反转 currState, 并调用 callback(currState, false)
并通过一系列操作, 更新显示正确的 on 或 off name
可以直接阅读源码, 获取更多细节, 源码不算长, 才 261 行
创建普通菜单
menuManager.addAndCreate(
{
name : "点击我",
accessKey: 'C', // 快捷键
callback(){
alert("menu is been clicked");
}
}
);
批量创建菜单
menuManager.addAndCreate 可以传入数组
menuManager.addAndCreate([
{
// 开关状态, 不写 default 的话, 默认为 true
default : true,
on : {
// 开关 开启状态的提示信息
name : "自动跳转状态: 开启✅ (点我关闭)",
},
off : {
// 开关 关闭状态的提示信息
name : "自动跳转状态: 关闭❎ (点我开启)",
},
accessKey: 'E', // 快捷键
callback(state, isInit){
// state 为当前开关状态, isInit 为是否为初始化
console.log(state, isInit)
}
},
{
name : "点击我",
accessKey: 'C', // 快捷键
callback(){
alert("menu is been clicked");
}
}
]);
API
下面的 api, 都返回了 menuManager, 方便链式调用
// 添加单条或多条菜单项
add(entryOrEntries)
// 关闭 menuManager 的日志
disableLog()
// add 添加后, 要 create 才能正常使用
create()
// add 并自动 create
addAndCreate(entryOrEntries)
综合练习
我们写一个小案例, 来熟悉 menuManager 的使用
案例目标: Google 自动聚焦
默认 Google 就是聚焦的, 这里的功能为, 自动展开搜索记录, 本案例只是为了练习
引入 vite-monkey-plugin
https://github.com/lisonge/vite-plugin-monkey
vite-monkey-plugin 支持 HMR: 本地 ctrl + s 保存,浏览器自动更新脚本。
使用 vite-monkey-plugin ,有诸多好处:
1.可以使用现代的 IDE 进行脚本开发,如我们这里使用免费的 WebStrom,WebStrom 在去年的 1024 程序员节,支持开源项目免费使用了。
2.使用了 vite 了,我们可以模块化开发脚本,使用 ES Module 来开发脚本,可以把脚本拆分为多个模块,方便复用和维护。
初始化项目
pnpm create monkey
名字叫: monkey-google-focus
选择 Vanilla, 也就是纯 JS 开发脚本
src下, main.js 清空, 其他全部删除
目录结构如下所示:
$ tree
.
|-- src
| `-- main.js
|-- package.json
|-- pnpm-lock.yaml
`-- vite.config.js
19 directories, 52 files
pnpm run dev
vite-monkey-plugin 默认创建出来的项目就是匹配 google, 无需配置
main.js 可以简单打印 console.log("hello world")
来检查是否生效
配置
export default defineConfig({
plugins: [
monkey({
entry: 'src/main.js',
userscript: {
icon: 'https://vitejs.dev/logo.svg',
namespace: 'npm/vite-plugin-monkey',
match: ['https://www.google.com/'],
},
}),
],
});
修改为
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
monkey({
entry: 'src/main.js', // 脚本入口
userscript: {
icon: 'https://vitejs.dev/logo.svg',
namespace: 'npm/vite-plugin-monkey',
match: ['https://www.google.com/*'],
version: "1.0.0",
author: "hzx", // 你的名字
license: "MIT", // 开源协议
description: "google search auto focus", // 介绍
},
server: { mountGmApi: true }, // 将 GM 函数注入为全局变量
}),
],
});
导入库
为了更好的代码提示, 我们选择直接把油猴库保存到本地
我们这里用了:
MenuManager:
https://scriptcat.org/zh-CN/script-show-page/3508
elmGetter 魔改版 (你也可以用原版, 这完全是习惯问题):
https://scriptcat.org/zh-CN/script-show-page/2847
把代码保存到本地
$ tree -I 'node_modules'
.
|-- src
| |-- lib
| | |-- elmGetter.js
| | `-- menuManager.js
| `-- main.js
|-- package.json
|-- pnpm-lock.yaml
`-- vite.config.js
3 directories, 7 files
修改为 ES module 导出:
- var menuManager = (() => {
+ export const menuManager = (() => {
两个都要修改
开始梭哈
elmGetter.get 使用起来, 其实就是异步的 querySelector:
import {elmGetter} from "./lib/elmGetter.js";
async function main() {
const searchEl = await elmGetter.get(".gLFyf")
searchEl.click();
}
main()
然后添加开关菜单栏:
import {elmGetter} from "./lib/elmGetter.js";
import {menuManager} from "./lib/menuManager.js";
async function main() {
menuManager.addAndCreate({
// 开关状态, 不写 default 的话, 默认为 true
default : true,
on : {
// 开关 开启状态的提示信息
name : "自动聚焦状态: 开启✅ (点我关闭)",
},
off : {
// 开关 关闭状态的提示信息
name : "自动聚焦状态: 关闭❎ (点我开启)",
},
accessKey: 'E', // 快捷键
// state 为当前开关状态, isInit 为是否为初始化
callback(state, isInit){
if (state) {
setFocus();
}
}
});
}
main()
async function setFocus() {
const searchEl = await elmGetter.get(".gLFyf")
searchEl.click();
}
如果 callback 要调用 async, 同时要保证这些 async 有序执行, 可以自己实现一个异步队列:
callback 中, 将 async 函数, addToQueue 就 ok 了
let queue = [];
let isProcessing = false;
async function processQueue() {
if (isProcessing || queue.length === 0) return;
isProcessing = true;
const task = queue.shift();
await task();
isProcessing = false;
await processQueue();
}
function addToQueue(task) {
queue.push(task);
processQueue();
}
export { addToQueue };
尾声
感谢你的阅读, 如果有任何问题, 欢迎评论区讨论
开源地址: https://github.com/HHsomeHand/monkey-lib-menu-manager
欢迎提 issue, 发 PR