2024年12月17日 星期二

Node.js 開發筆記 - 使用 Electron + Vite + Vue 開發一款 PC app ,可支援 mDNS 裝置搜尋 @ macOS 15.2



有點久沒用 Electron 開發 PC app ,協助同事從旁實驗一下新的架構,這次研究後,直接用 electron-vite 初始化專案,核心就是 Electron + Vite 的研發環境,前台是 Vue 前端畫面。原先在想是不是一律自行安裝套件且故意都裝最新版,想完後還是縮一下,回歸別人整理好的工具,而這次強調用 Vite 整合,就不從 electron-vue 開始。

開發環境:

% sw_vers 
ProductName: macOS
ProductVersion: 15.2
BuildVersion: 24C101
% nvm use --lts         
Now using node v22.12.0 (npm v10.9.0)

流水帳:

```
% npm create electron-vite@latest
Need to install the following packages:
create-electron-vite@0.7.1
Ok to proceed? (y) y


> npx
> create-electron-vite

✔ Project name: … my-electron-app
✔ Project template: › Vue

Scaffolding project in /private/tmp/my-electron-app...

Done. Now run:

  cd my-electron-app
  npm install
  npm run dev

npm notice
npm notice New major version of npm available! 10.9.0 -> 11.0.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.0.0
npm notice To update run: npm install -g npm@11.0.0
npm notice

% cd my-electron-app
my-electron-app % npm install express multicast-dns axios
my-electron-app % cat package.json 
{
  "name": "my-electron-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build && electron-builder",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.7.9",
    "express": "^4.21.2",
    "multicast-dns": "^7.2.5",
    "vue": "^3.4.21"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.4",
    "electron": "^30.0.1",
    "electron-builder": "^24.13.3",
    "typescript": "^5.2.2",
    "vite": "^5.1.6",
    "vite-plugin-electron": "^0.28.6",
    "vite-plugin-electron-renderer": "^0.14.5",
    "vue-tsc": "^2.0.26"
  },
  "main": "dist-electron/main.js"
}
my-electron-app % npm run dev
```

如此就有了一個畫面,而目錄結構:

```
% tree -I 'node_modules|dist|build'
.
├── README.md
├── dist-electron
│   ├── main.js
│   └── preload.mjs
├── electron
│   ├── electron-env.d.ts
│   ├── main.ts
│   └── preload.ts
├── electron-builder.json5
├── index.html
├── package-lock.json
├── package.json
├── public
│   ├── electron-vite.animate.svg
│   ├── electron-vite.svg
│   └── vite.svg
├── src
│   ├── App.vue
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

7 directories, 22 files
```

接著,要來擴充一下功能,讓這支程式可以使用 mDNS 協定偵測環境,這功能需實作在 Electron 架構上,在 main.ts 添加裝置搜尋:

```
$ cat electron/main.ts 
...

import mdns from 'multicast-dns'

...

// 使用一個全域變數紀錄偵測到的 mDNS 裝置,並記錄他初次出現的時間,也紀錄最後一次看到的時間
const mdnsDevices: { [key: string]: { name: string, type: string, firstSeen: number, lastSeen: number, data: any[], answers: any[] } } = {}

function startMdnsQuery() {
  const mdnsInstance = mdns()

  console.log('mDNS Query Start...')

  // 修改 name & type
  // dns-sd -B _services._dns-sd._udp local
  mdnsInstance.query({
    questions: [{
      //name: '_http._tcp.local',
      name: '_services._dns-sd._udp.local',
      type: 'PTR'
    }]
  })

  mdnsInstance.on('response', (response: { answers: string | any[] }) => {
    console.log('mDNS Response:', response)

    if (win && response.answers.length > 0) {
      for (const answer of response.answers) {
        try {
          // 建立一個 unique key 來代表一個裝置
          const key = `${answer.name}-${answer.type}`
          if (!mdnsDevices[key]) {
            mdnsDevices[key] = {
              name: answer.name,
              type: answer.type,
              data: [answer.data],
              firstSeen: Date.now(),
              lastSeen: Date.now(),
              answers: [answer]
            }
            console.log('ADD Device:')
            console.log(mdnsDevices[key])
          } else {
            mdnsDevices[key].lastSeen = Date.now()
            mdnsDevices[key].answers.push(answer)
            if (!mdnsDevices[key].data.includes(answer.data)) {
              mdnsDevices[key].data.push(answer.data)
            }
          }
          win.webContents.send('mdns-found', mdnsDevices)
        } catch (error) {
          console.error(error)
        }
      }
    }
  })
}
...

app.whenReady().then(() => {
  createWindow()
  startMdnsQuery()
})
```

在 preload.ts 增加資料回傳到前端網頁上:

```
$ cat electron/preload.ts 
...
contextBridge.exposeInMainWorld('mdnsAPI', {
  onFound: (callback: (devices: any[]) => void) => {
    ipcRenderer.on('mdns-found', (_event, devices) => {
      callback(devices)
    })
  }
})
```

App.vue:

```
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'

import { ref, onMounted } from 'vue'

const devices = ref<any[]>([])
onMounted(() => {
  // 若在 preload 中已暴露 mdnsAPI
  if (window.mdnsAPI && typeof window.mdnsAPI.onFound === 'function') {
    window.mdnsAPI.onFound((foundDevices) => {
      // 將接收到的裝置資料放入響應式變數
      devices.value = foundDevices
      console.log('Found devices:')
      console.log(foundDevices)
    })
  }
})
</script>

<template>
  <div>
    <a href="https://electron-vite.github.io" target="_blank">
      <img src="/electron-vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <!--
  <HelloWorld msg="Vite + Vue" />
  <hr />
  -->
  <div>
    <h3>已搜尋到的裝置:</h3>
    <ul id="deviceList">
      <li v-for="(device, index) in devices" :key="index">
        [{{ new Date(device.firstSeen).toLocaleString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }) }}] {{ device.name }} ({{ device.type }}) 
        <ul>
          <li v-for="(data, i) in device.data" :key="I">
            {{ data }}
          </li>
        </ul>
      </li>
    </ul>
  </div>
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}

#deviceList {
  text-align: left;
  list-style-type: none;
  padding: 0;
}
</style>
```

收工 ,有空再來做些什麼應用吧:github.com/changyy/study-my-electron-app

沒有留言:

張貼留言