今天的内容与Electron关系并不是很大,上篇内容我们实现了一个最简单的通过Electron通信来实现PlantUML图片的加载。本篇我们会引入pinia、naive-ui来完善一下我们的界面。同时对之前的一些代码报错进行优化。
Github:Jakentop/plantuml-editor: 基于electron+vue+vite实现的plantuml桌面编辑器 (github.com) Github提交标签:e88e02d755542efd1b35cb47b58cf28b6c27b033
引入pinia、naive-ui等依赖
为了今后的迭代,我们需要引入一个状态管理框架,并且将编辑器的一些行为与状态绑定。既然我们来到了vue3,那一定要推销下有vue团队打造的pinia了呀Introduction | Pinia (vuejs.org)。我很喜欢他不在需要添加Mutation即可直接实现状态的转化。 naive-ui则是一个很不错的vue3 UI库,我们有一些简单的UI界面会利用他来绘制。
执行pnpm install
pnpm install pinia naive-ui即可完成安装
pinia使用
pinia目录
又到了纠结目录的时刻了,相比vuex而言pinia更加的灵活。因此我决定定义一个目录,其中专门放置pinia相关的store文件。由于这是前端的内容因此他会放在render文件中
pinia基本使用
pinia使用起来非常简单,我们可以参考官网给的demo。
pinia store配置
可以看到我们只需要定义state和actions即可完成状态的管理。
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// could also be defined as
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
pinia 使用
在业务代码中我们只需要直接new一个定义的store对象即可实现状态管理
import useCounterStore from '@render/store/counter'
const editorStore = useCounterStore()
editorStore.increment()
naive-ui
安装 - Naive UI直接看官方教程吧,值的注意的是需要,在main.ts中添加pinia和naive-ui的依赖
import { createApp } from 'vue'
import naive from 'naive-ui'
import { createPinia } from 'pinia'
import App from './App.vue'
import './index.css'
const pinia = createPinia()
const app = createApp(App)
app.use(naive)
app.use(pinia)
app.mount('#app')
美化与重构界面
重构update方法
上一篇我们通过ref调用了Editor组件的update方法,该方法通过获取Monaco Editor的实例。并调用了getValue方法,而这里我们引入了Pinia,后面我们将左右的编辑器操作使用pinia封装,并记录每一次状态的变化。
// render/store/EditorStore.ts
// 编辑器状态管理
import { defineStore } from 'pinia'
import { toRaw } from 'vue'
export interface EditorPublicMethods {
getValue(): string
}
export default defineStore('editor', {
state: () => {
return {
editor: null,
}
},
actions: {
/**
* 获取编辑器内容
* @returns 当前编辑器的内容
*/
getValue(): string {
return this.editor ? toRaw(this.editor).getValue() : ''
},
/**
* 清空所有数据
*/
clearAll(): void {
toRaw(this.editor)?.setValue('')
},
/**
* 数据初始化
*/
init(editor: any) {
this.editor = editor
},
},
})
// Editor.vue <script></script>
import { onMounted, ref, toRaw } from 'vue'
import { editor } from 'monaco-editor'
import EditorStore from '@render/store/EditorStore';
const editorStore = EditorStore()
const el = ref()
let edit = null
onMounted(() => {
edit = editor.create(el.value, { automaticLayout: true })
editorStore.init(edit)
})
function getValue() {
return editorStore.getValue()
}
defineExpose({ getValue })
- 我们定义了两个方法,其中
init负责直接将editor实例提供给pinia中的state,getValue方法则是获取editor的值 - 注意后续所有对editor的操作我们都应该通过actions来完成
- 有一个小坑,使用pinia state包装后的变量也需要通过toRaw方法来获取其原始值,否则他会返回一个响应式的对象,导致调用页面卡死
- Editor.vue中我们引入了EditorStore,并且在onMounted创建的时候调用了init方法,将edit的实例放到状态机中
- 后续则直接调用store提供的getValue方法即可。
重构界面添加按钮
WorkSpace.vue
<!-- eslint-disable no-console -->
<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()
}
// 添加清空数据功能,清空value后赋予空字符串
function clearAll() {
value.value = ''
editor.value.clearAll()
}
</script>
<template>
<n-layout position="absolute" style="height:100%">
<n-layout-header style="height:6%">
<div class="grid grid-cols-12 grid-rows-1 h-full">
<div class="col-span-2 p-1">
<img class="w-auto h-auto max-h-full max-w-full " src="../assets/HelloElectron.jpg">
</div>
<div class="col-span-4" />
<div class="col-span-6">
<div class="space-x-2">
<div class="inline-block">
操作:
</div>
<n-button type="primary" @click="update">
查询
</n-button>
<n-button type="warning" @click="clearAll">
清空
</n-button>
<div class="inline-block text-gray-500">
(Ctrl+Enter查询 Ctrl+Backspace删除)
</div>
</div>
</div>
</div>
</n-layout-header>
<n-layout-content style="height:94%">
<!-- 此处的键盘事件还没有实现-->
<div class="absolute w-full h-full grid grid-cols-2" @keydown.ctrl.enter="update">
<!-- 编辑器 -->
<Editor ref="editor" class="h-full" />
<!-- 预览界面 -->
<Preview :data="value" />
</div>
</n-layout-content>
</n-layout>
</template>
- 这里主要是对UI的改动,可以参考下代码,对于electron没有太多的改动
Preview.vue
import { onMounted, ref, watch } from 'vue'
// 使用vite导入图片
import specturm_line from '../../assets/spectrum-line.png'
const props = defineProps(['data'])
const source = ref('')
const loading = ref(true)
onMounted(async () => {
renderImage(props.data)
})
watch(() => props.data, async (newdata) => {
renderImage(newdata)
})
async function renderImage(data) {
loading.value = true
if (data)
source.value = await window.electronAPI.umlPreviewBase64(data)
else
source.value = specturm_line
loading.value = false
}
- 对于预览窗口,如果入参的data为空字符串或者null,我们会直接返回一张缺省图
- 注意由于使用了vite打包工具,我们不可以直接用字符串写图片路径静态资源处理 {#static-asset-handling} | Vite中文网 (vitejs.cn)可以参考这篇文章,vite对静态资源的处理方式
- 同时我们抽象了一个方法来处理调用electronAPI的方法,同时加入了loading机制
Editor.vue
<script setup lang="ts">
import { onMounted, ref, toRaw } from 'vue'
import { editor } from 'monaco-editor'
import EditorStore from '@render/store/EditorStore'
const editorStore = EditorStore()
const el = ref()
let edit = null
onMounted(() => {
edit = editor.create(el.value, { automaticLayout: true, scrollBeyondLastLine: false })
editorStore.init(edit)
})
function getValue() {
return editorStore.getValue()
}
function clearAll() {
editorStore.clearAll()
}
defineExpose({ getValue, clearAll })
</script>
<template>
<div ref="el" />
</template>
- 这里我们需要注意,在创建Monaco Editor的时候,我们修改了两个参数
scrollBeyondLastLine: false:指的是不会出现额外的占位滚动(这里的占位滚动指的是当我们设置了editor的高度后,换行,他会在这个高度的基础上加上换行的高度)automaticLayout: true:这个如果为true可以理解为支持响应式,如果为false的话他就在初始化后不会变动了
解决render调用electronAPI飘红问题
这是在一开始就存在的问题了,由于我们使用了ts对类型的校验会比较严格。这里其实我们可以参考官方给的教程与TypeScript一同使用
// index.ts
import { contextBridge, ipcRenderer } from 'electron'
import type { IpcRenderer } from 'electron'
interface IElectronAPI {
umlPreviewBase64: (data: string) => Promise<string>
}
declare global {
interface Window {
ipcRenderer: IpcRenderer
electronAPI: IElectronAPI
}
}
contextBridge.exposeInMainWorld('electronAPI', <IElectronAPI>{
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),
},
)
- 简单理解就是扩展了Node的globalAPI中的功能
- 可以看到他扩展了window接口并实现了这两个Interface
总结
本篇的内容其实是比较简单的优化和配置。下篇我们将准备使用electron的弹窗实现打开并读取外部文件的功能,同时也会实现通过快捷键刷新Preview等功能。