这是一篇七月份就已经编辑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 使用ipcRenderer,ipcMain 实现进程之间的通信
通过 electron中ipcRenderer.sendSync与ipcRenderer.invoke比较 得知
send
也可以与invoke
搭配进行通信,该方式暂未尝试
【问题】默认 main.js 在运行过程中 console.log 中出现的中问会乱码,此时修改 package.json
中 electron:dev
字段方可解决:
"scripts": {
"electron:dev": "chcp 65001 && nodemon --watch main.js --exec electron ." // 增加 chcp 65001 &&
}
mtti 事件监听与发射
开发的同事是用了 mitt 依赖,这对我来说是陌生的,借此进行学习记录
【疑问】Vuex 和 mtti 的区别?慕课网已有相应的回答:“mitt 是一个轻量级的事件监听和发射器,适用于比较广泛的场景;vuex 更关注的是整体应用的状态(state),而不是以事件为主体或者说是目的的”
参考:
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详解一:彻底弄懂state、mapState、mapGetters、mapMutations、mapActions
首先,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 组件之间的通信,这里整合了项目中使用到的以及其他方法
参考:
父传子 - 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
当 useAttrs
和 defineProps
一起使用时,defineProps
优先级会更高,useAttrs
只能获取到 defineProps
没有获取的属性
且该方法是一次获取到子组件标签上的所有自定义属性
使用
useAttrs
函数可以接收父组件传递的属性和事件,在组件中可以直接使用这些属性和事件,无需在props
和emits
中声明和使用
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>
以上就是该项目中涉及的所有通信方法,具体使用根据业务需求灵活搭配