当前位置: 首页 > news >正文

记一次electron开发插件市场遇到的问题

序言

从写代码开始,看到那些允许人家开发自己的插件嵌入到应用中的且自由度很高的软件就觉得很厉害,如VSCode,刚好一直都在研究electron-vite 制作了一个用来可以写在简历上的项目

项目地址 羊驼的工具箱

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

初衷

1.开发这个小项目的初衷是为了研究electron-vite 研究多窗口代码 并且因为经常有一个问题就是项目文件夹太杂了 放在电脑的各个地方 或者随便命名了一个 找不到项目文件夹太麻烦
2. 要进到对应文件夹npm run 而且一个项目要启动一个后端代码+前端代码 且控制台太散乱了
3.做这个项目的时候 做完的时候 群友说 这个和utools有点像 我就看了一下utools 发现它是以插件的形式去加载 我就想到 那我的项目也能这样做吗

实现

说干就干 开始改造项目 添加插件管理页面

在这里插入图片描述

思路

  1. 定义目录plugins在不会被打包的目录resources下
  2. 定义插件json配置
  3. 启动时扫描插件目录获取所有配置文件
  4. 根据插件配置中的启用 判断是否是有效配置 去加载插件
  5. 正常插件:以约定好的方式 加载界面文件(注入默认的预加载脚本 封装代码 防止注册事件影响到主进程已注册的) 加载入口脚本文件到主进程中 注入electron api 以及窗口对象 并return一个unload 卸载事件的脚本
  6. 开发中的插件:选中正在开发的插件目录 配置有效性检测 检测配置中界面是端口开发还是本地文件开发 加载脚本注入参数 监听文件变动:界面端口运行不检测vue tsx文件 默认不检测static以及node_modules文件夹变动 每次文件变动卸载插件+加载插件 热更新保证效果

实现思路

已记载插件列表字典+加载插件函数+卸载插件函数 +开发插件对象(插件配置、插件路径、文件变动监听器(chokidar))
注册事件
1.获取插件列表:用于加载插件 已加载的不处理 未加载的添加进插件列表里
2.插件设置:开启或关闭插件
3.开发插件开启:选择目录后开启开发插件模式
4.开发插件关闭:关闭开发插件模式
函数
加载插件函数:根据模式 创建对应的操作 是否要监听文件变动 以及窗口加载界面的方式是读取本地还是窗口 发起更新列表请求
卸载插件函数:调用注入脚本的upload函数并且移除窗口 发起更新列表请求

核心代码

        let PLUGINS = {}let DEV_PLUGINS = nulllet DEV_PLUGINS_PATH = nulllet DEV_PLUGINS_WATCH = nulllet PLUGIN_SETTING = "setting.json"// 卸载插件const unLoadPlugin = (setting) => {if (!PLUGINS[setting.name]) returnPLUGINS[setting.name].js && PLUGINS[setting.name].js.unload && PLUGINS[setting.name].js.unload()killWin(PLUGINS[setting.name].win)PLUGINS[setting.name] = nullDataListener.broadcast(api.UPDATE.TOOLS)}// 加载插件const LoadPlugin = async (setting, dev = false) => {if (PLUGINS[setting.name]) returnlet type = setting.typeif (type == "window") {let electron = setting.electron || { width: 400, height: 300, webPreferences: {} }electron.webPreferences.preload = path.join(__dirname, '../../resources/js/preload.mjs')let win = new BrowserWindow({ ...electron, parent: Application.window })win.on("ready-to-show", () => {electron.center && win.center()})win.on("closed", () => {!dev && unLoadPlugin(setting)})win.setMenu(null)if (dev) {DEV_PLUGINS_WATCH && DEV_PLUGINS_WATCH.close()let ignored = [path.join(DEV_PLUGINS_PATH, "node_modules"), path.join(DEV_PLUGINS_PATH, "static"), path.join(DEV_PLUGINS_PATH, "*.mjs")]let dev = setting.dev || { local: true, port: null, devTools: true }if (dev.local) {DEV_PLUGINS_WATCH = chokidar.watch(DEV_PLUGINS_PATH, { ignored })win.loadFile(path.join(DEV_PLUGINS_PATH, setting.main))} else {//TODO 特殊处理preloadignored.push(path.join(DEV_PLUGINS_PATH, "/*.vue"))ignored.push(path.join(DEV_PLUGINS_PATH, "/*.tsx"))DEV_PLUGINS_WATCH = chokidar.watch(DEV_PLUGINS_PATH, { ignored })win.loadURL(`http://127.0.0.1:${dev.port}`)}DEV_PLUGINS_WATCH.on("change", (et, fn) => {unLoadPlugin(setting)LoadPlugin(setting, true)})dev.devTools && win.webContents.openDevTools()} else {win.loadFile(path.join(PLUGINS_PATH, setting.name, setting.main))}electron.bounds && !win.setBounds(electron.bounds)PLUGINS[setting.name] = {win,js: await LoadScript(setting, win)}}DataListener.broadcast(api.UPDATE.TOOLS)}

插件主进程入口函数

import moment from "moment"
export default async function (electron, window) {let test = await LoadScript("./static/test.js")let { ipcMain } = electronconsole.log(moment);console.log(test);let events = {"check-nvm": (event) => {}}for (let kv in events) {ipcMain.handle(kv, events[kv])}return {unload() {for (let kv in events) {// console.log(1);ipcMain.removeHandler(kv)}}}
}

在这里插入图片描述

最核心的问题

开发模式下的时候遇到的问题
1.加载脚本的时候 出现了Import 缓存问题
2.使用import引用js脚本时 也会缓存 编写工具类的话就会有问题
3.加载node_modules的问题

踩坑点 缓存问题 头部引入 node_modules的问题

直接使用await import(“入口文件”)加载时 就算重新await import v8还是会自己缓存起来 electron-vite是已esm的形式加载的只能Import 没办法通过require的缓存去删除

通过动态读取脚本数据
  let script = fs.readFileSync(path.join(setting.path, setting.script)).toString()const nonce = Date.now() + Math.random().toString(36).slice(2);const freshModule = await import("data:text/javascript,"+encodeURIComponent(injectedCode)+"#"+nonce);return freshModule.default(this.EletronPack(setting.name), win);

踩坑了:这个方式引入以后 确实在加载卸载插件重载了代码 但是出现一个问题 由于是动态创建的脚本 导致没有上下文对象 没办法加载node_modules数据以及引用路径下的脚本无法读取

由于esm没有__dirname 导致我想通过路径去await import()脚本都做不到 import.meta.url更是指向之进程的根路径

那我们只能进行代码改造 我们能拿到原始脚本数据 那我们就可以注入一些脚本

在开头注入 一个__dirname 让脚本知道自己位置

            const head = (path) => {return `var __dirname = "${encodeURIComponent(path)}"; __dirname=decodeURIComponent(__dirname);`}let injectedCode=`${head} ${script}`

然后我们在脚本里 引用一个同级的脚本 await import( path.join(__dirname,“xxx.js”))

什么?又报错了 一看 噢不支持盘符路径 那我就一个

import { pathToFileURL } from 'node:url';
await import(pathToFileURL(path.join(__dirname,"test.js")))

ok 加载完毕 改改代码保存一下试试 读出来了 那我再改改引用的脚本test.js 欸 怎么没有变化 又缓存了

解决方法:那我给这个脚本再注入一个方法LoadScript

const loadMethod = () => {return `const LoadScript=async (target_path)=> {let script = fs.readFileSync(scriptPath).toString();const nonce = Date.now() + Math.random().toString(36).slice(2);const freshModule = await import("data:text/javascript,"+encodeURIComponent(injectedCode)+"#"+nonce);return freshModule.default;}`
}let injectedCode=`${head} ${loadMethod()} ${script}`

在引用的脚本里 引用一个脚本 并更改那个脚本 ok 没有问题

    let test = await LoadScript("./static/test.js")
顶部import问题

当我开开心心准备编写逻辑的时候发现 导入fs path库 提示我重复导入库 那我就一个正则 把获取到的script的import先去除

   script = script.replace(/import\s+fs\s+from\s+['"]node:fs['"]/g, "")script = script.replace(/import\s+path\s+from\s+['"]node:path['"]/g, "")script = script.replace(/import\s+path\s+from\s+['"]node:url['"]/g, "")
node_modules 无法正确引入

动态脚本有一个很大的问题 就是没有上下文 导致它不知道从哪里去引入node_modules 让开发变得很麻烦 那怎么办? 试了好多种方式 LoadScript是无法正确加载的 通过AI获取到解决方法 就是先创建临时文件让它有上下文 加载到内存后 删除临时文件

继续修改方法 并改进 会出现的问题 比如脚本引用的脚本还想引用脚本的问题 以及它们的node_modules引入

        // 动态读取脚本const LoadScript = async (setting, win) => {// 动态读取插件下的脚本let script = fs.readFileSync(path.join(setting.path, setting.script)).toString()const head = (path) => {return `import fs from "node:fs"import path from "node:path"import url from "node:url";var __dirname = "${encodeURIComponent(path)}"; __dirname=decodeURIComponent(__dirname);`}const loadMethod = () => {return `const LoadScript=async (target_path)=>{let scriptPath=path.join(__dirname, target_path);let name=scriptPath.substring(scriptPath.lastIndexOf("\\\\")+1,scriptPath.lastIndexOf("."));let script = fs.readFileSync(scriptPath).toString();script = script.replace(/import\s+fs\s+from\s+['"]node:fs['"]/g, "")script = script.replace(/import\s+path\s+from\s+['"]node:path['"]/g, "")script = script.replace(/import\s+url\s+from\s+['"]node:url['"]/g, "")// 去除路径文件名scriptPath=scriptPath.substring(0,scriptPath.lastIndexOf("\\\\"));const head = (path) => {return "var __dirname ='"+encodeURIComponent(path)+"';"+"__dirname=decodeURIComponent(__dirname);"}const injectedCode=head(scriptPath)+script;const tmpFile=path.join(scriptPath,name+Date.now()+".mjs");try {fs.writeFileSync(tmpFile, injectedCode, "utf-8");const freshModule = await import(url.pathToFileURL(tmpFile).href);let target=typeof(freshModule.default)=="function"?freshModule.default(LoadScript):freshModule.defaultreturn target} catch (err) {console.log(err);return null}finally{fs.rmSync(tmpFile);}}`}script = script.replace(/import\s+fs\s+from\s+['"]node:fs['"]/g, "")script = script.replace(/import\s+path\s+from\s+['"]node:path['"]/g, "")script = script.replace(/import\s+path\s+from\s+['"]node:url['"]/g, "")const injectedCode = `${head(setting.path)}${loadMethod()}${script}`;const tmpFile = path.join(setting.path, `.temp-${Date.now()}.mjs`)fs.writeFileSync(tmpFile, injectedCode, "utf-8");try {// 2. 用真实文件路径 import,Node 会自动解析 pluginDir/node_modulesconst mod = await import(pathToFileURL(tmpFile).href);return mod.default(this.EletronPack(setting.name), win)} finally {fs.rmSync(tmpFile);}}

解决了所有踩坑的地方 终于可以安心开发插件了

在这里插入图片描述

http://www.dtcms.com/a/295399.html

相关文章:

  • Linux 简单介绍及基础命令
  • 云原生MySQL Operator开发实战(一):Operator基础与CRD设计
  • 基于Odoo的微信小程序全栈开发探索分析
  • 开源中国:以国产开源生态筑基,赋能智能研发全栈升级
  • 【王树森推荐系统】推荐系统涨指标的方法05:特殊用户人群
  • [数据结构]#7 哈希表
  • 国产化PDF处理控件Spire.PDF教程:Python 将 PDF 转换为 Markdown (含批量转换示例)
  • spring boot 整合 Spring Cloud、Kafka 和 MyBatis菜鸟教程
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段(9):ようなN
  • C++ 中值传参和引用传参
  • rust-数据结构
  • 聚观早报 | 猿编程推动中美青少年AI实践;华为Pura 80数字版售价公布;iPhone 17 Air电池曝光
  • Redis数据类型与内部编码
  • 国产数据库拐点已至:电科金仓用“融合+AI”重新定义下一代数据底座
  • rustfs/rustfs基于 Rust 的高性能分布式存储系统
  • 进程通信----匿名管道
  • 进阶向:基于Python的本地文件内容搜索工具
  • 加入淘宝联盟内容库,以便在B站等平台被推广
  • 我的新项目又来咯!
  • iOS 抓包工具有哪些?按能力划分的实用推荐与使用心得
  • 开发运维DevOps(附电子书资料)
  • 办公自动化入门:如何高效将图片整合为PDF文档
  • 7月25日 矩阵起源亮相深圳DA数智大会,解读多模态大模型驱动的数据处理新方法
  • 如何保证GPFS文件系统的强一致性
  • PDF转Markdown - Python 实现方案与代码
  • Go进阶高并发(多线程)处理教程
  • 中小企业安全落地:低成本漏洞管理与攻击防御方案
  • 新手操作steam搬砖项目,应该如何快速起步
  • 图机器学习(19)——金融数据分析
  • 深度分析Java类加载机制