一、前情提要
我们之前做了一个能够显示笔记属性的挂件:
思源笔记折腾记录-简单挂件-更美观地展示属性 - 知乎 (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)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于