Sakurairo

加载中...

首页文章专题归档关于友链

主题

返回文章列表
跨平台开发

Electron入门笔记-线程篇

众所周知Electron是基于Chromium,所以某种程度上可以算是一个小浏览器(?),这就形成了我们现在需要了解的一个多线程模型。 首先,我们讲讲Chrome里的多线程模型: 早期的浏览器都是使用单个进程来处理每一个标签页的所有功能,导...

2024-10-253 分钟 54 阅读

发布于 2024-10-25 05:57

·

作者:八云澈

众所周知Electron是基于Chromium,所以某种程度上可以算是一个小浏览器(?),这就形成了我们现在需要了解的一个多线程模型。

首先,我们讲讲Chrome里的多线程模型: 早期的浏览器都是使用单个进程来处理每一个标签页的所有功能,导致当一个标签页出现崩溃或者无响应的情况时,整个浏览器都面临崩溃或无响应。为了解决这个问题,Chrome开发团队使用多线程来处理每一个标签标签页,同时利用一个主进程(浏览器进程)来处理整个浏览器的生命周期。

Electron也继承了这一思想(还是Chromium 一直是Chromium),每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。而主线程主要起到的就是创建应用程序窗口和与原生api进行交互。

而我们渲染则调用的是渲染进程(类似与标签页),这就给我们一个使用框架进行开发的机会,不过有一个非常大的问题,在设计上渲染进程是无权访问require或者其他的nodeapi,所以这就引出下一个我们需要了解的概念:Preload脚本

在Preload脚本中包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。所以我们是能在Preload脚本中启动vite、webpack或者是node(Production)以实现框架热更新和部署的。

在项目中,我们一般的最佳实践是分为两个js文件,分别是主线程的main.js和调用node api的preload.js。渲染线程也就是页面专门放在renderer文件夹中。

主线程:

js
1//main.js2 3import { app, shell, BrowserWindow, ipcMain, ipcRenderer, utilityProcess } from 'electron'4 5import { join } from 'path'6 7import { electronApp, optimizer, is } from '@electron-toolkit/utils'8 9import icon from '../../resources/icon.svg'10 11import { dir } from 'console'12 13const fs = require('fs')14 15const path = require('path')16 17function createWindow() {18 19  // Create the browser window.20 21  const mainWindow = new BrowserWindow({22 23    width: 900,24 25    height: 670,26 27    show: false,28 29    autoHideMenuBar: true,30 31    ...(process.platform === 'linux' ? { icon } : {}),32 33    webPreferences: {34 35      preload: join(__dirname, '../preload/index.js'),36 37      sandbox: false38 39    }40 41  })42 43  44 45  mainWindow.on('ready-to-show', () => {46 47    mainWindow.show()48 49  })50 51  52 53  mainWindow.webContents.setWindowOpenHandler((details) => {54 55    shell.openExternal(details.url)56 57    return { action: 'deny' }58 59  })60 61  62 63  // HMR for renderer base on electron-vite cli.64 65  // Load the remote URL for development or the local html file for production.66 67  if (is.dev && process.env['ELECTRON_RENDERER_URL']) {68 69    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])70 71  } else {72 73    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))74 75  }76 77  78 79  //启动新的子线程来运行本地js脚本80 81  //捕获脚本的log并保存在脚本路径82 83  // 捕获脚本的运行状态包括错误84 85  ipcMain.handle('run-script', (event, scriptPath) => {86 87    const { fork } = utilityProcess88 89    const child = fork(scriptPath, [], { silent: true })90 91    //将运行状态返回给渲染进程92 93    mainWindow.webContents.send('script-running', true)94 95    console.log('成功将运行状态返回给渲染进程')96 97    // 将子进程的输出日志流式保存至文件98 99    // 文件路径为脚本路径加上时间戳加上.log后缀100 101    const logPath = scriptPath + '-' + Date.now() + '.log'102 103    const logStream = fs.createWriteStream(logPath, { flags: 'a' })104 105    child.stdout.pipe(logStream)106 107    child.stderr.pipe(logStream)108 109  110 111    // 将子进程的运行状态返回给渲染进程112 113    child.on('message', (message) => {114 115      mainWindow.webContents.send('script-message', message)116 117      console.log(message)118 119    })120 121  122 123    child.on('error', (error) => {124 125      mainWindow.webContents.send('script-error', error)126 127      // 将错误保存到脚本路径128 129      const errorPath = scriptPath + '-' + Date.now() + '.error'130 131      fs.appendFile(errorPath, error, (err) => {132 133        if (err) {134 135          console.error('Failed to save error:', err)136 137        } else {138 139          console.log('Error saved to', errorPath)140 141        }142 143      })144 145      console.error(error)146 147    })148 149    child.on('exit', (code) => {150 151      mainWindow.webContents.send('script-exit', false)152 153      console.log(`子进程退出,退出码 ${code}`)154 155    })156 157  })158 159}160 161  162 163// This method will be called when Electron has finished164 165// initialization and is ready to create browser windows.166 167// Some APIs can only be used after this event occurs.168 169app.whenReady().then(() => {170 171  // Set app user model id for windows172 173  electronApp.setAppUserModelId('com.electron')174 175  176 177  // Default open or close DevTools by F12 in development178 179  // and ignore CommandOrControl + R in production.180 181  // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils182 183  app.on('browser-window-created', (_, window) => {184 185    optimizer.watchWindowShortcuts(window)186 187  })188 189  190 191  // IPC test192 193  ipcMain.on('ping', () => console.log('pong'))194 195  196 197  createWindow()198 199  200 201  app.on('activate', function () {202 203    // On macOS it's common to re-create a window in the app when the204 205    // dock icon is clicked and there are no other windows open.206 207    if (BrowserWindow.getAllWindows().length === 0) createWindow()208 209  })210 211})212 213  214 215// Quit when all windows are closed, except on macOS. There, it's common216 217// for applications and their menu bar to stay active until the user quits218 219// explicitly with Cmd + Q.220 221app.on('window-all-closed', () => {222 223  if (process.platform !== 'darwin') {224 225    app.quit()226 227  }228 229})230 231  232 233// In this file you can include the rest of your app"s specific main process234 235// code. You can also put them in separate files and require them here.236 237  238 239//监听渲染进程的事件,调用dialog获取文件夹信息240 241ipcMain.handle('open-directory-dialog', async (event) => {242 243  console.log('open-directory-dialog')244 245  const { dialog } = require('electron')246 247  try {248 249    const result = await dialog.showOpenDialog({250 251      properties: ['openDirectory']252 253    })254 255    return result.filePaths[0]256 257  } catch (error) {258 259    console.log(error)260 261  }262 263})264 265  266 267//监听渲染进程的事件,从渲染进程中获取到本地文件夹路径,遍历获取文件夹中的js文件名称(过滤node_modules、.git文件夹)268 269//组成文件名称,文件路径对象数组 ,返回给渲染进程270 271  272 273ipcMain.handle('get-script-list', async (event, dirPath) => {274 275  console.log('get-script-list')276 277  278 279  const getJSFiles = (dirPath) => {280 281    let jsFiles = []282 283  284 285    // 读取目录内容286 287    const files = fs.readdirSync(dirPath)288 289  290 291    for (const file of files) {292 293      const filePath = path.join(dirPath, file)294 295      const stat = fs.statSync(filePath)296 297  298 299      if (stat.isDirectory()) {300 301        // 如果是目录,递归调用302 303        if (file !== 'node_modules' && file !== '.git') {304 305          if (file !== 'dist') {306 307            jsFiles = jsFiles.concat(getJSFiles(filePath))308 309          }310 311        }312 313      } else if (path.extname(file).toLowerCase() === '.js') {314 315        // 如果是 JS 文件,添加包含路径和名称的对象到结果数组316 317        jsFiles.push({318 319          path: filePath,320 321          name: file322 323        })324 325      }326 327    }328 329  330 331    return jsFiles332 333  }334 335  try {336 337    // const files = fs.readdirSync(dirPath)338 339    // 深度查找所有的js文件340 341    const scriptList = getJSFiles(dirPath)342 343    return scriptList344 345  } catch (error) {346 347    console.log(error)348 349  }350 351})352 353ipcMain.handle('read-file', async (event, filePath) => {354 355  try {356 357    let fs = require('fs').promises // 确保使用 fs.promises358 359    const bufferContent = await fs.readFile(filePath, () => {}, { encoding: 'utf8' })360 361    const content = bufferContent.toString('utf-8')362 363    return content364 365  } catch (error) {366 367    console.error('Error reading file:', error)368 369    throw error // 处理错误370 371  }372 373})374 375//修改后的脚本内容保存到本地376 377  378 379ipcMain.handle('save-script', async (event, filePath, content) => {380 381  try {382 383    let fs = require('fs').promises // 确保使用 fs.promises384 385    await fs.writeFile(filePath, content, 'utf8')386 387    return true388 389  } catch (error) {390 391    console.error('Error writing file:', error)392 393  394 395    return false396 397  }398 399})400 401  402 403//监听渲染进程的事件,获取传入的脚本路径中文件夹的最新log文件,返回给渲染进程404 405// @params scriptPath 脚本路径406 407ipcMain.handle('get-log-file', async (event, scriptPath) => {408 409  const dirPath = path.dirname(scriptPath)410 411  //获取文件夹中的所有log文件412 413  const files = fs.readdirSync(dirPath)414 415  // 利用修改时间筛选出最新的log文件416 417  const logFiles = files.filter((file) => path.extname(file).toLowerCase() === '.log')418 419  const latestLogFile = logFiles.sort(420 421    (a, b) =>422 423      fs.statSync(path.join(dirPath, b)).mtime.getTime() -424 425      fs.statSync(path.join(dirPath, a)).mtime.getTime()426 427  )[0]428 429  // 返回文件内容430 431  return latestLogFile432 433})

渲染线程:

js
1//preload.js2 3import { contextBridge } from 'electron'4 5import { electronAPI } from '@electron-toolkit/preload'6 7  8 9// Custom APIs for renderer10 11const api = {}12 13  14 15// Use `contextBridge` APIs to expose Electron APIs to16 17// renderer only if context isolation is enabled, otherwise18 19// just add to the DOM global.20 21if (process.contextIsolated) {22 23  try {24 25    contextBridge.exposeInMainWorld('electron', electronAPI)26 27    contextBridge.exposeInMainWorld('api', api)28 29  } catch (error) {30 31    console.error(error)32 33  }34 35} else {36 37  window.electron = electronAPI38 39  window.api = api40 41}

最后,我们来讲讲效率进程,作为主进程生成多个子进程UtilityProcess API。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。 效率进程可用于托管,例如:不受信任的服务, CPU 密集型任务或以前容易崩溃的组件 托管在主进程或使用Node.jschild_process.fork API 生成的进程中。 效率进程和 Node 生成的进程之间的主要区别.js child_process模块是实用程序进程可以建立通信 通道与使用MessagePort的渲染器进程。 当需要从主进程派生一个子进程时,Electron 应用程序可以总是优先使用 效率进程API 而不是Node.js child_process.fork API。

最佳实践:

js
1// 主进程2const { app, utilityProcess } = require('electron')3 4app.whenReady().then(() => {5  // 创建进程池管理多个utility process6  const processPool = []7  8  function createUtilityProcess() {9    const utility = utilityProcess.fork('worker.js', [], {10      stdio: 'pipe',11      serviceName: `worker-${processPool.length}`12    })13 14    // 错误处理15    utility.on('error', (err) => {16      console.error('Utility process error:', err)17    })18 19    // 退出处理20    utility.on('exit', (code) => {21      console.log(`Worker exited with code ${code}`)22      // 从进程池中移除23      const index = processPool.indexOf(utility)24      if (index > -1) {25        processPool.splice(index, 1)26      }27    })28 29    processPool.push(utility)30    return utility31  }32 33  // 使用示例34  const worker = createUtilityProcess()35  worker.postMessage({ type: 'START_TASK', data: {...} })36})
八云澈

Writer · Recorder

八云澈(Bayunche)

开发者 / 写作者 / 普通生活记录者 · 中国 · 深圳

喜欢把工作里的技术问题、读书时的触动、生活中的琐碎观察慢慢写下来。既想把复杂问题讲清楚,也想留住那些很快会被忘掉的日常。

Java 后端Go 工程化读书札记生活记录日常随笔

Continue Reading

相关文章

更多文章

上一篇

Electron入门笔记-简介篇

2024-10-25

下一篇

Electron入门笔记-通信篇

2024-10-25

评论区

读完这篇文章后,如果你也有类似经验或不同看法,欢迎继续交流。

💬 评论