HelloElectron—实现简单的UML展示(三)
今天我们将做一个最小的Demo,在不引入尽可能少的依赖下,做一个PlantUML的展示。实现的方式主要是通过页面发起请求到Electron主进程,主进程在发起请求到PlantUML Server。 GitHub提交在此处支持简单UML渲染 · Jakentop/plantuml-editor@ac3cc95 (github.com)
实现原理图
首先运行此Demo需要启动两个服务分别是Electron和PlantUML-Server(需要修改内部实现代码)。整个架构图自顶向下我们可以看到,前端通过Tailwind CSS布局,采用Monaco Editor作为编辑器。当用户输入后需要渲染生成PlantUML图时,则通过IPC通信将编辑器中的内容传递给主进程。而主进程则发起axios请求到PlantUML服务。并且首先调用该服务的Decode功能(需要改写代码),其次再调用Base64服务生成编码。随后主进程返回给渲染进程,并呈现图片。
一些注意事项:
- Tailwind CSS不是必须的,你可以把他理解成一个公共的CSS库,可以帮助开发者快速设计统一样式的前端界面
- Monaco编辑器是为了后续可以扩展,因此引入了他;虽然引入过程中磕磕绊绊但最终还是解决了
- 需要修改PlantUML Server的代码,稍后我会提供修改的地方以及原因。同时fork一份代码在我的仓库中。
- 可能大家会发现明明可以直接从Render向PlantUML发起请求,但为什么要通过主进程发起呢?
- 在这里我做了一个分离,目的是为了后续尝试将PlantUML Server集成到Electron中
- 我也看到了一些npm直接解析PlantUML的包无奈他们都太老了,但是日后依旧会考虑是否要集成他们
安装需要用到的依赖
本节我们需要新增依赖:monaco-editor、vite-plugin-monaco-editor、tailwindcss、postcss、axios
首先通过pnpm install monaco-editor vite-plugin-monaco-editor tailwindcss postcss axios
添加这些依赖
Tailwind CSS
推荐大家通过官方文档官方文档安装,目前在安装过程中遇到了一个问题,在Vscode中的index.css无法识别到Tailwind CSS语法。但是实际不影响使用
另外关于index.css的位置建议大家放在render目录下,本提交中我把它放在了src目录下这是不妥的。
vite-plugin-monaco-editor
由于monaco编辑器不可以直接支持vite,因此需要添加该vite插件。并且修改vite.config.ts
文件。在文件的plugins中添加该插件使得vite支持monaco编辑器。
PlantUML-Server改动
Jakentop/HelloElectron-PlantUML: HelloElectron 对PlantUML做的改动 (github.com)
PlantUML-Server是一个PlantUML的服务端,他可以接收text并将他们转换成压缩编码模式,另外一个接口则是可以通过压缩编码生成多样化的图像。 由于原生的转换压缩编码返回的是一个HTML,因此我们在这里需要做一点小的改动:
- 首先找到该方法
net.sourceforge.plantuml.servlet.PlantUmlServlet#doPost
- 其次修改末尾返回的结果,改成如下形式:
此项目的代码改动稍后我会上传到GitHub中。
启动PlantUMLServer
首先需要一个套完整的Java+Maven的环境,在执行了mvn install之后,只需要执行mvn jetty:run
命令即可。注意启动后会默认为8080端口需要注意端口是否被占用了。
main目录改动
从上面架构图看我们给main添加了两个功能分别是event.js和axios。另外我们需要主进程接收渲染进程的事件并执行axios操作
event.js
官方的demo中,将所有的IPC通信处理都放在了index.js文件中。但是实际上我们希望index.js应当值包括electron的创建窗口的相关代码,因此在这里我们添加了一个js文件用来集中整个软件中的所有时间。
创建event.ts
首先我们在main目录下创建event目录,并在目录中创建index.ts文件。随后编写如下代码
/* eslint-disable no-console */
import { ipcMain } from 'electron'
import axios from 'axios'
export const initEvent = () => {
/**
* 将plantuml转换到base64
*/
ipcMain.handle('uml:preview:base64', async (_event, text: string) => {
console.debug(`run preview base64:${text}`)
const code = (await axios.request({
url: 'http://localhost:8080/plantuml/uml/',
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: `text=${encodeURI(text)}`,
})).data
console.debug(code)
return (await axios.get(`http://localhost:8080/plantuml/base64/${code}`)).data
})
}
- 对外暴露initEvent这个入口方法,用于在main.ts中初始化注册所有需要监听的事件
ipcMain.handle
用于监听渲染器发送的通信请求,并返回结果给渲染器(参阅进程间通信 | Electron (electronjs.org))- 命名事件名称为
uml:preview:base64
并且接收一个string类型的text变量用于传递render进程提供的数据 - 注意这个事件使用了
async
修饰这是由于axios的请求是异步的需要等待。 - 其中的
code
变量则是请求了本地plantuml server的decode压缩编码接口(即我们修改的部分),注意请求格式和我此处保持一致即可。 - 其中的
return
内容则是将我第一次请求服务获取到的code,重新调用base64方法使得器返回生成的图片base64编码格式
对index.ts修改
在编写event的初始化方法后,我们需要对electron的启动类进行修改,让时间注册上去。同时由于我们使用的模板原先使用的是作者自己写的通信框架,我们需要将他原有的通信框架去掉并修改成我们自己的通信注册方法。
import { initEvent } from './event'
async function bootstrap() {
try {
await electronAppInit()
app.whenReady().then(() => {
createWindow()
initEvent()
})
// await createEinf({
// window: createWindow,
// controllers: [AppController],
// injects: [{
// name: 'IS_DEV',
// inject: !app.isPackaged,
// }],
// })
}
catch (error) {
console.error(error)
app.quit()
}
}
bootstrap()
- 添加了
whenReady
事件,创建了主窗口,并且初始化main
进程的事件处理 - 注释掉了
createEinf
这是原始模板作者引入的一个建议的IPC通信框架
preload目录改动
除了修改main进程外,我们还需要修改preload目录中的代码。preload脚本正如官方文档所说,他是用来初始化渲染器进程的脚本。他觉有完整的NodeApi可以访问,同时也隔离了渲染器进程中直接访问NodeAPI带来的影响。因此我们需要修改preload目录的index.ts文件为:
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
umlPreviewBase64: async (text: string) => await ipcRenderer.invoke('uml:preview:base64', text),
})
contextBridge.exposeInMainWorld(
'ipcRenderer',
{
invoke: ipcRenderer.invoke.bind(ipcRenderer),
on: ipcRenderer.on.bind(ipcRenderer),
removeAllListeners: ipcRenderer.removeAllListeners.bind(ipcRenderer),
},
)
- 这里我们主要关注上面的
contextBridge.exposeInMainWorld
方法 - 他的含义是,对render渲染器暴露了名为
electronAPI
的对象,同时该对象提供一个方法umlPreviewBase64
- 该方法接受一个text变量,并通过
ipcRenderer.invoke
方法发送一个事件到主进程(即上小节我们编写的代码段中) - 可以注意到
ipcRenderer.invoke
中第一个参数其实和上节在主进程的名称是一致的。 - 为什么我们不直接暴露ipcRenderer方法给渲染器进程使用?安全注意| Electron (electronjs.org)
render目录改动
完成了对Electron服务端的改动,我们终于可以开始写前端代码啦。由于这篇教程默认你有一定的vue、html、js、css等基础,因此我再这里不会详细的阐述为什么要这样写。但是我们与大家分享我遇到的坑和问题:
views目录
个人有一个习惯,vue中的页面放在views目录中,组件放在components中同时components中的子目录与views中对应。
App.vue
<template>
<WorkSpace />
</template>
- 不解释了,这里前期我们没有引入vue-router,后期在页面交互复杂后考虑引入vue-router
WorkSpace.vue
<script setup lang="ts">
import Editor from '@render/components/workspace/Editor.vue'
import Preview from '@render/components/workspace/Preview.vue'
import { ref } from 'vue'
const value = ref('')
const editor = ref()
function update() {
console.log(editor.value.getValue())
value.value = editor.value.getValue()
}
</script>
<template>
<div @click="update">123</div>
<div class="absolute w-full h-full grid grid-cols-2" @keydown.ctrl.enter="update">
<!-- 编辑器 -->
<Editor ref="editor" />
<!-- 预览界面 -->
<Preview :data="value" />
</div>
</template>
- WorkSpace在现阶段会作为主入口页面
- 定义了一个123的div标签,并绑定了
update
作为click事件的回调 update
方法会调用Editor
组件getValue()
方法。通过ref的方式暴露(注意这里ref是没有冒号的)update
方法会更新Preview
组件的data参数,即刷新新的Base64编码的图片- 注意:这里的
@keydown.ctrl.enter
实际上是无效的,具体原因暂时还不知道 #todo/了解为什么这里无效
components目录
Editor组件
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { editor } from 'monaco-editor'
const el = ref()
let edit = null
onMounted(() => {
edit = editor.create(el.value, { automaticLayout: true })
})
function getValue() {
return edit.getValue()
}
defineExpose({ getValue })
</script>
<template>
<div ref="el" />
</template>
- 可以看到
onMounted
生命周期中即通过ref
获取下面的el
标签,并调用monaco的创建编辑器方法 - 注意这里的
edit
变量不是响应式的,这里有一个坑:- 原先我使用了响应式发现调用他的
getValue()
方法会卡死 - 解决方案是如果使用ref包装,则需要使用toRaw()拿到原始数据才可以。因此我再这里直接没有使用ref包装
- 参考此篇文档提供的解决方案
- 原先我使用了响应式发现调用他的
- 注意
defineExpose
中的写法和props的写法是有差异的,需要直接暴露这个方法的引用才可以
Preview组件
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
const props = defineProps(['data'])
const source = ref('')
onMounted(async () => {
source.value = await window.electronAPI.umlPreviewBase64(props.data)
})
watch(() => props.data, async (newdata) => {
source.value = await window.electronAPI.umlPreviewBase64(newdata)
})
</script>
<template>
<div><img :src="source"></div>
</template>
- 对外暴露了
data
,用于获取他需要展示的base64图片 - 注意
onMounted
和watch
方法调用了window.electronAPI.umlPreviewBase64
。这里便是渲染器进程通过preload预处理的结果和主进程通信的过程! - 注意所有操作都是Promise的,这是由于我们的底层请求采用了axios,因此每一层都需要进行等待!
总结
编写完成后,我们只需要调用pnpm dev
即可展示最开始的界面,首次启动时会把空文本传给PlantUML server,此时他会传回一个缺省图。后续当我们编辑后,就可以点击123来实现刷新数据的过程。至此我们就成功完成了这个最简单的Demo。
接下来我们需要继续优化这个简单的Demo,例如:添加状态管理、通过UI模板美化我们的界面等。
这是GitHub当前提交的地址:支持简单UML渲染 · Jakentop/plantuml-editor@ac3cc95 (github.com)
需要同步做的小伙伴可以拉取整个项目:Jakentop/plantuml-editor: 基于electron+vue+vite实现的plantuml桌面编辑器 (github.com)并切换到ac3cc959e62ef2b72684d99fa887d16386e473e4提交中。
转载自:https://juejin.cn/post/7138687474825429029