HelloElectron——引入NaiveUI、Pinia简单美化(四)

2,890 阅读5分钟

今天的内容与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等功能。