vue3+electron+tailwindcss 集成踩坑指北~

1,784 阅读2分钟

背景

手上有个项目使用 gulp 构建 ts 脚本打包 js 的流程,ts 脚本都是使用面向对象依赖注入那一套的,打包成 js 之前要注入 json 参数。本来计划是和后端一起处理的,后面想到既然electron 有 node 的支持,那就试一下用 vue3 + electron 自己实现一下整个打包流程吧。

准备工作

参考 小满zs 文章 Vue3 Vite electron 开发桌面程序_vue3 vite 桌面应用开发-CSDN博客

安装依赖

# 创建Vue项目
 npm init vue 
# 安装依赖
npm install
# 一定要安装成开发依赖
npm install electron electron-builder -D 
# 安装超时 请使用某宝镜像 或者XX上网
npm config set electron_mirror=https://registry.npmmirror.com/-/binary/electron/

目录准备

新建一个目录 plugins 编写 vite 插件启动 electron

background.ts

import { app, BrowserWindow, ipcMain } from 'electron'
import fs from 'fs'
import path from 'path'

app.whenReady().then(() => {
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true, // 渲染进程使用 node
      contextIsolation: false, // 关闭渲染进程沙箱
      webSecurity: false // 关闭跨越检测
    },
    title: 'vite-electron'
  })
  win.setMenu(null) // 不设置顶部菜单
  win.setSize(1100, 700) // 设置初始窗口大小
  win.webContents.openDevTools() // 打开开发者工具
  // 根据命令行参数加载URL或本地文件
  if (process.argv[2]) {
    win.loadURL(process.argv[2])
  } else {
    win.loadFile('index.html')
  }
})

检查命令行参数,如果有参数则加载URL,否则加载本地文件index.html。在开发模式下,可以将URL指向本地的开发服务器,以便实现热更新和实时调试。在生产模式下,需要将URL指向本地的index.html文件,以便在本地运行Electron应用程序。

vite.electron.dev.ts

import type { AddressInfo } from 'net'
import type { Plugin } from 'vite'
import { spawn } from 'child_process'
import fs from 'fs'

export const ElectronDevPlugin = (): Plugin => {
  return {
    name: 'electron-dev',
    // 配置钩子
    configureServer(server) {
      const initElectron = () => {
        // 使用esbuild编译TypeScript代码为JavaScript
        require('esbuild').buildSync({
          entryPoints: ['src/background.ts'],
          bundle: true,
          outfile: 'dist/background.js',
          platform: 'node',
          target: 'node12',
          external: ['electron']
        })
      }
      initElectron()
      server?.httpServer?.once('listening', () => {
        const addressInfo = server.httpServer?.address() as AddressInfo
        const IP = `http://localhost:${addressInfo.port}`
        let electronProcess = spawn(require('electron'), ['dist/background.js', IP])
        console.log(IP)

        fs.watchFile('src/background.ts', () => {
          // 杀死当前的Electron进程
          electronProcess.kill()
          // 重新编译主进程代码并重新启动Electron进程
          initElectron()
          electronProcess = spawn(require('electron'), ['dist/background.js', IP])
        })
      })
    }
  }
}

configureServer是Vite的一个插件钩子函数,用于在Vite开发服务器启动时执行一些自定义逻辑。该函数接受一个ServerOptions对象作为参数,该对象包含有关当前Vite服务器的配置信息。在这个钩子函数中,您可以访问Vite服务器的HTTP服务器对象(httpServer),WebSocket服务器对象(wsServer)和Vite的构建配置对象(config)等。您可以使用这些对象来实现各种功能,例如自定义路由、添加中间件、实现实时重载和调试等。

vite.electron.build.ts

import type { Plugin } from 'vite'
import * as electronBuilder from 'electron-builder'
import path from 'path'
import fs from 'fs'

// 导出Vite插件函数
export const viteElectronBuild = (): Plugin => {
  return {
    name: 'vite-electron-build',

    // closeBundle是Vite的一个插件钩子函数,用于在Vite构建完成后执行一些自定义逻辑。
    closeBundle() {
      // 定义初始化Electron的函数
      const initElectron = () => {
        // 使用esbuild编译TypeScript代码为JavaScript
        require('esbuild').buildSync({
          entryPoints: ['src/background.ts'],
          bundle: true,
          outfile: 'dist/background.js',
          platform: 'node',
          target: 'node12',
          external: ['electron']
        })
      }

      // 调用初始化Electron函数
      initElectron()

      // 修改package.json文件的main字段 不然会打包失败
      const json = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
      json.main = 'background.js'
      fs.writeSync(fs.openSync('dist/package.json', 'w'), JSON.stringify(json, null, 2))

      // 创建一个空的node_modules目录 不然会打包失败
      fs.mkdirSync(path.join(process.cwd(), 'dist/node_modules'))

      // 使用electron-builder打包Electron应用程序
      electronBuilder
        .build({
          config: {
            appId: 'com.example.app',
            productName: 'onlysdk_xyx',
            directories: {
              output: path.join(process.cwd(), 'release'), //输出目录
              app: path.join(process.cwd(), 'dist') //app目录
            },
            nsis: {
              oneClick: false //取消一键安装
            },
            extraFiles: [
              {
                from: 'public',
                to: 'public'
              }
            ]
          }
        })
        // Promise is returned
        .then((result) => {
          console.log(JSON.stringify(result))
        })
        .catch((error) => {
          console.error(error)
        })
    }
  }
}

vite.config.ts

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { ElectronDevPlugin } from './plugins/vite.electron.dev'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { TDesignResolver } from 'unplugin-vue-components/resolvers'
import { viteElectronBuild } from './plugins/vite.electron.build'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(), // 支持tsx,jsx
    ElectronDevPlugin(), // 开发环境插件
    viteElectronBuild(),  // 生产打包插件
    // tdesign 自动导入组件插件
    AutoImport({
      resolvers: [
        TDesignResolver({
          library: 'vue-next'
        })
      ]
    }),
    Components({
      resolvers: [
        TDesignResolver({
          library: 'vue-next'
        })
      ]
    })
  ],
  base: './', // 文件相对路径,不然打包后会白屏
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  // tailwindcss 配置
  css: {
    postcss: {
      plugins: [require('tailwindcss'), require('autoprefixer')]
    }
  }
})

tailwindcss 集成

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

tailwindcss.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {}
  },
  corePlugins: {
    preflight: false
  },
  plugins: []
}

tailwindcss 在 electron 打包后会和引用的组件库 tdesign 有样式冲突问题

  • tailwindcss 没打包css样式文件到electron release工程;
  • tailwindcss 常见样式冲突 preflight 配置;
  • 调整引用 css 文件顺序;

以上引发冲突的常见配置都处理了,但 electron 打包后的产物 tailwindcss 还是没有生效,然后打开控制台检查页面元素,发现样式是已经生成打包进去的了,类名样式的权重和引入的组件库样式权重是一样,是组件库的样式把tailwindcss的样式给覆盖了。

解决方法

暂时没找到什么比较好的方案,只能在 tailwindcss.config.js 里配置权重important: true,将 tailwindcss 的样式权重都提升到最高暂时解决这个问题;

打包后的自定义文件夹(静态资源路径)

因为在项目中需要用到 node 操作一些 json 数据或者做一些 io 读写操作,所以希望打包后也能拿到对应配置文件的路径。

打包 option 配置

      electronBuilder
        .build({
          config: {
            appId: 'com.example.app',
            productName: 'onlysdk_xyx',
            directories: {
              output: path.join(process.cwd(), 'release'), //输出目录
              app: path.join(process.cwd(), 'dist') //app目录
            },
            nsis: {
              oneClick: false //取消一键安装
            },
            extraFiles: [
              {
                from: 'public',
                to: 'public'
              }
            ]
          }
        })

取消所用资源打包成asar的配置,并且配置extraFiles,配置不参与打包进resources文件夹的文件路径;

image.png image.png

获取资源路径

// process.cwd() 获取当前目录
const getExtraFilesPath = (filename: string) => {
  return path.join(process.cwd(), `/存放资源的文件夹/${filename}`)
}

进程通信

主进程注册方法 使用 ipcMain.handle 注册方法。

// 创建文件夹
ipcMain.handle('create-directory', async (event, dirName) => {
  const dirPath = path.join(`${getExtraFilesPath('config')}`, dirName)
  try {
    if (!fs.existsSync(dirPath)) {
      fs.mkdirSync(dirPath)
      return {
        code: 200,
        message: 'Success'
      }
    } else {
      return { code: 0, status: 'error', message: 'Directory already exists' }
    }
  } catch (error) {
    console.error('Failed to create directory:', error)
    return { code: 0, status: 'error', message: error }
  }
})

渲染进程调用方法 渲染进程使用ipcRenderer调用方法。

const createDirectory = async () => {
  try {
    const res =  await ipcRenderer.invoke('create-directory', 'test)
    if(res.code === 200){
       MessagePlugin.success('写入成功!')
    }
  } catch (error) {
    MessagePlugin.error('写入失败!')
  }
}

打包 npm 下载过慢问题

解决方法

完结

关于 electron 还有很多需要学习,electron-builder打包配置,以及如何通知安装用户下载更新,打包包体优化等问题。共勉~