一、前情提要:
我们之前做了一个简单的发布功能:
思源笔记折腾记录 - 稍微复杂一点的发布效果 - 链滴 (ld246.com)
但是它现在生成的发布有一些小问题,我们现在来修复它们。
二、整理代码
虽然可以就在现有的代码结构基础上开始修改,但是最好还是先调整一下代码结构,让它至少看起来更加合理一点。
之前我们是直接在 noobApi 里面实现了这个发布服务,但实际上它好像跟其他部分的功能没有太大的联系。
所以现在先把它移动到单独的代码片段功能当中。
之前的文件结构是这样:
我们现在把它改成这样:
把跟发布相关的内容都移动到 noob-service-syPublishServer 里面去。
这样就方便我们之后单独对它进行修改和处理,当然这样做了之后,代码模块的引入也需要修改,这个其实不难,我觉得自己尝试修改一下比较好,不过这里还是说一下:
首先是之前我们在 noobApi\index.js
中直接引入的服务代码,这回需要在代码片段引入,也就是加上这样一个代码片段:
conf.json
{
"id": "20221122201604-114514c",
"name": "发布服务器",
"memo": "",
"type": "js",
"enabled": true,
"content": "import ('/snippets/noob-service-syPublishServer/index.js')"
}
然后修改一下引入过程:
import 工作空间 from './workspace/index.js'
if(!window.noobApi){
if(window.require){
let {监听文件修改}= await import('./util/file.js')
let 监听选项 = {
监听路径:工作空间.代码片段路径,
监听配置:{
persistent :true,
recursive :true
},
文件类型:['js'],
事件类型:['change']
}
监听文件修改(监听选项)
//服务已经拆分出去了,所以不在这里引入了
//await import('./server/index.js')
}
await import('./api.js')
}
export default window.noobApi
noob-service-syPublishServer\index.js
if(window.require){
await import('./server/index.js')
}
这样之后,这个发布服务就是单独引入了,你可以单独开关它不影响其他代码片段。
修复目前存在的一些问题
1、同构修改管线
我们之前实现的渲染管线函数是这样的:
export function 生成管线渲染器(渲染管线, 模板路径) {
return async (req,res) => {
//这里是告诉浏览器,我返回的是一个html页面
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
let 渲染结果 = new DOMParser().parseFromString(fs.readFileSync(模板路径), "text/html");
//这里是一个循环,不断地把渲染结果和请求喂给它们,所以渲染管线中的每一步也都可以跳出去,直接响应请求
for await (let 渲染函数 of 渲染管线) {
try {
//如果渲染结果没有这个函数说明它不是数据了
if (!渲染结果.querySelector) {
let tempdoc = new DOMParser().parseFromString(
渲染结果,
"text/html"
);
渲染结果 = tempdoc;
}
if (渲染结果.完成) {
return 渲染结果;
}
if (渲染函数 instanceof Function) {
渲染结果 = (await 渲染函数(req, res, 渲染结果)) || "";
}
let 文字渲染结果 = "";
try {
文字渲染结果 = 渲染结果.querySelector("body").innerHTML;
} catch (e) {
文字渲染结果 = 渲染结果;
let tempdoc = new DOMParser().parseFromString(
文字渲染结果,
"text/html"
);
渲染结果 = tempdoc;
console.error(e);
}
} catch (e) {
console.error(e);
continue;
}
}
//如果有结果就返回结果
try {
if (渲染结果) {
res.end(渲染结果.documentElement.innerHTML)
}
else {
res.end('渲染出错,没有有效的结果')
}
} catch (e) {
渲染结果 = null
console.error(e)
}
//万一我们这里对它还有其他操作呢
return 渲染结果
}
}
在每一步修改的时候,我们都把 req(请求),res(响应)渲染结果(一个 document)丢给了渲染函数,这暂时没有什么问题,但是很多时候我们其实不太清楚哪些内容放到服务端生成比较好(那种每次访问都一样的内容),哪些内容放到浏览器里用代码生成比较好,所以干脆这样,让网页和服务端可以使用同样的修改过程。
但是网页端没有 req、res 这两个对象,所以我们干脆把获取最原始数据的步骤一次性完成,然后之后渲染就可以只依赖渲染结果,而 document 对象是浏览器和 electron 两边都有的,这样万一突然发现哪一步不合适在服务端做,写好的函数也可以用上。
所以这里我们修改一下渲染管线的生成逻辑,不再给渲染函数传入 req
和 res
,而是只传入当前的渲染结果,也不用每次都返回数据了,坏处就是初始渲染的数据需要提前写入,不过这样其实也有好处,这个之后再说:
const fs = require('fs')
export function 获取全部原始数据(document){
//注意这个document是传入的参数
let 代码形式原始数据 = document.getElementById('metadata').innerHTML.replace('window.metadata=','')
return JSON.parse(代码形式原始数据)
}
export function 设置原始数据属性值(属性名,属性值,document){
//注意这个document是传入的参数
let 原始数据 = 获取原始数据(document)
原始数据[属性名]=属性值
渲染结果.getElementById('metadata').innerHTML='window.metadata='+JSON.stringify(原始数据)
}
export function 初始化原始数据(原始数据,document){
document.getElementById('metadata').innerHTML='window.metadata='+JSON.stringify(原始数据)
}
export function 生成管线渲染器(渲染管线,模板路径,原始数据生成器){
return async(req,res)=>{
//这里是告诉浏览器,我返回的是一个html页面
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
let 渲染结果 = new DOMParser().parseFromString(fs.readFileSync(模板路径), "text/html");
if(!渲染结果.getElementById('metadata')){
//这样做了之后,在客户端也可以使用这两个函数
渲染结果.head.insertAdjacentHTML('afterbegin',`<script>${获取全部原始数据.toString()}${设置原始数据属性值.toString()}</script>`)
渲染结果.head.insertAdjacentHTML('afterbegin','<script id="metadata" data-render-status></script>')
}
//这一步我们把所有的数据都注入进去了
await 原始数据生成器(req,res,渲染结果)
for await(let 渲染函数 of 渲染管线){
await 渲染函数(渲染结果)
if(渲染结果.getElementById('metadata').getAttribute('data-render-status') ){
switch (渲染结果.getElementById('metadata').getAttribute('data-render-status')){
//根据renderStatus,来决定下一步做什么,这里先只做渲染完成之后的处理
case 'ended':
break
}
break
}
}
res.end(渲染结果.documentElement.innerHTML)
}
}
现在在浏览器打开 127.0.0.1
,可以在开发者工具的控制台中尝试一下:
可以看到,获取原始数据的函数在浏览器上也是可用的。
所以这样我们之前的那个渲染内容的函数也可以试着放到浏览器页面模板引入,它一样可以使用:
2、将页面改为只读
之前渲染的页面里,有很多 contenteditable
的元素:
这是因为思源的编辑器就是在 contenteditable
这个特性的基础之上实现的,我们直接使用了编辑器内部的 DOM 来当成渲染结果,所以它能编辑也很正常,你也可以先不改,尝试在页面里面输入一些内容看看效果怎么样,不过不要以为有了这个特性,元素可以修改了就可以很容易地弄出一个编辑器来,因为浏览器对这个的支持简直就是个天坑,要不然思源的编辑器也不会需要那么多代码了,关于这些可以参考这里:
contenteditable - HTML(超文本标记语言) | MDN (mozilla.org)
为什么都说富文本编辑器是天坑? - 知乎 (zhihu.com)
看完你就会觉得,还是用现成的好了,面向剪贴板编程才是效率最高的。。。。。。。。
不过我们并不需要这么麻烦,只要弄一个函数把这些可编辑元素去掉就好了
function 禁用编辑(document){
document.getElementById('publish-content').querySelectorAll(`[contenteditable]`).forEach(
element=>{element.setAttribute('contenteditable',false)}
)
}
然后把它添加到后端渲染管线里就可以了:
//let 默认渲染管线 = 生成管线渲染器([渲染页面内容], 默认模板路径,注入思源文档原始数据)
let 默认渲染管线 = 生成管线渲染器([渲染页面内容,禁用编辑], 默认模板路径,注入思源文档原始数据)
所以我之前要弄个渲染管线嘛,这样就可以一点点改,稍微方便一点。
3、修改块链接
现在页面不会再出现奇奇怪怪的可编辑状态了,但是你点击一下块链接就会发现,它们没有办法跳转,一点面子都不给的。
所以我们来把块链接修改一下:
function 修改块链接(document){
//这里是为了让块链接可以显示正确的颜色。
document.head.insertAdjacentHTML('beforeend','<style>span a{color:inherit !important}</style>')
document.querySelectorAll('span[data-type="block-ref"]').forEach(
块链接=>{
let 锚文本 = 块链接.innerText
块链接.innerHTML=`<a href='/${块链接.getAttribute('data-id')}'>${锚文本}</a>`
}
)
}
还是把它加到渲染管线里,这里就不重复了。
现在块链接就可以点击了。
4、修改超链接
这个时候块链接可以点击了,但是超链接还是不行,还是按照上面的步骤,对渲染结果进行一次修改
function 修改超链接(document){
document.querySelectorAll('span[data-type="a"]').forEach(
块链接=>{
let 锚文本 = 块链接.innerText
块链接.innerHTML=`<a href='${块链接.getAttribute('data-href')}'>${锚文本}</a>`
}
)
}
5、修改附件
之前我们并没有处理附件的问题,这里需要来处理一下,否则附件就会显示成这样:
这一步可能稍微有点麻烦,思源的附件并不是很简单地静态文件伺服,又不可能把附件的链接直接指向思源,那样人家就能看到你所有的附件也没办法过滤了,所以我们可能需要做一个代理才能完成。
显然自己做是不可能自己做的,只有拉一下现成的库才能够编程的样子,npm 上的库又多,大佬们说话又好听。。。。。。。
这里使用 'http-proxy-middleware'
这个包来完成代理的功能,有关它的更多信息可以参考项目本身的 readme。
const { createProxyMiddleware } = require('http-proxy-middleware')
const proxy = createProxyMiddleware({
target: `http://127.0.0.1:6806`,
changeOrigin: true,
})
怎么安装包之前已经说了很多次了,就不重复了,记得安装位置别搞错了,网速慢的话不要忘记加上 --registry=https://registry.npmmirror.com
,这是 npm 的淘宝镜像,我很想找个别的 npm 镜像但是没有找着.......
然后我们把它加到路由里面去:
function 判定附件权限(req){
//先假装我们有判断
return true
}
发布应用.use('/assets',(req,res,next)=>{
if(判定附件权限(req)){
//一定要记得调用next(),不然后面的函数就无效了
next()
}
},思源代理)
//这个路由的顺序记得放到后面
发布应用.use('/:blockID', 默认渲染管线)
这样之后,附件就能看到了:
不过这样还不够, 判断附件权限肯定不可能直接就是这样的.......
function 判定附件权限(req){
//先假装我们有判断
return true
}
之前使用了 publish-access='public'来标记文档为可以发布,所以只要某个附件出现在了这些 bi 里,我们就假定它是可以允许被其他人看到的好了。
所以如果要用 sql 来完成这个过滤的话,首先要取出所有能够发布的文档的 id:
select * from blocks where id in (select root_id from attributes where name = 'publish-access' and value = 'public')
然后再选择所有附件中被这些文档引用的:
select *
from assets
where root_id in (
select root_id from blocks where id in (select block_id from attributes where name = 'custom-publish-access' and value = 'public')
)
select *
from assets
where root_id in (
select root_id from blocks where id in (select block_id from attributes where name = 'custom-publish-access' and value = 'public')
)
不要忘记自定义属性需要 custom-
,这里这样写了之后,就可以拿到所有可访问文档中的附件了(其实现在对文档的过滤还没有加回去),然后比对一下想要获取的附件是不是在这个表里面就可以了。
async function 判定附件权限(req){
let sql = `select *
from assets
where root_id in (
select root_id from blocks where id in (select block_id from attributes where name = 'custom-publish-access' and value = 'public')
)`
let 可访问附件列表= await noobApi.核心api.sql({stmt:sql})
console.log(req.url)
return 可访问附件列表.find(
附件=>{
return '/'+ 附件.name == req.url
}
)
}
之前为了方便查看我先把文档权限的判断给去掉了,这里给它加回来:
async function 判定文档权限(块id){
let sql = `select * from blocks where id='${块id}' and root_id in (select block_id from attributes where name='custom-publish-access' and value='public')`
return (await noobApi.核心api.sql({stmt:sql}))[0]
}
发布应用.use('/:blockID',async(req,res,next)=>{
if(await 判定文档权限(req.params.blockID)){
next()
}else{
res.status('403')
res.setHeader('Content-Type',"text/html;charset=utf-8" );
res.end('不可访问此文档')
}
},默认渲染管线)
这样就只有设置了 publish-access 为 public 的文档以及它们引用过的附件才可以被访问了。
额 之前的 403 实在是热情了我怕访问的人受不了还是改得普通一点了。
6、加上页脚
再尝试一下加个页脚吧(其实是为了找个理由放自己的链接,你可以自己改掉)
function 添加页脚(document){
document.body.insertAdjacentHTML('beforeend',`
<div style='text-align:center'>
<div> poweredBy <a href='https://b3log.org/siyuan/' target='_blank'>siyuan@${window.siyuan.config.system.kernelVersion}</a> with <a target='_blank' href='https://www.chuanchengsheji.com'>noob </a></div>
<div> <a href='https://afdian.net/a/leolee9086'>请noob作者喝一杯咖啡</a></div>
</div>
`)
}
ok 基本搞定(其实还有很多问题)。
现在已经能够点击链接跳转和正常看到图片了。
页脚也还正常的样子
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于