上一章讲了动态脚本注入 但是有很大的问题 就是动态注入部分的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引入的时候 它的特性如下
- 通过静态 import 导入的模块是唯一的,共享相同的实例。
- 模块内部的变量和状态在所有导入该模块的地方是共享的。
- 模块的代码在第一次导入时执行,并且导出被缓存。
知道这个某个模块导入的时候是单例 那我们就好办了
空导出文件创建 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 我们的重载代码功能也是正常的 也动态更新了