The final result

Project address :Github

1. Start the project

1.1 Create a VUE-TS project

yarn create vite my-vue-app --template vue-ts
Copy the code

1.2 Installing the Electron and Packing Tool

// Install as a development dependency, no need after packaging
yarn add electron electron-builder -D
Copy the code

Install some of the widgets you need for development, which will be explained later

  • Kill-port: indicates the clearing port
  • Cross-env: Sets environment variables
  • NPM -run-all Executes script scripts in sequence
  • Concurrently execute script scripts
  • Tsc-watch: Compiles ts files and recompiles them if the files are modified
  • Wait-on Waits for the file or port to change and then executes the script script
yarn add kill-port  concurrently cross-env npm-run-all tsc-watch wait-on -D
Copy the code

1.3 Configuration before Startup

Modified project complete directory

1. Create an Enectron import file

A new electron/main. Ts

import { app, BrowserWindow} from "electron";
// Create window methods
import { createWindow } from "./utils/createWindow";

app.on("ready".() = > {
    createWindow(); // Create window
    Usually on macOS, when you click on the app icon in the dock, if there are no other open Windows, the program will create a new one.
    app.on("activate".() = > BrowserWindow.getAllWindows().length === 0 && createWindow());
});

Exit the program when all Windows are closed except macOS. Programs in the Dock do not exit when all macOS Windows are closed
app.on("window-all-closed".() = >{ process.platform ! = ="darwin" && app.quit();
});
Copy the code

A new electron/utils/createWindow. Ts

import { BrowserWindow } from "electron";
import * as path from "path";
/ * * * packages in json, the script through the cross - env NODE_ENV = production set the environment variable * 'production' | 'development' * /
const NODE_ENV = process.env.NODE_ENV;
/** Create window method */
function createWindow() {
// Generate window instances
    const Window = new BrowserWindow({
        minWidth: 1120.minHeight: 645.width: 1120.// * Specifies the default window size for starting the app
        height: 645.// * Specifies the default window size for starting the app
        frame: false.// * App border (including close, full screen, minimize button navigation bar) @false: hide
        transparent: true.// * App background is transparent
        hasShadow: false.// * App border shadow
        show: false.// Hide the window when it starts until the renderer finishes loading with ready-to-show listening events to prevent flickering during loading
        resizable: false.// Forbid manual window sizing
        webPreferences: {
            // Load the script
            preload: path.join(__dirname, ".."."preload")}});// Hide the window when it starts until the renderer finishes loading with ready-to-show listening events to prevent flickering during loading
    Window.once("ready-to-show".() = > {
        Window.show(); // Display window
    });
    
    // * The main window loads external links
    // In the development environment, load the vue project address of the vite startup
    if (NODE_ENV === "development") Window.loadURL("http://localhost:3920/"); 
}
// Export the module
export { createWindow };
Copy the code

Create the electron/preload. Ts file (empty file will do)

2. Write the TS declaration file

  • Move SRC /env.d.ts to types/env.d.ts.
  • Create new Types /electron.d.ts with the following contents
/ /? Extending the Window object
interface Window {
    /** * Electron ipcRenderer * will mount the process communication method to the window object, so add this interface to prevent error */
    ipc: import("electron").IpcRenderer;
}
Copy the code

3. Modify the TS configuration file

1. Edit tsconfig. Json

Vue compiles ts configuration files

{
    "compilerOptions": {
        / /...
        "baseUrl": "."./ / new
        "paths": { // Add a path alias
        "@ / *": ["src/*"]."~ / *": ["types/*"]}},// Modify to identify the *.d.ts file under the previously configured types folder
    "include": ["src/**/*.ts"."src/**/*.d.ts"."src/**/*.tsx"."src/**/*.vue"."types/**/*.d.ts"]}Copy the code
2. New tsconfig. E.j son

Electron compiles the TS configuration file

{
    / /? The tsconfig.json configuration item with the same name will be overwritten by this configuration file
    "extends": "./tsconfig.json"."compilerOptions": {
        "outDir": "output/build".// * Path to the generated file
        "noEmit": false.// * No output file is generated
        "module": "commonjs".// * Specify the module to use: 'commonJS ',' AMD ', 'system', 'umd' or 'es2015'
        "baseUrl": "."."sourceMap": false.// * Whether to generate the corresponding '.map' file
    },
    "include": [
    "electron"
    ] // * Specify the files to include (here specify all files in the electron folder)
}
Copy the code

4. Modify the vite. Config. Ts

export default defineConfig({
    plugins: [vue()],
    server: {
        strictPort: true.// * Fixed port (abort if port is occupied)
        host: true./ / 0.0.0.0
        port: 3920 // Specify the boot port}});Copy the code

5. Modify package.json to start the project

  • The script that
  1. Terminal operationyarn start / npm startStart the project
  2. start:
  • Clear port 3920(kill-port 3920)then(&)Parallel execution(concurrently -k)The commandvitewait-on tcp:3920 && npm-run-all watch
  • vite: Starts port 3920 specified by the Vite development server
  • wait-on tcp:3920 && npm-run-all watch: Listens on port 3920npm-run-all watch
  1. watch:
  • usetsc-watchCompile ts file into JS file; Through the configuration filetsconfig.e.jsonSpecify compile onlyelectronFolder under the file, and output the JS file tooutput/buildfolder

    When the TS file is compiled(--onSuccess), execute the commandnpm-run-all start:ect
  • Once started, TSC-Watch will continue to listen on the electron folder and recompile and run every time the file changes(--onSuccess)After the command
  1. start:ect:
  • Setting environment Variablescross-env NODE_ENV=development, electron main process startup can get this variable (see electron/utils/createWindow. Ts), and on the basis of judgment is to load external links or load file after packaging
  • Launch electron app and specify the entry file./output/build/main.js
{
  "name": "test"."version": "0.0.0"."license": "MIT"."scripts": {+"start": "kill-port 3920 && concurrently -k \"vite\" \"wait-on tcp:3920 && npm-run-all watch\"",
  + "watch": "tsc-watch --noClear -p tsconfig.e.json --onSuccess \"npm-run-all start:ect\"",
  + "start:ect": "cross-env NODE_ENV=development electron ./output/build/main.js"
  },
  "dependencies": {... },"devDependencies": {... }}Copy the code

Then, execute YARN start/NPM start to start the project, an unframed window with a transparent background. All code up to this step is in the history/ Item1 folder


2. Process communication

2.1 Basic Configuration

Due to the so-called ‘context isolation’ implemented by the electron document, the remote module is not used and all communication methods are relayed through preload.ts.

todo: For easier communication in vite+ Vue projects, use @vueuse/electron, a package that uses process communication via componsitionAPI, like import {useIpcRenderer} from ‘@vueuse/electron’ const ipcRenderer = useIpcRenderer() ipcRenderer.on(‘custom-event’, (event, … args) => {console.log(args)})

Edit the electron/preload. Ts

import { contextBridge, ipcRenderer } from "electron";
/** * The communication method is mounted to the window object * used in the renderer process: *  */
contextBridge.exposeInMainWorld("ipc", {
    send: (channel: string. args:any[]) = >ipcRenderer.send(channel, ... args),invoke: (channel: string. args:any[]) :Promise<any> => ipcRenderer.invoke(channel, ... args),on: (channel: string, listener: (event: Electron.IpcRendererEvent, ... args:any[]) = >void) = >{ipcRenderer.on(channel, listener); }});Copy the code
  • ContextBridge. ExposeInMainWorld: an objectipc:{send:xxx,on:xxx}Mount to the Window object.
  • Define a relay function ipc.send, called by the renderer processWindow.ipc.send (' event name ', parameter), the ipC. send method calls the ipcrenderer. send method and passes the parameters
  • Because it was added to the declaration fileipc: import("electron").IpcRenderer;, so it also has code hints

It can also be written as a function for a communication method

 * ipcMain.on('navBar', (event, val) => { * if (val == 'close') { ...... }} * * /

const sendNavBar = (channel:string,params:string) = > ipcRenderer.send(channel,params)
contextBridge.exposeInMainWorld("ipc", {
    sendNavBar:sendNavBar
});
Copy the code

2.2 Customizing control buttons

The three control buttons (maximize, minimize, and close) need to be customized due to the use of an unbounded window

@vue/ compiler-sFC: can eliminate console warnings when installing ANTD, unplugin-vue-Components: introduces component library plugins on demand, also supports elementPlus

yarn add ant-design-vue@next @vue/compiler-sfc unplugin-vue-components -D
Copy the code

Modify the vite. Config. Ts

./ * * click to import component library for = > https://zhuanlan.zhihu.com/p/423194571 * /
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
    plugins: [
        vue(),
        Components({
            resolvers: [AntDesignVueResolver()]
        })
    ],
......
});
Copy the code

Add three buttons to the page and click on any style you want

A new electron/utils/navbar. Ts

import { BrowserWindow, ipcMain } from "electron";
/ * * *@description Process communication Render process click top close, minimize... Button, {val} parameter, * main process through BrowserWindow fromWebContents (event. Sender) to the active window BrowserWindow instance, again through the minimize () * instance methods, such as the operation window@param {Electron.WebContents} event.sender
* @param val {'mini'|'big'|'close'}
* @example* window.ipc. Send ('navBar', val) // Render process * */
export function onNavbar() {
    ipcMain.on('navBar'.(event, val) = > {
        / * * * through BrowserWindow fromWebContents method to get the window instance * event. The sender is sending a message WebContents instance * /
        const window: Electron.BrowserWindow | null = BrowserWindow.fromWebContents(event.sender)
        if (val == 'mini') { window? .minimize() }// Minimize the window
        if (val == 'close') { window? .close() }// Close the window
        if (val == 'big') { // Full screen/Cancel full screen
            Createwindow.ts resizable (false) is disabled
            window? .setResizable(true)
            window? .isMaximized() ?window? .unmaximize() :window? .maximize();window? .setResizable(false)}}}Copy the code

Modify the electron/main. Ts

.import { onNavbar } from "./utils/navbar"; / / new
onNavbar(); / / new

app.on("ready".() = >{... });Copy the code

Add click events to buttons that send messages to the main process

<script setup lang="ts">
    const navBar = (val: string) = > {
        window.ipc.send("navBar", val);
    };
</script>

<template>
    <div style="-webkit-app-region: no-drag">
        <a-button @click="navBar('mini')" type="dashed" danger>To minimize the</a-button>
        <a-button @click="navBar('big')" type="dashed" danger>maximize</a-button>
        <a-button @click="navBar('close')" type="dashed" danger>Shut down</a-button>
    </div>
</template>
Copy the code

All code up to this point is in the history/ Item2 folder

2.3 Main Process Menu

2.3.1 Creating a menu

We wrap a predefined object into an AppMenu object using the men. buildFromTemplate method, and then at app startup (app.on(” Ready “,…) Set the Menu with the menu. setApplicationMenu method

A new electron/utils/menu. Ts

import { Menu } from "electron";
export function createAppMenu() {
    const AppMenu: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
        // On the MAC, the label of the first custom menuItem is overwritten by the application name
        // This label will be overwritten by 'build.productName =' background management 'in package.json packaging configuration
        { id: "1".label: "App".submenu: [{ id: "1-1".label: "Test"}]}, {id: "2".label: "Development".submenu: [{id: "2-1".label: "Test" },
                {id: "2-2".label: "Check elements".click(m, window, e){window? .webContents.openDevTools()}} ] } ];/** Create menu */
    const appMenu = Menu.buildFromTemplate(AppMenu);
    return appMenu;
}
Copy the code

Edit electron/main. Ts

import { createAppMenu } from "./utils/menu"; / / new. app.on("ready".() = > {
    // Set the app menu
    Menu.setApplicationMenu(createAppMenu());/ / new. });Copy the code

The App menu on MacOS is now set up

2.4 Render Process Menu (Windows Menu)

After the operation of 2.3.1, the menu on MacOS can be displayed. However, due to the use of a borderless window, the menu on Win is hidden along with the border, so you need to obtain and display it by yourself. The code of the menu bar similar to VScode is relatively stupid

  1. The main process can passMenu.getApplicationMenu()Get app menu
  2. When the APP starts, the request menu is communicated by the process, and the main process sends the recursively generated menu object to the renderer process
  3. The rendering process generates dropdown menus recursively through components such as a-Dropdown
  4. When the renderer clicks on a menu item, it sends the menu ID and the main process passesMenu.getApplicationMenu()? .getMenuItemByIdMethod to retrieve the menu object corresponding to the ID and call the click method

Edit electron/utils/menu.ts to add

import { Menu, ipcMain, BrowserWindow } from "electron";

interface menuObj {
    lable: string;
    id: string;
    type: string;
    child: menuObj[] | null;
}

export function onAppMenu() {
    // When the renderer requests a menu, it returns the menu if it is Windows, or null if it is MacOS
    ipcMain.handle("getAppMenu", (): menuObj[] | null= > process.platform == "darwin" ? null : getmenu());
    ipcMain.on("MenuClick".(event, menuItemId: string) = > {
        constmenuItem = Menu.getApplicationMenu()? .getMenuItemById(menuItemId); menuItem? .click(); }); }/ * * *@description Recursively generates an array of menus that are passed to the renderer to generate the upper-left menu bar * on Windows@returns {menuObj} menuArr:{ lable: string, id: string, type: string, child? : menuObj[] } */
function getmenu() {
    function menu(ims: Electron.MenuItem[]) {
        let menuArr: menuObj[] = [];
        ims.map((im) = > {
            let menuObj: menuObj = {
            lable: im.label,
            id: im.id,
            type: im.type,
            child: im.type == "submenu" && im.submenu ? menu(im.submenu.items) : null
            };
            menuArr.push(menuObj);
        });
        return menuArr;
    }
    const ims = Menu.getApplicationMenu() as Electron.Menu;
    return menu(ims.items);
}
Copy the code

Edit electron/main ts,

import { onAppMenu, createAppMenu } from "./utils/menu"; / / modify
onAppMenu(); / / new.Copy the code

The renderer takes the menu object and uses components such as a-Dropdown to generate the corresponding menu code to viewhistory/item2I won’t stick it here

2.5 Render process right-click menu

The main process can set the global right-click Menu for the app with menu.buildFromTemplate (contextmenu.popup ()), but only the system default style is ugly, so writing the Menu in the renderer has the advantage of setting different Menu items for different regions

New SRC/components/contentMenu. Vue

Antd’s A-Dropdown right-clicks the menu component, passes in the component to wrap through
, and calls different methods with the passed key when the menu item is clicked. Methods can be executed only in the render function, or main process methods can be called through process communication. You can write multiple contentMenu components that wrap different pages to display multiple right-click menus

<script lang="ts" setup>
    interface methods {
        [propName: string] :Function;
    }
    const hiddenSidebar = () = > console.log("hiddenSidebarfn");
    const openDevTool = (key: string) = > window.ipc.send("contentMenu", key);
    const fullScreen = (key: string) = > window.ipc.send("contentMenu", key);
    const metnods: methods = {
        hiddenSidebar,
        openDevTool,
        fullScreen
    };
    const handleClick = ({ key }: { key: string }) = > metnods[key] && metnods[key](key);
</script>

<template>
    <a-dropdown :trigger="['contextmenu']">
        <slot />
        <template #overlay>
            <a-menu @click="handleClick">
                <a-menu-item key="openDevTool">Check the element</a-menu-item>
                <a-menu-item key="hiddenSidebar">Hide/show the sidebar</a-menu-item>
                <a-menu-item key="fullScreen">Enter/exit full screen</a-menu-item>
            </a-menu>
        </template>
    </a-dropdown>
</template>
Copy the code

Edit the SRC/App. Vue

<script setup lang="ts">
    import ContentMenu from "./components/contentMenu.vue"; .</script>
<template>
    <ContentMenu>// Wrap the component to display the right-click menu with ContentMenu......</ContentMenu>
</template>
Copy the code

New electron/utils/contextMenu. Ts

import { ipcMain, BrowserWindow } from "electron";
interface methods {
    [propName: string] :Function;
}

/ * * *@desc: Render process click custom menu item, call main process method through process communication, realize render process right-click menu *@param {string } For example, 'fullScreen' is passed as the key name of methods, */
export function onContextMenu() {
    ipcMain.on("contentMenu".(event, key: string) = > {
        methods[key] && methods[key](event);
    });
}

// Open the console
const openDevTool = (e: Electron.IpcMainEvent) = > e.sender.openDevTools();

// Full screen/Push full screen
/** * Due to a bug in electron, the borderless transparent window isSetFullScreen on Win always returns false *, so on Windows, full screen is determined by mounting variables on the current window instance
type route = extendWindow & Electron.BrowserWindow;
interface extendWindow {
    isMax: boolean | null | undefined;
}
const fullScreen = async (e: Electron.IpcMainEvent) => {
    const window = BrowserWindow.fromWebContents(e.sender) as route; // Get the window instance
    const isMac = process.platform == "darwin"; // Check whether it is a MAC
    if (isMac) {
        // MAC enters/exits the simple full-screen mode
        const isSimpleFS = window.isSimpleFullScreen();
        window.setSimpleFullScreen(! isSimpleFS); }else {
        // win To enter/exit the full-screen mode
        window.isMax ? window.setFullScreen(false) : window.setFullScreen(true);
        window.isMax = !window.isMax; }};const methods: methods = {
    openDevTool,
    fullScreen
};
Copy the code

Modify the electron/main. Ts

.import { onContextMenu } from "./utils/contextMenu"; / / new
onContextMenu();/ / new.Copy the code

All code up to this point is in the history/ Item3 folder

3. Package projects

Package using the electron builder to install dependencies first

yarn add electron-builder -D
Copy the code

Modify the vite. Config. ts package to use relative paths

export default defineConfig({
    base: ". /".// * Pack relative paths, otherwise the CSS,js files will not be found when the electron loads index.html. });Copy the code

Edit the package.json description

  1. Yarn Build Starts to execute the packaging script
  2. Build :vue Package vue files in the output/dist directory
  3. Build: TSC compiles the TS electron file to js with the directory output/build
  4. Build: all packaged two platforms Can also run the build respectively: vue, build: TSC, build: MAC to package the specified platform
  5. "main": "output/build/main.js",: is specified in tsconfig.e.json, which compiles the TS file and prints it to the build directory electronic-Builder to find the package entry
  6. Preview: Use Electron to preview the packed Vue file
{..."main": "output/build/main.js"."scripts": {..."build": "npm-run-all build:vue build:tsc build:all"."build:vue": "vue-tsc --noEmit && vite build"."build:tsc": "tsc -p tsconfig.e.json"."build:all": "electron-builder --mac --windows"."build:mac": "electron-builder --mac"."build:win": "electron-builder --windows"."preview": "cross-env NODE_ENV=production electron ./output/build/main.js"
    },

    "dependencies": {
        "vue": "^ 3.2.16"
    },
    "devDependencies": {... },"build": {
        "appId": "com.lx000-website.electron-vue3-tpm-test"."productName": "The test app".// The packaged app name
        "copyright": "Copyright © 2021 < your name >"."directories": {
            "output": "output/app" // Package DMG,exe and other file output directory
        },
        "win": {
            "icon": "public/cccs.icns"./ / app icon
            "target": [ // win package target
                "nsis"./ / exe file
                "zip".// Compressed package, after decompression can be directly run
                "7z"]},// Specify which files to punch into the final installation package
        "files": [
            // Exclude the node_modules folder
            ! "" node_modules"."output/dist/**/*"."output/build/**/*"]."mac": {
            "category": "public.app-category.utilities.test"."icon": "public/cccs.icns"
        },
        "nsis": {
            "oneClick": false."allowToChangeInstallationDirectory": true}}}Copy the code