用 Wails 和 Vue-js 打造跨平臺桌面應用


背景

最近在觀看抖音直播時,我發現了一位專注編程的主播,他在錄製視頻時使用了一個自制的桌面計時工具。這個工具非常實用,每當直播到達半小時左右,主播就會暫停錄製並保存視頻。這不僅幫助他有效管理直播時間,也讓他的工作流程更加有條不紊。作爲一名長期從事開發的程序員,這一巧妙的工具引起了我的興趣,於是我決定動手開發一個類似的桌面計時應用。經過一番摸索,我使用 Wails 框架順利完成了這個項目。下面是最終實現效果:

項目實現

項目地址

在開始詳細介紹實現過程之前,先給出項目的 GitHub 地址 https://github.com/PFinal-tool/pf_clock可以在這裏找到完整的源碼和使用說明。

使用 Wails 進行開發

在過去的項目中,我已經多次使用 Wails 開發桌面應用,因此對這個框架的使用已經非常熟悉。Wails 是一個非常適合開發跨平臺桌面應用的框架,結合了 Go 語言的強大後端功能和現代前端框架的靈活性。如果你還不熟悉 Wails,可以參考我之前寫的相關文章,裏面有詳細的介紹和使用示例。

創建項目

首先,我們使用 Wails 生成一個新項目。在命令行中運行以下命令:

wails init -n clock -t vue

PS: 這裏的 -n 參數用於指定項目的名稱,-t 參數用於選擇前端框架,這裏選擇的是 Vue.js

由於前端需要使用 tailwindcss 所以需要安裝一下 tailwindcss

cd frontend
npx tailwindcss init -p
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

項目結構

項目的目錄結構如下所示:

README.md    app.go       build        config.json  frontend     go.mod       go.sum       main.go      node_modules wails.json

其中,frontend 文件夾包含了前端代碼,採用 Vue.js 進行開發;main.go 是項目的主入口,負責 Wails 的初始化和項目啓動。

main.go 修改:

在 main.go 文件中,進行了如下修改:

package main

import (
 "embed"
 "github.com/wailsapp/wails/v2"
 "github.com/wailsapp/wails/v2/pkg/menu"
 "github.com/wailsapp/wails/v2/pkg/menu/keys"
 "github.com/wailsapp/wails/v2/pkg/options"
 "github.com/wailsapp/wails/v2/pkg/options/assetserver"
 "github.com/wailsapp/wails/v2/pkg/options/mac"
 wr "github.com/wailsapp/wails/v2/pkg/runtime"
 "runtime"
)

//go:embed all:frontend/dist
var assets embed.FS    // 嵌入前端資源

//go:embed build/appicon.png
var icon []byte       // 嵌入程序的圖標

func main() {
 app := NewApp()
 AppMenu := menu.NewMenu()    // 創建菜單 
 ColckMenu := AppMenu.AddSubmenu("File") // 創建文件菜單

 ColckMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
  wr.Quit(app.ctx)
 }) // 增加了 退出菜單

 if runtime.GOOS == "darwin" {
  AppMenu.Append(menu.EditMenu()) 
 }

 err := wails.Run(&options.App{
  Title:  "clock",
  Width:  450,
  Height: 175,
  Menu:   AppMenu, // reference the menu above
  AssetServer: &assetserver.Options{
   Assets: assets,
  },
  Frameless:        true,
  DisableResize:    true,
  AlwaysOnTop:      true,
  BackgroundColour: &options.RGBA{R: 0, G: 0, B: 0, A: 0},  // 這裏設置了背景顏色爲透明,但是Wails v2 沒有怎麼生效
  OnStartup:        app.startup,
  Bind: []interface{}{
   app,
  },
  Mac: &mac.Options{
   TitleBar:             nil,
   Appearance:           "",
   WebviewIsTransparent: true,
   WindowIsTranslucent:  true,
   Preferences:          nil,
   DisableZoom:          false,
   About: &mac.AboutInfo{
    Title:   "PFClock",
    Message: "PFinalClub Clock",
    Icon:    icon,
   },
   OnFileOpen: nil,
   OnUrlOpen:  nil,
  },
 })

 if err != nil {
  println("Error:", err.Error())
 }
}

前端修改

爲前端引入了 tailwindcss,並進行了相關配置。tailwind.config.js 和 style.css 文件分別修改如下:

tailwind.config.js:

/** @type {import('tailwindcss').Config} */
const { addDynamicIconSelectors } = require('@iconify/tailwind')
export default {
  content: ['./index.html','./src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [
    addDynamicIconSelectors()
  ],
}

style.css:

/* ./src/index.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

....

至此, 基本工作準備的差不多了, 就可以開始開發功能了.

功能開發

後端開發

在 app.go 中增加了配置文件的加載邏輯:

type ClockConfig struct {
 ClockBgColor   string `json:"clock_bg_color"`
 ClockTextColor string `json:"clock_text_color"`
}

type TextConfig struct {
 BgColor     string `json:"text_bg_color"`
 TextColor   string `json:"text_color"`
 TextContent string `json:"text_content"`
}

// 配置結構體
type Config struct {
 ClockConfig
 TextConfig
}

func (a *App) initConfig() {
 configPath := "config.json"
 confg, err := a.loadDictsFromEmbedFS(configPath)
 if err != nil {
  fmt.Println(err)
 }
 a.Config = confg
}

func (a *App) loadDictsFromEmbedFS(fileName string) (Config, error) {
 fmt.Println(fileName)
 var config Config
 data, err := defaultDicts.ReadFile(fileName)
 if err != nil {
  return config, err
 }

 err = json.Unmarshal(data, &config)
 if err != nil {
  return config, err
 }
 fmt.Println(config)
 return config, err
}

func (a *App) GetAppConfig() Config {
 return a.Config
}

如上所示, 增加了一個 Config 結構體, 用於存儲配置信息. 然後增加了一個 initConfig 方法, 用於初始化配置信息. 然後增加了一個 loadDictsFromEmbedFS 方法, 用於從嵌入的文件系統中讀取配置信息.

config.json 文件內容如下:

{
  "clock_bg_color":"#FFFFFF",
  "clock_text_color":"#00000",
  "text_bg_color":"#1ab394",
  "text_color":"red",
  "text_content":"PFinalClub專業的技術社區"
}

如上所示, 配置了 clock_bg_color 和 clock_text_color 和 text_bg_color 和 text_color 和 text_content 等配置信息.

前端開發

使用 Vue 組件實現了定時器功能,組件包括 ClockSetter.vueFlipClock.vue 和 Flipper.vue。核心代碼在 App.vue 中實現:

基礎的功能 App.vue:

<template>
  <div id="app" @dblclick="toggleTimer" @keydown="handleKeydown" tabindex="0">
    <FlipClock ref="flipClockRef"></FlipClock>
    <ClockSetter :appConfig="appConfig"></ClockSetter>
  </div>
</template>

<script>
import { nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { GetAppConfig } from '../wailsjs/go/main/App';
import ClockSetter from './components/ClockSetter.vue';
import FlipClock from './components/FlipClock.vue';

export default {
  name: 'App',
  components: {
    FlipClock,
    ClockSetter
  },
  setup() {
    const appConfig = reactive({});
    const lastKey = ref('');
    const flipClockRef = ref(null);
    const isFlipClockMounted = ref(false); // Flag to check if FlipClock is mounted

    // 這裏異步 讀取了 app.go 中的 GetAppConfig 方法, 獲取配置信息
    GetAppConfig().then(config => {
      Object.assign(appConfig, config);
    }).catch(error => {
      console.error('Error fetching config:', error);
    });

    const start = () => {
      if (flipClockRef.value && isFlipClockMounted.value) {
        console.log('FlipClock is mounted and ready to start');
        flipClockRef.value.start();
      } else {
        console.error('FlipClock component is not yet mounted.');
        console.log('flipClockRef.value:', flipClockRef.value);
      }
    };

    const resetFlipClock = () => {
      if (flipClockRef.value && isFlipClockMounted.value) {
        console.log('Resetting FlipClock');
        flipClockRef.value.reset();
      }
    };

    const handleKeydown = (event) => {
      if (event.key === 's') {
        start();
      } else if (event.key === 'p') {
        flipClockRef.value.pause();
      } else if (event.key === 'r') {
        resetFlipClock();
      }
      lastKey.value = event.key;
    };

    onMounted(async () => {
      await nextTick(); // Wait for the DOM to update
      console.log(flipClockRef.value)
      document.getElementById('app').focus();
      window.addEventListener('keydown', handleKeydown);
      isFlipClockMounted.value = true;
      console.log('App mounted');
    });

    onBeforeUnmount(() => {
      window.removeEventListener('keydown', handleKeydown);
    });

    return {
      appConfig,
      lastKey,
      flipClockRef,
      start,
      resetFlipClock
    };
  }
}
</script>

至此 核心的代碼已經完成, 剩下的就是一些細節的調整和完善.

運行與打包

wails dev

效果沒毛病. 也能夠翻頁計時.

最後,通過以下命令將項目打包爲可執行文件:

wails build  --mac 

wails build -platform darwin/arm64 -o pf_clock_arm64 -- mac

wails build -platform windows/amd64 -o pf_clock.exe -- windows

wails build -platform linux/amd64 -o pf_clock_linux -- linux
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LcTjFl_O4JT026zlejJqGQ