手把手光速开发 Electron

你以为开发Electron需要先会一堆东西才能上手吗?其实你只需要会HTMLCSSJavaScript就可以了;

废话不多说,直接上手,让我们一起来开发一个Electron应用吧!

1. 创建项目

首先,我们需要创建一个项目,这里我们直接使用现成的模板,这样可以省去很多配置的麻烦;

模板地址:electron-vite

这个地址里面有一个项目create-electron-vite,可以看他的README,只需要一行命令就可以创建一个Electron项目:

sh 复制代码
# NPM
$ npm create electron-vite@latest electron-vite-project

# Yarn
$ yarn create electron-vite electron-vite-project

# PNPM
$ pnpm create electron-vite electron-vite-project

提供了三种方式来创建项目,这里大家可以根据自己的喜好来选择,这里我使用的是Vue模板;

你刚不是说只需要会HTMLCSSJavaScript就可以了吗?我不会Vue啊!

没关系,你要是不会Vue就把Vue相关的依赖都是删掉,直接用HTML也是可以的!

2. 安装依赖

项目创建完成之后,我们就进入了愉快的安装依赖的环节了;

为啥要说愉快呢?因为你大概率是下载不下来依赖,所以该倒水就去倒水,该拉屎就去拉屎!

倒完水拉完屎之后,咱们先将npm的源切换到淘宝源,老墙内的孩子了!

这里其实全局安装一个nrm也是可以的,就不用每次都自己手动去设置了;

下面的一些操作都是看自己需要可选的

shell 复制代码
# 官方镜像
npm config set registry https://registry.npmjs.org/

# 淘宝镜像
npm config set registry https://registry.npm.taobao.org/

# cnpm镜像
npm config set registry https://r.cnpmjs.org/

# nrm 安装
npm i -g nrm

# 查看 nrm 支持切换的源
nrm ls

# 使用 nrm 切换源
nrm use cnpm

当然就算这样有可能还是会失败,你可能会遇到如下的错误:

这个原因是因为网络的问题,你可以尝试ping一下github.com试试看,这个时候就会出现这个IP地址;

这个时候我们可能直接使用浏览器是可以访问的,但是安装依赖和ping就是不想,解决方案也很简单,就是去修改host文件;

我们先可以去这个网站:ping.chinaz.com;

然后它会出现很多github的独立IP,咱随便找一个ping一下,可以ping通就使用这个IP

Windows系统的host文件在:C:\Windows\System32\drivers\etc下面,找到host文件用记事本打开,在最下面新起一行,加上你能pingIP,如下:

shell 复制代码
140.82.112.3 github.com 

保存之后重新安装依赖,如果没生效的话可以在控制台输入如下命令,刷新DNS

shell 复制代码
ipconfig /flushdns

如果还是不行的话,那么建议换手机网络试试看。

3. 讲点细节和注意事项

移除 TS

首先提个醒,该项目是用TS进行创建的,如果用不惯或者说不会用TS的同学,现在就给TS先移除掉;

其实主要是我用不惯TS,我最开始用这个框架本身是想着给TS延续下去的,但是出现了几个很严重的问题;

一个是项目文件多了之后非常吃性能,可能是我电脑垃圾;

第二是开发环境下没有抛出任何异常,打包死活都不行,每个文件都有问题,解决了很多问题之后还是不行,查阅了大量的资料,离谱的是有一次修改配置之后成功了,然后第二天又不行了;

没有任何觉得TS不好的意思,可能是我技术没到家,我直接从入门到放弃了;

移除TS首先需要卸载TS相关的依赖:

shell 复制代码
npm uni typescript vue-tsc

然后打开package.json,将其中的scripts属性下的build修改成如下:

json 复制代码
{
  "scripts": {
    "build": "vite build && electron-builder"
  }
}

接着删除所有的*.d.ts文件和tsconfig.json等相关文件:

然后修改所有后缀为.ts的文件为.js,注意需要删除类型标注,其他他里面的类型标注并没有多少;

接着就是删除*.vue文件中的script标签上的lang="ts"的属性了,还是一样记得删除类型标注;

最后再将vite.config.js文件中的所有*.ts的入口文件配置修改成*.js即可;

依赖管理相关

在原始的项目中,vue的依赖是放在dependencies中的,这里我建议是将所有的依赖都放到devDependencies中的;

有人可能会说你这样不规范,开发依赖和生产依赖怎么可以混着放在一起呢?

实际情况是这两个属性本身约束的是包管理器,不约束你的代码怎么写,放到哪里都可以被引用到;

这个框架在打包的时候会将dependencies中的依赖打进去,会导致包体积变大,通常来说不使用node-gyp相关的模块的依赖,都是不需要将其放到dependencies,至于使用node-gyp模块相关怎么处理后面会讲到;

4. 打包

咱先不开发,直接打包,这玩意不同于我们平时开发的Web应用,需要打包成可执行文件才能运行,如果打包不能用那开不开发也没啥意义;

打包很简单,直接执行npm run build即可,当然大概率你是不会成功的,因为你可能会卡在download阶段;

咱们直接点上面截图提供的链接,然后下载对应的包,下载完成之后,直接将这个包放到:C:\Users\[User]\AppData\Local\electron\Cache目录下即可;

注意:需要先将打包的进程停止,再将包放到对应的目录下,否则包会被删除;

然后再次执行npm run build,这个时候还会有一个electron-builder包的下载,一样的操作;

不过electron-builder下面的包需要先解压出来,例如winCodeSign就需要解压到C:\Users\[User]\AppData\Local\electron-builder\Cache\winCodeSign目录下;

最终的目录应该是:C:\Users\[User]\AppData\Local\electron-builder\Cache\winCodeSign\winCodeSign-2.6.0

后面还会有几个nsis的包,也是一样的操作,解压到对应的目录下即可,如下图就是我的目录最终的样子:

然后命令成功之后,项目的根目录下就会生成一个release的目录,下面的YourAppName-Windows-0.0.0-Setup.exe就是我们的安装包了,可以安装起来看看效果吧!

4.1 打包配置

打包是通过electron-builder来完成的,具体的配置可以参考官方文档:www.electron.build/

这里的项目根目录下有一个electron-builder.json5的配置文件,这个就是应用打包配置,可以根据自己的需求来修改;

具体的一些配置可以参考这篇文章,写的非常详细:electron-builder通用配置(翻译)

5. 开发

终于来到了愉快的开发阶段了,先来熟悉一下目录结构:

这里我的目录标黄的都是自动生成的,不用管,看一下前端相关的代码,应该很熟悉的,就是一个Vue项目代码的目录结构;

然后就是electron的代码,就两个文件,一个是main.js,一个是preload.js,非常简洁;

由于咱们是光速开发,所以这里就不过多讲解electron的相关知识了,但是核心怎么使用的肯定会给大家理清楚;

5.1 electron 相关

5.1.1 main.js

先看一下main.js文件,这个文件是electron的入口文件,里面的代码换行加注释也就60行,咱核心就看两个地方:

js 复制代码
function createWindow() {
    // 创建一个浏览器窗口(我们看到的应用页面)
    win = new BrowserWindow({
        // 设置图标,就是应用左上角的小图标
        icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
        
        // 配置 web 首选项,会有很多配置,具体看官网介绍
        webPreferences: {
            // 设置预加载脚本,预加载脚本可以理解为在你们的 index.html 中多插入了一个 script 标签,但是它可以使用 nodejs 的 api
            preload: path.join(__dirname, 'preload.js'),
        },
    })

    // 测试渲染进程和主进程通信
    // Test active push message to Renderer-process.
    win.webContents.on('did-finish-load', () => {
        win?.webContents.send('main-process-message', (new Date).toLocaleString())
    })

    // 区分开发环境和生产环境下加载的不同页面
    if (VITE_DEV_SERVER_URL) {
        win.loadURL(VITE_DEV_SERVER_URL)
    } else {
        // win.loadFile('dist/index.html')
        win.loadFile(path.join(process.env.DIST, 'index.html'))
    }
}

// 应用已经准备就绪,可以使用 whenReady 来进行监听,准备就绪之后就可以创建窗口了
app.whenReady().then(createWindow)

他这个里面还会有其他的一些代码,上面都写好了注释,我就不过多去讲解了;

5.1.2 preload.js

接下来就是preload.js相关的,这里主要看最开头的一些代码:

js 复制代码
import { contextBridge, ipcRenderer } from 'electron'

// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))

// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
function withPrototype(obj) {
  const protos = Object.getPrototypeOf(obj)

  for (const [key, value] of Object.entries(protos)) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) continue

    if (typeof value === 'function') {
      // Some native APIs, like `NodeJS.EventEmitter['on']`, don't work in the Renderer process. Wrapping them into a function.
      obj[key] = function (...args) {
        return value.call(obj, ...args)
      }
    } else {
      obj[key] = value
    }
  }
  return obj
}

这一点代码信息量非常大,我就不在代码里面写注释了,直接讲:

  • contextBridge: 这个属性的作用主要是用来向渲染进程注入一些相关的API

    • exposeInMainWorld: 就是contextBridge中的一个方法,它接收两个参数,一个是key,一个是对象; 例如上面的这个contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer)) 就代表着我在渲染进程可以使用window.ipcRenderer.xxx,这个.xxx就是withPrototype(ipcRenderer)的返回结果; 当然我们前端玩家应该都知道可以省略window,直接写ipcRenderer.xxx

      • 这里这样做的目的是因为默认情况下渲染进程不能直接使用nodejs相关的能力,只有主进程可以使用,如果需要开启渲染进程使用nodejs的能力, 就需要在上面提到的创建浏览器窗口那里增加如下配置:

        json5 复制代码
        {
            webPreferences: {
              nodeIntegration: true,
              contextIsolation: false,
            }
        }

        注意:启用nodeIntegration和关闭contextIsolation在生产环境中是不安全的,因为可能会被注入恶意代码; 同时这两个配置是需要搭配使用的,使用这种配置就不能使用 contextBridge 了;

        查看更多:www.electronjs.org/docs/latest...

    • 还有一个方法是exposeInIsolatedWorld,就不过多介绍了;

  • withPrototype:方法就是将一个对象上的所有原型方法复制到自己身上,这样就可以保证这些原型方法在特定的上下文中正常执行,这个方案可以说是非常巧妙的。

他在这里这样处理了之后,我们就可以在没有开启nodeIntegration的情况下,在渲染进程中使用ipcRenderer了;

5.1.3 主线程与渲染线程间的通讯

上面说的ipcRenderer就是一个渲染进程中的管道对象,我们可以通过ipcRenderer.on来监听主线程发送过来的消息;

例如这个示例中,在上面的main.js中有这样一段代码:

js 复制代码
win.webContents.on('did-finish-load', () => {
    win?.webContents.send('main-process-message', (new Date).toLocaleString())
})

这里的win?.webContents.send('main-process-message', (new Date).toLocaleString())就是在向渲染进程发送消息;

在渲染进程中就通过window.ipcRenderer.on来监听消息,这里具体可以看src/main.js中有这样一段代码:

js 复制代码
window.ipcRenderer.on('main-process-message', (_event, message) => {
    console.log(message)
})

渲染进程如果想向主进程发送消息也是一样的,可以使用sendinvokesendSyncpostMessage

而主进程处理不同的消息会使用不同的函数,如下:

js 复制代码
// 渲染进程发送消息
ipcRenderer.send('message', 'message');

// 主进程监听消息 send/sendSync/postMessage 都使用 on 来监听
ipcMain.on('message', (e) => {
    console.log(e)
})

// 渲染进程发送消息
ipcRenderer.invoke('message', 'message').then(res => {
    console.log(res)
})

// 主进程监听消息 invoke 使用 handle 来监听
ipcMain.handle('message', (e) => {
    console.log(e);
    return 'hello'
})

通讯就简单的介绍到这里,总得来说这个和前端的事件处理的使用方式非常相似,因为底层继承的也是EventEmitter,更多的可以去看文档:

5.2 前端相关

前端就是一个Vue项目,如果不想用Vue的也可以不用,按照electron的使用方式,本质上就是一个浏览器,只要浏览器上能跑的项目,这里都可以跑;

现在我们可以直接控制台输入npm run dev来查看我们的项目运行效果了;

5.2.1 优化

在执行npm run dev的时候会打开一个electron的窗口,这个窗口其实就是一个浏览器窗口;

部分电脑(并不是所有的)可能会在窗口打开的时候长时间显示一个白屏,我们就来优化这个问题;

首先我们需要在main.js中的创建窗口的时候增加一个配置:

js 复制代码
win = new BrowserWindow({
    // ...
    show: false,
})

// 可以通过设置背景色来观察优化效果,通过切换添加优化方案和不添加优化方案,来查看效果
win.setBackgroundColor('hsl(230, 100%, 50%)')

win.once('ready-to-show', () => {
    win.show()
})

可参考:使用 ready-to-show 事件

这个事件是渲染进程第一次完成绘制的时候会触发,可以简单的理解为window.onload事件,但是window.onload通常要比它更晚一些;

因为window.onload是需要等待资源下载完成才会触发,如果资源在没有下载完成之前,这个页面还是一个空白;

所以我们还可以使用线程通讯的方式来优化它,我们可以在preload.js中增加如下代码:

js 复制代码
window.onload = () => {
    ipcRenderer.send('on-ready')
}

然后在main.js中增加如下代码:

js 复制代码
import { app, BrowserWindow, ipcMain } from 'electron'

// ...

ipcMain.on('on-ready', () => {
    win.show()
})

按照这个思路,例如我们在Vue项目中优化白屏,可以在根组件挂载完成之后再去触发窗口的显示;

但是这个项目本身就有一个loading的效果,在src/main.js中可以看到有关闭这个loading的通知,如果你不想要这个loading效果就可以替换成上面提到的优化方案:

js 复制代码
createApp(App).mount('#app').$nextTick(() => {
  // Remove Preload scripts loading
  postMessage({ payload: 'removeLoading' }, '*')

  // Use contextBridge
  window.ipcRenderer.on('main-process-message', (_event, message) => {
    console.log(message)
  })
})

5.2.2 布局

可以看到这个头部可能并不是我们想要的,包括原生提供的菜单也是一样的,但是electron并没有提供相关的配置来修改,如果想要自定义就只能关闭然后自己写一个;

我们可以在创建窗口的时候,增加如下配置,来关闭原生的菜单:

js 复制代码
win = new BrowserWindow({
    // ...
    frame: false,
})

关闭之后我们就需要自己去写顶部区域了,这个就当做写普通前端项目就行了,主要是顶部栏固定显示就好;

我这里使用的Vue3,所以就按照Vue3的做法来了,顶部固定那么中间的内容变化,最好的做法肯定是使用vue-router了;

首先安装依赖,非常熟悉的环节:

shell 复制代码
npm install vue-router@4 -D

然后在src目录下新建一个router目录,然后在里面新建一个index.js文件,内容如下:

js 复制代码
import {createRouter, createWebHashHistory} from 'vue-router'

const routes = []

const router = createRouter({
    history: createWebHashHistory(),
    routes,
});

export default router

接着在src/main.js中使用这个路由:

js 复制代码
import {createApp} from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

const app = createApp(App);

app.use(router);
app.mount('#app')
        .$nextTick(() => {
          // Remove Preload scripts loading
          postMessage({payload: 'removeLoading'}, '*')

          // Use contextBridge
          window.ipcRenderer.on('main-process-message', (_event, message) => {
            console.log(message)
          })
        })

我这里将链式调用的操作给修改成普通的调用方式了,链式调用有时候会很舒服,但是有时候代码可读性会变得很差,编码嘛,怎么舒服怎么来,大家自己也可以使用自己的方式来写;

接着我们在src/App.vue中增加一个router-view

vue 复制代码
<template>
  <router-view/>
</template>

<script setup>
</script>

<style scoped>
</style>

这里面原先有一些代码,都全删掉,然后再将src/components/HelloWorld.vue删掉,接着再删掉src/style.css中的所有代码,只需要加上如下代码即可:

css 复制代码
html,
body {
  margin: 0;
  padding: 0;
}

接着再在src下新建一个layout目录,下面新建如下文件:

  • Layout.vue
vue 复制代码
<template>
  <div class="layout">
    <top-bar/>
    <app-main/>
  </div>
</template>

<script setup>
import TopBar from "./TopBar.vue";
import AppMain from "./AppMain.vue";

defineOptions({
  name: "Layout"
})

</script>

<style scoped>
.layout {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: auto;
}
</style>
  • AppMain.vue
vue 复制代码
<template>
  <div class="app-main">
    <router-view/>
  </div>
</template>

<script setup>
</script>

<style scoped>
.app-main {
  background: aliceblue;
  flex-grow: 1;
}

p {
  word-break-wrap: nowrap;
}
</style>
  • TopBar.vue
vue 复制代码
<template>
  <div class="top-bar">
    <label>{{ title }}</label>

    <i class="fa-solid fa-window-minimize" style="margin-left: auto;" @click="handleMinimize"></i>
    <i v-show="!hasRestore" class="fa-solid fa-window-maximize" @click="handleMaximize"></i>
    <i v-show="hasRestore" class="fa-solid fa-window-restore" @click="handleRestore"></i>
    <i class="fa-solid fa-xmark" @click="handleClose"></i>
  </div>
</template>

<script setup>
import {ref} from "vue"

defineOptions({
  name: "TopBar"
})

const title = ref('标题');

const hasRestore = ref(false);

const handleMinimize = () => {
  window.ipcRenderer.invoke('on-minimize');
}

const handleMaximize = () => {
  window.ipcRenderer.invoke('on-maximize').then(_ => {
    hasRestore.value = true;
  })
}

const handleRestore = () => {
  window.ipcRenderer.invoke('on-restore').then(_ => {
    hasRestore.value = false;
  })
}

const handleClose = () => {
  window.ipcRenderer.invoke('on-close');
}


</script>

<style scoped>
.top-bar {
  position: sticky;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  height: 28px;
  -webkit-app-region: drag;
}

.fa-solid {
  padding: 10px;
  cursor: pointer;
  -webkit-app-region: no-drag;
}

.fa-solid:hover {
  background: #eeeeee;
}
</style>

这里的图标使用的是:fontawesome,V6免费版,已经能覆盖很多场景了,不行也可以使用iconfont

使用方式也非常简单,将下载下来的资源包中的css/all.min.css放到assets/css下面,然后将资源包中的webfonts下所有的文件放到assets\webfonts下,最后将all.min.csssrc/main.js中引用即可;

这里正好使用到了上面提到的渲染进程和主线程之间通讯的知识,那么我们还需要在主进程监听监听代码:

js 复制代码
ipcMain.handle('on-minimize', () => {
  win.minimize();
  return true;
})


ipcMain.handle('on-maximize', () => {
  win.maximize();
  return true;
})


ipcMain.handle('on-restore', () => {
  win.unmaximize();
  return true;
})


ipcMain.handle('on-close', () => {
  win.close();
  return true;
})

现在只需要将router配置一下就大功告成:

js 复制代码
import {createRouter, createWebHashHistory} from 'vue-router'
import Layout from '../layout/Layout.vue'

const routes = [{
  path: '/',
  component: Layout,
  redirect: '/home',
  children: [
    {
      path: '/home',
      component: () => import('../views/home/index.vue')
    }, {
      path: '/test',
      component: () => import('../views/test/index.vue')
    }
  ]
}]

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router

我这里新建了两个页面,页面位置在src/views下,这个就不过多介绍了;

5.2.3 优化开发体验(可选)

配置别名

工欲善其事必先利其器,咱们开发环境一定要舒服,首先配置一个别名,咱们可以在vite.config.js中加上如下配置:

js 复制代码
// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      "@": 'src',
      "#": 'electron'
    }
  }
  // 省略其他配置
})

这样我们在引用资源路径的时候,如果层级过深就不用一直../的写了,例如在views/home/index.vue想要引用views/test/Component.vue就可以直接写@/views/test/Component.vue

这个大家应该都用过,就不多介绍了;

配置自动导入

自动导入依赖unplugin-auto-import插件,我们先安装它:

shell 复制代码
npm i -D unplugin-auto-import

然后在vite.config.js中加上如下配置:

js 复制代码
// https://vitejs.dev/config/
export default defineConfig({
  // 省略其他配置
  plugins: [
    AutoImport({
      imports: [
        'vue',
        'vue-router'
      ]
    })
  ]
})

这样我们在使用vuesetup语法时,就不需要写import {ref, computed} from 'vue'这样的代码了,直接使用就行;

其他

这个项目没有eslint.editorconfig.env支持,这些都可以根据自己的实际情况进行添加;

至于其他less这种也没有添加,就目前的css的能力来说,如果只使用less的嵌套和变量个人觉得没必要使用;

还有其他库的,例如vuex(pinia)等,根据个人实际情况进行添加即可;

最后还有就是一些在nodejs环境下使用的库,部分可能在开发环境下可能是没问题的,但是到了生产环境就会出问题,有的甚至在开发环境下都无法使用,这些要根据实际情况进行调整;

而这一部分超出了本文光速开发electron的原则了,因为要学习前端之外的知识了,例如SQLite就需要学习数据库相关的知识;

总结

elecrton最大的问题就是编译之后的文件以及文件路径的处理,需要增加一些配置或者修改部分库源码中的路径引用才能正常使用;

当然这些问题在本篇中完全不会出现,所以想快速体验electron并且不踩坑,我觉得我这篇内容完全够用;

作为一名前端开发者能快速体验桌面应用的开发,确实electron是一个都非常好的选择,这次分享就到这里;

相关推荐
志凌海纳SmartX几秒前
《SmartX ELF 虚拟化核心功能集》发布,详解 80+ 功能特性和 6 例金融实践
数据库·金融·架构·虚拟化·分布式存储·超融合·vmware替代
pan_junbiao8 分钟前
Vue使用axios二次封装、解决跨域问题
前端·javascript·vue.js
秋沐19 分钟前
vue3中使用el-tree的setCheckedKeys方法勾选失效回显问题
前端·javascript·vue.js
浮华似水36 分钟前
Yargs里的Levenshtein距离算法
前端
江喜原42 分钟前
微服务下设计一个注解标识是否需要登录
java·微服务·架构·登录
_.Switch1 小时前
Python Web 架构设计与性能优化
开发语言·前端·数据库·后端·python·架构·log4j
libai1 小时前
STM32 USB HOST CDC 驱动CH340
java·前端·stm32
Shall#1 小时前
Innodb存储架构
数据库·mysql·架构
Java搬砖组长2 小时前
html外部链接css怎么引用
前端
GoppViper2 小时前
uniapp js修改数组某个下标以外的所有值
开发语言·前端·javascript·前端框架·uni-app·前端开发