Electron에서 Vue 템플릿으로 애플리케이션 메뉴 만들기
Electron에서 Vue 템플릿으로 애플리케이션 메뉴 만들기
지난 몇 달 동안 저는 Serve 이라는 앱을 개발했습니다. Laravel용 로컬 개발 환경을 쉽게 설정할 수 있는 Electron 앱입니다.
최신 릴리스에서는 응용 프로그램 메뉴를 개편하고 싶었습니다. 하지만 기존 Electron API에 몇 가지 제한 사항이 있어 Vue 구성 요소에서 메뉴를 정의하는 방법을 알아내는 임무에 착수했습니다.
메인 및 렌더러 컨텍스트
Electron 앱에 익숙하지 않은 경우 주요 아키텍처 개념을 빠르게 살펴보겠습니다.
Electron 앱에는 메인 프로세스와 렌더러 프로세스의 두 가지 프로세스가 있습니다. 주요 프로세스는 노드 환경이며 파일 시스템에 액세스할 수 있습니다. 렌더러 프로세스는 브라우저 환경이며 애플리케이션의 UI 처리를 담당합니다.
프로세스는 '프로세스 간 통신'(IPC)을 통해 서로 통신할 수 있습니다. IPC는 본질적으로 프로세스 전반에 걸쳐 작동하는 이벤트 시스템입니다.
Electron의 메뉴 API.
애플리케이션 메뉴를 생성하기 위한 기존 API는 메인 프로세스에서 작동합니다. 여기에는 하위 메뉴 및 메뉴 항목을 나타내는 JSON 개체 템플릿을 구축하는 작업이 포함됩니다.
import { Menu } from 'electron'
Menu.setApplicationMenu(
Menu.buildFromTemplate(
{
label: 'File',
submenu: [
{
label: 'New project',
accelerator: 'CmdOrCtrl+n',
click: () => console.log('New project')
},
{
label: 'Import project',
accelerator: 'CmdOrCtrl+i',
click: () => console.log('Import project')
}
]
}
)
)
위의 예는 두 개의 메뉴 항목이 있는 '파일' 하위 메뉴를 만듭니다.
기존 API의 문제점
기존 API에서 몇 가지 제한 사항을 발견했습니다. 우선 전체 메뉴 구조를 만들면 상당히 지저분한 JSON 트리가 됩니다. 이 JSON 객체는 읽기 어렵고 쉽게 이해하기 어렵습니다.
둘째, Serve의 렌더러 프로세스가 Vue 애플리케이션을 실행하고 있습니다. 하지만 메인 프로세스에서 메뉴가 정의되어 있을 때 위의 예에서 'createProject'와 같은 메소드를 호출할 수 없습니다. 왜냐하면 그것이 Vuex 스토어에서 액션이 될 것이기 때문입니다.
마지막으로 사용자의 위치에 따라 응용 프로그램 메뉴를 업데이트하고 싶었습니다. 사용자가 앱에서 프로젝트로 이동한 경우 '프로젝트 시작'과 같은 프로젝트별 메뉴 항목을 활성화하고 싶습니다. 그러나 사용자가 앱의 프로젝트 내부에 없으면 해당 메뉴 항목을 비활성화하고 싶습니다. 즉, 반응형 메뉴를 찾고 있었습니다.
내가 사용할 수 있는 API 정의하기
이 시점에서 대체 구문을 실험하기로 결정했습니다. 이상적으로는 JSON 개체 대신 Vue 구성 요소를 사용하여 메뉴 구조를 정의하고 싶었습니다. 다음은 내가 사용하려는 구문을 사용하여 위와 동일한 메뉴입니다.
<template>
<Menu>
<Submenu label="File">
<MenuItem
accelerator="CmdOrCtrl+n"
@click="() => console.log('New project')"
>
New project
</MenuItem>
<MenuItem
accelerator="CmdOrCtrl+I"
@click="() => console.log('Import project')"
>
Import project
</MenuItem>
</Submenu>
</Menu>
</template>
This syntax solves all the limitations I found. It's easier to scan and update the menu structure. It's defined in a Vue component, so it's automatically reactive. And since it's a Vue component, it lives in the renderer process and thus has access to the Vue context.
새 API 구현
At this point, I had to try and implement the new syntax I had defined.
The first step was figuring out how to tell the main process that the renderer process defines the menu.
I created a registerMenu
메서드를 호출하고 메인 프로세스에서 호출합니다.
const registerMenu = () => {
ipcMain.on('menu', (__, template = []) => {
Menu.setApplicationMenu(
Menu.buildFromTemplate(templateWithListeners)
)
})
}
It defines a listener on the IPC channel 'menu'. It receives the template for the menu as a parameter in the callback. Lastly, it builds the application menu from the given template.
In the renderer process, I created three Vue components: Menu, Submenu, and MenuItem.
메뉴 구성 요소
The Menu component is responsible for controlling the state of the menu template and sending it over to the main process when it updates.
`
import { Fragment } from 'vue-fragment'
import EventBus from '@/menu/EventBus'
export default {
components: {
Fragment,
},
data() {
return {
template: {},
}
},
mounted() {
EventBus.$on('update-submenu', template => {
this.template = {
...this.template,
[template.id]: template,
}
})
},
watch: {
template: {
immediate: true,
deep: true,
handler() {
window.ipc.send('menu', Object.values(this.template))
},
},
},
render(createElement) {
return createElement(
Fragment,
this.$scopedSlots.default(),
)
},
}
`
The component doesn't render any UI, but it returns the children of the component to execute them in the render method.
The two most interesting things to look at is the 'template' watcher and the EventBus. The EventBus communicates between the Menu component and the Submenu components nested inside it. I didn't want to manually pass all events from the Submenu components up to the Menu components as that would clutter the API.
The EventBus listen for events from the Submenu components. The submenu emits an event with the template for that submenu. In the Menu component, I update the state of the entire template.
The 'template' watcher is responsible for sending the entire template tree to the main process when the template updates.
하위 메뉴 구성 요소
The Submenu component is responsible for controlling all the menu items inside it and sending the state up to the Menu component when it updates.
`
import { v4 as uuid } from 'uuid'
import { Fragment } from 'vue-fragment'
import EventBus from '@/menu/EventBus'
export default {
components: {
Fragment,
},
props: {
label: String,
role: {
type: String,
validator: role =>
[
'appMenu',
'fileMenu',
'editMenu',
'viewMenu',
'windowMenu',
].includes(role),
},
},
data() {
return {
id: uuid(),
submenu: {},
}
},
computed: {
template() {
if (this.role) {
return {
id: this.id,
role: this.role,
}
}
return {
id: this.id,
label: this.label,
submenu: Object.values(this.submenu),
}
},
},
mounted() {
EventBus.$on('update-menuitem', template => {
if (template.parentId !== this.id) {
return
}
this.submenu = {
...this.submenu,
[template.id]: template,
}
})
},
watch: {
template: {
immediate: true,
deep: true,
handler() {
this.$nextTick(() => {
EventBus.$emit('update-submenu', this.template)
})
},
},
},
render(createElement) {
return createElement(
Fragment,
this.$scopedSlots.default(),
)
},
}
`
As with the Menu component, it doesn't render any UI, but the render method still needs to return all its children to execute the code in the MenuItem components.
The component uses the EventBus to communicate with both the Menu component and the MenuItem components. It listens for updates in MenuItem components.
Since the EventBus sends events to all Submenu components, it needs a unique id to control whether the menu item that emits the event is inside this specific submenu. Otherwise, all the submenus would contain all the menu items.
MenuItem 구성 요소
The MenuItem component is responsible for controlling the state of a single menu item object and emit it up the tree when it updates.
`
import { v4 as uuid } from 'uuid'
import EventBus from '@/menu/EventBus'
export default {
props: {
role: {
type: String,
validator: role =>
[
'undo',
'redo',
'cut',
'copy',
'paste',
// ...
].includes(role),
},
type: {
type: String,
default: 'normal',
},
sublabel: String,
toolTip: String,
accelerator: String,
visible: {
type: Boolean,
default: true,
},
enabled: {
type: Boolean,
default: true,
},
checked: {
type: Boolean,
default: false,
},
},
data() {
return {
id: uuid(),
}
},
computed: {
template() {
return {
id: this.id,
role: this.role,
type: this.type,
sublabel: this.sublabel,
toolTip: this.toolTip,
accelerator: this.accelerator,
visible: this.visible,
enabled: this.enabled,
checked: this.checked,
label: return this.$scopedSlots.default()[0].text.trim(),
}
},
},
watch: {
template: {
immediate: true,
handler() {
EventBus.$emit('update-menuitem', {
...JSON.parse(JSON.stringify(this.template)),
click: () => this.$emit('click'),
parentId: this.$parent.template.id,
})
},
},
},
render() {
return null
},
}
`
The MenuItem doesn't render any UI either. Therefore it can simply return null.
The component receives many props that correspond to the options you can give a menu item in the existing api.
An example I used earlier is the enabled
메뉴 항목의 활성화 여부를 제어할 수 있는 소품입니다.
템플릿이 업데이트되면 템플릿과 상위 ID가 있는 모든 하위 메뉴 구성 요소에 이벤트를 내보냅니다.
함께 모아서
모든 개별 조각이 생성되면 모두 함께 결합할 시간입니다. AppMenu 컴포넌트를 만들어 App.vue
에 포함시켰습니다.
<template>
<Menu>
<Submenu label="File">
<MenuItem
accelerator="CmdOrCtrl+n"
@click="() => console.log('New project')"
>
New project
</MenuItem>
<MenuItem
accelerator="CmdOrCtrl+I"
@click="() => console.log('Import project')"
>
Import project
</MenuItem>
</Submenu>
</Menu>
</template>
At this point, I discovered a pretty big issue, though. None of the click event handlers worked.
클릭 핸들러 다루기
After some debugging, I found the issue. IPC communication is event-based, and it's not possible to include a JS function in the event object. But that's what I was doing in the template of a menu item:
{
label: 'New project',
click: () => this.$emit('click'),
// ...
}
The solution was hacky but worked. I omitted the click handler from the menu item objects. In the registerMenu
함수, 모든 메뉴 항목에 클릭 핸들러를 첨부했습니다.
export const registerMenus = win => {
ipcMain.on('menu', (__, template = []) => {
let templateWithListeners = template.map(group => {
return {
...group,
submenu: group.submenu.map(item => {
return {
...item,
click: () =>
win.webContents.send('menu', { id: item.id }),
}
}),
}
})
Menu.setApplicationMenu(Menu.buildFromTemplate(templateWithListeners))
})
}
The click handler sends an event on the menu
IPC 채널. AppMenu에서 메인 이벤트로부터 이벤트를 수신하고 EventBus를 사용하여 다른 이벤트를 전송합니다.
window.ipc.receive('menu', response => {
EventBus.$emit('clicked', response.id)
})
Lastly, in MenuItem, I can listen for the event on the EventBus and emit a click event.
`
EventBus.$on('clicked', id => {
if (id !== this.id) {
return
}
this.click()
})
`
결론
The code examples in this article are simplified a bit. You can view the menu I created for Serve here and view the source code for the menu here.
All in all, I'm happy with the outcome. My menu is now easier to maintain, it's reactive, and it simplified the rest of the app because I can call Vuex actions directly from the menu.
If you are a Laravel developer, you should check out Serve. It automatically manages PHP, Node, databases, and all that kind of stuff for you. If you are not a Laravel developer, keep an eye out because Serve will support other frameworks and languages in the future.
Reference
이 문제에 관하여(Electron에서 Vue 템플릿으로 애플리케이션 메뉴 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://dev.to/bjornlindholmdk/create-application-menus-with-vue-templates-in-electron-3pff
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
import { Menu } from 'electron'
Menu.setApplicationMenu(
Menu.buildFromTemplate(
{
label: 'File',
submenu: [
{
label: 'New project',
accelerator: 'CmdOrCtrl+n',
click: () => console.log('New project')
},
{
label: 'Import project',
accelerator: 'CmdOrCtrl+i',
click: () => console.log('Import project')
}
]
}
)
)
<template>
<Menu>
<Submenu label="File">
<MenuItem
accelerator="CmdOrCtrl+n"
@click="() => console.log('New project')"
>
New project
</MenuItem>
<MenuItem
accelerator="CmdOrCtrl+I"
@click="() => console.log('Import project')"
>
Import project
</MenuItem>
</Submenu>
</Menu>
</template>
This syntax solves all the limitations I found. It's easier to scan and update the menu structure. It's defined in a Vue component, so it's automatically reactive. And since it's a Vue component, it lives in the renderer process and thus has access to the Vue context.
새 API 구현
At this point, I had to try and implement the new syntax I had defined.
The first step was figuring out how to tell the main process that the renderer process defines the menu.
I created a registerMenu
메서드를 호출하고 메인 프로세스에서 호출합니다.
const registerMenu = () => {
ipcMain.on('menu', (__, template = []) => {
Menu.setApplicationMenu(
Menu.buildFromTemplate(templateWithListeners)
)
})
}
It defines a listener on the IPC channel 'menu'. It receives the template for the menu as a parameter in the callback. Lastly, it builds the application menu from the given template.
In the renderer process, I created three Vue components: Menu, Submenu, and MenuItem.
메뉴 구성 요소
The Menu component is responsible for controlling the state of the menu template and sending it over to the main process when it updates.
`
import { Fragment } from 'vue-fragment'
import EventBus from '@/menu/EventBus'
export default {
components: {
Fragment,
},
data() {
return {
template: {},
}
},
mounted() {
EventBus.$on('update-submenu', template => {
this.template = {
...this.template,
[template.id]: template,
}
})
},
watch: {
template: {
immediate: true,
deep: true,
handler() {
window.ipc.send('menu', Object.values(this.template))
},
},
},
render(createElement) {
return createElement(
Fragment,
this.$scopedSlots.default(),
)
},
}
`
The component doesn't render any UI, but it returns the children of the component to execute them in the render method.
The two most interesting things to look at is the 'template' watcher and the EventBus. The EventBus communicates between the Menu component and the Submenu components nested inside it. I didn't want to manually pass all events from the Submenu components up to the Menu components as that would clutter the API.
The EventBus listen for events from the Submenu components. The submenu emits an event with the template for that submenu. In the Menu component, I update the state of the entire template.
The 'template' watcher is responsible for sending the entire template tree to the main process when the template updates.
하위 메뉴 구성 요소
The Submenu component is responsible for controlling all the menu items inside it and sending the state up to the Menu component when it updates.
`
import { v4 as uuid } from 'uuid'
import { Fragment } from 'vue-fragment'
import EventBus from '@/menu/EventBus'
export default {
components: {
Fragment,
},
props: {
label: String,
role: {
type: String,
validator: role =>
[
'appMenu',
'fileMenu',
'editMenu',
'viewMenu',
'windowMenu',
].includes(role),
},
},
data() {
return {
id: uuid(),
submenu: {},
}
},
computed: {
template() {
if (this.role) {
return {
id: this.id,
role: this.role,
}
}
return {
id: this.id,
label: this.label,
submenu: Object.values(this.submenu),
}
},
},
mounted() {
EventBus.$on('update-menuitem', template => {
if (template.parentId !== this.id) {
return
}
this.submenu = {
...this.submenu,
[template.id]: template,
}
})
},
watch: {
template: {
immediate: true,
deep: true,
handler() {
this.$nextTick(() => {
EventBus.$emit('update-submenu', this.template)
})
},
},
},
render(createElement) {
return createElement(
Fragment,
this.$scopedSlots.default(),
)
},
}
`
As with the Menu component, it doesn't render any UI, but the render method still needs to return all its children to execute the code in the MenuItem components.
The component uses the EventBus to communicate with both the Menu component and the MenuItem components. It listens for updates in MenuItem components.
Since the EventBus sends events to all Submenu components, it needs a unique id to control whether the menu item that emits the event is inside this specific submenu. Otherwise, all the submenus would contain all the menu items.
MenuItem 구성 요소
The MenuItem component is responsible for controlling the state of a single menu item object and emit it up the tree when it updates.
`
import { v4 as uuid } from 'uuid'
import EventBus from '@/menu/EventBus'
export default {
props: {
role: {
type: String,
validator: role =>
[
'undo',
'redo',
'cut',
'copy',
'paste',
// ...
].includes(role),
},
type: {
type: String,
default: 'normal',
},
sublabel: String,
toolTip: String,
accelerator: String,
visible: {
type: Boolean,
default: true,
},
enabled: {
type: Boolean,
default: true,
},
checked: {
type: Boolean,
default: false,
},
},
data() {
return {
id: uuid(),
}
},
computed: {
template() {
return {
id: this.id,
role: this.role,
type: this.type,
sublabel: this.sublabel,
toolTip: this.toolTip,
accelerator: this.accelerator,
visible: this.visible,
enabled: this.enabled,
checked: this.checked,
label: return this.$scopedSlots.default()[0].text.trim(),
}
},
},
watch: {
template: {
immediate: true,
handler() {
EventBus.$emit('update-menuitem', {
...JSON.parse(JSON.stringify(this.template)),
click: () => this.$emit('click'),
parentId: this.$parent.template.id,
})
},
},
},
render() {
return null
},
}
`
The MenuItem doesn't render any UI either. Therefore it can simply return null.
The component receives many props that correspond to the options you can give a menu item in the existing api.
An example I used earlier is the enabled
메뉴 항목의 활성화 여부를 제어할 수 있는 소품입니다.
템플릿이 업데이트되면 템플릿과 상위 ID가 있는 모든 하위 메뉴 구성 요소에 이벤트를 내보냅니다.
함께 모아서
모든 개별 조각이 생성되면 모두 함께 결합할 시간입니다. AppMenu 컴포넌트를 만들어 App.vue
에 포함시켰습니다.
<template>
<Menu>
<Submenu label="File">
<MenuItem
accelerator="CmdOrCtrl+n"
@click="() => console.log('New project')"
>
New project
</MenuItem>
<MenuItem
accelerator="CmdOrCtrl+I"
@click="() => console.log('Import project')"
>
Import project
</MenuItem>
</Submenu>
</Menu>
</template>
At this point, I discovered a pretty big issue, though. None of the click event handlers worked.
클릭 핸들러 다루기
After some debugging, I found the issue. IPC communication is event-based, and it's not possible to include a JS function in the event object. But that's what I was doing in the template of a menu item:
{
label: 'New project',
click: () => this.$emit('click'),
// ...
}
The solution was hacky but worked. I omitted the click handler from the menu item objects. In the registerMenu
함수, 모든 메뉴 항목에 클릭 핸들러를 첨부했습니다.
export const registerMenus = win => {
ipcMain.on('menu', (__, template = []) => {
let templateWithListeners = template.map(group => {
return {
...group,
submenu: group.submenu.map(item => {
return {
...item,
click: () =>
win.webContents.send('menu', { id: item.id }),
}
}),
}
})
Menu.setApplicationMenu(Menu.buildFromTemplate(templateWithListeners))
})
}
The click handler sends an event on the menu
IPC 채널. AppMenu에서 메인 이벤트로부터 이벤트를 수신하고 EventBus를 사용하여 다른 이벤트를 전송합니다.
window.ipc.receive('menu', response => {
EventBus.$emit('clicked', response.id)
})
Lastly, in MenuItem, I can listen for the event on the EventBus and emit a click event.
`
EventBus.$on('clicked', id => {
if (id !== this.id) {
return
}
this.click()
})
`
결론
The code examples in this article are simplified a bit. You can view the menu I created for Serve here and view the source code for the menu here.
All in all, I'm happy with the outcome. My menu is now easier to maintain, it's reactive, and it simplified the rest of the app because I can call Vuex actions directly from the menu.
If you are a Laravel developer, you should check out Serve. It automatically manages PHP, Node, databases, and all that kind of stuff for you. If you are not a Laravel developer, keep an eye out because Serve will support other frameworks and languages in the future.
Reference
이 문제에 관하여(Electron에서 Vue 템플릿으로 애플리케이션 메뉴 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/bjornlindholmdk/create-application-menus-with-vue-templates-in-electron-3pff텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)