electron-vite 动态加载脚本 实现动态插件
上一章讲了动态脚本注入 但是有很大的问题 就是动态注入部分的loadSript后 要通过入参的方式才能获取electron和win 本次讲解如何解放这种耦合 让脚本更加自由
代码回顾
这里有一个很不方便的的问题 就是我引入以后 我想单独传递win或electron 要去修改这个很恶心的字符串数据 不能直接在本地调用
// 动态读取脚本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);}}
解决方案
在import引入的时候 它的特性如下
- 通过静态 import 导入的模块是唯一的,共享相同的实例。
- 模块内部的变量和状态在所有导入该模块的地方是共享的。
- 模块的代码在第一次导入时执行,并且导出被缓存。
知道这个某个模块导入的时候是单例 那我们就好办了
空导出文件创建 share.mjs
export default {}
主进程中import该文件 并对其初始化
let share = nullconst getShare = async () => {share = await import(SHARE_PATH)share = share.defaultshare.win = {}share.loadScript = {}}getShare()
修改我们的LoadPlugin脚本 放入需要共享的模块
// 对应插件的窗体
share.win[setting.name] = win
// 共享的electron库数据
share.electron = this.EletronPack
share.console = this.CustomConsole()
// 给后续需要导入脚本的对应插件添加导入函数 每个函数都是基于插件根路径去加载 所以loadScript每个内容都不一样 隔离函数去调用除目录下的脚本
share.loadScript[setting.name] = (target_path) => {let t_p = target_path.replace(/\//g, "/")return LoadScript({ path: setting.path, name: setting.name, script: t_p })
}
PLUGINS[setting.name] = {}
PLUGINS[setting.name].win = win
PLUGINS[setting.name].js = await LoadScript(setting)
修改LoadScript脚本 重点
import share from “${SHARE_PATH}” 这个是核心代码 等于我们加载了这个share 这样我们可以提取出来我们之前定义的东西
// 动态读取脚本
const LoadScript = async (setting) => {// 判断是不是开发的插件let dev = DEV_PLUGINS && DEV_PLUGINS.name == setting.name// 动态读取插件下的脚本let script = fs.readFileSync(path.join(setting.path, setting.script)).toString()// 自定义的console 因为process.stdout 这些捕捉不到自己线程的输出 除非是spawn的const ConsoleString = `let Console=new Proxy(share.console,{get(target,prop){return function (...args) {return target[prop].apply(this,[__filename,...args]);};}});`// 定义注入的头部 重点是第一句 导入我们的共享包// 并且将里面所需要的东西提出出来 并定义 让我们在脚本中能获取到// 补充types/index.d.ts 让编辑器识别即可const head = (path) => {return `import share from "${SHARE_PATH}"var __filename="${encodeURIComponent(setting.script)}";__filename=decodeURIComponent(__filename);var __dirname = "${encodeURIComponent(path)}"; __dirname=decodeURIComponent(__dirname);let electron=share.electron("${setting.name}");let win=share.win["${setting.name}"];let loadScript=share.loadScript["${setting.name}"];${dev ? ConsoleString : "let Console=console;"}`}const injectedCode = `${head(setting.path)}${script}`;// console.log(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// 捕获错误 并输出正确行数&& mod.default && mod.default().catch((err) => {const lines = err.stack.split("\n")let match = lines[1].match(/at (.*) \((.*):(\d+):(\d+)\)/);let message = `at ${match[1]}(${match[2]}:${Number(match[3]) - EXTRA_LINE}:${match[4]})`dev && Application.window.webContents.send(api.TOOLS.TOOLS_DEV_CONSOLE, "error", [setting.script, lines[0], message])if (!dev) {throw Error(`${setting.name}插件加载失败`)}})} finally {fs.rmSync(tmpFile);}
}
编写index.d.ts 让编辑器知道我们引入了什么
编写后在需要编辑器提示的js代码中加入
/// <reference path="./types/index.d.ts"/>
import { BrowserWindow, dialog,ipcMain,screen, shell } from "electron"
import { Sequelize } from "./sequelize/index"
import { ModelCtor,Model,Attributes,ModelOptions,ModelAttributes } from "./sequelize/model"
export declare global {/*** @description: 加载需要热更新的脚本* @param {string} path 路径 已插件根路径为准* @return {T} 根据脚本export 为准*/declare function loadScript<T>(path: string): T/*** @description: 调试控制台输出* @return {*}*/declare const Console = {log(...args: any[]): void {},error(...args: any[]): void {},warn(...args: any[]): void {},trace(...args: any[]): void {}}/*** @description: 开放的electron权限*/ declare const electron = {database: {createDatabase<M extends Model, TAttributes = Attributes<M>>(modelName: string,attributes: ModelAttributes<M, TAttributes>,options?: ModelOptions<M>): ModelCtor<M>{},checkTableExist(name: string): Promise<boolean>},dialog,screen,ipcMain,BrowserWindow,shell}/*** @description: 当前窗体*/ declare const win: BrowserWindow;/*** @description: 完整路径*/ declare const __dirname: string;/*** @description: 当前文件名*/ declare const __filename: string;}
打包运行 测试代码
打包后在运行的应用中 开发插件 并添加测试代码
在加载的脚本中index.js通过loadScript(“./static/test.js”) 并在test.写下如下代码
/// <reference path="../types/index.d.ts"/>Console.log(win.getSize())
查看引用脚本输出
添加故意报错的代码 查看控制台 我们能拿到报错的地方以及错误的行数
注释错误代码 查看控制台
ok 我们的重载代码功能也是正常的 也动态更新了