Skip to content

插件开发求助:想问问有什么方法能获取到最新又完整的文档DOM结构呢 #13313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Achuan-2 opened this issue Nov 29, 2024 · 24 comments

Comments

@Achuan-2
Copy link
Member

Achuan-2 commented Nov 29, 2024

In what scenarios do you need this feature?

发现用protyle.protyle.toolbar.setInlineMark添加样式和protyle.insert插入文字后
立即用/api/filetree/getDoc和/api/block/getBlockKramdown,获取不到最近插入的样式,需要等500ms,才能获取到最新的完整DOM
而直接用document的DOM结构又获取不到完整的DOM结构

Describe the optimal solution

想问问有什么帮法能获取到最新的DOM结构呢

Describe the candidate solution

No response

Other information

No response

@Achuan-2 Achuan-2 changed the title 插件开发求助:想问问有什么帮法能获取到最新的DOM结构呢 插件开发求助:想问问有什么方法能获取到最新又完整的文档DOM结构呢 Nov 29, 2024
@zxhd863943427
Copy link
Contributor

这是思源机制的根本矛盾,因为思源在编辑后,必须写入了文档,重新读取,才能获得更新后的文档。所以是比较无解的。

目前只能要么你定死一个等待时间,要么用新开一个ws进行监听,发现更新后在获取md内容。

当然还有一个一劳永逸的方案:劝说D实现ast持久化,就像我在ld帖子里捣鼓的那样。这样就不需要写入后重新读取也能获得更新内容了 :)

@Achuan-2
Copy link
Member Author

Achuan-2 commented Nov 30, 2024

这是思源机制的根本矛盾,因为思源在编辑后,必须写入了文档,重新读取,才能获得更新后的文档。所以是比较无解的。

目前只能要么你定死一个等待时间,要么用新开一个ws进行监听,发现更新后在获取md内容。

当然还有一个一劳永逸的方案:劝说D实现ast持久化,就像我在ld帖子里捣鼓的那样。这样就不需要写入后重新读取也能获得更新内容了 :)

哦哦,好的,谢谢大佬回复

@88250
Copy link
Member

88250 commented Nov 30, 2024

还是得持久化的,不然某些极端情况(比如掉电或者宕机)可能会导致数据未写入丢失。

后面可能可以考虑通过内存映射方案实现更好的 IO,这个 issue 我先关闭了,楼主可以试下监听 websocket transactions。

@88250 88250 closed this as completed Nov 30, 2024
@Achuan-2
Copy link
Member Author

Achuan-2 commented Nov 30, 2024

ws 不会写,暂时先用 await new Promise(resolve => setTimeout(resolve, 500));顶顶吧
希望有空的大佬欢迎指导下,感激不尽🙏

@Vanessa219
Copy link
Member

Vanessa219 commented Nov 30, 2024

调用完成后用 protyle.wysiwyg.element 应该可以获取到最新的 DOM,这个是和 500ms 之后获取的有什么不一致么?

@Achuan-2
Copy link
Member Author

Achuan-2 commented Nov 30, 2024

调用完成后用 protyle.wysiwyg.element 应该可以获取到最新的 DOM,这个是和 500ms 之后获取的有什么不一致么?

我需要文档完整的dom,进行批量替换,怕思源的动态加载机制,获得的不是完整的dom
protyle.wysiwyg.element会是完整的dom吗

@Vanessa219
Copy link
Member

setInlineMark 和 insert 后是完整的。

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 1, 2024

setInlineMark 和 insert 后是完整的。

ok,我试试,谢谢v姐

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 1, 2024

setInlineMark 和 insert 后是完整的。

哇可以的,想问问protyle.wysiwyg.element修改后,怎么保存呢,
我抄了一位大佬的代码,但是感觉效果不好,会失去焦点打断输入,不知道有没有更好的保存方式
而且发现这种方式只能保存添加的span元素,如果是对块进行重排,就无法保存

export function saveViaTransaction(protyleElem) {
    let protyle: HTMLElement
    if (protyleElem != null) {
        protyle = protyleElem
    }
    if (protyle === null)
        protyle = document.querySelector(".card__block.fn__flex-1.protyle:not(.fn__none) .protyle-wysiwyg.protyle-wysiwyg--attr")
    if (protyle === null)
        protyle = document.querySelector('.fn__flex-1.protyle:not(.fn__none) .protyle-wysiwyg.protyle-wysiwyg--attr') //需要获取到当前正在编辑的 protyle
    let e = document.createEvent('HTMLEvents')
    e.initEvent('input', true, false)
    protyle.dispatchEvent(e)
}

@Vanessa219
Copy link
Member

保存为变量还是文本?

DOM 上有很多事件的,没看明白需求和这个解决方案。

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 2, 2024

保存为变量还是文本?

DOM 上有很多事件的,没看明白需求和这个解决方案。

我的需求是,脚注插件如果要有序编号的话,需要更新全部的脚注块引锚文本,以及对末尾的脚注内容块位置进行重排,
我的问题是:修改锚文本,以及重排div位置后如何让思源笔记保存数据,让修改有效,而不是刷新就没了

@Vanessa219
Copy link
Member

脚注的 div 是如何得到的?每次刷新的时候再按照这个方法获取填充就可以了吧。

要让脚注保存为思源文件的话,需要按照块的形式放置在 wysiwyg.element 元素内就可以了。

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 2, 2024

脚注的 div 是如何得到的?每次刷新的时候再按照这个方法获取填充就可以了吧。

要让脚注保存为思源文件的话,需要按照块的形式放置在 wysiwyg.element 元素内就可以了。

插件添加的内容都加了自定义属性,能直接找到,都是直接用思源原生的块实现的

我直接修改wysiwyg.element,寻找并替换块引锚文本,对有特定自定义属性的块进行重排,看样子是修改了,按f5刷新,就又到原来样子了,用上面的saveViaTransaction,只能保存修改的块引,重排的div保存不了,于是我只能用更耗时的updateBlock,把修改后的wysiwyg.element传入进行保存,能保存成功,但是体验不是很好,耗时,会导致块临时丢失再恢复

@TCOTC
Copy link
Contributor

TCOTC commented Dec 2, 2024

没有动态加载出来也能改吗

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 2, 2024

没有动态加载出来也能改吗

好像测试了下,不是完整的😂……
只显示我设置的动态加载32个块
PixPin_2024-12-02_21-32-12

@Vanessa219
Copy link
Member

修改完以后调用 "/api/transactions" 这个接口应该就可以持久化了

fetchPost("/api/transactions", {
            session: Constants.SIYUAN_APPID,
            app: Constants.SIYUAN_APPID,
            transactions: [{
        id,
        data: blockHTML,
        action: "update"
    }]
        });

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 2, 2024

修改完以后调用 "/api/transactions" 这个接口应该就可以持久化了

fetchPost("/api/transactions", {
            session: Constants.SIYUAN_APPID,
            app: Constants.SIYUAN_APPID,
            transactions: [{
        id,
        data: blockHTML,
        action: "update"
    }]
        });

好滴谢谢v姐,那请问为什么我wysiwyg.element获取的html不是完整的dom呢,只有动态加载的部分

我是在工具栏添加按钮,把protyle.protyle 传给生成脚注的函数,再用protyle.protyle.wysiwyg.element来替换内容,https://github.com/Achuan-2/siyuan-plugin-blockref-footnote/blob/35f420a08e88702ef22da2bd3812dc65d02a8227/src/index.ts#L1132

@Vanessa219
Copy link
Member

动态的话,只会加载部分,界面不会卡顿。可以监听 loaded-protyle-dynamic 来改进。

@wish5115
Copy link

wish5115 commented Dec 3, 2024

我也没太懂大佬们说的ws监听,不过我尝试了下,可能大佬们说的是这样吧,你可以试试看。

@88250 @zxhd863943427 不对的地方请帮忙指正!谢谢!

// see https://github.com/siyuan-note/siyuan/blob/1710194122495d282a51650441d9fc80804561bb/app/src/layout/Model.ts
(() => {
    // 创建socket客户端
    createSocketClient(siyuan.ws.ws.url);

    // 当收到消息时被调用
    function onReceivedMessage(event) {
        const message = parseJson(event.data);
        if(message.cmd === 'transactions') {
            // 这里获取更新后的数据
            console.log(message);
        }
    }

    // 创建socket客户端
    function createSocketClient(url) {
        // 连接 WebSocket 服务器
        const socket = new WebSocket(url);
    
        socket.onopen = function(event) {
            console.log('WebSocket opened');
        };
    
        socket.onmessage = onReceivedMessage;
    
        socket.onclose = function(event) {
            console.log('WebSocket closed!');
        };
    
        socket.onerror = function(error) {
            console.error('WebSocket Error:', error);
        };

        return socket;
    }

    // 客户端向服务器发送消息
    function sendMessage(socket, message) {
        if (socket.readyState === WebSocket.OPEN) {
            socket.send(message);
        } else {
            console.error('WebSocket connection is not open');
        }
    }

    // 解析json
    function parseJson(jsonString) {
        let json = {};
        try {
            json = JSON.parse(jsonString || '{}');
        } catch(e) {
            json = {};
            console.error('parseJson error', e);
        }
        return json;
    }
})()

@wish5115
Copy link

wish5115 commented Dec 3, 2024

确实可以,刚才写了个测试代码

测试结果如下
image

测试代码
// see https://github.com/siyuan-note/siyuan/blob/1710194122495d282a51650441d9fc80804561bb/app/src/layout/Model.ts
(() => {
    // 创建socket客户端
    createSocketClient(siyuan.ws.ws.url);
let time = 0, blockId = '';
// 记录刚刚输入完数据
document.body.addEventListener('keyup', async (event) => {
    time = new Date().getTime();

    // 更新前
    blockId = event.target.lastElementChild?.dataset?.nodeId;
    const result = await fetchSyncPost('/api/block/getBlockDOM', {id: blockId});
    console.log('before transactions', result.data?.dom);
})

// 当收到消息时被调用
async function onReceivedMessage(event) {
    const message = parseJson(event.data);
    if(message.cmd === 'transactions') {
        // 这里获取更新后的数据
        console.log(new Date().getTime() - time, message);

        // 更新后
        const result = await fetchSyncPost('/api/block/getBlockDOM', {id: blockId});
        console.log('after transactions', result.data?.dom);
    }
}

// 创建socket客户端
function createSocketClient(url) {
    // 连接 WebSocket 服务器
    const socket = new WebSocket(url);

    socket.onopen = function(event) {
        console.log('WebSocket opened');
    };

    socket.onmessage = onReceivedMessage;

    socket.onclose = function(event) {
        console.log('WebSocket closed!');
    };

    socket.onerror = function(error) {
        console.error('WebSocket Error:', error);
    };

    return socket;
}

// 客户端向服务器发送消息
function sendMessage(socket, message) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(message);
    } else {
        console.error('WebSocket connection is not open');
    }
}

// 解析json
function parseJson(jsonString) {
    let json = {};
    try {
        json = JSON.parse(jsonString || '{}');
    } catch(e) {
        json = {};
        console.error('parseJson error', e);
    }
    return json;
}

// 发送api请求
async function fetchSyncPost(url, data, returnType = 'json') {
    const init = {
        method: "POST",
    };
    if (data) {
        if (data instanceof FormData) {
            init.body = data;
        } else {
            init.body = JSON.stringify(data);
        }
    }
    try {
        const res = await fetch(url, init);
        const res2 = returnType === 'json' ? await res.json() : await res.text();
        return res2;
    } catch(e) {
        console.log(e);
        return returnType === 'json' ? {code:e.code||1, msg: e.message||"", data: null} : "";
    }
}

})()

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 3, 2024

@wish5115 学习了,我之后尝试下

@wish5115
Copy link

wish5115 commented Dec 3, 2024

@wish5115 学习了,我之后尝试下

又得一简便方法,不用再次启动客户端监听了,只需要监听思源已有的客户端的消息事件即可,如下

siyuan.ws.ws.addEventListener('message', (e) => {
    const msg = JSON.parse(e.data);
    if(msg.cmd === "transactions") {
        // 这里获取更新后的数据
        console.log(msg);
    }
});

而且,之前的方法中, URL参数中的id参数最好重新生成,不然可能与思源已有会话id一样,详见 https://github.com/siyuan-note/siyuan/blob/1710194122495d282a51650441d9fc80804561bb/kernel/util/websocket.go

@Achuan-2
Copy link
Member Author

Achuan-2 commented Dec 3, 2024

@wish5115 哇,感谢大佬

@wish5115
Copy link

wish5115 commented Dec 4, 2024

@wish5115 哇,感谢大佬

终极方案。

封装成了仅调用一次的便捷方案。

如果持续监听用原生更方便。

// 当块被保存时执行回调(仅执行一次)
function whenBlockSaved(filter) {
    return new Promise((resolve, reject) => {
        const ws = siyuan.ws.ws;
        const clearEvent = () => ws.removeEventListener('message', handler);
        const handler = (event) => {
            try {
                const msg = JSON.parse(event.data);
                if(msg.cmd === "transactions") {
                    if(typeof filter === 'function') {
                        if(filter(msg)) resolve(msg);
                    } else {
                        resolve(msg);
                    }
                    clearEvent();
                }
            } catch(e) {
                reject(e);
                clearEvent();
            }
        }
        clearEvent();
        ws.addEventListener('message', handler);
    });
}

// 调用示例

// 链式调用
// whenBlockSaved().then((msg) => {
//     console.log(msg);
// });

// 异步调用
// const msg = await whenBlockSaved();
// console.log(msg);

// 过滤条件
// whenBlockSaved(msg => {
//     return msg.data.find(item => item.doOperations.find(item2 => item2.action === 'update' && item2.id === '20241204115642-hmpp8kh'));
// }).then((msg) => {
//     console.log(msg);
// });

// const filter = msg => msg.data.find(item => item.doOperations.find(item2 => item2.action === 'update' && item2.id === '20241204115642-hmpp8kh'));
// const msg = await whenBlockSaved(filter);
// console.log(msg);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants