前言

最近剛好有機會要寫有圖形化介面的程式。想來想去感覺可以寫寫看 Electron!於是乎本篇就這樣誕生了。

主要內容

建立 Vue 專案

1yarn create vite test-001 --template vue
2cd test-001
3yarn
4yarn dev

加入路徑別名

把專案中的 src 加上別名 @

 1mv vite.config.js vite.config.cjs.bk
 2
 3cat << EOF > vite.config.cjs
 4import { defineConfig } from 'vite'
 5import Vue from '@vitejs/plugin-vue'
 6import path from 'path'
 7
 8// https://vitejs.dev/config/
 9export default defineConfig({
10	plugins: [
11		Vue(),
12	],
13	resolve: {
14		alias: {
15			'@': path.resolve(__dirname, './src'),
16		},
17	},
18})
19EOF

安裝及設定 Tailwindcss

 1yarn add -D tailwindcss postcss autoprefixer
 2cat << EOF > postcss.config.cjs
 3module.exports = {
 4  plugins: {
 5    tailwindcss: {},
 6    autoprefixer: {},
 7  }
 8}
 9EOF
10
11cat << EOF > tailwind.config.cjs
12module.exports = {
13  content: [
14    "./index.html",
15    "./src/**/*.{vue,js,ts,jsx,tsx}",
16  ],
17  theme: {
18    extend: {},
19  },
20  plugins: [],
21}
22EOF
23
24mkdir -p src/styles
25
26cat << EOF > src/styles/base.css
27@layer base {
28    html, body, #app {
29        @apply h-full w-full;
30    }
31    body, #app {
32        @apply bg-[#333333];
33        @apply font-light;
34    }
35}
36EOF
37
38cat << EOF > src/styles/components.css
39@layer components {
40    * {
41        @apply outline outline-1 outline-red-500;
42    }
43}
44EOF
45
46cat <<EOF > src/styles/index.css
47@import "tailwindcss/base";
48@import "base.css";
49
50@import "tailwindcss/components";
51@import "components.css";
52
53@import "tailwindcss/utilities";
54EOF
55
56sed -i "3iimport '@/styles/index.css'" src/main.js
57
58sed -i '/<\/template>/i \ \ \<h1\ class=\"text-red-200\"\>Hello\ world\<\/h1\>' src/App.vue

加入 Unplugin Icons

 1
 2yarn add --dev  unplugin-vue-components unplugin-icons @iconify/json
 3
 4mkdir -p src/components
 5sed -i "4iimport Components from 'unplugin-vue-components/vite'" vite.config.cjs
 6sed -i "4iimport IconsResolver from 'unplugin-icons/resolver'" vite.config.cjs
 7sed -i "4iimport Icons from 'unplugin-icons/vite'" vite.config.cjs
 8
 9sed -i '/plugins:/a \ \ \ \ \ Components({\
10     dirs: ["src/components"],\
11     resolvers: [\
12	  IconsResolver({\
13	      prefix: false,\
14	      enabledCollections: ["mdi"],\
15	  })\
16      ],\
17    }),\
18    Icons(),' vite.config.cjs
19    
20sed -i '/<\/template>/i \ \ \<mdi-account-heart\ \/\>' src/App.vue

加入 Vue Router 和 vite-plugin-pages

 1yarn add vue-router@4
 2yarn add --dev vite-plugin-pages
 3
 4mkdir -p src/router
 5
 6cat << EOF > src/router/index.js
 7import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
 8import routes from "~pages";
 9
10const router = createRouter({
11  // refer to: https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/commonIssues.html
12  history: import.meta.env.PROD ? createWebHashHistory() : createWebHistory(import.meta.env.BASE_URL),
13  routes,
14});
15
16export default router;
17EOF
18
19mkdir -p src/pages
20sed -i "4iimport Pages from 'vite-plugin-pages'" vite.config.cjs
21sed -i '/plugins:/a \ \ \ \ \ Pages({\
22		dirs: [\
23			{ dir: "src/pages", baseRoute: "" },\
24		],\
25	}),' vite.config.cjs
26
27sed -i "3iimport Router from './router'" src/main.js
28sed -i 's/createApp(App)/createApp(App).use(Router)/' src/main.js
29
30cat << EOF > src/App.vue
31<template>
32  <div class="w-full h-full p-1">
33    <RouterView></RouterView>
34  </div>
35</template>
36EOF
37
38cat << EOF > src/pages/index.vue
39<template>
40    <div class="flex w-full h-full rounded overflow-hidden">
41        <div :class="[
42            'transition-all duration-500 ease-out',
43            'h-full w-0 lg:block lg:w-2/12 flex-shrink-0',
44            'max-w-xs'
45        ]"></div>
46        <div class="flex h-full w-full">
47          <img src="@/assets/vue.svg" class="w-1/4" alt="Vue logo" />
48          <div class="flex flex-col">
49              <HelloWorld />
50          </div>
51        </div>
52    </div>
53</template>
54EOF

安裝及配置 Electron

 1yarn add --dev electron concurrently wait-on cross-env electron-builder
 2
 3mkdir -p electron
 4
 5cat << EOF > electron/main.cjs
 6const { app, BrowserWindow } = require('electron')
 7const path = require('path')
 8
 9const NODE_ENV = process.env.NODE_ENV
10
11function createWindow() {
12  const mainWindow = new BrowserWindow({
13    width: 800,
14    height: 600,
15    show: false,
16    autoHideMenuBar: true,
17    webPreferences: {
18      preload: path.join(__dirname, 'preload.cjs')
19    }
20  })
21
22  mainWindow.once('ready-to-show', () => {
23    mainWindow.show()
24  })
25
26  mainWindow.loadURL(
27      NODE_ENV === 'development'
28      ? 'http://localhost:5173'
29      : \`file://\${path.join(__dirname, '../dist/index.html')}\`
30  );
31
32  if (NODE_ENV === "development") {
33    mainWindow.webContents.openDevTools()
34  }
35}
36
37app.whenReady().then(() => {
38  createWindow()
39
40  app.on('activate', function () {
41    if (BrowserWindow.getAllWindows().length === 0) createWindow()
42  })
43})
44
45app.on('window-all-closed', function () {
46  if (process.platform !== 'darwin') app.quit()
47})
48EOF
49
50cat << EOF > electron/preload.cjs
51window.addEventListener('DOMContentLoaded', () => {
52  const replaceText = (selector, text) => {
53    const element = document.getElementById(selector)
54    if (element) element.innerText = text
55  }
56
57  for (const dependency of ['chrome', 'node', 'electron']) {
58    replaceText(\`\${dependency}-version\`, process.versions[dependency])
59  }
60})
61EOF
62
63sed -i '/export default/a \ \ \ \ base: "./",' vite.config.cjs
64
65sed -i '/version/a "main": "electron/main.cjs",' package.json
66
67sed -i '/"build/a "electron": "wait-on tcp:5173 && cross-env NODE_ENV=development electron .",' package.json
68sed -i '/"build":/a "electron:serve": "concurrently -k \\\"yarn dev\\\" \\\"yarn electron\\\"\",' package.json
69sed -i '/"build":/a "electron:build": "vite build && electron-builder",' package.json
70
71sed -i '$d' package.json && sed -i '$d' package.json && cat << EOF >> package.json
72  },
73  "build": {
74    "appId": "com.my-website.my-app",
75    "productName": "MyApp",
76    "copyright": "Copyright © 2022 Yuan",
77    "mac": {
78      "category": "public.app-category.utilities"
79    },
80    "nsis": {
81      "oneClick": false,
82      "allowToChangeInstallationDirectory": true
83    },
84    "files": [
85      "dist/**/*",
86      "electron/**/*"
87    ],
88    "directories": {
89      "buildResources": "assets",
90      "output": "dist_electron"
91    }
92  }
93}  
94EOF
95
96yarn electron:serve

修正安全提示中的

1sed -i '/<meta name/a \ \ \ \ \<meta http-equiv="Content-Security-Policy" content="script-src '\''self'\''" /\>' index.html

Add Makefile

為了不用每次編譯時要下什麼指令,所以寫個 Makefile 省心 ~

 1TAB="$(printf '\t')"
 2
 3cat << EOF > Makefile
 4all: run
 5
 6build:
 7${TAB}yarn electron:build
 8
 9.PHONY: web
10web:
11${TAB}yarn dev
12
13.PHONY: run
14run:
15${TAB}yarn electron:serve
16
17.PHONY: clean
18clean:
19${TAB}\$(RM) -r dist dist_electron
20EOF

額外記下來的

順手記下這次有用到的一些東西。

使用 Axios

 1yarn add axios
 2
 3mkdir -p src/composables
 4
 5echo << EOF > src/composables/useApi.js
 6import axios from 'axios';
 7
 8const instance = axios.create({
 9  baseURL: 'http://localhost:8088/',
10  timeout: 1000,
11  headers: {
12    "Content-Type": "application/json",
13  }
14});
15
16const useAxios = ({url, method, arg}) => { 
17    return instance[method](url, arg)
18}
19
20export default useAxios;
21EOF

Electron 建立視窗後在背景做點事

1app.on('ready', async () => {
2  const { execFile } = require('child_process')
3  
4  execFile('./resources/server/CCSNoteServer.exe')
5  
6  createWindow()
7})
1const { ipcRenderer } = require('electron')

小結

筆都在這一篇快要完成的時候更新了 vite 的版本,從 vite 2.99 更新到 3.0.0。 接下來世界就變了 🤣

migrate to ESM

migrate to ESM

vite 預設使用的 Port 從 3000 改到了 5173。

dev port

dev port

修正後基本上只要把上面的指令直接貼在終端機中就可以了。

最後附上執行結果!

1make
screenshot

screenshot

參考連結