本帖最后由 溯水流光 于 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