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

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

一、前情提要

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

思源笔记折腾记录-简单挂件-更美观地展示属性 - 知乎 (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)

  • 思源笔记

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

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

    22012 引用 • 87776 回帖 • 2 关注

欢迎来到这里!

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

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