一、前情提要:
我们之前通过对接 wechatsync 浏览器插件,实现了把笔记发布到知乎等各个平台的功能。
但是这样发布之后,笔记可能跟思源里面显示的就不太一样了,那么有没有办法“所见即所得”地把自己的笔记发布为网站呢?
二、前置知识:
nodejs
还记得在很早的时候,我们实现对代码片段文件夹监听并且自动重新加载思源界面的时候,使用了这样一个函数吗?
const fs = require('fs')
当时我顺嘴提过,这个是 nodejs 模块的引入方法。
那么 nodejs 是个什么东西呢?
看看这里(重复,敲键盘,跟我复读一遍:菜鸟教程是个好网站):
Node.js 教程 | 菜鸟教程 (runoob.com)
前端程序员估计还不算,但是不懂其他编程语言,额,说的就是我了。
ok,那很适合我。
electron
额,为什么又说到这个了呢?
还是一样的,看文档吧:
简介 | Electron (electronjs.org)
然后我们去看一下,思源的安装文件夹:
这还看不出来啥对吧
再看看里面的 \resources\app
嘿嘿嘿,看到了没有:electron,破案了(屁咧,隔几次升级公告里都有升级 electron 版本的好嘛)。
额,这里扩展一点说,为什么我经常说思源的扩展比 obsidian 要容易:
上面这是思源的 resources 文件夹,下面这个是 obsidian 的:
能看出区别吗?
思源的源代码(编译后的)是直接暴露在这个文件夹下面的,obsidian 里面只有一个 app.sar。
然后更进一步的,我们打开思源的 main.js:
这个就是思源启动的时候,前端所运行的后台代码了(有点绕,其实是主进程的代码,但是这个不是现在的重点),你可以通过改动它来修改思源的启动过程。
所以说不要再闹出这种笑话了好吗:
electron 的渲染进程和 BrowserWindow
看到上面的没有,这个的意思是说,如果窗口在创建的时候,nodeIntegration
为 true 的话,就可以在窗口的中获取 nodejs 环境。
而思源创建的主窗口是这样的:
goooood,我们有 node 环境了,所以之前才可以这样做:
if(window.require){ let {监听文件修改}= await import('./util/file.js') let 监听选项 = { 监听路径:工作空间.代码片段路径, 监听配置:{ persistent :true, recursive :true }, 文件类型:['js'], 事件类型:['change'] } 监听文件修改(监听选项) } export function 监听文件修改(监听选项){ const fs = require('fs') fs.watch(监听选项.监听路径,监听选项.监听配置,(type,fileName)=>{ if(监听选项.事件类型.indexOf(type)>=0){ let 扩展名 = fileName.split('.').pop() if(监听选项.文件类型.indexOf(扩展名)>=0){ window.location.reload() } } }) }
好了,条件基本上就是这样,我们可以开始鼓捣了。
二、发布服务的实现过程
1、安装 nodejs 用于包管理
按说思源的渲染进程里面已经有了 node 环境,我们应该不需要一个 nodejs 了,但是这里我们还是安装一个,不为了别的,单纯就是为了用它管理和下载包方便实现一些功能。
Node.js 安装配置 | 菜鸟教程 (runoob.com)
nodejs 里有好几个能够实现 web 服务器的包,这次我们使用 express,因为它比较简单嘛。
首先打开 vscode(我使用的是它啦,你用别的也不是不行)
然后用打开文件夹的功能,打开我们的 snippets 文件夹
然后像上面一样,在集成终端中打开 noobApi 文件夹。
这个时候你应该能够看到上面这样的界面了。
然后在这里输入:
npm i express --registry=https://registry.npmmirror.com
闪过一堆不明觉历的代码之后我们的文件夹里面就多了这么几个文件:
noobApi\package.json
这个是模块配置文件,暂时可以不用管它。
noobApi\package-lock.json
这个是依赖版本信息,暂时也可以不用管它。
noobApi\node_modules
这个文件夹是 emm ,黑洞,暂时要管它。
node_modules 文件夹是 node 的依赖包所在的位置,我们之前安装的 express 就在这个里面。
好了,现在已经有了 express
我们尝试一下引入它吧:
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
noobApi\index.js
const express = require('express') console.log(express)
不出意外,一切正常的话,emm,你会看到这样一个友善而且醒目的报错:
这是为什么呢?
在渲染进程中直接使用 require 引入模块的话,起始地址其实是应用的根目录,也就是之前看到 main.js 的地方。
所以我们需要一点点操作:
noobApi\server\util\requireHacker.js
import { workspaceDir } from "./file.js" let re = null let realRequire = null if (window.require) { const fs = require("fs") const path = require("path") if (!window) { const window = global } if (window.require.cache) { realRequire = window.require } if (realRequire) { const path = require("path") re = function (moduleName, base) { if (module) { let _load = module.__proto__.load if (!module.__proto__.load.hacked) { module.__proto__.load = function (filename) { let realfilename = filename try { (_load.bind(this))(filename) } catch (e) { if (e.message.indexOf('Cannot find module') >= 0 && e.message.indexOf(filename) >= 0) { if (global.ExternalDepPathes) { let flag let modulePath global.ExternalDepPathes.forEach(depPath => { if (fs.existsSync(path.join(depPath, moduleName))) { if (!flag) { console.file_warn ? console.file_warn(`模块${moduleName}未找到,重定向到${path.join(depPath, moduleName)}`) : console.warn(`模块${moduleName}未找到,重定向到${path.join(depPath, moduleName)}`) filename = path.join(depPath, filename); try { (_load.bind(this))(filename) flag = true } catch (e) { } } else { console.warn(`模块${moduleName}在${modulePath}已经找到,请检查外部路径${path.join(depPath, moduleName)}是否重复安装`) } } }); if (!flag) { console.error(e) throw new Error(`无法加载模块${realfilename}`) } } else { console.error(e) throw new Error(`无法加载模块${realfilename}`) } } else { throw (e) } } } module.__proto__.load.hacked = true } } if (!window.realRequire) { window.realRequire = realRequire } let that = window if (base) { moduleName = path.resolve(base, moduleName) } if (workspaceDir) { if (this) { that = this } try { if (that.realRequire) { let _module = that.realRequire(moduleName) return _module } else { let _module = window.realRequire(moduleName) return _module } } catch (e) { if (e.message.indexOf('Cannot find module') >= 0) { if (!(moduleName.startsWith("/") || moduleName.startsWith("./") || moduleName.startsWith("../"))) { if (global.ExternalDepPathes) { let flag let modulePath global.ExternalDepPathes.forEach(depPath => { if (fs.existsSync(path.join(depPath, moduleName))) { if (!flag) { console.file_warn ? console.file_warn(`模块${moduleName}未找到,重定向到${path.join(depPath, moduleName)}`) : console.warn(`模块${moduleName}未找到,重定向到${path.join(depPath, moduleName)}`) moduleName = path.join(depPath, moduleName) modulePath = path.join(depPath, moduleName) flag = true } else { console.warn(`模块${moduleName}在${modulePath}已经找到,请检查外部路径${path.join(depPath, moduleName)}是否重复安装`) } } }); } } else { moduleName = path.resolve(module.path, moduleName) } try { let _module _module = that.realRequire(moduleName) return _module } catch (e) { throw e } } else { throw e } } } else return window.require(moduleName) } } } if (window.require && re) { window.require = re window.realRequire = realRequire if (window.realRequire && window.realRequire.cache) { window.realRequire.cache.electron.__proto__.realRequire = realRequire.cache.electron.__proto__.require window.realRequire.cache.electron.__proto__.require = re } window.require.setExternalDeps = (path) => { if (!window.ExternalDepPathes) { window.ExternalDepPathes = [] } if (path && !window.ExternalDepPathes.indexOf(path) >= 0) { window.ExternalDepPathes.push(path) window.ExternalDepPathes = Array.from(new Set(window.ExternalDepPathes)) } } re.setExternalDeps(`${workspaceDir}`) window.require.setExternalBase = (path) => { if (!window.ExternalDepPathes) { window.ExternalDepPathes = [] } if (!window.ExternalBase) { window.ExternalBase = path } else { console.error('不能重复设置外部依赖路径') } } } export default window.require
这样做了之后,就可以设置外部依赖来引入包了:
import {代码片段路径} from './util/file.js' import './util/requireHacker.js' require.setExternalDeps(代码片段路径 + `/noobApi/node_modules`) const express = require('express') console.log(express)
现在没报错了,说明我们的引入是成功的。
现在 express 也有了,让我们来弄一个 hello world
import {代码片段路径} from './util/file.js' import './util/requireHacker.js' require.setExternalDeps(代码片段路径 + `/noobApi/node_modules`) const http = require('http') const express = require('express') const 发布应用 = express() const 发布端口 ='80' 发布应用.use('/',(req,res)=>{ res.end('hellow world') }) let 发布服务器 = http.createServer(发布应用); 发布服务器.listen(发布端口, () => { console.log("发布服务已经启动") })
这时你打开浏览器,输入 127.0.0.1 就会看到这个:
好了,我们的发布服务就这样写完了。。。。。。吗?
肯定是没有啊,现在它还只能显示一个 hello world
啊
所以这里要让它能显示出思源的文档。
渲染文档发布结果
我们来创建一个函数,还记得之前做发布到知乎的时候,获取渲染结果的时候,我们使用了这样几个接口:
let 块id = noobApi.自定义菜单.当前菜单.菜单状态.当前块id let stmt = `select * from blocks where id in (select root_id from blocks where id = "${块id}" )` let 文档数据 = (await noobApi.核心api.sql({ stmt: stmt }))[0] let 文档内容 = await noobApi.核心api.exportPreview( { "id": 文档数据.id } ) let 文档属性 = await noobApi.核心api.getDocInfo( { "id": 文档数据.id } ) let 发布数据 = {} 发布数据.title = 文档属性.ial.title 发布数据.markdown = 文档数据.markdown 发布数据.content = 转换图片地址(文档内容) 发布数据.desc = 文档属性.ial.memo 发布数据.thumb = 文档属性.ial['title-img']
其中 exportPreview
(api/export/exportPreview
)可以获取导出的文档预览。
所以这里我们可以这样做:
async function 渲染页面(块id){ let stmt = `select * from blocks where id in (select root_id from blocks where id = "${块id}" )` let 文档数据 = (await noobApi.核心api.sql({ stmt: stmt }))[0] let 文档内容 = await noobApi.核心api.exportPreview( { "id": 文档数据.id } ) let 文档属性 = await noobApi.核心api.getDocInfo( { "id": 文档数据.id } ) let 发布数据 = {} 发布数据.title = 文档属性.ial.title 发布数据.markdown = 文档数据.markdown 发布数据.content = 转换图片地址(文档内容) 发布数据.desc = 文档属性.ial.memo 发布数据.thumb = 文档属性.ial['title-img'] return 发布数据 } function 转换图片地址(文档内容) { let div = document.createElement('div') div.innerHTML = 文档内容.content ? 文档内容.content : 文档内容.html div.querySelectorAll('[src]').forEach( el => { if (el.getAttribute('src').startsWith('assets')) { el.setAttribute('src', window.location.origin + '/' + el.getAttribute('src')) } } ) div.innerHTML += '<p>本文使用<a href="https://b3log.org/siyuan/">思源笔记</a>写作</p>' div.innerHTML += '<p>本文使用<a href="http://publish.chuanchengsheji.com/">椽承设计</a><a href="https://github.com/leolee9086/snippets">小工具</a>配合同步</p>' return div.innerHTML }
然后使用这个函数来渲染响应:
发布应用.use('/',async(req,res)=>{ //这个id是思源的帮助页面首页啦 let 块id = '20200812220555-lj3enxa' let 页面数据 = await 渲染页面(块id) res.end(页面数据.content) })
这个时候,我们再去访问 127.0.0.1
就会看到:
emmmmmmmm
这是因为我们编码问题,如果不使用合适的编码的话,就会出现这样蛋疼的错误。
所以需要改一下正确的编码,其实只需要这样就可以了:
发布应用.use('/',async(req,res)=>{ let 块id = '20200812220555-lj3enxa' let 页面数据 = await 渲染页面(块id) //指定一下编码 res.setHeader("Content-Type", "text/html; charset=utf-8") res.end(页面数据.content) })
这个时候再查看它,就会看到:
这样就看到正常的内容了。
不过现在它的 id 是写死的,所以我们来搞一下根据 id 渲染:
根据 id 渲染文档内容:
这里的问题是怎么获取 id,我们看一下这个函数:
发布应用.use('/',async(req,res)=>{ let 块id = '20200812220555-lj3enxa' let 页面数据 = await 渲染页面(块id) res.setHeader("Content-Type", "text/html; charset=utf-8") res.end(页面数据.content) })
它的含义是在访问路径匹配 '/'
的时候,就使用后面的回调函数来响应请求,而这个回调函数一般有三个参数 (req,res,next)
。
其中 req 就代表了请求。
显然我们要获取块 id 的话,只能从请求中获取。
这里有几种办法,可以参考这里:
Express routing - Express 中文文档 | Express 中文网 (expressjs.com.cn)
查了一下文档之后,就试试用这种方式吧:
依样画葫芦,把我们的路由改成这样:
发布应用.use('/',async(req,res,next)=>{ console.log(req) req.url=='/'? res.redirect('/20200812220555-lj3enxa'):null next() }) 发布应用.use('/:blockID',async(req,res)=>{ console.log(req.params) let 块id = req.params.blockID let 页面数据 = await 渲染页面(块id) res.setHeader("Content-Type", "text/html; charset=utf-8") res.end(页面数据.content) })
然后现在我们再试着访问一下(这个 id 就是这篇文档的):
访问首页(127.0.0.1)的话,就会重定向到帮助文档首页啦:
现在你在局域网上的小伙伴应该已经能够访问你的文档内容了,但是现在所有的文档都能够被访问,好像不大好,我们通过 publish-access 属性设置一下它,如果这个属性值是 public
的话,就能够访问,如果不是的话,就返回一个亲切而且友好的提示:
if (文档属性.ial && 文档属性.ial['custom-publish-access']) { let 发布数据 = {} 发布数据.title = 文档属性.ial.title 发布数据.markdown = 文档数据.markdown 发布数据.content = 转换图片地址(文档内容) 发布数据.desc = 文档属性.ial.memo 发布数据.thumb = 文档属性.ial['title-img'] return 发布数据 } else { return {content:'<h1 style="font-size:200px;color:red">这个文档除了我自己谁都不准看</h1>'} }
你看,无论是谁看到这个温暖的大红色,都会理解背后那个温柔而羞涩的作者的对吧。
或者你也可以自己改得更加热情一点:
ok,现在你想要把文档给谁看又不想他能够修改的话,只需要把 < 你的局域网 ip>/< 块的 id> 发给他,他就能够看到文档的内容啦(我个人建议如果你不希望被打死的话最好不要用上面那两条提示语)。
你说公网发布?这个不是这次要说的,下次再说,下次再说.
目前的代码片段的地址位于:
leolee9086/snippets (github.com)
如果你希望看到实现过程的话可以看看这个位置
里面能够看到我所有的提交记录。
就像这样,越早的记录对新手应该越友好。。。。。吧。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于