Description
允许嵌入块内执行 Js 以增强复杂查询能力的提案
In what scenarios do you need this feature?
-
需求:希望能允许在嵌入块内执行 JS,以弥补原始的 SQL 本身逻辑不足的缘故
-
理由:SQL 虽然强大,但是由于缺失基本的顺序执行。条件片段等逻辑,在处理复杂逻辑查询方面会异常受限
- 一方面:我个人高强度使用 SQL 查询,但是还是会遇到「即便写了非常复杂的 sql 语句仍然得不到满意的查询效果」的情况
- 另一方面:友商的查询语法选择了自定义的 DSL,虽然不能像 SQL 那样通用强大,但是却可以定义简单的执行逻辑,从而在特定的复杂查询方面胜过 SQL
-
思路:
- 不引入新的 DSL,而是直接允许使用 js 代码来控制嵌入块的内部逻辑
- 在不变更思源本体机制的前提下,通过一些小 trick 变相能在嵌入块中执行 js 逻辑
- 原则:动刀子尽可能小,绝对不涉及大规模代码更新甚至重构
Describe the optimal solution
具体的实施方案分为两个部分:
- 第一部分:如何告知思源,某一个嵌入块要执行 js 代码而非单纯的执行 SQL 查询
- 第二部分:如何具体的将 js 翻译成需要查询的块。
1. 通过(在嵌入块开头加入 shebang || 设置自定义属性)来声明本嵌入块是否要执行 js
-
方案总结
-
方案一:当嵌入块内容以以下声明为开头的时候,将这个嵌入块视为一个可执行 js 的嵌入
//!js
-
方案二:设置自定义属性,特定属性的嵌入块会被认为应该作为 js 来执行
-
-
具体说明
嵌入块的相关代码在https://github.com/siyuan-note/siyuan/blob/master/app/src/protyle/render/blockRender.ts,其中第 32 行会读取嵌入块的内容并当作一个 sql 查询内容,然后交给
/api/search/searchEmbedBlock
API 执行所以只需要在 32 行后面加上判断即可获知改嵌入块是否需要用 js 来执行
2. 如何将 js 转化为嵌入块查询:两个独立的方案
-
基本原理
-
规定: JS嵌入块最后一句,应当返回一个
Block[]
-
在前端部分,将 JS 嵌入块代码放入
Function
对象中,并提供一个sql
api 用于查询 sqlsql
api 可以直接比照前端的 api- 为了保证数据安全,可以在 Function 对象内部禁用
fetch
等可能引起安全问题的 API
-
根据 JS 嵌入块代码返回的 Block 列表来构造嵌入块,具体而言有两种方案
- 【优选】后端方案:让后端加一个 API,好处是非常直接直观后面也方便继续优化,坏处是后端需要加一个额外的 API
- 【次选】前端方案:把 js 翻译成
selelct * from id in (...)
语句,坏处是不够直接且可能让前端代码逻辑变复杂,好处是后端完全无感
-
2.1 后端方案:后端提供一个新的 API,直接给定 block list 返回嵌入块查询
由弘哥首先提议
我后面也研究了一下后端嵌入块查询的调用过程,觉得应该可以实现。只需要去掉 search.go
的调用,直接用前端给的 block list 就行。
2.2 前端方案:将 js 执行过程拼接为新的 SQL 语句
基本思路是:
-
执行 js 代码,获取 block list
-
将 block list 拼成
select * from blocks where id in ("id1", "id2", ...)
,比如这样:const func = async () => { let sql = `select * from blocks where content like "%Python%" and root_id != '20231114083336-jti8mwu' and type not in ('t', 's') order by updated desc limit 8 ;`; const blocks = await api.sql(sql); let idList = blocks.map((block) => `"${block.id}"`); sql = `select * from blocks where id in (${idList.join(",")})`; return sql }
-
将新的构造的 SQL 语句发往后端
-
将返回结果按照发送时候的顺序重新排列一下
- 我自己做了一下测试,发现
select * from blocks where id in ("id1", "id2", ...)
语句可以查询到正确的结果,但是嵌入块内部DOM的排列顺序却是乱的,所以为了保证顺序正确,需要前端在 searchEmbedBlock 的返回结果中将response.data.blocks
重新排序一下。
- 我自己做了一下测试,发现
补充:为什么不想做成插件
- 希望操作更加顺滑
- 希望复用原生的嵌入块显示能力
- 插件能力有限,就比如如果要使用插件,只能用「前端方案」,但是由于无法控制后端的逻辑,所以查询的结果会乱序
Metadata
Metadata
Assignees
Type
Projects
Status
Activity
88250 commentedon Nov 16, 2023
后端方案中需要新增的内核接口麻烦描述一下入参出参。
另外,这里的
//!js
是参考 sh 吗?可能在前端开发看来会理解为 非js 😂
[-]允许嵌入块内执行 Js 以增强复杂查询能力的提案[/-][+]Query embed block supports executing JavaScript[/+]frostime commentedon Nov 16, 2023
关于出入参数,也许可以参考一下
/api/search/searchEmbedBlock
的接口?比如 searchEmbedBlock 的输入是可以仿照这个接口,直接输入给定的 block id
至于输入和 searchEmbedBlock 保持一致,都是吧。
那个
//!js
确实就是无脑模仿sh
,当时如果不合适也可以换成别的,比如就一个//js
;或者不一定用 shenbang,用块自定义属性判断也行。😂88250 commentedon Nov 16, 2023
能否来一段脚本示例,比如完成某个功能的,这样我们好具体再讨论看看。
frostime commentedon Nov 16, 2023
以下面这个例子为例,这个是我使用
RunJs
插件运行的,这个插件就是基于Function
来运行 js 的。下面这个案例的目标是,将查询的结果,按照笔记本自定义排序的大小来排列。程序最后返回一个
Promise<Blocks[]>
对象,可以在调用Function
对象的代码中获取返回值。前端获取到返回的
Block[]
后,可以将对应的id
传入includeIDs
参数,从而来要求内核获取相应的笔记内容。实际操作的时候,还可以做一些简化,比如可以允许用户只编写
query
函数内部的内容,然后前端部分将其包裹在一个async () => {${code}}
内部从而规避 js 不能在顶层域中 await 的限制。88250 commentedon Nov 17, 2023
感谢,我还需要确认下,需要内核新开接口主要原因是 /api/query/sql 接口返回不了嵌入块接口需要的格式对吧,后半段要使用已有嵌入块渲染的代码?
frostime commentedon Nov 17, 2023
对,或者更确切的说是由于 SQL 语法本身的一些缺陷,有些复杂一点的查询难以或者无法实现;希望可以通过引入 js 的程序逻辑来增强查询的能力。
66 remaining items