编辑器项目复盘-数据通信

这是一篇七月份就已经编辑80%的文章,生生拖到十一月份才发,我那万恶的拖延症!

其实一直承认自己技术很差。曾经还自诩自己在百度销售体系中没有太荒废光阴,但是在经过最近两个兼职工作(Electron、RuoYi)后发现,自己还是过于托大了……


书接上文 编辑器项目复盘-初始化

在顺利运行好环境后,为了更好的监视运行结果,可以根据不同环境进行配置

const createWindow = () => {
  if (app.isPackaged) {
    // 打包环境
    mainWindow.loadFile(path.join(__dirname, "./index.html")); // 默认与 main.js 同级的 index.html
    // mainWindow.loadURL('http://localhost:8080') // 远程服务地址
  } else {
    // 开发环境
    mainWindow.loadURL("http://localhost:8080"); // 监视前台运行
    mainWindow.webContents.openDevTools(); // 打开 DevTools
  }
}

【问题】在使用过程中前端会遇到 Uncaught TypeError: window.require is not a function 的错误,该错误是因为渲染进程属于浏览器端,该进程没有集成 Node 的环境导致,需要在 main.js 中 new BrowserWindow 对象中追加设置,如下:

const createWindow = () => {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true // 允许在子窗口中重新启用 Node
      contextIsolation: false // 上下文隔离
    },
  })
}

electron 与 vue 通信

该项目后期由于采用协作开发模式,所以为了避免冲突采用了两种不同的通信方式:

在 Electron 有 ipcMain 和 ipcRenderer 两个模块来实现进程间的通信。

ipcMain 模块是 Electron 提供的一个用于主进程接收消息的模块。
当在渲染进程中使用 ipcRenderer 发送消息时,主进程可以使用 ipcMain 来监听并响应这些消息。

// main.js 执行操作
const { ipcMain } = require("electron")
ipcMain.on('to-main', (event, data) => { // 接收前端数据
  mainWindow.webContents.send('to-vue', '返回给前端的数据')
})

// 前端执行操作
const { ipcRenderer } = window.require("electron")
ipcRenderer.send("to-main", "发送给 main.js 的数据") // 前端发送给 main.js
ipcRenderer.on("to-main", (event, data) => {
  console.log('接收 main.js 返回的数据:' + data)
})

相比之下,我更推荐以下写法

// main.js 使用 ipcMain.handle 监听事件
// 每当渲染器进程通过 file:uploadFile 通道发送 ipcRender.invoke 消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise 返回到最初的 invoke 调用
const { app, ipcMain} = require("electron");
app.whenReady().then(() => {
  ipcMain.handle("file:uploadFile", async (event, data) => {
    return '返回给前端的数据'
  });
})

// 预加载脚本 preload.js 通过预加载脚本暴露 ipcRenderer.invoke
window.electronAPI = {
  uploadFile: (file) => ipcRenderer.invoke('file:uploadFile', file)
}

// 前端 实际调用
const data = await window.electronAPI.handleIpcMain("发送给 main.js 的数据")
console.log('接收 main.js 返回的数据:' + data)

这种写法需要 禁用上下文隔离的情况下使用预加载,链接中还介绍了如何启动上下文隔离进行数据传输的方法,但由于启动后渲染进程将运行在一个孤立的渲染器上下文中,此时将不能直接访问 Node.js 的 API,所以在此不做介绍

上述代码中的预加载脚本 preload.js 放置在 main.js 同级目录下,且需要在 new BrowserWindow 中追加字段 preload: path.join(__dirname, "/preload.js") 调用

参考:
Electron 进程间通信的四种方式

electron 使用ipcRenderer,ipcMain 实现进程之间的通信

通过 electron中ipcRenderer.sendSync与ipcRenderer.invoke比较 得知 send 也可以与 invoke 搭配进行通信,该方式暂未尝试

【问题】默认 main.js 在运行过程中 console.log 中出现的中问会乱码,此时修改 package.jsonelectron:dev 字段方可解决:

"scripts": {
  "electron:dev": "chcp 65001 && nodemon --watch main.js --exec electron ." // 增加 chcp 65001 &&
}

mtti 事件监听与发射

开发的同事是用了 mitt 依赖,这对我来说是陌生的,借此进行学习记录

【疑问】Vuex 和 mtti 的区别?慕课网已有相应的回答:“mitt 是一个轻量级的事件监听和发射器,适用于比较广泛的场景;vuex 更关注的是整体应用的状态(state),而不是以事件为主体或者说是目的的”

参考:

vue3 也有总线传值 (mitt使用闭坑指南)

vuex和mitt

mtti 依照参考使用起来非常简单,首先需要在 src 同级新建 mtti 文件夹,并在内部创建 index.js

// mtti/index.js
import mitt from "mitt"
const mitter = mitt()
export default mitter

之后在兄弟组件中通过 mitter.emit、mitter.on 发送和接受即可

// ChildrenOne.vue
<script setup>
import mitter from "../mitt"; // 第一步:导入 mitt
import { reactive } from "vue"

const state = reactive({ // 第二步:声明一个变量
  count: 0
})

const addCount = () => { // 第三步:定义一个触发传值的事件
  state.count++
  // 第四步:通过导入的mitt实例调用 emit 的方法
  // 参数一:总线事件的名称、参数二:需要传递的参数
  mitter.emit("addCount", state.count)
}
</script>
// ChildrenTwo.vue
<script setup>
import mitter from "../mitt"; // 第一步:导入 mitt
import { ref, onUnmounted } from "vue"

const getAddCount = ref(0)

// 【注意】如果不进行注销,则会多次触发事件
onUnmounted(() => {
  mitter.off('addCount')
})

// 第二步:通过 mitt 的 on 事件来监听 emit 发射过来的参数
// 参数一:总线事件的名称、参数二:回调函数
mitter.on('addCount', val => {
  getAddCount.value = val
  console.log(val)
})
</script>

Vuex 数据状态存储

在项目中我主要使用 Vuex 状态管理对所需数据进行传递

参考:

Vuex 是什么?

Vue3 学习——vue中使用vuex(超详细)

Vuex详解一:彻底弄懂state、mapState、mapGetters、mapMutations、mapActions

vuex中modules的基本用法

首先,vue 全局引用 Vuex

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store' // 全局引用
app.use(store).mount('#app')

// vue
import { useStore } from 'vuex'
const store = useStore()
console.log(store.getters.getUser) // 获取数据
store.commit('updataUser', '前端传递来的数据') // 同步修改数据

之后 Vuex 同样需要在 src 同级新建 store 文件夹,并在内部创建 index.js

其中包含 state、getters、mutations、actions 对象,最主要的代码如下:

import { createStore } from 'vuex'
export default createStore({
  // 1、存储所有全局数据
  state: {
    user: ""
  },

  // 2、需要通过计算获取 state 里的内容获取的数据
  // 只能读取不可修改
  getters: {
   getUser (state) {
      return state.user // 对应前端使用 store.getters.getUser 获取数据状态
    }
  },

  // 3、定义对 state 的各种操作
  // why 不直接实现在mutation里需要写到action里?
  // mtations 不能执行异步操作。aq:从云端获取信息 --> (等待云端反馈)更新到 state 异步操作
  // 因此说:对于异步操作需要放到action里,简单的直接赋值操作可以直接放到mutation里
  mutations: { // 对应前端使用 store.commit('updataUser', '前端传递来的数据')
    updataUser(state, user){
      state.user = user;
    }
  },

  // 3、定义对 state 的各种操作
  // actions 无法直接修改 state,需要在 mutations 里更新
  // mutation 不支持异步,所以需要在 action 里写 api 到 url
  actions: { // 对应前端使用 store.dispatch("login", {}) 设置数据状态
    // 比说 action 定义了更新 state 的操作
    // 但是不可对其直接修改
    // 所有的修改操作必须放到 mutations 里
    login: (context, data) => {
      $.ajax({
        url: '',
        type: 'POST',
        data: {},
        success(resp) {
          context.commit('updataUser', resp.access)
        }
      })
    }
  }
})

mapState、mapGetters、mapMutations、mapActions

使用 this.$store.state 可以方便的获取 state 值,但是在实际使用中如果想引用多个值则需要一一引入很不方便,此时需要使用 mapState 将 state 值映射为实例的 computed 属性

export default {
  name: 'home',
  computed: {...mapState(['user', ……])}
}

我们映射的时候,想给计算属性起个新的名字,不想用原有的属性名,那就可以像下面这样做,用对象的形式去别名

export default {
  name: 'home',
  computed: {...mapState({myUser: 'user', ……})}
}

mapGetters 辅助函数的用法同 mapState

mapMutations 是将所有 mutations 里面的方法映射为实例 methods 里面的方法

export default {
  name: 'home',
  methods: {...mapMutations(['addAge'])}
}

Vuex 允许我们将 store 分割成模块(Module), 而每个模块拥有自己的 state、getters、mutation、action 等

-store
    -index.js
    -modules
        -app.js // 第一个分割模块,拥有自己独立的 state、getters、mutation、action
        -bus.js // 第二个分割模块,拥有自己独立的 state、getters、mutation、action

// index.js
import Vue from 'vue'
import Vuex from 'vuex'

/**
 * 方式一:手动引入模块
 import bus from './modules/bus'
 import app from './modules/app'
 Vue.use(Vuex)
 let store = new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    namespaced: true,
    app,
    bus
  }
});
*/

/**
 * 方式二:动态引入模块
 const path = require('path')
 const files = require.context('./modules', false, /\.js$/)
 let modules = {}
 files.keys().forEach(key => {
    let name = path.basename(key, '.js')
    modules[name] = files(key).default || files(key)
 })
 let store = new Vuex.Store({
    state: {},
    mutations: {},
    actions: {},
    modules
 });
*/

export default store

使用增加相应映射路径即可

// vue
this.$store.commit('app/setUser', {name: '张三'});

// js
import store from '@/store'
let curUser = store.app.user
store.commit("app/setUser", user)

持久化

用于解决页面切换数据丢失的问题,需要安装以下依赖

npm install vuex-persistedstate

然后,在你的 Vuex store 中增加配置

import Vue from 'vue';
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  plugins: [createPersistedState({
    storage: window.sessionStorage, // 或者 localStorage
  })]
});

Vue3 组件通信

最后再简单记录下项目中 vue 组件之间的通信,这里整合了项目中使用到的以及其他方法

参考:

【干货】Vue3 组件通信方式详解

父传子 - props

// 父组件
<template>
    <ChildrenOne :state="state"/>
</template>

<script setup>
import { ref } from 'vue'
import ChildrenOne from './components/ChildrenOne.vue'
const state = ref(0)
</script>

// 子组件
<template>
    <div>{{ state }}</div>
</template>

<script setup>
import { defineProps, ref } from 'vue'
const props = defineProps({
  state: Number
})
const state = ref(props.state)
</script>

父传子 - useAttrs

useAttrsdefineProps 一起使用时,defineProps 优先级会更高,useAttrs 只能获取到 defineProps 没有获取的属性

且该方法是一次获取到子组件标签上的所有自定义属性

补充:vue3中的useAttrs()的使用和注意点

使用 useAttrs 函数可以接收父组件传递的属性和事件,在组件中可以直接使用这些属性和事件,无需在propsemits中声明

和使用 defineProps 接收属性时相比,useAttrs 的优先级要低

// 父组件
<template>
    <ChildrenThree :state="state" @click="parentAttrs"/>
</template>

<script setup>
import { ref } from 'vue'
import ChildrenOne from './components/ChildrenOne.vue'
const state = ref(0)
const parentAttrs = () => {
  console.log('子组件 ChildrenThree 通过 useAttrs 获取父组件标签事件')
}
</script>

// 子组件
<template>
  <div>
    <h2>ChildrenThree</h2>
    <button @click="parentAttrs">触发父组件 useAttrs 的事件</button>
  </div>
</template>

<script setup>
import { useAttrs } from 'vue'
const { state, parentAttrs } = useAttrs()
console.log('useAttrs 获取父组件标签变量:' + state)
</script>

子传父 - emit

// 子组件
<template>
  <button @click="myClick">子传父</button>
</template>

<script setup>
import { ref, defineEmits } from 'vue'
const getMittCount = ref(0)
const emit = defineEmits(['toParentOnt'])
const myClick = () => {
  emit('toParentOnt', getMittCount.value)
}
</script>

// 父组件
<template>
    <ChildrenTwo @toParentOnt="toParentOnt"/>
</template>

<script setup>
import ChildrenTwo from './components/ChildrenTwo.vue'
const toParentOnt = (msg) => {
  console.log('子传父', msg)
}
</script>

子传父 - ref 准确说是父组件主动触发子组件内部数据

// 子组件
<script setup>
import { ref, defineExpose } from 'vue'
const getMittCount = ref(0)
defineExpose({
  count: getMittCount.value,
  countFun () {
    console.log('这是子组件的方法')
  }
})
</script>

// 父组件
<template>
    <button @click="childRef">获取子组件数据和方法</button>
    <ChildrenOne ref="comp"/>
</template>

<script setup>
import ChildrenOne from './components/ChildrenOne.vue'
const comp = ref(null)
const childRef = () => {
  console.log('ChildrenTwo 变量:' + comp.value.count)
  comp.value.countFun()
}
</script>

爷孙双向通信

<script setup>
import { ref, provide } from 'vue'
const state = ref(0)
provide('state', state.value)
</script>

<script setup>
import { inject } from 'vue'
console.log('inject 获取父组件标签变量:' + inject('state'))
</script>

以上就是该项目中涉及的所有通信方法,具体使用根据业务需求灵活搭配

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注