[js] 不用插件:绑定思源事件总线(eventBus)

本贴最后更新于 224 天前,其中的信息可能已经时移俗易

不多说了,用的人都懂。

简洁版 (不推荐,有风险)

优点:代码简洁。

缺点:要求用户至少安装一个插件。

⚠️ 风险警告:当用户关闭第一个插件时,之前的绑定会失效。

function eventBusOn(eventName, callback) {
    const plugin = window.siyuan.ws.app.plugins[0];
    if(!plugin) {
        console.log('绑定事件'+eventName+'失败,请至少安装一个插件');
        return false;
    }
    plugin.eventBus.on(eventName, callback);
    return true;
}
function eventBusOff(eventName, callback) {
    const plugin = window.siyuan.ws.app.plugins[0];
    if(!plugin) {
        console.log('解绑事件'+eventName+'失败,请至少安装一个插件');
        return false;
    }
    plugin.eventBus.off(eventName, callback);
    return true;
}

自定义 Plugin 版

优点:兼容性好。

缺点:代码量大。

// 参考 https://github.com/siyuan-note/siyuan/blob/f660b2ddfa98f93f778bbbbe5abc3bfaa5a932b6/app/src/plugin/index.ts
function eventBusOn(eventName, callback) {
    const plugin = getMyPlugin();
    plugin.eventBus.on(eventName, callback);
}
function eventBusOff(eventName, callback) {
    const plugin = getMyPlugin();
    plugin.eventBus.off(eventName, callback);
}
function getMyPlugin(pluginName = "my-custom-plugin") {
    let myPlugin = window.siyuan.ws.app.plugins.find(item=>item.name === pluginName);
    if(myPlugin) return myPlugin;
    class EventBus {
        constructor(name = "") {
            this.eventTarget = document.createComment(name);
            document.appendChild(this.eventTarget);
        }
        on(type, listener) {
            this.eventTarget.addEventListener(type, listener);
        }
        once(type, listener) {
            this.eventTarget.addEventListener(type, listener, { once: true });
        }
        off(type, listener) {
            this.eventTarget.removeEventListener(type, listener);
        }
        emit(type, detail) {
            return this.eventTarget.dispatchEvent(new CustomEvent(type, { detail, cancelable: true }));
        }
    }
    class Plugin {
        constructor(options) {
            this.app = options.app||window.siyuan.ws.app.appId;
            this.i18n = options.i18n;
            this.displayName = options.displayName||options.name;
            this.name = options.name;
            this.eventBus = new EventBus(options.name);
            this.protyleSlash = [];
            this.customBlockRenders = {};
            this.topBarIcons = [];
            this.statusBarIcons = [];
            this.commands = [];
            this.models = {};
            this.docks = {};
            this.data = {};
            this.protyleOptionsValue = null;
        }
        onload() {}
        onunload() {}
        uninstall() {}
        async updateCards(options) { return options; } // 返回选项本身
        onLayoutReady() {}
        addCommand(command) {}
        addIcons(svg) {}
        addTopBar(options) { return null; } // 模拟返回null
        addStatusBar(options) { return null; } // 模拟返回null
        // 去掉设置,参考 https://github.com/siyuan-note/siyuan/blob/dae6158860cc704e353454565c96e874278c6f47/app/src/plugin/openTopBarMenu.ts#L25
        //openSetting() {}
        loadData(storageName) { return Promise.resolve(null); }
        saveData(storageName, data) { return Promise.resolve(); }
        removeData(storageName) { return Promise.resolve(); }
        getOpenedTab() { return {}; } // 返回空对象
        addTab(options) { return () => {}; } // 返回空函数模拟模型
        addDock(options) { return {}; } // 返回空对象模拟 dock
        addFloatLayer(options) {}
        updateProtyleToolbar(toolbar) { return toolbar; } // 返回toolbar本身
        set protyleOptions(options) {}
        get protyleOptions() { return this.protyleOptionsValue; }
    }
    myPlugin = new Plugin({name:pluginName});
    window.siyuan.ws.app.plugins.push(myPlugin);
    return myPlugin;
}

已封装到 openAny 中。

[js] 连续点击 openAny,小代码,大作用,让一切触手可达

在 openAny 中调用方法示例:

const handler = (args)=>console.log(args);

openAny.on('open-menu-link', handler);

openAny.once('open-menu-link', handler);

openAny.off('open-menu-link', handler);

openAny.emit('your event name', data);

折中版(推荐)

优点:代码简洁和稳定的结合。同时,该功能更强大,还可以通过这种继承实现无插件的方式对插件函数调用等。

缺点:至少得安装一个插件才行。

function eventBusOn(eventName, callback) {
    const pluginName = 'my-custom-plugin';
    if(window.siyuan.ws.app.plugins?.length === 0) {
        console.log('绑定事件'+eventName+'失败,请至少安装一个插件');
        return false;
    }
    let myPlugin = window.siyuan.ws.app.plugins.find(item=>item.name === pluginName);
    if(!myPlugin) {
        const Plguin = Object.getPrototypeOf(window.siyuan.ws.app.plugins[0].constructor);
        const MyPlugin = class extends Plguin{};
        myPlugin = new MyPlugin({app:window.siyuan.ws.app.appId, name:pluginName, displayName:pluginName});
        myPlugin.openSetting = null; // 防止顶部插件按钮添加设置菜单
        window.siyuan.ws.app.plugins.push(myPlugin);
    }
    myPlugin.eventBus.on(eventName, callback);
    return true;
}
function eventBusOff(eventName, callback) {
    const pluginName = 'my-custom-plugin';
    if(window.siyuan.ws.app.plugins?.length === 0) {
        console.log('绑定事件'+eventName+'失败,请至少安装一个插件');
        return false;
    }
    let myPlugin = window.siyuan.ws.app.plugins.find(item=>item.name === pluginName);
    if(!myPlugin) {
        console.log('解绑事件'+eventName+'失败,未找到名为'+pluginName+'的插件');
        return false;
    }
    myPlugin.eventBus.off(eventName, callback);
    return true;
}
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119768 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1985 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • EmptyLight

    第一时间想的是能在主题里做什么,想了一段时间之后还是脑袋空空,不过可扩展性很大

    1 回复
  • wilsons

    是的,如果愿意,还可以把插件的其他功能加进来。

    如果本来就是插件的话题,这个用不到其实。

    1 回复
  • EmptyLight

    如果在开发环境里面用的是 ts,应该可以把插件的 d.ts 定义文件引入进来,在编辑插件 API 部分获得类似插件的开发效果

    1 回复
  • wilsons

    是的,只要不报错,仅提供需要的实现即可,达到轻量级目的。

    我刚开始本来打算用代理,仅有 evenBus 属性实现,但发现思源默认插件方法属性返回值等都是存在的,如果兼容做好,代码量和实现空类差不多,甚至更多,关键可读性还不好。

  • emmmm,集市里有一款插件叫 OpenAPIdoge

    1 回复
  • wilsons

    是的,runjs 也支持。

    但这些需要依赖用户安装了插件,自用还不错。

    这个好处是可以不依赖其他插件,后面会集成到 openAny 中。😀

    尚未发现副作用。

  • w 佬,请问如何通过 js 实现通过快捷键对当前 tab 或者弹出的思源窗口设置成右侧(下侧)分屏?

    1 回复
  • wilsons

    openAny 可以实现。

    另外你这里的“或”是指实现任意一个就行吗?还是“和”?

    弹出的思源窗口是指悬浮窗?比如鼠标悬浮到引用上时弹出的悬浮窗?

    1 回复
  • 主要是悬浮的, 比如一个 引用笔记, 或者提及, 都是悬浮打开的, 这时如果能快捷打开到 分屏 就很方便了.

    1 回复
  • wilsons

    悬浮窗分屏显示

    需要先安装 openAny [js] 连续点击 openAny,小代码,大作用,让一切触手可达

    // 一键让悬浮窗分屏显示
    setTimeout(()=>{
        // 设置分屏方向 right 向右 down 向下
        const splitTo = 'right';
      
        // 设置快捷键
        const pressKey = 'alt+z';
      
        openAny.setKeymap(pressKey, async (event, {sleep, whenElementExist}) => {
            event.preventDefault();
            event.stopPropagation();
            // 函数嵌套中最好使用新实例,避免内外相互等待而死锁
            const openAny = new OpenAny();
            // 把悬浮窗变为标签
            await openAny.click('.block__popover [data-type="stickTab"]');
            // 等待把悬浮窗变为标签
            await sleep(100);
            // 获取悬浮窗标签
            const tab = await whenElementExist(()=>document.querySelector('[data-type="wnd"].layout__wnd--active .layout-tab-bar li.item--focus')||document.querySelector('[data-type="wnd"] .layout-tab-bar li.item--focus'));
            // 打开悬浮窗标签右键菜单
            await openAny.press('mouseright', tab);
            // 选择菜单中的分屏方式
            await openAny.click(`#commonMenu [data-id="splitMove${splitTo==='down'?'B':'R'}"]`);
        });
    }, 2000);
    
    1 回复
  • 大神啊, 太感谢了.正是我要的.

    我在本论坛多次要求作者实现一个类似的快速分屏的办法, 每次都被各种拒绝.

  • 我目前发现两个问题,请确认一下:

    1. updateProtyleToolbar(toolbar) 需要原样返回:updateProtyleToolbar(toolbar) { return toolbar; },返回空数组的话会清空工具栏,导致工具栏无法显示。相关代码:https://github.com/siyuan-note/siyuan/blob/f660b2ddfa98f93f778bbbbe5abc3bfaa5a932b6/app/src/protyle/toolbar/index.ts#L70

    2. openSetting() {} 执行之后会在右上角的插件菜单添加一个空选项

      image.png

    1 回复
    1 操作
    JeffreyChen 在 2025-06-04 19:05:32 更新了该回帖
  • wilsons 1 1 评论

    感谢反馈!这两个问题已解决!

    我对比了一遍原 plugin 对象的属性和方法,应该没有问题了。

    主要修改点

    image.png

    你的代码注释没改
    JeffreyChen 1 赞同
  • wilsons

    @JeffreyChen 折中版问世了

    折中版

请输入回帖内容 ...
wilsons
正式入驻知乎了,以后新贴主要在这里。 欢迎大家订阅关注! 你的关注对我是莫大鼓励,也能让我持续产出优质内容,我们一起成长 🙏 点这里立即关注:https://www.zhihu.com/people/wilsonses