electron-vite 动态加载脚本 实现动态插件

上一章讲了动态脚本注入 但是有很大的问题 就是动态注入部分的loadSript后 要通过入参的方式才能获取electron和win 本次讲解如何解放这种耦合 让脚本更加自由

代码回顾

这里有一个很不方便的的问题 就是我引入以后 我想单独传递win或electron 要去修改这个很恶心的字符串数据 不能直接在本地调用

javascript 复制代码
        // 动态读取脚本
        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.default
                        return 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_modules
                const mod = await import(pathToFileURL(tmpFile).href);
                return mod.default(this.EletronPack(setting.name), win)
            } finally {
                fs.rmSync(tmpFile);
            }
        }

解决方案

在import引入的时候 它的特性如下

  1. 通过静态 import 导入的模块是唯一的,共享相同的实例。
  2. 模块内部的变量和状态在所有导入该模块的地方是共享的。
  3. 模块的代码在第一次导入时执行,并且导出被缓存。

知道这个某个模块导入的时候是单例 那我们就好办了

空导出文件创建 share.mjs

javascript 复制代码
export default {}

主进程中import该文件 并对其初始化

javascript 复制代码
        let share = null
        const getShare = async () => {
            share = await import(SHARE_PATH)
            share = share.default
            share.win = {}
            share.loadScript = {}
        }
        getShare()

修改我们的LoadPlugin脚本 放入需要共享的模块

javascript 复制代码
// 对应插件的窗体
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 这样我们可以提取出来我们之前定义的东西

javascript 复制代码
// 动态读取脚本
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_modules
        const 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代码中加入

javascript 复制代码
 /// <reference path="./types/index.d.ts"/>
javascript 复制代码
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.写下如下代码

javascript 复制代码
/// <reference path="../types/index.d.ts"/>

Console.log(win.getSize())

查看引用脚本输出

添加故意报错的代码 查看控制台 我们能拿到报错的地方以及错误的行数 注释错误代码 查看控制台

ok 我们的重载代码功能也是正常的 也动态更新了

相关推荐
给力学长5 小时前
自习室预约小程序的设计与实现
java·数据库·vue.js·elementui·小程序·uni-app·node.js
有事没事实验室5 小时前
node.js中的path模块
前端·css·node.js·html·html5
该用户已不存在12 小时前
Node.js 真的取代了PHP吗?
前端·后端·node.js
一个很帅的帅哥12 小时前
Webpack 和 Vite 的关键区别
前端·webpack·前端框架·node.js
sq80018 小时前
listr2 入门教程2-Node.js持续显示任务运行状态
node.js
Mr_兔子先生19 小时前
2025盛夏版:基于electron37+vite7的Vue桌面客户端保姆教程
vue.js·electron·vite
koooo~1 天前
node.js中的fs与path模块
node.js
刘大猫.1 天前
npm ERR! cb() never called!
前端·npm·node.js·npm install·npmm err·never called
李先生9302 天前
Puppeteer最新迁移和服务
前端·node.js