此文档适用于 LiteLoaderQQNT 插件开发的初学者,只需要了解基本的 JavaScript 语法就可以快速上手 LiteLoaderQQNT 插件开发。
LiteLoaderQQNT 是 NTQQ 的插件加载器,一般在 QQNT 的环境内简称为 LiteLoader。LiteLoaderQQNT 可以为 NTQQ 添加各种功能。
- DLLHijackMethod 用于过掉 QQNT Patch 文件检测。(64 位系统选择
dbghelp_x64.dll下载,32 位系统选择dbghelp_x86.dll下载) - LiteLoaderQQNT LiteLoaderQQNT 本体,直接选择
LiteLoaderQQNT.zip下载即可
右键任务栏中托盘图标中的 QQ 并在弹出的菜单中选择 退出,将 dbghelp_x64.dll (如果是 32 位系统则为 dbghelp_x86.dll) 文件重命名为 dbghelp.dll,右键桌面 QQ 快捷方式,选择 "打开文件所在位置(I)",复制 dbghelp.dll 到与 QQ.exe 文件同级的目录下
右键桌面 QQ 快捷方式,选择 "打开文件所在位置(I)" 切换到 QQ 的目录,查看当前目录是否存在 versions 目录。若存在 versions 目录,则打开 versions => 版本号 => resources => app 目录;若不存在 versions 目录,则打开当前目录的 resources => app 目录。这个 app 目录就是 QQ 的应用目录。
将 LiteLoaderQQNT.zip 解压到任意目录下,推荐存放在系统 文档 目录下。(这里以系统 文档 目录为例)
在 app 目录下找到 app_launcher 目录并进入,创建 liteloader.js 文件,并写入以下代码:(其中 C:\Users\Cheese\Documents\LiteLoaderQQNT 为你的 LiteLoaderQQNT 的安装目录)
require(String.raw`C:\Users\Cheese\Documents\LiteLoaderQQNT`)在 app 目录下找到 package.json 文件并编辑,找到 "main": 那行并修改为以下代码:
"main": "./app_launcher/liteloader.js",双击桌面上的 QQ 图标,单击登录按钮,在主界面出现之后,单击左下角的 "菜单" 按钮,选择 "设置" ,查看设置列表是否出现 "LiteLoaderQQNT" 选项。若出现则表示安装成功。
- Visual Studio Code 开源的轻量级代码编辑器。
右击桌面空白处,选择 "新建(W)",选择 "文件夹(F)",输入项目名称后回车。


在项目文件夹中创建一个名为 manifest.json 的文件,并写入以下代码:


{
// 必选,当前版本为 4
"manifest_version": 4,
// 可选,插件类型,可写 extension | theme | framework
"type": "extension",
// 必选,插件名字
"name": "插件模板",
// 必选,代码内标识
"slug": "plugin_template",
// 必选,插件描述
"description": "LiteLoaderQQNT的插件模板",
// 必选,版本号
"version": "0.1.0",
// 可选,插件的图标,写入相对路径字符串,也可以是 null
"icon": null,
// 可选,设置选项的图标,写入相对路径字符串,也可以是 null
"thumb": null,
// 必选,作者们的信息
"authors": [
{
"name": "LiteLoaderQQNT", // 必选,作者名字
"link": "https://github.com/LiteLoaderQQNT" // 必选,作者链接
}
],
// 可选,插件依赖项,写入插件slug名
"dependencies": [
"slug"
],
// 必选,插件支持的系统平台,Windows: win32 | Linux: linux | MacOS: darwin
"platform": [
"win32",
"linux",
"darwin"
],
// 可选,要注入的脚本
"injects": {
"renderer": "./src/renderer.js", // 可选,渲染进程
"main": "./src/main.js", // 可选,主进程
"preload": "./src/preload.js" // 可选,预加载脚本
},
// 可选,插件仓库信息
"repository": {
// 必选,仓库短地址
"repo": "LiteLoaderQQNT/Plugin-Template",
// 必选,分支名称
"branch": "main",
// 可选,不写将使用仓库源码
"release": {
// 必选,tag 名称,不推荐写 latest
"tag": "0.1.0",
// 可选,release 内的文件名,不填会直接下载 tag 的源码
"file": "Plugin-Template.zip"
}
}
}| 参数 | 作用 |
|---|---|
| manifest_version | 插件的版本,目前只支持 4 |
| type | 插件类型,可写 extension (插件)、theme (主题) 和 framework (框架) |
| name | 插件名字 |
| slug | 插件代码内标识 (可以理解为插件的包名) |
| description | 插件描述 |
| version | 版本号 |
| icon | 插件的图标,写入相对路径字符串,也可以是 null |
| thumb | 设置选项的图标,写入相对路径字符串,也可以是 null |
| authors | 作者们的信息 |
| authors.name | 作者的名字 |
| authors.link | 作者的链接 |
| dependencies | 插件依赖项,写入插件的 slug 名 |
| platform | 插件支持的系统平台,Windows: win32、Linux: linux、MacOS: darwin |
| injects | 要注入的脚本 |
| injects.renderer | 渲染进程 |
| injects.main | 主进程 |
| injects.preload | 预加载脚本 |
| repository | 插件仓库信息 |
| repository.repo | 仓库短地址(例: https://github.com/LiteLoaderQQNT/Plugin-Template => LiteLoaderQQNT/Plugin-Template) |
| repository.branch | 分支名称 |
| repository.release | release 内的文件名,不填会直接下载 tag 的源码 |
| repository.release.tag | tag 名称,不推荐写 latest |
| repository.release.file | release 内的文件名,不填会直接下载 tag 的源码 |
在 VS Code 中按下 Ctrl+H,在 查找 输入框中输入 .*\/\/.+\n? 并勾选 正则表达式,替换 输入框为空。按下 Ctrl+Alt+Enter 即可。
LiteLoader 插件加载顺序如下:
main.js主进程preload.js预加载脚本renderer.js渲染进程
// 获取 LiteLoader 的根目录
const root_path=LiteLoader.path.root;
// 获取 LiteLoader 的配置文件路径
const profile_path=LiteLoader.path.profile;
// 获取 LiteLoader 的数据文件夹路径
const data_path=LiteLoader.path.data;
// 获取 LiteLoader 的插件文件夹路径
const plugins_path = LiteLoader.path.plugins;
// 获取 QQ NT 的版本信息
const qqnt_version = LiteLoader.versions.qqnt;
// 获取 LiteLoader 的版本信息
const liteloader_version = LiteLoader.versions.liteloader;
// 获取 Node.js 的版本信息
const node_version = LiteLoader.versions.node;
// 获取 Chromium 的版本信息
const chrome_version = LiteLoader.versions.chrome;
// 获取 Electron 的版本信息
const electron_version = LiteLoader.versions.electron;
// 获取操作系统平台
const platform = LiteLoader.os.platform;
// 获取 LiteLoader 的包信息
const liteloader_package = LiteLoader.package.liteloader;
// 获取 QQ NT 的包信息
const qqnt_package = LiteLoader.package.qqnt;
// 获取插件列表
const plugins = LiteLoader.plugins;
// 设置配置 LiteLoader.api.config.set (setConfig)
// 参数1: 插件 slug 字符串; 参数2: JSON 对象
const result_set = LiteLoader.api.config.set(slug, new_config);
// 获取配置 LiteLoader.api.config.get (getConfig)
// 参数1: 插件 slug 字符串; 参数2: 默认返回配置字符串
const result_get = LiteLoader.api.config.get(slug, default_config);
// 在下次启动时安装插件
// 参数1: 压缩包 / manifest.json 文件路径; 参数2: 是否撤销安装(为 true 时则撤销安装)
LiteLoader.api.plugin.install(plugin_path, undone = false);
// 在下次启动时删除插件
// 参数1: 插件 slug; 参数2: 是否删除插件的数据文件夹; 参数3: 是否撤销删除(为 true 时则撤销删除)
LiteLoader.api.plugin.delete(slug, delete_data = false, undone = false);
// 禁用/启用插件
// 参数1: 插件 slug; 参数2: 是否撤销禁用(为 true 时则为启用)
LiteLoader.api.plugin.disable(slug, undone = false);
// 打开外部链接
// shell.openExternal(url: string, options?: OpenExternalOptions): Promise<void>;
// 参数1: url; 参数2: options:
// {
// activate = true, // macOS - 是否将打开的应用程序置于前台
// workingDirectory = "C:\\TestDirectory", // Windows - 设置工作目录
// logUsage = false, // Windows - 是否记录使用情况
// }
LiteLoader.api.openExternal(url, options);
// 打开文件路径
// shell.openPath(path: string): Promise<string>;
// 参数1: path (以桌面的默认方式打开给定的文件。)
LiteLoader.api.openPath(path);
// LiteLoader API 定义
const LiteLoader = {
path: {
// LiteLoader 的根目录
root: root_path,
// LiteLoader 的配置文件路径
profile: profile_path,
// LiteLoader 的数据文件夹路径
data: data_path,
// LiteLoader 的插件文件夹路径
plugins: plugins_path
},
versions: {
// QQNT 的版本信息
qqnt: qqnt_version.curVersion ?? qqnt_version.version,
// LiteLoader 的版本信息
liteloader: liteloader_package.version,
// Node.js 的版本信息
node: process.versions.node,
// Chromium 的版本信息
chrome: process.versions.chrome,
// Electron 的版本信息
electron: process.versions.electron
},
os: {
// 操作系统平台
platform: process.platform
},
package: {
// LiteLoader 的包信息
liteloader: liteloader_package,
// QQNT 的包信息
qqnt: qqnt_package
},
// 插件列表
plugins: {},
api: {
config: {
// 设置配置
set: setConfig,
// 获取配置
get: getConfig
},
plugin: {
// 安装插件
install: pluginInstall,
// 删除插件
delete: pluginDelete,
// 启用/禁用插件
disable: pluginDisable
},
// 打开外部链接
openExternal: shell.openExternal,
// 打开文件路径
openPath: shell.openPath
}
};| 接口名称 | 接口参数 | 接口说明 |
|---|---|---|
onBrowserWindowCreated |
(window) |
浏览器窗口创建后执行 |
onLogin |
(uid) |
登录后执行 |
无
| 接口名称 | 接口参数 | 接口说明 |
|---|---|---|
onSettingWindowCreated |
settingInterface |
监听插件设置窗口创建事件,调用插件的 onSettingWindowCreated 方法。 |
onVueComponentMount |
component |
监听 Vue 组件挂载事件,调用插件的 onVueComponentMount 方法。 |
onVueComponentUnmount |
component |
监听 Vue 组件卸载事件,调用插件的 onVueComponentUnmount 方法。 |
main.js
// 监听窗口创建事件
function onBrowserWindowCreated(){
alert("欢迎使用 Example 插件!");
}main.js
// 监听登录事件
function onLogin(uid){
alert("用户: "+ uid + "\n欢迎使用 Example 插件!");
}main.js
module.exports.onBrowserWindowCreated = window => {
window.webContents.on("did-stop-loading", async () => {
const id = window.id;
const url = window.webContents.getURL();
if (window.webContents.getURL().includes("#/main/message") || window.webContents.getURL().includes("#/chat")){
// 当前是聊天窗口
alert("当前窗口 ID 为 " + id + "\n当前窗口 URL 为 " + url + "\n当前窗口标题为 " + window.getTitle() + "\n当前窗口为聊天界面" + "\n当前窗口大小为 " + window.getSize() + "\n当前窗口位置为 " + window.getPosition())
}else{
// 当前不是聊天窗口
alert("当前窗口 ID 为 " + id + "\n当前窗口 URL 为 " + url + "\n当前窗口标题为 " + window.getTitle() + "\n当前窗口不为聊天界面" + "\n当前窗口大小为 " + window.getSize() + "\n当前窗口位置为 " + window.getPosition())
}
})
}content.html
<!-- 推荐使用 LiteLoader 官方的 Web Components 组件 -->
<!-- https://liteloaderqqnt.github.io/docs/web-components.html -->
<p>Hello, World!</p>main.js
// 导入 Electron 模块
const {ipcMain} = require("electron");
// 导入文件模块
const fs = require("fs");
// 导入路径模块
const path = require("path");
// 读取设置界面内容
ipcMain.handle("LiteLoader.example11.getSettingContent", () => fs.readFileSync(path.join(
LiteLoader.plugins.example11.path.plugin, 'content.html'), 'utf-8'))preload.js
// Electron 主进程与渲染进程 交互的桥梁
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("example11", {
// 定义获取设置内容的方法
getSettingContent: () => ipcRenderer.invoke("LiteLoader.example11.getSettingContent"),
});renderer.js
// 监听设置窗口创建事件
export const onSettingWindowCreated = async view => {
try {
view.innerHTML = await window.example11.getSettingContent();
} catch (e) {
alert(e)
}
}<template id="plugin-item">
<setting-item>
<div class="info-area">
<div>
<slot class="icon"></slot>
<div>
<setting-text>
<slot class="name"></slot>
</setting-text>
<setting-text data-type="secondary">
<slot class="description"></slot>
</setting-text>
</div>
</div>
<setting-text data-type="secondary">
<span>版本:<slot class="version"></slot></span>
<span>作者:<slot class="authors"></slot></span>
<span>仓库:<slot class="repo"></slot></span>
</setting-text>
</div>
<setting-button data-type="secondary" class="manager">管理</setting-button>
<setting-modal class="manager-modal">
<setting-section data-title="开关">
<setting-panel>
<setting-list data-direction="column">
<setting-item>
<setting-text>启动时加载此插件</setting-text>
<setting-switch class="enable"></setting-switch>
</setting-item>
</setting-list>
</setting-panel>
</setting-section>
<setting-section data-title="删除">
<setting-panel>
<setting-list data-direction="column">
<setting-item>
<setting-text>删除我的插件数据</setting-text>
<setting-switch class="keepdata"></setting-switch>
</setting-item>
<setting-item>
<setting-text>在下次启动时删除</setting-text>
<setting-switch class="uninstall"></setting-switch>
</setting-item>
</setting-list>
</setting-panel>
</setting-section>
</setting-modal>
</setting-item>
</template>
<setting-section data-title="版本" class="versions">
<setting-panel>
<setting-list data-direction="row" class="current">
<setting-item class="liteloader">
<setting-text>LiteLoader</setting-text>
<setting-text data-type="secondary"></setting-text>
</setting-item>
<setting-item class="qqnt">
<setting-text>QQNT</setting-text>
<setting-text data-type="secondary"></setting-text>
</setting-item>
<setting-item class="electron">
<setting-text>Electron</setting-text>
<setting-text data-type="secondary"></setting-text>
</setting-item>
<setting-item class="chromium">
<setting-text>Chromium</setting-text>
<setting-text data-type="secondary"></setting-text>
</setting-item>
<setting-item class="nodejs">
<setting-text>Node.js</setting-text>
<setting-text data-type="secondary"></setting-text>
</setting-item>
</setting-list>
</setting-panel>
<setting-panel>
<setting-list data-direction="column" class="new">
<setting-item>
<setting-text>正在发现 LiteLoaderQQNT 是否有新版本</setting-text>
<setting-button data-type="secondary">你先别急</setting-button>
</setting-item>
</setting-list>
</setting-panel>
</setting-section>
<setting-section data-title="插件" class="plugins">
<setting-panel>
<setting-list data-direction="column" class="plugin">
<setting-item class="install">
<div>
<setting-text>安装新插件</setting-text>
<setting-text data-type="secondary">支持类型:解压前 zip 格式文件 | 解压后 manifest.json 文件</setting-text>
</div>
<setting-button data-type="secondary">选择文件</setting-button>
</setting-item>
<setting-item class="loader">
<setting-text>插件加载器</setting-text>
<setting-switch></setting-switch>
</setting-item>
</setting-list>
</setting-panel>
<setting-panel>
<setting-list data-title="扩展" is-collapsible data-direction="column" class="extension">
</setting-list>
</setting-panel>
<setting-panel>
<setting-list data-title="主题" is-collapsible data-direction="column" class="theme">
</setting-list>
</setting-panel>
<setting-panel>
<setting-list data-title="依赖" is-collapsible data-direction="column" class="framework">
</setting-list>
</setting-panel>
</setting-section>
<setting-section data-title="目录" class="path">
<setting-panel>
<setting-list data-direction="column">
<setting-item class="root">
<div>
<setting-text>本体目录位置</setting-text>
<setting-text data-type="secondary">LiteLoaderQQNT 本体的安装位置,可以安装到任意位置</setting-text>
<setting-text data-type="secondary"></setting-text>
</div>
<setting-button data-type="secondary">进入目录</setting-button>
</setting-item>
<setting-item class="profile">
<div>
<setting-text>数据目录位置</setting-text>
<setting-text data-type="secondary">可通过 LITELOADERQQNT_PROFILE 环境变量指定数据存放位置</setting-text>
<setting-text data-type="secondary"></setting-text>
</div>
<setting-button data-type="secondary">进入目录</setting-button>
</setting-item>
</setting-list>
</setting-panel>
</setting-section>
<setting-section data-title="关于" class="about">
<setting-panel>
<setting-list data-direction="column">
<setting-item>
<div>
<setting-text>LiteLoaderQQNT</setting-text>
<setting-text data-type="secondary">LiteLoaderQQNT 是 QQNT 的插件加载器,一般在 QQNT 的环境内简称为LiteLoader</setting-text>
<setting-text data-type="secondary">它可以让你自由地为 QQNT 添加各种插件,并实现例如美化主题、增加功能等各种功能</setting-text>
</div>
</setting-item>
<setting-item>
<div>
<setting-text>开源协议</setting-text>
<setting-text data-type="secondary">MIT License</setting-text>
</div>
</setting-item>
</setting-list>
</setting-panel>
<setting-panel>
<setting-list data-direction="column">
<setting-item>
<div>
<setting-text>LiteLoaderQQNT 官网</setting-text>
<setting-text data-type="secondary">https://liteloaderqqnt.github.io</setting-text>
</div>
<setting-button data-type="secondary" class="liteloaderqqnt">进去瞅瞅</setting-button>
</setting-item>
<setting-item>
<div>
<setting-text>GitHub 仓库</setting-text>
<setting-text data-type="secondary">https://github.com/LiteLoaderQQNT</setting-text>
</div>
<setting-button data-type="secondary" class="github">进去瞅瞅</setting-button>
</setting-item>
<setting-item>
<div>
<setting-text>Telegram 群组</setting-text>
<setting-text data-type="secondary">https://t.me/LiteLoaderQQNT</setting-text>
</div>
<setting-button data-type="secondary" class="group">进去瞅瞅</setting-button>
</setting-item>
<setting-item>
<div>
<setting-text>Telegram 频道</setting-text>
<setting-text data-type="secondary">https://t.me/LiteLoaderQQNT_Channel</setting-text>
</div>
<setting-button data-type="secondary" class="channel">进去瞅瞅</setting-button>
</setting-item>
</setting-list>
</setting-panel>
<setting-panel>
<setting-list data-direction="column">
<setting-item>
<setting-text class="hitokoto_text">正在加载:Hitokoto - 一言</s>
<div></div>
</setting-item>
<setting-item>
<div></div>
<div>
<span>——</span>
<span class="hitokoto_author">LiteLoaderQQNT</span>
</div>
</setting-item>
</setting-list>
</setting-panel>
</setting-section>LiteLoader 的加载流程如下:
- QQ NT 启动
- 加载 LiteLoader 的
init.js - 加载 LiteLoaderAPI 接口 (定义 LiteLoader 接口)
- 加载 Loader Core 的插件加载器 (载入所有插件)
- 加载 LiteLoader 的
main.js - 加载 MainLoader,加载所有插件的
main.js文件
main.js有 onBrowserWindowCreated 和 onLogin 两个函数,
分别在浏览器窗口创建后和登录后执行。 - 加载 Protocol Scheme 注册 local:// 自定义协议
- 加载 Preload 用于注入页面的预加载脚本
- 加载 Preload LiteLoader API 向页面注入 LiteLoader 的 API
- 加载 Preload Loader Core 用于加载所有插件的
preload.js文件 - 加载 Renderer 注入页面渲染
- 加载 SettingInterface 提供关于设置界面接口
- 加载 Web Component 为开发者提供组件接口
- 加载 Loader Core RendererLoader 用于加载所有插件的
renderer.js文件 - 加载 QQ NT 主界面
安装 LiteLoaderQQNT 时修改了 QQ 的配置文件 package.json 的 main 字段,当 QQ 被加载的时候,实际上加载的是当前目录 app_launcher 中的 liteloader.js 文件,而 liteloader.js 的内容是 require(String raw `C:\Users\Cheese\Documents\LiteLoaderQQNT\`),这里需要了解 require 函数和 String.raw 函数。
require 函数用于加载模块,其参数为模块的绝对路径,这个路径可以为文件或者是文件夹。当加载文件夹时,require 就会读取文件夹下的 package.json 中的 main 字段,然后加载 main 字段指向的 JS 文件;若不存在 main 字段,或者是根本不存在 package.json 文件,则默认查找该文件夹下的 index.js 文件作为模块来载入。)
String.raw 函数用于返回一个字符串,该字符串使用原始的字符串字面量,即不会解析转义字符。
// 创建名为 str 的变量,他的值为 String.raw`\n`,其中 `` 为 模板字符串,\n 为转义字符
let str = String.raw`\n`;
// 输出 str 的值,此时输出的不是换行而是 \n
console.log(str); // \n
// 判断 str 的值是否为 \n,此时输出 true
console.log(str === '\\n'); // true
// 判断 str 的值是否为换行符,此时输出 false
console.log(str === '\n'); // false了解了 require 函数和 String.raw 函数,那么 liteloader.js 的作用就是加载 LiteLoaderQQNT 文件夹,而 LiteLoaderQQNT 文件夹下的 package.json 内容如下:
{
"name": "liteloader-qqnt",
"version": "1.2.2",
"description": "QQNT 插件加载器 LiteLoaderQQNT —— 轻量 · 简洁 · 开源 · 福瑞",
"main": "./src/init.js",
"private": true,
"license": "MIT",
"author": {
"name": "LiteLoaderQQNT",
"url": "https://github.com/LiteLoaderQQNT"
},
"homepage": "https://liteloaderqqnt.github.io",
"bugs": "https://github.com/LiteLoaderQQNT/LiteLoaderQQNT/issues",
"repository": {
"type": "git",
"url": "https://github.com/LiteLoaderQQNT/LiteLoaderQQNT.git"
}
}可以看到里面的 main 字段指向了 ./src/init.js 文件,也就是当前 package.json 所在文件夹中 src 文件夹里的 init.js 文件
// 加载 LiteLoaderQQNT 的 API 模块
require("./liteloader_api/main.js");
// 加载插件加载器
require("./loader_core/plugin_loader.js");
// 加载 LiteLoaderQQNT 的入口文件
require("./main.js");
// 调用 path 模块中的 join 方法拼接路径,等效于 require(process.resourcesPath + "/app/app_launcher/index.js")
require(require("path").join(process.resourcesPath, "app/app_launcher/index.js"));
// 延迟 0 ms 后还原 QQ 的 package.json 中的 main 字段指向的文件
setTimeout(() => {
// 判断当前 QQNT 的构建版本号是否大于等于 28060
if (LiteLoader.package.qqnt.buildVersion >= 28060) {
// 设置 QQ 的 main 字段指向的入口文件
global.launcher.installPathPkgJson.main = "./application/app_launcher/index.js";
} else {
// 设置 QQ 的 main 字段指向的入口文件
global.launcher.installPathPkgJson.main = "./app_launcher/index.js";
}
}, 0);LiteLoaderQQNT 的 API 模块位于 ./liteloader_api 文件夹,该文件夹下有 main.js 和 index.js 两个文件, main.js 文件是 LiteLoaderQQNT 的核心模块, index.js 文件是 LiteLoaderQQNT 的入口文件, main.js 文件的内容如下:
// 获取默认的配置文件
// config.json 文件如下
// {
// "enable_plugins": true, // 是否启用所有插件
// "disabled_plugins": [], // 禁用的插件列表
// "deleting_plugins": {}, // 下次启动时要删除的插件列表
// "installing_plugins": {} // 下次启动时要安装的插件列表
// }
// 获取默认的配置文件
const default_config = require("../settings/static/config.json");
// 从 Node 环境中加载 electron 的 ipcMain 和 shell 模块
// ipcMain: 处理从渲染器进程(网页)发送的异步和同步消息
// shell: 提供了与系统原生应用程序交互的功能,比如打开外部链接、文件等。
const { ipcMain, shell } = require("electron");
// 加载 nodejs 的 path 和 fs 模块
// path: 用于处理文件路径,如拼接路径等
// 例如: path.dirname(path) 获取指定路径所在的文件夹名称
const path = require("node:path");
// fs: 提供了文件系统操作的 API,如读写文件、创建目录等。
// 例如: fs.readFileSync(path) 读取指定路径的文件内容
const fs = require("node:fs");
// 加载 admZip 模块,用于压缩和解压缩文件
const admZip = (() => {
// .node文件是Node.js中的一个模块,可以包含JavaScript代码、JSON或者编译过的C/C++扩展。
// 指定 major 模块路径(内部)
const major_node = path.join(process.resourcesPath, "app/major.node");
// 指定 launcher 模块路径(外部)
const launcher_node = path.join(process.resourcesPath, "app/app_launcher/launcher.node");
// 调用 fs 模块的 existsSync 函数判断 major.node 是否存在
if (fs.existsSync(major_node)) {
// 如果存在,则加载 major.node
require(major_node).load("internal_admzip", module);
}
else {
// 如果不存在,则加载 launcher.node
require(launcher_node).load("external_admzip", module);
}
// 返回加载的 exports.admZip.default 给外部的 admZip
return exports.admZip.default;
})();
// 获取 LiteLoaderQQNT 的根目录
const root_path = path.join(__dirname, "..", "..");
// 获取 LiteLoaderQQNT 的配置文件
const profile_path = process.env.LITELOADERQQNT_PROFILE ?? root_path;
// 获取 LiteLoaderQQNT 的数据目录
const data_path = path.join(profile_path, "data");
// 获取 LiteLoaderQQNT 的插件目录
const plugins_path = path.join(profile_path, "plugins");
// 获取 LiteLoaderQQNT 的 package.json 文件
const liteloader_package = require(path.join(root_path, "package.json"));
// 获取 QQNT 的 package.json 文件
const qqnt_package = require(path.join(process.resourcesPath, "app/package.json"))
// 获取 QQNT 的版本信息
const qqnt_version = (() => {
const config_filepath = path.join(process.resourcesPath, "app/versions/config.json");
return fs.existsSync(config_filepath) ? require(config_filepath) : qqnt_package;
})();
// 通过插件的 slug 名称写入配置文件
// 成功则返回 true,失败则返回 false
function setConfig(slug, new_config) {
try {
// 获取插件的 config.json 配置文件路径
const config_path = path.join(data_path, slug, "config.json");
// 创建 config.json 的父文件夹
fs.mkdirSync(path.dirname(config_path), { recursive: true });
// 以 UTF-8 的编码格式写入 config.json 配置文件
// JSON.stringify() 方法用于将 JSON 对象转换为 JSON 字符串
fs.writeFileSync(config_path, JSON.stringify(new_config, null, 4), "utf-8");
return true;
} catch {
return false;
}
}
// 通过插件的 slug 名称读取配置文件
// 返回读取到的 JSON 对象
function getConfig(slug, default_config) {
try {
// 获取插件的 config.json 配置文件路径
const config_path = path.join(data_path, slug, "config.json");
// 判断配置文件是否存在
if (fs.existsSync(config_path)) {
// 以 UTF-8 的编码格式读取配置文件
const config = JSON.parse(fs.readFileSync(config_path, "utf-8"));
// 合并默认配置和读取到的配置
return Object.assign({}, default_config, config);
}
else {
// 若不存在则以默认配置创建配置文件
setConfig(slug, default_config);
return Object.assign({}, default_config, {});
}
} catch {
// 返回默认配置
return default_config;
}
}
// 安装插件
// 第一个参数可以为压缩包路径也可以为解压的插件中的 manifest.json 文件
// 若第二个参数为 true 时则撤销安装插件
function pluginInstall(plugin_path, undone = false) {
try {
// 判断指定的路径是否为文件(不存在也为 false)
if (fs.statSync(plugin_path).isFile()) {
// 判断文件的扩展名是否为 .zip 格式
if (path.extname(plugin_path).toLowerCase() == ".zip") {
// 创建 admZip 对象
const plugin_zip = new admZip(plugin_path);
// 遍历压缩包中的所有条目
for (const entry of plugin_zip.getEntries()) {
// 判断文件名是否为 manifest.json 且不是文件夹
if (entry.entryName == "manifest.json" && !entry.isDirectory) {
// 读取 manifest.json 的 slug 名称
// entry.getData() 用于读取条目的数据
// JSON.parse() 方法用于将 JSON 字符串转换为 JSON 对象
// const { slug } = 可以直接获取 JSON 对象中的属性
const { slug } = JSON.parse(entry.getData());
// 判断插件是否已安装,若已安装则删除
if (slug in LiteLoader.plugins) LiteLoader.api.plugin.delete(slug, false, false);
// 获取 LiteLoader 的配置
const config = LiteLoader.api.config.get("LiteLoader", default_config);
// 如果 undone 为 true 则撤销安装
if (undone) delete config.installing_plugins[slug];
// 否则添加到正在安装的插件列表
else config.installing_plugins[slug] = {
// 插件文件路径
plugin_path: plugin_path,
// 插件文件类型
plugin_type: "zip"
};
// 更新 LiteLoader 的配置
LiteLoader.api.config.set("LiteLoader", config);
return true;
}
}
}
// 通过 manifest.json 文件安装插件
if (path.basename(plugin_path) == "manifest.json") {
// 获取插件的 slug 名称
const { slug } = JSON.parse(fs.readFileSync(plugin_path));
// 判断插件是否已安装,若已安装则删除
if (slug in LiteLoader.plugins) LiteLoader.api.plugin.delete(slug, false, false);
// 获取 LiteLoader 的配置
const config = LiteLoader.api.config.get("LiteLoader", default_config);
// 如果 undone 为 true 则撤销安装
if (undone) delete config.installing_plugins[slug];
// 否则添加到正在安装的插件列表
else config.installing_plugins[slug] = {
// 插件文件路径
plugin_path: plugin_path,
// 插件文件类型
plugin_type: "json"
};
// 更新 LiteLoader 的配置
LiteLoader.api.config.set("LiteLoader", config);
return true;
}
}
} catch (error) {
// 从调试台输出错误信息
console.error(error);
}
return false;
}
// 根据 slug 删除插件
// 第一个参数为插件的 slug
// 第二个参数为是否删除插件的数据文件夹
// 第三个参数为是否撤销删除
function pluginDelete(slug, delete_data = false, undone = false) {
// 判断插件是否存在,若不存在则直接返回 true 结束函数
if (!(slug in LiteLoader.plugins)) return true;
// 获取插件文件夹和数据文件夹路径
// LiteLoader.plugins[slug].path 为一个 JSON 对象
const { plugin, data } = LiteLoader.plugins[slug].path;
// 获取 LiteLoader 的配置
const config = LiteLoader.api.config.get("LiteLoader", default_config);
// 如果 undone 为 true 则撤销删除
if (undone) delete config.deleting_plugins[slug];
// 否则添加到将删除的插件列表
else config.deleting_plugins[slug] = {
// 插件文件路径
plugin_path: plugin,
// 插件数据文件夹路径
data_path: delete_data ? data : null
};
// 更新 LiteLoader 的配置
LiteLoader.api.config.set("LiteLoader", config);
}
// 启用/禁用插件
// 第一个参数为插件的 slug 名称
// 第二个参数为是否撤销禁用
function pluginDisable(slug, undone = false) {
// 获取 LiteLoader 的配置
const config = LiteLoader.api.config.get("LiteLoader", default_config);
// 如果 undone 为 true 则撤销禁用
if (undone) config.disabled_plugins = config.disabled_plugins.filter(item => item != slug);
// 否则添加到禁用的插件列表
else config.disabled_plugins = config.disabled_plugins.concat(slug);
// 更新 LiteLoader 的配置
LiteLoader.api.config.set("LiteLoader", config);
}
// 定义 LiteLoader API
const LiteLoader = {
path: {
// LiteLoader 的根目录
root: root_path,
// LiteLoader 的配置文件路径
profile: profile_path,
// LiteLoader 的数据文件夹路径
data: data_path,
// LiteLoader 的插件文件夹路径
plugins: plugins_path
},
versions: {
// QQNT 的版本信息
qqnt: qqnt_version.curVersion ?? qqnt_version.version,
// LiteLoader 的版本信息
liteloader: liteloader_package.version,
// Node.js 的版本信息
node: process.versions.node,
// Chromium 的版本信息
chrome: process.versions.chrome,
// Electron 的版本信息
electron: process.versions.electron
},
os: {
// 操作系统平台
platform: process.platform
},
package: {
// LiteLoader 的包信息
liteloader: liteloader_package,
// QQNT 的包信息
qqnt: qqnt_package
},
// 插件列表
plugins: {},
api: {
config: {
// 设置配置
set: setConfig,
// 获取配置
get: getConfig
},
plugin: {
// 安装插件
install: pluginInstall,
// 删除插件
delete: pluginDelete,
// 启用/禁用插件
disable: pluginDisable
},
// 打开外部链接
openExternal: shell.openExternal,
// 打开文件路径
openPath: shell.openPath
}
};
// 将 LiteLoader 对象挂载到全局
const whitelist = [
// LiteLoader 的根目录
LiteLoader.path.root,
// LiteLoader 的配置文件路径
LiteLoader.path.profile,
// LiteLoader 的数据文件夹路径
LiteLoader.path.data,
// LiteLoader 的插件文件夹路径
LiteLoader.path.plugins,
];
try {
whitelist.push(fs.realpathSync(LiteLoader.path.root));
whitelist.push(fs.realpathSync(LiteLoader.path.profile));
whitelist.push(fs.realpathSync(LiteLoader.path.plugins));
whitelist.push(fs.realpathSync(LiteLoader.path.data));
} catch { };
// 将 LiteLoader 对象挂载到全局
Object.defineProperty(globalThis, "LiteLoader", {
// 不可修改
configurable: false,
// 不可枚举
get() {
const stack = new Error().stack.split("\n")[2];
if (whitelist.some(item => stack.includes(item))) {
return LiteLoader;
}
}
});
// 将 LiteLoader 对象挂载到 window
ipcMain.on("LiteLoader.LiteLoader.LiteLoader", (event) => {
// 阻止事件继续传播
event.returnValue = {
...LiteLoader,
api: void null
}
});
// 监听 LiteLoader.api
ipcMain.handle("LiteLoader.LiteLoader.api", (event, name, method, args) => {
try {
if (name == method) return LiteLoader.api[method](...args);
else return LiteLoader.api[name][method](...args);
} catch (error) {
return null;
}
});Loader Core 是一个核心模块,负责加载插件、启用插件、禁用插件等核心功能。
位于 ./loader_core/ 文件夹中,在 LiteLoader 的加载流程中,他是在 LiteLoader API 之后被加载的,加载的文件为 loader_core.js。
// 引入 Electron 的 app 和 dialog 模块
// app 模块用于获取 Electron 的基本信息
// 例如: app.quit() 退出 Electron
// dialog 模块用于弹出对话框
// 例如: dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] }) 弹出对话框
const { app, dialog } = require("electron");
// 加载 nodejs 的 path 和 fs 模块
// path: 用于处理文件路径,如拼接路径等
// 例如: path.dirname(path) 获取指定路径所在的文件夹名称
const path = require("node:path");
// fs: 提供了文件系统操作的 API,如读写文件、创建目录等。
// 例如: fs.readFileSync(path) 读取指定路径的文件内容
const fs = require("node:fs");
// config.json 文件如下
// {
// "enable_plugins": true, // 是否启用所有插件
// "disabled_plugins": [], // 禁用的插件列表
// "deleting_plugins": {}, // 下次启动时要删除的插件列表
// "installing_plugins": {} // 下次启动时要安装的插件列表
// }
// 获取默认的配置文件
const default_config = require("../settings/static/config.json");
// 加载 admZip 模块,用于压缩和解压缩文件
const admZip = (() => {
// .node文件是Node.js中的一个模块,可以包含JavaScript代码、JSON或者编译过的C/C++扩展。
// 指定 major 模块路径(内部)
const major_node = path.join(process.resourcesPath, "app/major.node");
// 指定 launcher 模块路径(外部)
const launcher_node = path.join(process.resourcesPath, "app/app_launcher/launcher.node");
// 调用 fs 模块的 existsSync 函数判断 major.node 是否存在
if (fs.existsSync(major_node)) {
// 如果存在,则加载 major.node
require(major_node).load("internal_admzip", module);
}
else {
// 如果不存在,则加载 launcher.node
require(launcher_node).load("external_admzip", module);
}
// 返回加载的 exports.admZip.default 给外部的 admZip
return exports.admZip.default;
})();
// 定义 output 函数,用于输出日志,会在前面加上绿色的 [LiteLoader] 字样
const output = (...args) => console.log("\x1b[32m%s\x1b[0m", "[LiteLoader]", ...args);
// 获取 LiteLoader 的默认配置
const config = LiteLoader.api.config.get("LiteLoader", default_config);
// 通过 slug 删除插件
function deletePlugin(slug) {
try {
// 获取插件的删除路径和数据路径
const { plugin_path, data_path } = config.deleting_plugins[slug];
// 递归删除插件的数据文件,recursive: true 表示启用递归删除
if (data_path) fs.rmSync(data_path, { recursive: true });
// 递归删除插件的根目录,recursive: true 表示启用递归删除
fs.rmSync(plugin_path, { recursive: true });
}
catch (error) {
// 输出错误信息
output("Deleting Plugin Error", error);
// 在程序准备创建窗口时弹出错误对话框
app.on("ready", () => {
// 弹出错误对话框
dialog.showMessageBox(null, {
// 设置对话框的类型
type: "error",
// 设置对话框的标题
title: "LiteLoaderQQNT",
// 设置对话框的内容
message: `删除插件时报错,请检查并手动删除\n${error}`
});
});
}
finally {
// 最后删除 slug
delete config.deleting_plugins[slug];
// 更新 LiteLoader 的配置
LiteLoader.api.config.set("LiteLoader", config);
}
}
// 通过 slug 安装插件
function InstallPlugin(slug) {
try {
// 获取插件的文件路径和文化部类型
const { plugin_path, plugin_type } = config.installing_plugins[slug];
// 获取插件在 LiteLoader 存储的文件夹
const dest_path = path.join(LiteLoader.path.plugins, slug);
// 判断插件是否已存在,如果存在,则重命名
if (fs.existsSync(dest_path)) {
fs.renameSync(dest_path, `${dest_path}_${parseInt(Math.random() * 100000)} `);
}
// 判断插件文件是否为 ZIP 文件
if (plugin_type == "zip") {
// 解压 ZIP 文件到插件文件夹
new admZip(plugin_path).extractAllTo(dest_path);
}
// 判断插件文件是否为 JSON 文件
if (plugin_type == "json") {
// 复制插件所在的文件夹到插件文件夹
fs.cpSync(path.dirname(plugin_path), dest_path, { recursive: true });
}
}
catch (error) {
// 输出错误信息
output("Installing Plugin Error", error);
// 在程序准备创建窗口时弹出错误对话框
app.on("ready", () => {
// 弹出错误对话框
dialog.showMessageBox(null, {
// 设置对话框的类型
type: "error",
// 设置对话框的标题
title: "LiteLoaderQQNT",
// 设置对话框的内容
message: `安装插件时报错,请检查并手动安装\n${error}`
});
});
}
finally {
// 最后删除 slug
delete config.installing_plugins[slug];
// 更新 LiteLoader 的配置
LiteLoader.api.config.set("LiteLoader", config);
}
}
// 查找所有插件
function findAllPlugin() {
// 定义 plugins 为插件列表
const plugins = [];
try {
// 创建 LiteLoader 的插件文件夹
fs.mkdirSync(LiteLoader.path.plugins, { recursive: true });
// 遍历插件文件夹
for (const pathname of fs.readdirSync(LiteLoader.path.plugins, "utf-8")) {
try {
// 获取插件的 manifest.json 文件
const filepath = path.join(LiteLoader.path.plugins, pathname, "manifest.json");
// 读取 manifest.json 文件
const manifest = JSON.parse(fs.readFileSync(filepath, "utf-8"));
// 判断 manifest_version 插件版本是否为 4 则插入到 plugins 列表
if (manifest.manifest_version == 4) plugins.push({ pathname, manifest });
}
catch { continue }
}
}
catch (error) {
// 输出错误信息
output("Find Plugin Error", error);
// 在程序准备创建窗口时弹出错误对话框
app.on("ready", () => {
// 弹出错误对话框
dialog.showMessageBox(null, {
// 设置对话框的类型
type: "warning",
// 设置对话框的标题
title: "LiteLoaderQQNT",
// 设置对话框的内容
message: `在读取数据目录时报错了!请检查插件目录或忽略继续启动\n${error}`
});
});
}
// 返回插件列表
return plugins;
}
// 获取插件信息
// 第一个参数为插件的名称
// 第二个参数为 manifest 配置
function getPluginInfo(pathname, manifest) {
// 判断插件是否与当前平台不兼容
const incompatible_platform = !manifest.platform.includes(LiteLoader.os.platform);
// 判断插件是否被禁用
const disabled_plugin = config.disabled_plugins.includes(manifest.slug);
// 获取插件的绝对路径
const plugin_path = path.join(LiteLoader.path.plugins, pathname);
// 获取插件的数据路径
const data_path = path.join(LiteLoader.path.data, manifest.slug);
// 获取插件的 main.js 文件
const main_file = path.join(plugin_path, manifest?.injects?.main ?? "");
// 获取插件的 preload.js 文件
const preload_file = path.join(plugin_path, manifest?.injects?.preload ?? "");
// 获取插件的 renderer.js 文件
const renderer_file = path.join(plugin_path, manifest?.injects?.renderer ?? "");
return {
manifest: manifest,
incompatible: incompatible_platform,
disabled: disabled_plugin,
path: {
plugin: plugin_path,
data: data_path,
injects: {
main: fs.statSync(main_file).isFile() ? main_file : null,
preload: fs.statSync(preload_file).isFile() ? preload_file : null,
renderer: fs.statSync(renderer_file).isFile() ? renderer_file : null
}
}
}
}
// 加载所有插件
function loadAllPlugin() {
// 查找所有插件
const plugins = findAllPlugin();
// 创建依赖列表
const dependencies = new Set();
// 遍历插件的文件名和 manifest 配置
for (const { pathname, manifest } of plugins) {
// 输出插件信息
output("Found Plugin:", manifest.name);
// 获取插件信息
LiteLoader.plugins[manifest.slug] = getPluginInfo(pathname, manifest);
// 将插件的依赖项加到依赖列表中
manifest.dependencies?.forEach?.(slug => dependencies.add(slug));
}
// 获取插件的 slug 列表
const slugs = plugins.map(plugin => plugin.manifest.slug);
// 获取插件的依赖项中没有的依赖项
const missing = [...dependencies].filter(slug => !slugs.includes(slug));
// 遍历缺少的依赖项
for (const slug of missing) {
// 输出缺少的依赖项
output("Missing Plugin:", slug);
// 在程序准备创建窗口时弹出错误对话框
app.on("ready", () => {
// 弹出错误对话框
dialog.showMessageBox(null, {
// 设置对话框的类型
type: "warning",
// 设置对话框的标题
title: "LiteLoaderQQNT",
// 设置对话框的内容
message: `插件缺少依赖:${slug}`
});
});
}
}
// 删除上次要删除的插件
for (const slug in config.deleting_plugins) {
// 调用 deletePlugin 函数删除插件
deletePlugin(slug);
}
// 安装上次要安装的插件
for (const slug in config.installing_plugins) {
// 调用 InstallPlugin 函数安装插件
InstallPlugin(slug);
}
// 加载插件
if (config.enable_plugins) {
// 加载所有插件
loadAllPlugin();
}继 LiteLoaderAPI 接口,LoaderCore 载入所有插件之后,LiteLoader 的主要文件 main.js 开始被加载:
// 获取 LoaderCore 的 MainLoader 类
const { MainLoader } = require("./loader_core/main.js");
// 获取协议方案
const { protocolRegister } = require("./protocol_scheme/main.js");
// 加载 Node 的 path 模块
const path = require("path");
// 初始化 loader 加载器
const loader = new MainLoader().init();
// 代理的浏览器窗口对象
function proxyBrowserWindowConstruct(target, argArray, newTarget) {
// 创建被代理的浏览器窗口对象
const window = Reflect.construct(target, argArray, newTarget);
// 监听浏览器窗口 send 函数事件
window.webContents.send = new Proxy(window.webContents.send, {
apply(target, thisArg, [channel, ...args]) {
if (channel.includes("IPC_DOWN_")) {
// 判断账号是否登录成功
if (args?.[1]?.[0]?.cmdName == "nodeIKernelSessionListener/onSessionInitComplete") {
// 获取登陆的 uid
const uid = args[1][0].payload.uid;
// 传入到 loader 的 onLogin 函数
loader.onLogin(uid);
}
}
return Reflect.apply(target, thisArg, [channel, ...args]);
}
});
// 注入预加载文件
window.webContents._getPreloadPaths = new Proxy(window.webContents._getPreloadPaths, {
apply(target, thisArg, argArray) {
return [
...Reflect.apply(target, thisArg, argArray),
path.join(LiteLoader.path.root, "src/preload.js")
];
}
});
// 加载自定义协议
protocolRegister(window.webContents.session.protocol);
// 加载插件
// 给 loader 加载器 onBrowserWindowCreated 函数传入 window 对象
loader.onBrowserWindowCreated(window);
return window;
}
// 监听窗口创建并加载代理
require.cache["electron"] = new Proxy(require.cache["electron"], {
get(target, property, receiver) {
const electron = Reflect.get(target, property, receiver);
return property != "exports" ? electron : new Proxy(electron, {
get(target, property, receiver) {
// 通过拦截的方式,代理创建的窗口
const BrowserWindow = Reflect.get(target, property, receiver);
return property != "BrowserWindow" ? BrowserWindow : new Proxy(BrowserWindow, {
construct: proxyBrowserWindowConstruct
});
}
});
}
});LoaderCore 的 MainLoader 类位于 loader_core/main.js 中,用于初始化和管理插件:
// 拓扑排序函数
// 第一个参数为依赖项列表
// 会按照依赖关系进行排序
function topologicalSort(dependencies) {
// 创建排序后的插件列表
const sorted = [];
// 创建已访问的插件列表
const visited = new Set();
const visit = (slug) => {
// 如果插件已经访问过,则跳过
if (visited.has(slug)) return;
// 将插件添加到已访问的插件列表
visited.add(slug);
// 获取插件信息
const plugin = LiteLoader.plugins[slug];
// 遍历插件的依赖项添加到已访问的插件列表
plugin.manifest.dependencies?.forEach(depSlug => visit(depSlug));
// 将插件添加到排序后的插件列表
sorted.push(slug);
}
dependencies.forEach(slug => visit(slug));
return sorted;
}
// 创建全局的 MainLoader 类
exports.MainLoader = class {
#exports = {};
init() {
// 加载所有依赖插件
for (const slug of topologicalSort(Object.keys(LiteLoader.plugins))) {
// 通过 slug 获取插件信息
const plugin = LiteLoader.plugins[slug];
// 跳过禁用和不适用当前平台的插件
if (plugin.disabled || plugin.incompatible) {
continue;
}
// 加载插件的 main.js 文件
if (plugin.path.injects.main) {
try {
// 加载插件的 main.js 文件
this.#exports[slug] = require(plugin.path.injects.main);
}
catch (e) {
// 如果加载失败则保存错误到插件信息中
plugin.error = { message: `[Main] ${e.message}`, stack: e.stack };
}
}
}
return this;
}
// 当浏览器窗口创建时调用
onBrowserWindowCreated(window) {
// 遍历所有插件
for (const slug in this.#exports) {
// 获取插件信息
const plugin = this.#exports[slug];
// 调用插件的 onBrowserWindowCreated 函数
plugin.onBrowserWindowCreated?.(window);
}
}
// 当账号登录时调用
onLogin(uid) {
// 遍历所有插件
for (const slug in this.#exports) {
// 获取插件信息
const plugin = this.#exports[slug];
// 调用插件的 onLogin 函数
plugin.onLogin?.(uid);
}
}
}LiteLoaderNTQQ 自定义了一种 local:// 协议,协议的实现在 protocol_scheme/main.js,以下是使用自定义协议的例子:
// 访问本地的文件
// 等效于 C:/test.txt
const url1 = new URL("local:///C:/test.txt");
// 在 local 协议中,root 等效于 LiteLoader.path.root
// 等效于 LiteLoaderQQNT文件夹根目录/package.json
const url2 = new URL("local:///root/package.json");
// 在 local 协议中,profile 等效于 LiteLoader.path.profile
// 等效于 LiteLoaderQQNT文件夹根目录/package.json
// 或者等效于其他的配置文件目录
const url3 = new URL("local:///profile/package.json");自定义协议的实现 protocol_scheme/main.js:
// 加载 Electron 里的 app、protocol 和 net 模块
const { app, protocol, net } = require("electron");
// 加载 Node 的 path 模块
const path = require("path");
// 在程序准备创建窗口时添加自定义协议
app.on("ready", () => {
// 定义 local 协议
const schemes = ["local"];
// 获取原有的协议列表
const old_schemes = app.commandLine.getSwitchValue("fetch-schemes");
// 合并协议列表
const new_schemes = [old_schemes, ...schemes].join(",");
// 更改 fetch-schemes 参数
app.commandLine.appendSwitch("fetch-schemes", new_schemes);
});
// 注册自定义协议
protocol.registerSchemesAsPrivileged([
{
// 自定义协议名称
scheme: "local",
// 是否支持标准协议
privileges: {
// 不支持标准协议
standard: false,
// 支持跨域
allowServiceWorkers: true,
// 不支持跨源请求
corsEnabled: false,
// 支持 Fetch API
supportFetchAPI: true,
// 支持流
stream: true,
// 绕过 CSP 内容安全策略
bypassCSP: true
}
}
]);
// 注册全局协议
exports.protocolRegister = (protocol) => {
// 检查协议是否已经注册
if (!protocol.isProtocolRegistered("local")) {
// 注册协议
protocol.handle("local", (req) => {
// 解析请求的 URL
const { host, pathname } = new URL(decodeURI(req.url));
// 获取文件路径
const filepath = path.normalize(pathname.slice(1));
// 根据不同的主机名返回不同的文件路径
switch (host) {
// 根目录
case "root": return net.fetch(`file:///${LiteLoader.path.root}/${filepath}`);
// 配置目录
case "profile": return net.fetch(`file:///${LiteLoader.path.profile}/${filepath}`);
// 插件目录
default: return net.fetch(`file://${host}/${filepath}`);
}
});
}
}LiteLoaderQQNT 通过给 window.webContents._getPreloadPaths 添加代理的方法注入 ./preload.js 脚本:
// 监听 HTML 文档加载完成事件
document.addEventListener("DOMContentLoaded", () => {
// 创建 script 标签
const script = document.createElement("script");
// 设置 script 标签的属性
script.type = "module";
// 加载 renderer.js
script.src = `local://root/src/renderer.js`;
// 插入 script 标签到页面的头部后面
document.head.prepend(script);
});
// 运行外部脚本
// 定义全局 runPreloadScript 函数
//
Object.defineProperty(globalThis, "runPreloadScript", {
// 不可删除或重新配置其特性
configurable: false,
// 限制函数只能访问以下属性:
// require、process、Buffer、global、
// setImmediate、clearImmediate、exports、module
value: content => new Function(
// require 函数
// 用于加载 Node.js 模块
"require",
// process 类 例如:
// process.platform() 获取系统平台名称
// process.version() 获取 Node.js 版本号
"process",
// Buffer 类 用于处理二进制数据 例如:
// Buffer.from("Hello, World!") 从字符串创建 Buffer 对象
// Buffer.alloc(10) 创建一个长度为 10 的 Buffer 对象
"Buffer",
// global 全局对象 例如:
// global.process 获取全局环境的 process 变量
"global",
// setImmediate 通常用于将代码推迟到当前操作完成后执行
// 相比于 setTimeout(fn, 0), setImmediate 通常具有更好的性能
"setImmediate",
// 取消设置的 setImmediate 回调
"clearImmediate",
// exports 导出对象
"exports",
// module 模块对象 例如: module.exports = {}
"module",
// 插件的内容
content
)(...arguments)
});
// 加载插件 Preload
(async () => {
// 运行 ./liteloader_api/preload.js
runPreloadScript(await (await fetch(`local://root/src/liteloader_api/preload.js`)).text());
// 运行 ./loader_core/preload.js
runPreloadScript(await (await fetch(`local://root/src/loader_core/preload.js`)).text());
})();在上文中提到 ./preload.js 脚本,它通过 window.webContents._getPreloadPaths 添加代理的方式注入了 ./liteloader_api/preload.js 脚本,该脚本为 LiteLoader 的 API 模块,提供了一些 API 供插件使用。
// 引入 Electron 的模块
// ipcRenderer 用于在渲染进程中与主进程通信。
// contextBridge 用于安全地将对象暴露给渲染进程的全局对象。
const { ipcRenderer, contextBridge } = require("electron");
// 定义 invokeAPI 函数
// 通过 ipcRenderer 实现,用于调用主进程的 API。
function invokeAPI(name, method, args) {
return ipcRenderer.invoke("LiteLoader.LiteLoader.api", name, method, args);
}
// 定义 LiteLoader 接口
Object.defineProperty(globalThis, "LiteLoader", {
value: {
...ipcRenderer.sendSync("LiteLoader.LiteLoader.LiteLoader"),
api: {
config: {
// 获取配置项
get: (...args) => invokeAPI("config", "get", args),
// 设置配置项
set: (...args) => invokeAPI("config", "set", args)
},
plugin: {
// 安装插件
install: (...args) => invokeAPI("plugin", "install", args),
// 删除插件
delete: (...args) => invokeAPI("plugin", "delete", args),
// 禁用插件
disable: (...args) => invokeAPI("plugin", "disable", args)
},
// 打开外部链接
openExternal: (...args) => invokeAPI("openExternal", "openExternal", args),
// 打开文件路径
openPath: (...args) => invokeAPI("openPath", "openPath", args)
}
}
});
// 暴露 LiteLoader 对象到渲染进程的全局对象
contextBridge.exposeInMainWorld("LiteLoader", LiteLoader);Preload Loader Core 用于调用插件的 preload.js 脚本。
// 引入 Electron 的模块
const { contextBridge } = require("electron");
// 拓扑排序函数
// 第一个参数为依赖项列表
// 会按照依赖关系进行排序
function topologicalSort(dependencies) {
// 创建排序后的插件列表
const sorted = [];
// 创建已访问的插件列表
const visited = new Set();
const visit = (slug) => {
// 如果插件已经访问过,则跳过
if (visited.has(slug)) return;
// 将插件添加到已访问的插件列表
visited.add(slug);
// 获取插件信息
const plugin = LiteLoader.plugins[slug];
// 遍历插件的依赖项添加到已访问的插件列表
plugin.manifest.dependencies?.forEach(depSlug => visit(depSlug));
// 将插件添加到排序后的插件列表
sorted.push(slug);
}
dependencies.forEach(slug => visit(slug));
return sorted;
}
// 创建类并初始化
(new class {
async init() {
// 插件错误列表
const preloadErrors = {}
// 遍历所有插件
for (const slug of topologicalSort(Object.keys(LiteLoader.plugins))) {
// 获取插件信息
const plugin = LiteLoader.plugins[slug];
// 跳过已禁用、不兼容、或存在错误的插件
if (plugin.disabled || plugin.incompatible || plugin.error) {
continue;
}
// 判断插件是否有预加载脚本
if (plugin.path.injects.preload) {
try {
// 运行插件的 preload 脚本
runPreloadScript(await (await fetch(`local:///${plugin.path.injects.preload}`)).text());
}
catch (e) {
// 记录插件的错误信息
preloadErrors[slug] = { message: `[Preload] ${e.message}`, stack: e.stack };
}
}
}
// 暴露插件错误列表到渲染进程的全局对象
contextBridge.exposeInMainWorld("LiteLoaderPreloadErrors", preloadErrors);
return this;
}
}).init();// 加载 LiteLoaderQQNT 的组件
import "./components/renderer.js";
// 加载 LiteLoaderQQNT 的彩蛋
import "./easter_eggs/renderer.js";
// 加载 Settings 模块的设置界面接口
import { SettingInterface } from "./settings/renderer.js";
// 加载 Loader Core 的 RendererLoader 渲染加载器
import { RendererLoader } from "./loader_core/renderer.js";
// 初始化渲染加载器
const loader = await new RendererLoader().init();
// 寻找指定元素
function findElement(selector, callback) {
// 创建观察者
const observer = (_, observer) => {
// 查找元素
const element = document.querySelector(selector);
// 判断 element 是否不为 null 空值
if (element) {
callback(element);
// 断开观察者模式
observer?.disconnect?.();
// 返回 true 给 observer
return true;
}
// 返回 false 给 observer
return false;
}
// 判断元素是否找到
if (!observer()) {
// 监听页面变化直到页面有这个元素
new MutationObserver(observer).observe(document, {
subtree: true,
attributes: false,
childList: true
});
}
}
// 监听页面变化
function watchURLHash(callback) {
// 判断当前 URL 的哈希值是否不包含 #/blank
if (!location.hash.includes("#/blank")) {
// 调用 callback 回调,参数为 URL 的哈希值
callback(location.hash);
}
// 若当前 URL 的哈希值包含 #/blank
else {
// 添加导航成功事件监听器
navigation.addEventListener("navigatesuccess", () => {
// 调用 callback 回调,参数为 URL 的哈希值
callback(location.hash)
}, { once: true });
}
}
// 指定页面触发
watchURLHash((currentHash) => {
// 判断当前 URL 的哈希值是否包含 #/setting
if (currentHash.includes("#/setting")) {
// 创建设置界面接口
const settingInterface = new SettingInterface();
// 查找 .setting-tab .nav-bar
findElement(".setting-tab .nav-bar", () => {
// 调用设置接口的初始化函数
settingInterface.SettingInit();
// 调用 loader_core 的 loader 加载器的 onSettingWindowCreated 函数
loader.onSettingWindowCreated(settingInterface);
});
}
});
// 创建代理对象
Proxy = new Proxy(Proxy, {
// 拦截 new 操作符
construct(target, argArray, newTarget) {
// 获取组件实例
const component = argArray[0]?._;
// 获取组件元素
const element = component?.vnode?.el;
// 判断组件的 uid 是否大于等于 0
if (component?.uid >= 0) {
// 判断组件元素是否不为 null 空值
if (element) {
// 监听组件卸载
watchComponentUnmount(component);
// 记录组件实例
recordComponent(component);
// 调用 loader_core 的 loader 加载器的 onVueComponentMount 函数
loader.onVueComponentMount(component);
// 监听组件挂载事件
} else watchComponentMount(component);
}
// 通过反射返回新的实例
return Reflect.construct(target, argArray, newTarget);
}
});
// 记录组件实例
function recordComponent(component) {
// 获取组件的虚拟节点对应的 DOM 元素
let element = component.vnode.el;
// 循环向上寻找 HTMLElement
while (!(element instanceof HTMLElement)) {
element = element.parentElement;
}
// 判断元素是否已经记录过组件实例
// 插入组件到 元素.__VUE__ 数组中
if (element.__VUE__) element.__VUE__.push(component);
// 否则创建元素.__VUE__ 数组
else element.__VUE__ = [component];
// 为元素添加 vue-component 类
element.classList.add("vue-component");
}
// 监听组件挂载
function watchComponentMount(component) {
// 存储组件的 DOM 元素
let value = null;
// 存储是否已经挂载的标志
let hooked = false;
// 重写组件的 vnode.el 属性
Object.defineProperty(component.vnode, "el", {
// 返回 DOM 元素
get() { return value },
// 设置 DOM 元素
set(newValue) {
// 设置 DOM 元素
value = newValue;
// 判断是否已挂载
if (!hooked && this.el) {
hooked = true;
// 监听组件卸载
watchComponentUnmount(component);
// 调用 loader_core 的 loader 加载器的 onVueComponentMount 函数
loader.onVueComponentMount(component);
}
if (value) {
// 记录组件实例
recordComponent(component);
}
}
});
}
// 监听组件卸载
function watchComponentUnmount(component) {
// 存储卸载标志
let value = null;
// 存储是否 hook 的标志
let unhooked = false;
// 重写组件的 isUnmounted 属性
Object.defineProperty(component, "isUnmounted", {
get() { return value },
set(newValue) {
value = newValue;
if (!unhooked && this.isUnmounted) {
// 存储是否 hook 的标志
unhooked = true;
// 调用 loader_core 的 loader 加载器的 onVueComponentUnmount 函数
loader.onVueComponentUnmount(component);
}
}
});
}SettingInterface 为 LiteLoader 插件提供了设置界面接口,可用于创建插件设置界面。
// 获取默认配置
import default_config from "./static/config.json" with {type: "json"};
// 创建全局 SettingInterface 类
export class SettingInterface {
// 获取 LiteLoader 导航栏
#liteloader_nav_bar = document.createElement("div");
// 获取 LiteLoader 设置界面
#liteloader_setting_view = document.createElement("div");
// 获取设置面板元素
#setting_view = document.querySelector(".setting-main .q-scroll-view");
// 获取设置界面标题
#setting_title = document.querySelector(".setting-main .setting-title");
constructor() {
// 初始化导航栏样式
this.#liteloader_nav_bar.classList.add("nav-bar", "liteloader");
// 初始化设置面板的样式
this.#liteloader_setting_view.classList.add("q-scroll-view", "scroll-view--show-scrollbar", "liteloader");
// 初始化显示状态
this.#liteloader_setting_view.style.display = "none";
// 添加导航栏到页面中
document.querySelector(".setting-tab").append(this.#liteloader_nav_bar);
// 添加设置面板到页面中
document.querySelector(".setting-main .setting-main__content").append(this.#liteloader_setting_view);
// 监听导航栏点击事件
document.querySelector(".setting-tab").addEventListener("click", event => {
// 克隆导航栏元素
const nav_item = event.target.closest(".nav-item");
// 判断对象是否不为 null
if (nav_item) {
// 判断是否为 LiteLoader 插件
if (nav_item.parentElement.classList.contains("liteloader")) {
// 隐藏设置面板
this.#setting_view.style.display = "none";
// 显示 LiteLoader 的设置面板
this.#liteloader_setting_view.style.display = "block";
}
// 否则显示默认设置面板
else {
// 显示默认设置面板
this.#setting_view.style.display = "block";
// 隐藏 LiteLoader 的设置面板
this.#liteloader_setting_view.style.display = "none";
}
// 重新设定激活状态
this.#setting_title.childNodes[1].textContent = nav_item.querySelector(".name").textContent;
// 移除所有激活状态
document.querySelectorAll(".setting-tab .nav-item").forEach(element => {
element.classList.remove("nav-item-active");
});
// 添加激活状态
nav_item.classList.add("nav-item-active");
}
});
}
// 添加插件设置面板
add(plugin) {
// 指定默认缩略图路径
const default_thumb = `local://root/src/settings/static/default.svg`;
// 指定插件缩略图路径
const plugin_thumb = `local:///${plugin.path.plugin}/${plugin.manifest?.thumb}`;
// 获取实际有效缩略图
const thumb = plugin.manifest.thumb ? plugin_thumb : default_thumb;
// 克隆导航栏元素
const nav_item = document.querySelector(".setting-tab .nav-item").cloneNode(true);
// 创建设置面板元素
const view = document.createElement("div");
// 移除导航栏激活状态
nav_item.classList.remove("nav-item-active");
// 设置导航栏的 data-slug 属性
nav_item.setAttribute("data-slug", plugin.manifest.slug);
// 设置导航栏的图标
appropriateIcon(thumb).then(async text => nav_item.querySelector(".q-icon").innerHTML = text);
// 设置导航栏的名称
nav_item.querySelector(".name").textContent = plugin.manifest.name;
// 监听导航栏点击事件
nav_item.addEventListener("click", event => {
if (!event.currentTarget.classList.contains("nav-item-active")) {
this.#liteloader_setting_view.textContent = null;
this.#liteloader_setting_view.append(view);
}
});
// 添加导航栏
this.#liteloader_nav_bar.append(nav_item);
// 创建设置面板
view.classList.add("tab-view", plugin.manifest.slug);
return view;
}
// 初始化设置面板
SettingInit() {
// 获取 CSS 样式
const style = document.createElement("link");
style.rel = "stylesheet";
style.type = "text/css";
style.href = "local://root/src/settings/static/style.css";
// 使样式生效
document.head.append(style);
// 创建设置面板
const view = this.add({
manifest: {
// 标识
slug: "config_view",
// 名称
name: "LiteLoaderQQNT",
// 图标
thumb: "./src/settings/static/default.svg"
},
path: {
// 目录
plugin: LiteLoader.path.root
}
});
// 获取 view.html 内容并显示在刚刚添加的 view 中
fetch("local://root/src/settings/static/view.html").then(async res => {
// 设置 view 视图的内容
view.innerHTML = await res.text();
// 初始化版本信息
initVersions(view);
// 初始化插件列表
initPluginList(view);
// 初始化路径
initPath(view);
// 初始化关于
initAbout(view);
});
}
// 创建错误视图
// 第一个参数: 错误信息
// 第二个参数: 插件 slug
// 第三个参数: 错误视图
createErrorView(error, slug, view) {
const navItem = document.querySelector(`.nav-item[data-slug="${slug}"]`);
navItem.classList.add("error");
navItem.title = "插件加载出错";
view.classList.add("error");
view.innerHTML =
`<h2>🙀 插件加载出错!</h2>
<p>可能是版本不兼容、Bug、冲突或文件损坏等导致的</p>
🐞 错误信息
<textarea readonly rows="8">${error.message}\n${error.stack}</textarea>
🧩 插件信息
<textarea readonly rows="12">${JSON.stringify(LiteLoader.plugins[slug])}</textarea>
<textarea readonly rows="3">${JSON.stringify(Object.keys(LiteLoader.plugins))}</textarea>
🖥️ 环境信息
<textarea readonly rows="3">${JSON.stringify({ ...LiteLoader.versions, ...LiteLoader.os })}</textarea>
<small>* 此页面仅在插件加载出现问题出现,不代表插件本身有设置页</small>`; // 没必要格式化json,方便截图
}
}
// 自适应图标
async function appropriateIcon(pluginIconUrlUsingLocalPotocol) {
// 判断插件图标是否以 SVG 结尾
if (pluginIconUrlUsingLocalPotocol.endsWith('.svg')) {
// 返回加载的 SVG
return await (await fetch(pluginIconUrlUsingLocalPotocol)).text();
} else {
// 返回图片标签
return `<img src="${pluginIconUrlUsingLocalPotocol}"/>`;
}
}
// 初始化版本信息控件
async function initVersions(view) {
// LiteLoader 版本
const liteloader = view.querySelectorAll(".versions .current .liteloader setting-text");
// QQNT 版本
const qqnt = view.querySelectorAll(".versions .current .qqnt setting-text");
// Electron 版本
const electron = view.querySelectorAll(".versions .current .electron setting-text");
// Chromium 版本
const chromium = view.querySelectorAll(".versions .current .chromium setting-text");
// Node.js 版本
const nodejs = view.querySelectorAll(".versions .current .nodejs setting-text");
// 显示 LiteLoader 版本信息
liteloader[1].textContent = LiteLoader.versions.liteloader;
// 显示 QQNT 版本信息
qqnt[1].textContent = LiteLoader.versions.qqnt;
// 显示 Electron 版本信息
electron[1].textContent = LiteLoader.versions.electron;
// 显示 Chromium 版本信息
chromium[1].textContent = LiteLoader.versions.chrome;
// 显示 Node.js 版本信息
nodejs[1].textContent = LiteLoader.versions.node;
// LiteLoader 标题
const title = view.querySelector(".versions .new setting-text");
// LiteLoader 更新按钮
const update_btn = view.querySelector(".versions .new setting-button");
// 跳转链接函数
const jump_link = () => LiteLoader.api.openExternal(update_btn.value);
const try_again = () => {
// 设置标题内容
title.textContent = "正在瞅一眼 LiteLoaderQQNT 是否有新版本";
// 设置更新按钮显示内容
update_btn.textContent = "你先别急";
update_btn.value = null;
// 移除点击事件
update_btn.removeEventListener("click", jump_link);
update_btn.removeEventListener("click", try_again);
// 检测是否有新版
const repo_url = LiteLoader.package.liteloader.repository.url;
// 拼接仓库地址
const release_latest_url = `${repo_url.slice(0, repo_url.lastIndexOf(".git"))}/releases/latest`;
// 获取最新版本号
fetch(release_latest_url).then((res) => {
const new_version = res.url.slice(res.url.lastIndexOf("/") + 1);
// 判断当前版本是否与最新版本不相等
if (LiteLoader.versions.liteloader != new_version) {
// 设置标题内容
title.textContent = `发现 LiteLoaderQQNT 新版本 ${new_version}`;
// 设置更新按钮显示内容
update_btn.textContent = "去瞅一眼";
update_btn.value = res.url;
// 移除点击事件
update_btn.removeEventListener("click", try_again);
update_btn.addEventListener("click", jump_link);
}
// 若版本相等则不需要更新
else {
// 设置标题内容
title.textContent = "暂未发现 LiteLoaderQQNT 有新版本,目前已是最新";
// 设置更新按钮显示内容
update_btn.textContent = "重新发现";
update_btn.value = null;
// 移除点击事件
update_btn.removeEventListener("click", jump_link);
update_btn.addEventListener("click", try_again);
}
}).catch((e) => {
// 设置标题内容
title.textContent = `检查更新时遇到错误:${e}`;
// 设置更新按钮显示内容
update_btn.textContent = "重新发现";
update_btn.value = null;
// 移除点击事件
update_btn.removeEventListener("click", jump_link);
update_btn.addEventListener("click", try_again);
});
};
// 检查版本更新
try_again();
}
// 初始化插件列表控件
async function initPluginList(view) {
// 获取插件控件模板
const plugin_item_template = view.querySelector("#plugin-item");
// 获取插件安装按钮控件
const plugin_install_button = view.querySelector(".plugins .plugin .install setting-button");
// 获取插件加载开关控件
const plugin_loader_switch = view.querySelector(".plugins .plugin .loader setting-switch");
// 获取插件类型列表控件
const plugin_lists = {
// 插件类型
extension: view.querySelector(".plugins .extension"),
// 主题类型
theme: view.querySelector(".plugins .theme"),
// 框架类型
framework: view.querySelector(".plugins .framework"),
};
// 获取 “选择文件” 按钮
const input_file = document.createElement("input");
// 限制文件选择
input_file.type = "file";
// 限制文件类型
input_file.accept = ".zip,.json";
// 监听文件选择
input_file.addEventListener("change", async () => {
// 获取文件路径
const filepath = input_file.files?.[0]?.path;
// 获取 LiteLoader 配置
const config = await LiteLoader.api.config.get("LiteLoader", default_config);
// 判断是否已安装
const has_install = Object.values(config.installing_plugins).some(item => item.plugin_path == filepath);
const is_install = await LiteLoader.api.plugin.install(filepath, has_install);
alert(is_install ? (has_install ? "已取消安装此插件" : "将在下次启动时安装") : "无法安装无效插件");
input_file.value = null;
});
// 监听点击事件
plugin_install_button.addEventListener("click", () => input_file.click());
// 监听插件加载开关
const config = await LiteLoader.api.config.get("LiteLoader", default_config);
plugin_loader_switch.toggleAttribute("is-active", config.enable_plugins);
plugin_loader_switch.addEventListener("click", () => {
// 获取当前状态
const isActive = plugin_loader_switch.hasAttribute("is-active");
// 切换状态
plugin_loader_switch.toggleAttribute("is-active", !isActive);
// 更新配置
config.enable_plugins = !isActive;
// 保存配置
LiteLoader.api.config.set("LiteLoader", config);
});
// 获取各种类型插件数量
const plugin_counts = {
// 插件类型
extension: 0,
// 主题类型
theme: 0,
// 框架类型
framework: 0
}
// 遍历插件
for (const [slug, plugin] of Object.entries(LiteLoader.plugins)) {
// 跳过不兼容插件
if (plugin.incompatible) {
continue;
}
// 获取默认图标
const default_icon = `local://root/src/settings/static/default.png`;
// 获取插件图标
const plugin_icon = `local:///${plugin.path.plugin}/${plugin.manifest?.icon}`;
// 获取显示图标
const icon = plugin.manifest?.icon ? plugin_icon : default_icon;
// 获取插件列表
const plugin_list = plugin_lists[plugin.manifest.type] || plugin_lists.extension;
// 复制插件模板
const plugin_item = plugin_item_template.content.cloneNode(true);
// 获取项目图标
const plugin_item_icon = plugin_item.querySelector(".icon");
// 获取项目名称
const plugin_item_name = plugin_item.querySelector(".name");
// 获取项目描述
const plugin_item_description = plugin_item.querySelector(".description");
// 获取项目版本
const plugin_item_version = plugin_item.querySelector(".version");
// 获取项目作者
const plugin_item_authors = plugin_item.querySelector(".authors");
// 获取项目仓库
const plugin_item_repo = plugin_item.querySelector(".repo");
// 获取插件管理按钮
const plugin_item_manager = plugin_item.querySelector(".manager");
// 获取插件管理弹窗
const plugin_item_manager_modal = plugin_item.querySelector(".manager-modal");
// 获取启用按钮
const manager_modal_enable = plugin_item_manager_modal.querySelector(".enable");
// 获取保留数据
const manager_modal_keepdata = plugin_item_manager_modal.querySelector(".keepdata");
// 获取卸载按钮
const manager_modal_uninstall = plugin_item_manager_modal.querySelector(".uninstall");
// 设置插件图标
plugin_item_icon.innerHTML = await appropriateIcon(icon);
// 设置插件名称
plugin_item_name.textContent = plugin.manifest.name;
// 设置插件标题
plugin_item_name.title = plugin.manifest.name;
// 设置插件描述
plugin_item_description.textContent = plugin.manifest.description;
// 设置插件标题
plugin_item_description.title = plugin.manifest.description;
// 获取版本链接控件
const version_link = document.createElement("setting-link");
// 设置插件版本
version_link.textContent = plugin.manifest.version;
// 添加版本链接
plugin_item_version.append(version_link);
plugin.manifest.authors?.forEach((author, index, array) => {
const author_link = document.createElement("setting-link");
author_link.textContent = author.name;
author_link.dataset["value"] = author.link;
plugin_item_authors.append(author_link);
if (index < array.length - 1) {
plugin_item_authors.append(" | ");
}
});
if (plugin.manifest.repository) {
const { repo, branch } = plugin.manifest.repository
const repo_link = document.createElement("setting-link");
repo_link.textContent = repo;
repo_link.dataset["value"] = `https://github.com/${repo}/tree/${branch}`;
plugin_item_repo.append(repo_link);
} else plugin_item_repo.textContent = "暂无仓库信息";
plugin_item_manager_modal.dataset["title"] = plugin.manifest.name;
plugin_item_manager.addEventListener("click", () => {
plugin_item_manager_modal.toggleAttribute("is-active");
});
manager_modal_enable.toggleAttribute("is-active", !config.disabled_plugins.includes(slug));
manager_modal_enable.addEventListener("click", () => {
const isActive = manager_modal_enable.hasAttribute("is-active");
manager_modal_enable.toggleAttribute("is-active", !isActive);
LiteLoader.api.plugin.disable(slug, !isActive);
});
manager_modal_keepdata.toggleAttribute("is-active", !!config.deleting_plugins?.[slug]?.data_path);
manager_modal_keepdata.addEventListener("click", async () => {
const isActive = manager_modal_keepdata.hasAttribute("is-active");
manager_modal_keepdata.toggleAttribute("is-active", !isActive);
const config = await LiteLoader.api.config.get("LiteLoader", default_config);
if (slug in config.deleting_plugins) LiteLoader.api.plugin.delete(slug, !isActive, false);
});
manager_modal_uninstall.toggleAttribute("is-active", !!config.deleting_plugins?.[slug]);
manager_modal_uninstall.addEventListener("click", () => {
const isActive = manager_modal_uninstall.hasAttribute("is-active");
manager_modal_uninstall.toggleAttribute("is-active", !isActive);
const keepdata = manager_modal_keepdata.hasAttribute("is-active");
LiteLoader.api.plugin.delete(slug, keepdata, isActive);
});
plugin_list.append(plugin_item);
plugin_counts.total++;
plugin_counts[plugin.manifest.type]++;
}
plugin_lists.extension.dataset["title"] = `扩展 ( ${plugin_counts.extension} 个插件 )`;
plugin_lists.theme.dataset["title"] = `主题 ( ${plugin_counts.theme} 个插件 )`;
plugin_lists.framework.dataset["title"] = `依赖 ( ${plugin_counts.framework} 个插件 )`;
}
async function initPath(view) {
const root_path_content = view.querySelectorAll(".path .root setting-text")[2];
const root_path_button = view.querySelector(".path .root setting-button");
const profile_path_content = view.querySelectorAll(".path .profile setting-text")[2];
const profile_path_button = view.querySelector(".path .profile setting-button");
root_path_content.textContent = LiteLoader.path.root;
root_path_button.addEventListener("click", () => LiteLoader.api.openPath(LiteLoader.path.root));
profile_path_content.textContent = LiteLoader.path.profile;
profile_path_button.addEventListener("click", () => LiteLoader.api.openPath(LiteLoader.path.profile));
}
async function initAbout(view) {
const liteloaderqqnt = view.querySelector(".about .liteloaderqqnt");
const github = view.querySelector(".about .github");
const group = view.querySelector(".about .group");
const channel = view.querySelector(".about .channel");
liteloaderqqnt.addEventListener("click", () => LiteLoader.api.openExternal("https://liteloaderqqnt.github.io"));
github.addEventListener("click", () => LiteLoader.api.openExternal("https://github.com/LiteLoaderQQNT"));
group.addEventListener("click", () => LiteLoader.api.openExternal("https://t.me/LiteLoaderQQNT"));
channel.addEventListener("click", () => LiteLoader.api.openExternal("https://t.me/LiteLoaderQQNT_Channel"));
// Hitokoto - 一言
let visible = true;
const hitokoto_text = view.querySelector(".about .hitokoto_text");
const hitokoto_author = view.querySelector(".about .hitokoto_author");
const observer = new IntersectionObserver((entries) => {
visible = entries[0].isIntersecting;
});
observer.observe(hitokoto_text);
async function trueUpdate() {
const { hitokoto, creator } = await (await fetch("https://v1.hitokoto.cn")).json();
hitokoto_text.textContent = hitokoto;
hitokoto_author.textContent = creator;
}
async function fetchHitokoto() {
// 页面不可见或一言不可见时不更新
if (document.hidden || !visible) {
return;
}
await trueUpdate();
};
trueUpdate();
setInterval(fetchHitokoto, 1000 * 10);
}LiteLoader 的 components/renderer.js 为 LiteLoader 插件提供了自定义组件渲染器,以方便开发者快速开发界面。LiteLoader 官方有对 components/renderer.js 的详细文档,可以参考 LiteLoaderQQNT 官方文档。这里仅放代码供参考:
import style from "./static/style.css" with { type: "css" };
const html_url = "local://root/src/components/static/template.html";
const html_file = await (await fetch(html_url)).text();
const template = new DOMParser().parseFromString(html_file, "text/html");
Object.defineProperty(globalThis, "SettingElementStyleSheets", {
value: new class {
#styleSheets = [];
#callbacks = [];
set styleSheets(value) {
this.#styleSheets = value;
for (const callback of this.#callbacks) {
callback(this.#styleSheets);
}
}
get styleSheets() {
return this.#styleSheets;
}
on(callback) {
this.#callbacks.push(callback);
callback(this.#styleSheets);
}
}
});
class SettingElementBase extends HTMLElement {
constructor(element_id) {
super();
this.attachShadow({ mode: "open" });
this._template = template.getElementById(element_id);
this._content = this._template.content.cloneNode(true);
this._slot = this.shadowRoot.querySelector("slot");
this.shadowRoot.append(this._content);
SettingElementStyleSheets.on((styleSheets) => {
this.shadowRoot.adoptedStyleSheets = styleSheets;
});
}
attributeChangedCallback() {
this.update();
}
update() {
return;
}
}
SettingElementStyleSheets.styleSheets = [style];
customElements.define("setting-section", class extends SettingElementBase {
static observedAttributes = ["data-title"];
constructor() {
super("setting-section");
this._title = this.shadowRoot.querySelector("h1");
this.update();
}
update() {
this._title.textContent = this.dataset["title"];
}
});
customElements.define("setting-panel", class extends SettingElementBase {
constructor() {
super("setting-panel");
}
});
customElements.define("setting-list", class extends SettingElementBase {
static observedAttributes = ["data-title", "data-direction", "is-collapsible", "is-active", "is-disabled"];
constructor() {
super("setting-list");
this._head = this.shadowRoot.querySelector("setting-item");
this._title = this.shadowRoot.querySelector("h2");
this._slot = this.shadowRoot.querySelector("slot");
this._head.addEventListener("click", () => {
this.toggleAttribute("is-active");
});
this.update();
new MutationObserver((_, observer) => {
observer.disconnect();
this.update();
observer.observe(this, { childList: true });
}).observe(this, { childList: true });
}
update() {
this._title.textContent = this.dataset["title"];
const slot_children = this._slot.assignedElements();
this.querySelectorAll("setting-divider").forEach(node => node.remove());
// 折叠列表
if (this.hasAttribute("is-collapsible")) {
this._head.classList.toggle("hidden", false);
slot_children.forEach((node, index) => {
const setting_divider = document.createElement("setting-divider");
if (this.dataset["direction"] == "column") {
setting_divider.dataset["direction"] = "row";
node.dataset["direction"] = "row";
}
if (index < slot_children.length) {
node.before(setting_divider);
}
});
}
// 普通列表
else {
this._head.classList.toggle("hidden", true);
slot_children.forEach((node, index) => {
const setting_divider = document.createElement("setting-divider");
if (this.dataset["direction"] == "column") {
setting_divider.dataset["direction"] = "row";
node.dataset["direction"] = "row";
}
if (this.dataset["direction"] == "row") {
setting_divider.dataset["direction"] = "column";
node.dataset["direction"] = "column";
}
if (index + 1 < slot_children.length) {
node.after(setting_divider);
}
});
}
}
});
customElements.define("setting-item", class extends SettingElementBase {
static observedAttributes = ["data-direction"];
constructor() {
super("setting-item");
}
});
customElements.define("setting-select", class extends SettingElementBase {
static observedAttributes = ["is-disabled"];
constructor() {
super("setting-select");
this._title = this.shadowRoot.querySelector("input");
this._button = this.shadowRoot.querySelector(".menu-button");
this._context = this.shadowRoot.querySelector("ul");
const click = () => {
this._context.classList.toggle("hidden");
if (!this._context.classList.contains("hidden")) {
window.addEventListener("pointerup", pointerup);
this._context.style.width = getComputedStyle(this).getPropertyValue("width");
}
else {
window.removeEventListener("pointerup", pointerup);
this._context.style.width = null;
}
}
const pointerup = (event) => {
if (event.target.tagName != "SETTING-SELECT") {
click();
}
}
this._button.addEventListener("click", click);
this._context.addEventListener("click", (event) => {
if (event.target.tagName == "SETTING-OPTION" && !event.target.hasAttribute("is-selected")) {
for (const node of this.querySelectorAll("setting-option[is-selected]")) {
node.toggleAttribute("is-selected");
}
event.target.toggleAttribute("is-selected");
this._title.value = event.target.textContent;
this.dispatchEvent(new CustomEvent("selected", {
bubbles: true,
composed: true,
detail: {
name: event.target.textContent,
value: event.target.dataset["value"]
}
}));
}
});
this._title.value = this.querySelector("setting-option[is-selected]")?.textContent;
}
});
customElements.define("setting-option", class extends SettingElementBase {
static observedAttributes = ["data-value", "is-selected", "is-disabled"];
constructor() {
super("setting-option");
}
});
customElements.define("setting-switch", class extends SettingElementBase {
static observedAttributes = ["is-active", "is-disabled"];
constructor() {
super("setting-switch");
}
});
customElements.define("setting-button", class extends SettingElementBase {
static observedAttributes = ["data-type", "is-disabled"];
constructor() {
super("setting-button");
}
});
customElements.define("setting-text", class extends SettingElementBase {
static observedAttributes = ["data-type"];
constructor() {
super("setting-text");
}
});
customElements.define("setting-link", class extends SettingElementBase {
static observedAttributes = ["data-value"];
constructor() {
super("setting-link");
this.addEventListener("click", () => {
if (this.dataset["value"]) {
LiteLoader.api.openExternal(this.dataset["value"]);
}
});
}
});
customElements.define("setting-divider", class extends SettingElementBase {
static observedAttributes = ["data-direction"];
constructor() {
super("setting-divider");
}
});
customElements.define("setting-modal", class extends SettingElementBase {
static observedAttributes = ["data-title", "is-active"];
constructor() {
super("setting-modal");
this._title = this.shadowRoot.querySelector(".title");
this._close = this.shadowRoot.querySelector(".close");
this._modal = this.shadowRoot.querySelector(".modal");
this._close.addEventListener("click", () => this.toggleAttribute("is-active"));
this._modal.addEventListener("click", () => this.toggleAttribute("is-active"));
this.update();
}
update() {
this._title.textContent = this.dataset["title"];
}
});Loader Core 的 RendererLoader 为插件的 renderer.js 文件提供了监听页面渲染的接口,如插件的 onSettingWindowCreated、onVueComponentMount、onVueComponentUnmount 事件。
// 拓扑排序函数
// 第一个参数为依赖项列表
// 会按照依赖关系进行排序
function topologicalSort(dependencies) {
// 创建排序后的插件列表
const sorted = [];
// 创建已访问的插件列表
const visited = new Set();
const visit = (slug) => {
// 如果插件已经访问过,则跳过
if (visited.has(slug)) return;
// 将插件添加到已访问的插件列表
visited.add(slug);
// 获取插件信息
const plugin = LiteLoader.plugins[slug];
// 遍历插件的依赖项添加到已访问的插件列表
plugin.manifest.dependencies?.forEach(depSlug => visit(depSlug));
// 将插件添加到排序后的插件列表
sorted.push(slug);
}
dependencies.forEach(slug => visit(slug));
return sorted;
}
// 定义全局 RendererLoader 类
export class RendererLoader {
#exports = {};
async init() {
// 确保 Preload 加载完毕
if (!window.LiteLoaderPreloadErrors) {
// 等待 Preload 加载完毕
await new Promise(resolve => {
const check = () => (window.LiteLoaderPreloadErrors ? resolve() : setTimeout(check));
check();
});
}
// 加载插件
for (const slug of topologicalSort(Object.keys(LiteLoader.plugins))) {
const plugin = LiteLoader.plugins[slug];
if (plugin.disabled || plugin.incompatible) {
continue;
}
const error = plugin.error || LiteLoaderPreloadErrors[slug];
if (error) {
this.#exports[slug] = { error };
continue
}
if (plugin.path.injects.renderer) {
try {
// 加载插件的 renderer.js 文件
this.#exports[slug] = await import(`local:///${plugin.path.injects.renderer}`);
}
catch (e) {
// 捕获错误
this.#exports[slug] = { error: { message: `[Renderer] ${e.message}`, stack: e.stack } };
}
}
}
return this;
}
// 监听插件设置窗口创建
onSettingWindowCreated(settingInterface) {
// 遍历插件 slug 列表
for (const slug in this.#exports) {
// 获取插件信息
const plugin = this.#exports[slug];
try {
// 如果插件有错误,则抛出错误
if (plugin.error) throw plugin.error;
// 调用插件的 onSettingWindowCreated 方法
plugin.onSettingWindowCreated?.(settingInterface.add(LiteLoader.plugins[slug]));
}
catch (e) {
// 调用 SettingInterface 的 createErrorView 方法创建错误视图
const view = settingInterface.add(LiteLoader.plugins[slug]);
settingInterface.createErrorView(e, slug, view);
}
}
}
// 监听 Vue 组件挂载
onVueComponentMount(component) {
// 遍历插件 slug 列表
for (const slug in this.#exports) {
// 获取插件信息
const plugin = this.#exports[slug];
// 如果插件有错误,则跳过
plugin.onVueComponentMount?.(component);
}
}
// 监听 Vue 组件卸载
onVueComponentUnmount(component) {
// 遍历插件 slug 列表
for (const slug in this.#exports) {
// 获取插件信息
const plugin = this.#exports[slug];
// 如果插件有错误,则跳过
plugin.onVueComponentUnmount?.(component);
}
}
}- Chii-devtools 适用于 QQ NT 的 F12 开发者工具
Just F5 适用于 QQ NT 的页面刷新插件


