思源笔记折腾记录 - 做一个白板 - 最最最基础的卡片显示

本贴最后更新于 891 天前,其中的信息可能已经时移世改

一、前情提要

我们之前做了一个能够显示笔记属性的挂件:

思源笔记折腾记录-简单挂件-更美观地展示属性 - 知乎 (zhihu.com)

已经学会怎么写网页和引入包了,所以现在基础的知识已经有了,现在让我们来实现一个简单的白板吧(手动滑稽)。

二、原理和实现过程

其实上面真的不是完全开玩笑啦, 要实现一个完善的白板工具,确实挺困难的,但是只是要用卡片的形式显示一下数据的话还真的不算是很难啦。

1、使用到的库:

vue3-moveable

首先我们需要一个能够把数据转换成 html 元素的工具,这个就使用 VUE 吧。

然后我们需要一个能够让卡片到处移动的工具,这里使用一个 vue 的库:

moveable/packages/vue3-moveable at master · daybrush/moveable (github.com)

这是它的 storybook。

Basic - Resizable ⋅ Storybook (daybrush.com)

可以看到这个可以让 DOM 元素可以在屏幕上移动、缩放甚至变个形啥的。

所以我们就使用它吧。

vue3-sfc-loader

没错,还是坚持绝不打包一百年不动摇。。。。。。。

其实不是啦。只是我觉得这个东西比较方便大家能够随时修改源码。

所以在这儿再提一次,实际上这回我们不使用它,而是借用思源的渲染进程,在里面跑一个 vite,这样就能够在保持能够继续快速改源码的同时,使用 vite 的一些特性了。

vite

这个就是我们这次要使用的开发工具了。

具体的来说,需要使用的是 vite@2.9.15。

至于为什么是具体到 2.9.15 呢,你可以尝试一下使用更新的版本,看一下到底会出什么问题就明白了。

不过这回不是要使用它打包,只是用它的开发服务器,作为在本地运行的笔记软件,直接使用 vite 来跑一个小功能应该对思源来说也不会是太离谱的事情。

2、创建服务

我们在之前实现了一个简单的发布服务,这次也还是差不多。

先在 snippets 里面创建一个文件夹 noob-service-vite,这个不是用来跑白板应用的,而是用来运行 vite 的。

然后在它里面安装一下 vite

noob-service-vite> npm i vite@2.9.15 --registry=https://registry.npmmirror.com

嗯,就像上面这样.

然后再创建一个文件:

noob-service-vite\index.js

const vite = require('vite')

再在代码片段里面引入它(记住上面所有文件夹都是在 工作空间\data\snippets):

{ "id": "20221201201802-zymmyxs", "name": "vite服务器", "memo": "", "type": "js", "enabled": true, "content": "import ('/snippets/noob-service-vite/index.js')" }

然后你就会看到这个啦:

image

还是一样的,我们需要之前那个 requireHacker,不过它好像之后还挺常用,所以我把它弄到了 noobApi 里面,这样安装依赖的时候就直接安装到 snippets 算了,反正我们也不大可能在这里大量引入同一个包的不同版本

require.setExternalDeps(workspaceDir+'/data/snippets/')

好吧这里还是说一下 requireHacker 到底干了点什么:

import { workspaceDir } from "../../noob-service-syPublishServer/server/util/file.js" let re = null let realRequire = null if (window.require) { const fs = require("fs") const path = require("path") if (!window) { const window = global } //require函数有一个缓存 if (window.require.cache) { //把原本的require赋值给realRequire中间变量 realRequire = window.require } if (realRequire) { //path是nodejs的内部模块,用来解析文件路径,我们是在electron环境中,所以它也可以使用 const path = require("path") re = function (moduleName, base) { //module变量的原型链上有一个load对象 if (module) { let _load = module.__proto__.load if (!module.__proto__.load.hacked) { module.__proto__.load = function (filename) { let realfilename = filename try { //bind的含义是让函数内部的this指向后面的参数 (_load.bind(this))(filename) } catch (e) { //这是说如果load的时候找不到模块的话,就到设置过的外部依赖文件夹里面去找 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) { //这里的file_warn是一个注入的函数,用于将日志写入到文件,这边没有用到 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,也就是改掉window对象上的require window.require = re window.realRequire = realRequire if (window.realRequire && window.realRequire.cache) { //这里是覆盖掉所有的require,让它们可以支持外部依赖 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('不能重复设置外部依赖路径') } } }

好了基本上就是让 require 函数在找模块的时候多找几个地方而已。

这里还是一样

import { 代码片段路径 } from '../noobApi/util/file.js' import requireHacker from '../noobApi/util/requireHacker.js' if(window.require){ const path = require('path') require.setExternalDeps(path.join(代码片段路径,'noob-service-vite/node_modules')) require('vite') }

欸,这回没有报错了。

不过之前我们已经在 noob-service-syPublisher 里面弄了一个 node_modules

然后又在这里搞了一个。。。。。

所以这里干脆就都统一放到 snnipets 里的 node_modules 里面算了,反正自己的笔记里面也不大可能用到很复杂的模块管理。

所以需要删除原来 noob-service-syPublishernoob-service-vite 里面的 node_modulespackage.json 还有 package-lock.json,然后在 snippets 里面安装一遍(这里其实会造成多个平台使用的时候造成一些问题,但是不管了,首先假定你全都只用 windows 或者全都只用 linux,会用多个平台的应该也不需要看这么白的东西了)

然后你还需要安装一个 @vitejs/plugin-vue@2.3.3,因我们的 vite 版本是 2.9.15,所以这里需要指定插件版本。

这样做完了之后,我们来搞一个 vite 的应用吧:

noob-service-vite\index.js

import noobApi from '../noobApi/index.js' //这个之后肯定是要合并一下的,不过先这样吧 import { 工作空间路径 } from '../noobApi/util/file.js' if (window.require) { const path = require('path') const vite = require('vite') let vite服务注册表 = {} //先写死,之后再说 let 白板开发服务 = await 创建vite服务(工作空间路径 + '/data/viteWidgets/whiteBoard', 6809) console.log(白板开发服务) //如果不记得这是怎么一回事了,回头再搜索一下promise function 创建vite服务(文件夹路径, 端口) { return new Promise((resolve, reject) => { let vite配置文件路径 = path.join(文件夹路径, 'vite.config.js') if (!vite服务注册表.文件夹路径) { vite.createServer({ configFile: vite配置文件路径, root: 文件夹路径, }).then(server => { vite服务注册表[文件夹路径] = server server.listen(端口) window.open(`http://127.0.0.1:${端口}`) resolve(server) } ) } }) } }

直接这个时候是肯定会报错的,因为这时候 viteWidets 和下面的 whiteBoard 文件夹都还不存在。。。。。。。

创建 \data\viteWidgets\whiteBoard 文件夹之后,在里面写一个 vite.config.js

//vite已经由代码片段提供了,所以这个文件夹里面不需要安装 import { defineConfig } from 'vite' //vue插件也是一样 import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [ vue(), ], base: './', server:{ //这后面的是对思源的各种接口的代理,你如果写插件的话也可以试试 proxy:{ "/stage":{ target:"http://127.0.0.1:6806/stage", changeOrigin: true, rewrite: path => path.replace(/^\/stage/, '') }, "/stage/js":{ target:"http://127.0.0.1:6806/stage/js", changeOrigin: true, rewrite: path => path.replace(/^\/stage/, '') }, "/widgets":{ target:"http://127.0.0.1:6806/widgets", changeOrigin: true, rewrite: path => path.replace(/^\/widgets/, '') }, "/api":{ target:"http://127.0.0.1:6806/api", changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') }, "/assets":{ target:"http://127.0.0.1:6806/assets", changeOrigin: true, rewrite: path => path.replace(/^\/assets/, '') }, "/appearance":{ target:"http://127.0.0.1:6806/appearance", changeOrigin: true, rewrite: path => path.replace(/^\/appearance/, '') }, "/snippets":{ target:"http://127.0.0.1:6806/snippets", changeOrigin: true, rewrite: path => path.replace(/^\/snippets/, '') }, "/ws":{ target:"ws://127.0.0.1:6806/ws", changeOrigin: true, ws:true, rewrite: path => path.replace(/^\/ws/, '') }, }, cors: { allowedHeaders:['Content-Type', 'Authorization'] }, hmr:{ overlay:true }, }, resolve:{alias:[]} })

然后随便写一个 html 吧

data\viteWidgets\whiteBoard\index.html

<!DOCTYPE html> <head> <script type="module" src="./src/index.js"></script> </head> <body> </body>

data\viteWidgets\whiteBoard\src\index.js

document.body.innerHTML=document.body.innerHTML+'hello world' setInterval(()=>document.querySelector('#time').innerHTML='现在时间是'+new Date().toTimeString(),1000)

好了,现在可以看到效果了:

演示时间

现在我们终于可以真的开始写白板了。

创建一个简单的白板:

1、安装 vue3-moveable

在 whiteBoard 文件夹下面安装一个就是了,还是一样的,如果网速慢的话,指定一下 registry,更多的参数可以参考这里 :

npm-install | npm Docs (npmjs.com)

差点忘了还要安装一个 vue。。。。

2、创建应用

改一下之前的 index.html 那些:

data\viteWidgets\whiteBoard\src\index.html

<!DOCTYPE html> <head> <script type="module" src="./src/index.js"></script> </head> <body> <div id="app"></div> </body>

data\viteWidgets\whiteBoard\src\index.js

import {createApp} from 'vue' import App from './app.vue' createApp(App).mount("#app")

data\viteWidgets\whiteBoard\src\app.vue

<template> <div>hello vue</div> <div>{{当前时间}}</div> </template> <script setup> import {ref} from 'vue' let 当前时间 = ref(null) setInterval( ()=>{ 当前时间.value ='现在时间是:'+ new Date().toTimeString() },1000 ) </script>

然后就可以看到之前的效果了:

image

vue 实现了响应式的界面编程,所以就不用像之前那样自己去改 DOM 了,至于性能反正以我们的水平,就算自己上手优化也肯定还不如 vue 直接搞,就别纠结“原生 js 性能最强”了。

3、最简单的白板

现在我们来实现一个最最简单的白板。

最基础的,先来试试一张卡片:

whiteBoard\src\app.vue

<template> <div class="container" @click="卡片被激活=false"> <div ref="卡片框架元素" class="card_frame" @click="e=>{e.stopPropagation();卡片被激活=true}" > <div class="card_body"> 到处拖拖看 </div> </div> <Moveable className="moveable" v-if="卡片被激活" :target="卡片框架元素" :draggable="true" :scalable="true" :resizable="true" :rotatable="true" :keepRatio="false" @drag="onDrag" @scale="onScale" @rotate="onRotate" @resize="onResize" > </Moveable> </div> </template> <script setup> import Moveable from "vue3-moveable"; import { reactive, ref,onMounted } from 'vue' const 卡片框架元素 = ref(null) const 卡片被激活 =ref(false) const 卡片尺寸 = reactive({ 边框宽度:1, 内边距:15, 宽度:300, 高度:400, }) //这里的都是事件回调 function onDrag(e) { 卡片框架元素.value.style.transform = e.transform; } function onScale(e) { 卡片框架元素.value.style.transform = e.drag.transform; } function onRotate(e) { 卡片框架元素.value.style.transform = e.drag.transform; } function onResize(e) { 卡片框架元素.value.style.width = `${e.width}px`; 卡片框架元素.value.style.height = `${e.height}px`; 卡片框架元素.value.style.transform = e.drag.transform; } </script> <style scoped> .container{ width: 100%; height:100% } .card_frame{ font-size: large; box-sizing: border-box; width:v-bind('卡片尺寸.高度+"px"'); height:v-bind('卡片尺寸.高度+"px"'); margin: 0%; padding:5px; transform: translate(603px, 270px); } .card_body{ border:v-bind('`${卡片尺寸.边框宽度}px solid grey`') ; border-radius: 15px; width:v-bind('`calc(100% - ${2*(卡片尺寸.边框宽度+卡片尺寸.内边距)}px)`') ; height:v-bind('`calc(100% - ${2*(卡片尺寸.边框宽度+卡片尺寸.内边距)}px)`') ; padding:v-bind('`${卡片尺寸.内边距}px`'); background-color: white; } </style>

效果就像这样:

一张卡片

现在我们就有了一个能够显示内容的白板了,但是不可能一直都只显示这个对吧,来试试显示点别的。

之前试过用 exportPreview 这个接口来获取文档导出的 html,这次还是试一下它:

let 卡片内容 = ref({}) fetch('/api/export/preview', { method: 'POST', body: JSON.stringify( { id: '20221204091100-tf8z0um' } ) }).then( data => { return data.json() } ).then( json => { if (json.data) { 卡片内容.value = json.data } } )

这样之后,卡片就显示出我们想要的数据了

image

这样我们就有了一个显示最基础的卡片的小玩意了,之后只需要让它能够显示更多的卡片,就能够用来当成一个简单的白板使了。


代码片段的仓库在这里

leolee9086/snippets (github.com)

viteWdigets 的仓库在这里

leolee9086/viteWidgets (github.com)

  • 思源笔记

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

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

    25482 引用 • 105378 回帖

相关帖子

欢迎来到这里!

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

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