手把手光速开发 Electron

手把手光速开发 Electron

你认为开发Electron需求先会一堆东西才干上手吗?其实你只需求会HTMLCSSJavaScript就能够了;

废话不多说,直接上手,让咱们一同来开发一个Electron运用吧!

1. 创立项目

首要,咱们需求创立一个项目,这儿咱们直接运用现成的模板,这样能够省去许多装备的麻烦;

模板地址:electron-vite

这个地址里边有一个项目create-electron-vite,能够看他的README,只需求一行指令就能够创立一个Electron项目:

手把手光速开发 Electron

# 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也是能够的,就不必每次都自己手动去设置了;

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

# 官方镜像
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

当然就算这样有或许仍是会失利,你或许会遇到如下的过错:

手把手光速开发 Electron

这个原因是由于网络的问题,你能够测验ping一下github.com试试看,这个时分就会呈现这个IP地址;

这个时分咱们或许直接运用浏览器是能够访问的,可是装置依靠和ping便是不想,处理计划也很简单,便是去修正host文件;

咱们先能够去这个网站:ping.chinaz.com;

然后它会呈现许多github的独立IP,咱随意找一个ping一下,能够ping通就运用这个IP

Windows体系的host文件在:C:WindowsSystem32driversetc下面,找到host文件用记事本翻开,在最下面新起一行,加上你能pingIP,如下:

140.82.112.3 github.com

保存之后重新装置依靠,假如没收效的话能够在控制台输入如下指令,刷新DNS

ipconfig /flushdns

假如仍是不可的话,那么主张换手机网络试试看。

3. 讲点细节和留意事项

移除 TS

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

其实主要是我用不惯TS,我最开始用这个结构自身是想着给TS延续下去的,可是呈现了几个很严重的问题;

一个是项目文件多了之后十分吃功能,或许是我电脑废物;

第二是开发环境下没有抛出任何反常,打包死活都不可,每个文件都有问题,处理了许多问题之后仍是不可,查阅了大量的材料,离谱的是有一次修正装备之后成功了,然后第二天又不可了;

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

移除TS首要需求卸载TS相关的依靠:

npm uni typescript vue-tsc

然后翻开package.json,将其间的scripts特点下的build修正成如下:

{
  "scripts": {
    "build": "vite build && electron-builder"
  }
}

接着删去一切的*.d.ts文件和tsconfig.json等相关文件:

手把手光速开发 Electron

然后修正一切后缀为.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阶段;

手把手光速开发 Electron

咱们直接点上面截图供给的链接,然后下载对应的包,下载完结之后,直接将这个包放到:C:Users[User]AppDataLocalelectronCache目录下即可;

留意:需求先将打包的进程中止,再将包放到对应的目录下,不然包会被删去;

然后再次履行npm run build,这个时分还会有一个electron-builder包的下载,相同的操作;

不过electron-builder下面的包需求先解压出来,例如winCodeSign就需求解压到C:Users[User]AppDataLocalelectron-builderCachewinCodeSign目录下;

最终的目录应该是:C:Users[User]AppDataLocalelectron-builderCachewinCodeSignwinCodeSign-2.6.0

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

手把手光速开发 Electron

然后指令成功之后,项意图根目录下就会生成一个release的目录,下面的YourAppName-Windows-0.0.0-Setup.exe便是咱们的装置包了,能够装置起来看看作用吧!

4.1 打包装备

打包是经过electron-builder来完结的,详细的装备能够参阅官方文档:www.electron.build/

这儿的项目根目录下有一个electron-builder.json5的装备文件,这个便是运用打包装备,能够依据自己的需求来修正;

详细的一些装备能够参阅这篇文章,写的十分详细:electron-builder通用装备(翻译)

5. 开发

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

手把手光速开发 Electron

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

然后便是electron的代码,就两个文件,一个是main.js,一个是preload.js,十分简练;

由于咱们是光速开发,所以这儿就不过多解说electron的相关知识了,可是中心怎样运用的必定会给咱们理清楚;

5.1 electron 相关

5.1.1 main.js

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

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相关的,这儿主要看最最初的一些代码:

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的能力, 就需求在上面说到的创立浏览器窗口那里添加如下装备:
        {
            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中有这样一段代码:

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中有这样一段代码:

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

烘托进程假如想向主进程发送音讯也是相同的,能够运用sendinvokesendSyncpostMessage

而主进程处理不同的音讯会运用不同的函数,如下:

// 烘托进程发送音讯
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中的创立窗口的时分添加一个装备:

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中添加如下代码:

window.onload = () => {
    ipcRenderer.send('on-ready')
}

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

import { app, BrowserWindow, ipcMain } from 'electron'
// ...
ipcMain.on('on-ready', () => {
    win.show()
})

依照这个思路,例如咱们在Vue项目中优化白屏,能够在根组件挂载完结之后再去触发窗口的显现;

可是这个项目自身就有一个loading的作用,在src/main.js中能够看到有封闭这个loading的通知,假如你不想要这个loading作用就能够替换成上面说到的优化计划:

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

能够看到这个头部或许并不是咱们想要的,包括原生供给的菜单也是相同的,可是electron并没有供给相关的装备来修正,假如想要自定义就只能封闭然后自己写一个;

咱们能够在创立窗口的时分,添加如下装备,来封闭原生的菜单:

win = new BrowserWindow({
    // ...
    frame: false,
})

封闭之后咱们就需求自己去写顶部区域了,这个就作为写一般前端项目就行了,主要是顶部栏固定显现就好;

我这儿运用的Vue3,所以就依照Vue3的做法来了,顶部固定那么中间的内容改变,最好的做法必定是运用vue-router了;

首要装置依靠,十分熟悉的环节:

npm install vue-router@4 -D

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

import {createRouter, createWebHashHistory} from 'vue-router'
const routes = []
const router = createRouter({
    history: createWebHashHistory(),
    routes,
});
export default router

接着在src/main.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

<template>
  <router-view/>
</template>
<script setup>
</script>
<style scoped>
</style>

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

html,
body {
  margin: 0;
  padding: 0;
}

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

  • Layout.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
<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
<template>
  <div class="top-bar">
    <label>{{ title }}</label>
    <i class="fa-solid fa-window-minimize" @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下一切的文件放到assetswebfonts下,最终将all.min.csssrc/main.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装备一下就功德圆满:

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中加上如下装备:

// 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插件,咱们先装置它:

npm i -D unplugin-auto-import

然后在vite.config.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是一个都十分好的挑选,这次共享就到这儿;