我正在参加「掘金·启航计划」
大家好,我是 Lvzl, 一个三年工作经验的前端小菜鸡,在掘金平台分享一些 平时学习的感悟 & 实际项目场景 的文章。
本文主要内容:通过安装一个npm包实现electron的简单集成。
前言
- 笔者对
electron开发只是小白水平, - 为什么要开发一个包实现集成?就是笔者突然的一个想法,然后动手实现了,仅此而已。
- 笔者说的集成只是最基础的集成(安装一个包,让你能够通过
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 这只是一个示例文件,看下文档对其描述
- style.css 样式文件
手动集成
我们现在的项目大都是基于 webpack、vite 这种构建工具的,分为开发环境 和 生产环境 等。所以集成方式也有所区别,在笔者看来手动集成应该是这样的:
-
生产环境:项目打包后都会生成一些压缩脚本、样式文件、还有
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脚本的添加。
集成思路
- 将
.electron文件夹复制到项目根目录 - 将
.electron.helper.json文件复制到项目根目录 - 往项目
package.json中添加四个script,稍后代码中看 - 第三部添加的脚本中用到了
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的一些钩子:
其中
postinstall指安装过后的钩子,在package.json中配置一下:
"scripts": {
"postinstall": "node helper/index.js"
},
看下效果:
安装依赖包:
可以看到,文件已复制,脚本已经生成。接着我们启动一下看看效果:
存在的问题
- 在开发环境时,考虑到通过
electron加载,如何实现文件热更新,笔者通过监听窗口的显示/隐藏,再次执行刷新动作,但这并不是热更新,哈哈哈。 - 写入脚本时,把启动开发环境的命令写死为
dev了,把打包的命令写死为build了。 - 生产的集成,感觉怪怪的,有经验的掘友可以指点一下。
总结
到此,咱们的需求就简单的实现了,感觉还行,只不过笔者对electron的了解很少,有些地方可能实现的不合理,只是笔者个人的想法。欢迎各位掘友踊跃发表看法。