你还在手动集成Electron?我开发了一个包,安装即集成

439 阅读5分钟

我正在参加「掘金·启航计划」

大家好,我是 Lvzl, 一个三年工作经验的前端小菜鸡,在掘金平台分享一些 平时学习的感悟 & 实际项目场景 的文章。

本文主要内容:通过安装一个npm包实现electron的简单集成。

前言

  1. 笔者对electron开发只是小白水平,
  2. 为什么要开发一个包实现集成?就是笔者突然的一个想法,然后动手实现了,仅此而已。
  3. 笔者说的集成只是最基础的集成(安装一个包,让你能够通过electron的能力打开系统的窗口并显示项目首页)

electron快速入门

electron的官方文档中,点击快速入门,提到带我们实现一个 Hello World 的简单示例,类似electron/electron-quick-start。于是笔者就没有继续读文档了,就把这个示例 clone 下来了。示例中主要的文件如下图:

package.json

{
  "name": "electron-quick-start",
  "version": "1.0.0",
  "description": "A minimal Electron application",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "repository": "https://github.com/electron/electron-quick-start",
  "keywords": [
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],
  "author": "GitHub",
  "license": "CC0-1.0",
  "devDependencies": {
    "electron": "^21.1.1"
  }
}
  • main.js 入口文件,当执行 electron . 时,会根据package.json中的main配置为入口,当然也可以自己指定入口文件。
  • index.html 是加载的首页
  • preload.js 预加载脚本(通过预加载脚本从渲染器访问Node.js)
  • renderer.js 这只是一个示例文件,看下文档对其描述 image.png
  • style.css 样式文件

手动集成

我们现在的项目大都是基于 webpackvite 这种构建工具的,分为开发环境 和 生产环境 等。所以集成方式也有所区别,在笔者看来手动集成应该是这样的:

  • 生产环境:项目打包后都会生成一些压缩脚本、样式文件、还有index.html,手动改改electron的入口文件就好了。然后打包为桌面应用。

  • 开发环境:开发环境我们的访问地址不是index.html,而是对应的IP + PORT,看了下文档发现有个loadUrl,可以填写URL,那简单集成也就不难了。笔者不过多描述了,感兴趣的掘友可以自己去试试。

// 在主进程中.
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 600 })

// Load a remote URL
win.loadURL('https://github.com')

// Or load a local HTML file
win.loadFile('index.html')

接下来,将介绍笔者如何实现一个npm包,来达到简单集成的效果。

实现npm包集成electron

npm包的目录结构:

  • .electron 文件夹下的就是上面见到的那些文件,笔者弄了两个入口文件分别对应开发和生产,见名知意。
  • file-handler.js就是一些对文件的操作方法。
  • index.js 是负责文件的复制,写入,以及package.json脚本的添加。

集成思路

  1. .electron 文件夹复制到项目根目录
  2. .electron.helper.json 文件复制到项目根目录
  3. 往项目package.json 中添加四个script,稍后代码中看
  4. 第三部添加的脚本中用到了npm-run-all这个包,因此判断项目本身依赖有没有这个包,没有就自动安装。

具体实现

.electron.helper.json 配置文件

{
  "SERVER_URL": "http://localhost:8000/", // 开发环境访问地址
  "BUILD_OUT_DIR": "dist" // 打包输出目录
}

main.dev.js

开发文件入口文件,打开窗口时加载.electron.helper.json 配置文件中配置的开发环境地址。监听窗口的显示/隐藏,再次执行刷新动作。

const { app, BrowserWindow } = require('electron')
const path = require('path')
const helperConfig = require('../.electron.helper.json')

let showTime = 0
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    },
    show: false
  })
  mainWindow.once('ready-to-show', () => {
    mainWindow.show()
  })
  mainWindow.on('show', () => {
    // 开发环境,每次显示时先刷新一下
    showTime++
    showTime > 1 && mainWindow.loadURL(helperConfig.SERVER_URL)
  })
  // 加载开发环境地址
  mainWindow.loadURL(helperConfig.SERVER_URL)
}

app.whenReady().then(() => {
  createWindow()
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

main.prod.js

// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron')
const path = require('path')
const helperConfig = require('../.electron.helper.json')

function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    },
    show: false
  })
  mainWindow.once('ready-to-show', () => {
    mainWindow.show()
  })
  mainWindow.loadFile(`../${helperConfig.BUILD_OUT_DIR}/index.html`)
}
app.whenReady().then(() => {
  createWindow()
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})
app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

file-handler.js

该文件中实现了文件/目录的复制,json文件的格式化写入。

const { readdirSync, statSync, writeFileSync, mkdirSync, readFileSync } = require('fs')
// 路径处理
const PATH = require('path')

const REPLACE_URL = '/node_modules/electron-vite-helper/helper'

const REPLACE_URL2 = '/node_modules/electron-vite-helper'

function read(currentPath, res) {
  const dirs = readdirSync(PATH.resolve(currentPath))
  dirs.forEach(dir => {
    const current = currentPath + '/' + dir
    const fileStatus = statSync(current)
    if (fileStatus.isDirectory()) {
      res.push({ type: 'dir', path: current })
      read(current, res)
    } else {
      res.push({ type: 'file', path: current })
    }
  })
}

function genFilePathArray(resourcePath) {
  const res = []
  read(resourcePath, res)
  return res
}

// 写入文件
function writeFiles(filePathArray, targetDirName) {
  targetDirName && mkdirSync(process.cwd().replace(REPLACE_URL2, '') + '/' + targetDirName)
  filePathArray.forEach(file => {
    const { type, path } = file
    if (type === 'file') {
      const data = readFileSync(path)
      writeFileSync(path.replace(REPLACE_URL, ''), data)
    } else {
      mkdirSync(path.replace(REPLACE_URL, ''), { recursive: true })
    }
  })
}
// 复制文件
function copyFile(filepath) {
  writeFiles([{ type: 'file', path: filepath }])
}
// 复制目录
function copyDir(path, target) {
  const filePathArray = genFilePathArray(path)
  writeFiles(filePathArray, target)
}
// 写入json文件,注意JSON.stringify的配置(实现自动加入空格)
function writeJsonFile(filepath, data) {
  writeFileSync(filepath, JSON.stringify(data, null, 2))
}

module.exports = {
  writeJsonFile,
  copyDir,
  REPLACE_URL2,
  copyFile
}

index.js

集成的主要逻辑就在这个文件中,思路应该还算清晰,添加了注释:

// #!/usr/bin/env node
const { REPLACE_URL2, copyFile, copyDir, writeJsonFile } = require('./file-handler.js')

const path = require('path')

const ChildProcess = require('child_process')
// 复制目录
copyDir(path.resolve(__dirname, './.electron'), '.electron')
// 复制文件
copyFile(path.resolve(__dirname, './.electron.helper.json'))
// 写入脚本
const scripts = {
  'electron:dev': 'electron .electron/main.dev.js',
  'electron:prod': 'electron .electron/main.prod.js',
  'both:dev': 'run-p dev electron:dev',
  'both:prod': 'run-p build:pro electron:prod'
}
const CWD = process.cwd().replace(REPLACE_URL2, '')
const JSON_FILE_PATH = CWD + '/package.json'

const data = require(JSON_FILE_PATH)
// 判断项目本身依赖有没有 npm-run-all 这个包
const NPM_RUN_ALL_INSTALLED =
  (Reflect.has(data, 'dependencies') && Reflect.has(data.dependencies, 'npm-run-all')) || (Reflect.has(data, 'devDependencies') && Reflect.has(data.devDependencies, 'npm-run-all'))

Object.keys(scripts).forEach(script => {
  data.scripts[script] = scripts[script]
})

writeJsonFile(JSON_FILE_PATH, data)
// 如果没有,安装
if (!NPM_RUN_ALL_INSTALLED) {
  ChildProcess.exec('npm install npm-run-all -D', { cwd: CWD })
}

还剩最后一步,如何实现安装这个包的时候,自动去执行 index.js 这个文件呢? 笔者查阅npm文档发现了npm install的一些钩子: image.png 其中postinstall指安装过后的钩子,在package.json中配置一下:

"scripts": {
    "postinstall": "node helper/index.js"
},

看下效果:

安装依赖包: 1.gif

可以看到,文件已复制,脚本已经生成。接着我们启动一下看看效果:

1.gif

存在的问题

  1. 在开发环境时,考虑到通过electron加载,如何实现文件热更新,笔者通过监听窗口的显示/隐藏,再次执行刷新动作,但这并不是热更新,哈哈哈。
  2. 写入脚本时,把启动开发环境的命令写死为 dev 了,把打包的命令写死为 build 了。
  3. 生产的集成,感觉怪怪的,有经验的掘友可以指点一下。

总结

到此,咱们的需求就简单的实现了,感觉还行,只不过笔者对electron的了解很少,有些地方可能实现的不合理,只是笔者个人的想法。欢迎各位掘友踊跃发表看法。

往期内容

Vue3指令——搜索框输入防抖实现

我是怎么开发一个Babel插件来实现项目需求的?

用css动画写一个“灵动岛”玩玩吧

Node.js 连接 MySql 统计组件属性的使用情况