一、前情提要
我们之前做了一个能够显示笔记属性的挂件:
思源笔记折腾记录-简单挂件-更美观地展示属性 - 知乎 (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')" }
然后你就会看到这个啦:
还是一样的,我们需要之前那个 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-syPublisher
和 noob-service-vite
里面的 node_modules
和 package.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>
然后就可以看到之前的效果了:
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 } } )
这样之后,卡片就显示出我们想要的数据了
这样我们就有了一个显示最基础的卡片的小玩意了,之后只需要让它能够显示更多的卡片,就能够用来当成一个简单的白板使了。
代码片段的仓库在这里
leolee9086/snippets (github.com)
viteWdigets 的仓库在这里
leolee9086/viteWidgets (github.com)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于