Electron学习

Electron学习

一. Electron安装

自己从零开始,方便理解Electron的内部机制

js 复制代码
// 项目初始化
npm init -y
//  下载Electron  不用 Electron Forge
npm  install  --save-dev  electron@latest

二. 创建一个窗口

添加 index.html
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>hello world</h1>
</body>
</html>
添加 main.js
js 复制代码
// 导入 Electron 的核心模块
// app: 控制应用程序的生命周期事件
// BrowserWindow: 创建和控制浏览器窗口
const { app, BrowserWindow } = require('electron')
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
     // 创建一个新的浏览器窗口实例
  const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 600, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true  // 允许在渲染进程中使用 Node.js 功能
    }
  })
 // 加载应用程序的主页面
  // 这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
})
在package.json文件下scripts 添加运行命令

json 复制代码
{
  "name": "lectron",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "dependencies": {
    "electron": "^39.2.6"
  },
  "devDependencies": {},
  "scripts": {
    "start": "electron ."
  },
 
  "keywords": [],
  "author": "",
  "license": "ISC"
}

注意: package.json文件下 需要修改为 "main": "main.js", 要不然会报错。

运行命令

bash 复制代码
npm run  start

三. 打开调试者工具

mainWindow.webContents.openDevTools() // 打开开发者工具

js 复制代码
const { app, BrowserWindow } = require('electron')
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
  const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 600, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true  // 允许在渲染进程中使用 Node.js 功能
    }
  })

  mainWindow.webContents.openDevTools() // 打开开发者工具
  // 这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
})

四.热更新mian.js文件

因为每次修改mian.js文件 。需要关闭程序,重新运行命令。mian修改过的内容才生效。所以我们需要热更。不需要关闭程序,重新运行命令

nodemon文件监视工具,检测文件变化并自动重启进程

安装 nodemon

bash 复制代码
npm install  nodemon --save-dev
json 复制代码
// --watch main.js
监视 main.js文件的变化(Electron 主进程入口文件)
// --exec "electron ."
检测到变化时执行的命令:  electron .启动当前目录的 Electron 应用
"scripts": {
    "start": "electron .",
    "start:wacth": "nodemon --watch main.js --exec \"electron .\""
  },

五. Electron进程和线程

主进程Main Process

在 Electron应用都有一个单一的主进程,作为应用程序入口点。主进程在Node.js 运行。它具备所以Node.jsAPI的能力。

  • 窗口管理
  • 应用程序生命周期
  • 原生API
渲染进程Renderer Process

每个Electron应用会为每个打开的 BrowserWindow 生成一个单独的渲染进程。

渲染器无权直接访问require或其他Node.js API

进程间通信(IPC)

IPC通道名称

  • ipcMain
  • ipcRenderer

预加载脚本

为了将electron的不同类型的进程接在一起,我们需要使用被称为预加载preload的特殊脚本。

1.单向通信 - 从渲染器进程到主进程

使用ipcRenderer.send API发送消息,然后使用ipcMain.onAPI接收。

实现通过输入框修改应用标题

一. 添加输入框

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>hello world</h1>
    Title: <input id="title" />
    <button id="btn" type="button">Set</button>

    <h1 id="info"></h1>
  </body>
  <script src="./renderer.js"></script>
</html>

二. 添加renderer.js 编写 点击按钮事件和渲染Node版本号逻辑

js 复制代码
// 获取页面上 ID 为 'info' 的元素
const info = document.getElementById('info')
console.log(window.versions,'window.versions')
// 设置该元素的内容为版本信息
info.innerHTML = `Chrome (v${window.versions.chrome}), Node.js (v${window.versions.node})`
// 获取页面上 ID 为 'btn' 的按钮元素
const btn = document.getElementById('btn')
// 获取页面上 ID 为 'title' 的输入框元素
const titleInput = document.getElementById('title')
// 给按钮添加点击事件监听器
btn.addEventListener('click', () => {
     // 从输入框中获取用户输入的值
    const title = titleInput.value
     // 调用预加载脚本暴露的 window.electron.setTitle 方法
    // 该方法会通过 IPC 通知主进程设置窗口标题
    window.electron.setTitle(title)
    
   
  })

三. 预加载脚本(preload.js)

它使用contextBridge来安全地暴露一些Node.js和Electron的功能给渲染进程(即网页)。

代码中使用了三个exposeInMainWorld调用,分别暴露了'versions'、'electron'和'require'三个全局变量。

js 复制代码
// contextBridge: 安全地向渲染进程暴露 API 的桥梁
// ipcRenderer: 渲染进程与主进程通信的模块
const { contextBridge, ipcRenderer } = require("electron");
/**
 * 通过 contextBridge 向渲染进程暴露 Node.js 和 Chromium 版本信息
 * 安全地将只读数据暴露给 window.versions 对象
 */
contextBridge.exposeInMainWorld('versions', {
    node: process.versions.node, // Node.js 运行时版本号
    chrome: process.versions.chrome // Chromium 引擎版本号
  })
/**
 * 向渲染进程暴露自定义的 electron API
 * 创建一个安全的 window.electron 对象,包含 setTitle 方法
 * @param {string} title - 要设置的窗口标题
 */
contextBridge.exposeInMainWorld("electron", {
  setTitle: (title) => ipcRenderer.send("set-title", title),
});
/**
 * 危险操作!暴露 Node.js 的 require 函数到渲染进程
 * ⚠️ 严重安全警告:这会导致 XSS 漏洞和系统级攻击风险
 * 绝对不要在真实项目中这样做!
 */
contextBridge.exposeInMainWorld("require", require);

四.修改main.js

preload: path.join(__dirname, 'preload.js') // 预加载脚本路径 ipcMain.on('set-title',handlsSetTitle) // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件 handlsSetTitle 修改应用标题

js 复制代码
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

const createWindow = () => {
     // 创建一个新的浏览器窗口实例
   const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 700, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
      preload:path.join(__dirname, 'preload.js') // 预加载脚本
    }
  })
  mainWindow.webContents.openDevTools() // 打开开发者工具
  //  加载应用程序的主页面  这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
}
/**
 * 处理设置窗口标题的 IPC 消息
 * @param {Electron.IpcMainEvent} event - IPC 事件对象
 * @param {string} title - 要设置的新窗口标题
 */

const handlsSetTitle = (event, title) => {
    console.log('title', event, title)
     // 获取发送消息的 WebContents 对象
    const webContents = event.sender
     // 通过 WebContents 找到对应的 BrowserWindow 实例
    const win = BrowserWindow.fromWebContents(webContents)
    // 设置窗口标题
    win.setTitle(title)
    
}

// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
    // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
    ipcMain.on('set-title',handlsSetTitle)
    // 创建主窗口
    createWindow()
})
2.双向通信 - 从渲染器进程到主进程,再从主进程到渲染器进程
  • 使用ipcRender.invoke 进行发送
  • 使用ipcMain.handle 来进行响应
  • 它的第二个函数被用一个回调。然后返回值将作为Promise返回到最初的invoker调用
实现本地保存输入框内容,并且页面显示文件大小

一. 添加输入框

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>hello world</h1>
    <!-- 标题输入框 -->
    Title: <input id="title" />
    <button id="btn" type="button">Set</button>
    <h1 id="info"></h1>
    <!-- 文本输入 -->
    Content: <input id="content"/>
    <button id="btn2" type="button">Write</button>
    <h1 id="counter"></h1>
  </body>
  <script src="./renderer.js"></script>
</html>

二.在preload.js,添加点击事件

js 复制代码
const btn2 = document.getElementById('btn2')
const contentInput = document.getElementById('content')
const counter = document.getElementById('counter')
btn2.addEventListener('click', async () => {
    const content = contentInput.value
    const len = await window.electron.writeFile(content)
    console.log(len)
    counter.innerHTML = `文件大小: ${len}`
    const c = await fs.promises.readFile('test.txt', { encoding: 'utf-8' })
    counter.innerHTML += `文件内容: ${c}`
  })

三.在预设脚本preload.js ,使用ipcRender.invoke 进行发送

js 复制代码
contextBridge.exposeInMainWorld("electron", {
  setTitle: (title) => ipcRenderer.send("set-title", title),
  // 调用主进程的 writeFile 方法,并将内容作为参数传递
  writeFile: (content) => ipcRenderer.invoke('write-file', content), 
});

四.修改mian.js

pcMain.handle('write-file', handleWriteFile) // 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求

handleWriteFile //

js 复制代码
// 导入 Electron 的核心模块
// app: 控制应用程序的生命周期事件
// BrowserWindow: 创建和控制浏览器窗口
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const createWindow = () => {
     // 创建一个新的浏览器窗口实例
   const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 700, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
      preload:path.join(__dirname, 'preload.js') // 预加载脚本
    }
  })
  mainWindow.webContents.openDevTools() // 打开开发者工具
  //  加载应用程序的主页面  这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
}
/**
 * 处理设置窗口标题的 IPC 消息
 * @param {Electron.IpcMainEvent} event - IPC 事件对象
 * @param {string} title - 要设置的新窗口标题
 */

const handlsSetTitle = (event, title) => {
    console.log('title', event, title)
     // 获取发送消息的 WebContents 对象
    const webContents = event.sender
     // 通过 WebContents 找到对应的 BrowserWindow 实例
    const win = BrowserWindow.fromWebContents(webContents)
    // 设置窗口标题
    win.setTitle(title)
    
}

/**
 * 
 * @param {*} event 
 * @param {*} content 
 * @returns 
 */
async function handleWriteFile(event, content) {
    console.log('the content', content)
    await fs.promises.writeFile('test.txt', content)
    const stats = await fs.promises.stat('test.txt')
    return stats.size
  }
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
    // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
    ipcMain.on('set-title',handlsSetTitle)
// 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求
    ipcMain.handle('write-file', handleWriteFile) 
    // 创建主窗口
    createWindow()
})
3.单向通信- 从主进程到渲染进程
  • 使用win.webContets.send进行发送
  • 使用ipcRenderer.on接收

进入页面,页面开始每3秒加3

1.修改mian.js

js 复制代码
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const createWindow = () => {
     // 创建一个新的浏览器窗口实例
   const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 700, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
      preload:path.join(__dirname, 'preload.js') // 预加载脚本
    }
  })
  mainWindow.webContents.openDevTools() // 打开开发者工具
  //  加载应用程序的主页面  这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')

  return mainWindow
}
/**
 * 处理设置窗口标题的 IPC 消息
 * @param {Electron.IpcMainEvent} event - IPC 事件对象
 * @param {string} title - 要设置的新窗口标题
 */

const handlsSetTitle = (event, title) => {
    console.log('title', event, title)
     // 获取发送消息的 WebContents 对象
    const webContents = event.sender
     // 通过 WebContents 找到对应的 BrowserWindow 实例
    const win = BrowserWindow.fromWebContents(webContents)
    // 设置窗口标题
    win.setTitle(title)
    
}

/**
 * 
 * @param {*} event 
 * @param {*} content 
 * @returns 
 */
async function handleWriteFile(event, content) {
    console.log('the content', content)
    await fs.promises.writeFile('test.txt', content)
    const stats = await fs.promises.stat('test.txt')
    return stats.size
  }
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
    // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
    ipcMain.on('set-title',handlsSetTitle)
// 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求
    ipcMain.handle('write-file', handleWriteFile) 
    // 创建主窗口
    createWindow()



    let counter = 1
    const win = createWindow()
    // remote.enable(win.webContents)
    win.webContents.send('update-counter', counter)
    setInterval(() => {
      counter += 3
      win.webContents.send('update-counter', counter)
    }, 3000)
})
  1. index.html 添加显示数字元素

    html 复制代码
     <!-- 显示数字元素 -->
    
        <h1 id="time"></h1>
  2. renderer.js

    js 复制代码
    const time = document.getElementById('time')
    // 
      window.electron.onUpdateCounter((value) => {
        time.innerText = value.toString()
      })
  3. preload.js添加 onUpdateCounter

    js 复制代码
     */
    contextBridge.exposeInMainWorld("electron", {
      setTitle: (title) => ipcRenderer.send("set-title", title),
      // 调用主进程的 writeFile 方法,并将内容作为参数传递
      writeFile: (content) => ipcRenderer.invoke('write-file', content), 
      // 调用主进程的 readFile 方法,并将回调函数作为参数传递
      onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
    });

六. Electron Forge

Electron Forge 是一个用于打包和分发 Electron 应用程序的工具。 它将 Electron 的构建工具生态系统统一到一个可扩展的界面中,这样每个人都可以直接上手制作 Electron 应用。

它的亮点:

  • 📦 应用打包和代码签名
  • 🚚 Windows、macOS 和 Linux 上的可定制安装程序(DMG、deb、MSI、PKG、AppX 等)
  • ☁️ 云提供商(GitHub、S3、Bitbucket 等)的自动化发布流程
  • ⚡️ 易于使用的 Webpack 和 TypeScript 样板模板
  • ⚙️ 原生 Node.js 模块支持
  • 🔌 可扩展的 JavaScript 插件 API
初始化一个新的 Forge 项目
bash 复制代码
# my-app-electron 项目名称
#  --template=vite-typescript 模本用vite+typescript
npm init create-electron-app@latest my-app-electron1 -- --template=vite-typescript
# 进入项目
cd my-app-electron
#运行项目
npm start
结合vue3
bash 复制代码
# 安装vue
npm install vue
#安装vite识别vue插件
npm install --save-dev @vitejs/plugin-vue

七. 应用打包

macOS打包

需要安装 需通过Electron Forge@electron-forge/maker-dmg插件实现

bash 复制代码
npm install @electron-forge/maker-dmg

操作系统:仅能在macOS上打包DMG(因DMG是macOS专属格式)

forge.config.ts打包文件配置

ts 复制代码
import type { ForgeConfig } from '@electron-forge/shared-types';
// 每一种打包类型都设计一个单独的npm
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
// macOS
import { MakerDMG } from '@electron-forge/maker-dmg';
import { VitePlugin } from '@electron-forge/plugin-vite';
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import { FuseV1Options, FuseVersion } from '@electron/fuses';

const config: ForgeConfig = {
    // 基础打包配置
  packagerConfig: {
    asar: true,// 启用 ASAR 归档格式(将应用程序打包为单个文件)
  },
  rebuildConfig: {},
    // 制作包的工具
  makers: [
      // windows 安装包
    new MakerSquirrel({}),
         new MakerDMG({
      name: 'YourAppName', // DMG文件名(默认与packagerConfig.name一致)
      background: 'assets/dmg-background.png', // DMG窗口背景图(推荐1440x900像素)
      icon: 'assets/icon.icns', // DMG窗口中的应用图标(与packagerConfig.icon一致)
      iconSize: 80, // 图标大小(像素)
       format:'ULFO', // 使用ULFO格式,兼容性更好
      }
    })
    new MakerZIP({}, ['darwin']), // ZIP 压缩包生成器(排除 macOS 平台)
    new MakerRpm({}),// RPM 包生成器 (RedHat/CentOS/Fedora)
    new MakerDeb({}),// DEB 包生成器 (Debian/Ubuntu)
  ],
  plugins: [
    new VitePlugin({
      build: [
        {
          entry: 'src/main.ts', // 主进程入口文件
          config: 'vite.main.config.ts',// 主进程专用的 Vite 配置文件
          target: 'main',// 指定目标为 Electron 主进程
        },
        {
          entry: 'src/preload.ts', // 预加载脚本入口文件
          config: 'vite.preload.config.ts',
          target: 'preload',
        },
      ],
      renderer: [
        {
          name: 'main_window',
          config: 'vite.renderer.config.ts',
        },
      ],
    }),
    // Fuses are used to enable/disable various Electron functionality
    // at package time, before code signing the application
    new FusesPlugin({
      version: FuseVersion.V1,
      [FuseV1Options.RunAsNode]: false,
      [FuseV1Options.EnableCookieEncryption]: true,
      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
      [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
      [FuseV1Options.OnlyLoadAppFromAsar]: true,
    }),
  ],
};

export default config;

打包命令

bash 复制代码
# 1. package 命令
npm  run  package 
# 效果:生成可执行程序,但还不是安装包,直接双击就可以用
#2. make 命令
npm run make
# 效果: 生成完整的安装包,需要安装完毕以后使用

可执行程序和安装包区别

维度 可执行程序(package) 安装包(make)
运行方式 直接双击运行(无需安装) 需运行安装向导,将文件部署到系统目录
系统影响 不修改系统目录(或仅临时缓存) 写入系统目录、注册元数据、创建快捷方式
分发形式 单文件/文件夹(如 .exe.app 安装包(如 .msi.pkg.dmg
用户体验 适合快速测试或便携使用 适合正式发布,提供标准化安装/卸载流程

package打包文件结构

📂 输出目录结构(以 Windows 为例)

md 复制代码
out/
└── your-app-name-win32-x64/
    ├── your-app-name.exe          # 主可执行文件(Electron 封装)
    ├── resources/
    │   ├── app.asar               # 应用代码(ASAR 归档格式)
    │   ├── electron.asar          # Electron 运行时(可选)
    │   └── ...                    # 其他资源文件
    ├── node_modules/              # 生产依赖(仅限被引用的模块)
    ├── locales/                   # 多语言文件(如 zh-CN.pak)
    ├── swiftshader/               # GPU 渲染备用库(无显卡驱动时)
    └── ...                        # 其他平台相关文件

什么ASAR

一、ASAR 的本质:为什么用它?

传统 Electron 应用打包时,会将代码直接放在 resources/app目录下(明文可见),存在两个问题:

  1. 源码暴露风险:用户可直接查看/修改 JS/CSS 代码;
  2. 文件系统性能差:大量小文件读取效率低(尤其 Windows)。

ASAR 解决了这些问题:

  • 归档整合 :将分散的文件打包成单个 .asar文件,类似 ZIP 但更高效(无需解压即可随机访问);
  • 保护源码:虽然非加密(可用工具解压),但增加了直接阅读的门槛;
  • 提升性能:减少文件系统调用次数,加快应用启动速度。

二、ASAR 文件结构(以 app.asar为例)

ASAR 文件内部是一个虚拟文件系统 ,结构与你的项目目录一致(但排除了 devDependencies和无关文件)。例如,若你的项目结构如下:

csharp 复制代码
your-project/
├── package.json       # 生产依赖声明(仅保留 dependencies)
├── src/
│   ├── main.js        # 主进程代码
│   └── renderer/      # 渲染进程代码
│       ├── index.html
│       └── app.js
├── static/            # 静态资源(图片、字体等)
│   └── logo.png
└── node_modules/      # 仅包含被引用的生产依赖(精简后)

app.asar解压后的虚拟结构完全一致(但实际存储为二进制归档):

scss 复制代码
app.asar (虚拟目录)
├── package.json
├── src/
│   ├── main.js
│   └── renderer/
│       ├── index.html
│       └── app.js
├── static/
│   └── logo.png
└── node_modules/      # 精简后的生产依赖

三、ASAR 的核心操作(开发必备)

  1. 解压 ASAR 文件(查看/修改源码)

若需调试或修改打包后的代码,可先将 app.asar解压为明文目录:

bash 复制代码
# 安装 asar 工具(Electron 内置,也可单独安装)
npm install -g @electron/asar

# 解压 app.asar 到 unpacked 目录
asar extract out/your-app-win32-x64/resources/app.asar unpacked/

解压后,unpacked/目录即为明文的项目结构,可直接编辑代码。

  1. 重新打包为 ASAR

修改完成后,将明文目录重新打包为 app.asar

shell 复制代码
asar pack unpacked/ new-app.asar  # 将 unpacked/ 打包为 new-app.asar
# 替换原文件:cp new-app.asar out/your-app-win32-x64/resources/app.asar
  1. 禁用 ASAR(明文目录模式)

若需完全明文(如开发阶段调试),可在 forge.config.js中关闭 ASAR:

java 复制代码
// forge.config.js
module.exports = {
  packagerConfig: {
    asar: false,  // 禁用 ASAR,生成明文 app 目录(而非 app.asar)
  },
};

此时,package命令会在 resources/下生成 app/明文目录(而非 app.asar)。

相关推荐
L、2185 小时前
Flutter 与 OpenHarmony 跨端融合新范式:基于 FFI 的高性能通信实战
flutter·华为·智能手机·electron·harmonyos
500847 小时前
鸿蒙 Flutter 安全组件开发:加密输入框与脱敏展示组件
flutter·华为·electron·wpf·开源鸿蒙
L、2181 天前
性能调优实战:Flutter 在 OpenHarmony 上的内存、渲染与启动速度优化指南
javascript·华为·智能手机·electron·harmonyos
500841 天前
鸿蒙 Flutter 分布式硬件调用:跨设备摄像头 / 麦克风共享
分布式·flutter·华为·electron·wpf·开源鸿蒙
梦鱼1 天前
我踩了 72 小时的 Electron webview PDF 灰色坑,只为告诉你:别写这行代码!
前端·javascript·electron
song5011 天前
鸿蒙 Flutter 语音交互进阶:TTS/STT 全离线部署与多语言适配
分布式·flutter·百度·华为·重构·electron·交互
L、2181 天前
Flutter 与 OpenHarmony 的“共生进化论”:从技术融合到生态共建
javascript·flutter·华为·智能手机·electron·harmonyos
500841 天前
鸿蒙 Flutter 分布式数据同步:DistributedData 实时协同实战
分布式·flutter·华为·electron·开源·wpf·音视频
北极象1 天前
Electron 通用技术架构分析
javascript·架构·electron