手把手光速开发 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是一个都非常好的选择,这次分享就到这里;

相关推荐
zhougl99635 分钟前
html处理Base文件流
linux·前端·html
花花鱼39 分钟前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_42 分钟前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo2 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!5 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷6 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript