Fix-CSS-Relative-Paths
This commit is contained in:
235
DOC.MD
Normal file
235
DOC.MD
Normal file
@@ -0,0 +1,235 @@
|
||||
# ResourceShare.js 系统原理文档 (V1.3.7.4)
|
||||
|
||||
**版本:** V1.3.7.5
|
||||
**发布日期:** 2026
|
||||
**开发团队:** GVSDS Team
|
||||
**许可协议:** MIT License
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
`ResourceShare.js` 是一个高性能的跨域资源共享系统,专门设计用于解决复杂 Web 应用(特别是包含大量 iframe 嵌套的单页应用)中的资源重复加载、依赖管理混乱以及脚本执行顺序不可控等问题。
|
||||
|
||||
### 1.1 核心特性
|
||||
* **跨级资源共享:** 利用 `postMessage` 和 `Blob URL` 技术,使得同源下的子页面可以直接复用父页面已加载的 JS/CSS 资源,极大减少网络请求。
|
||||
* **智能脚本阻塞:** 在资源加载期间自动阻塞页面上的业务脚本,确保核心依赖(如 jQuery, Layui)先于业务代码执行。
|
||||
* **虚拟路径映射:** 为内联脚本和远程加载的脚本生成带 `VM_` 前缀的 SourceURL,解决调试时代码定位混乱的问题。
|
||||
* **可视化 UI 面板:** 提供原生 JavaScript 编写的加载日志、进度条和错误诊断面板,无需依赖第三方 UI 库。
|
||||
* **自动诊断系统:** 内置规则引擎,能够识别常见的错误(如 jQuery 未定义、CORS 错误)并给出修复建议。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
系统采用模块化类结构设计,主要分为以下几个核心部分:
|
||||
|
||||
### 2.1 类结构概览
|
||||
1. **`ResourceShareManager` (核心管理器)**: 负责资源加载、缓存管理、脚本拦截、跨页面通信协调。
|
||||
2. **`ResourceShareUI` (UI 管理器)**: 负责 DOM 元素的渲染(启动页、进度条、日志面板、错误弹窗)。
|
||||
3. **`ResourceShareLogger` (日志系统)**: 统一日志输出入口,负责根据配置过滤日志,并分发到 DevTools 控制台和 UI 面板。
|
||||
4. **`ResourceShareElement` (Web Components)**: 自定义标签 `<resource-share>`,用于声明页面需要加载的依赖资源。
|
||||
|
||||
### 2.2 初始化流程
|
||||
1. **配置合并**: 脚本加载时,首先执行 `deepMerge`,将用户定义的 `window.RS_CONFIG` 与系统默认配置合并。
|
||||
2. **单例检测**: 检查 `window.resourceShareManager` 是否存在,若不存在则实例化。
|
||||
3. **环境检测**: 判断当前页面是 `Top Level` (顶级父页面) 还是 `Sub Level` (子页面)。
|
||||
4. **组件初始化**:
|
||||
* 初始化 Logger。
|
||||
* 若 `ENABLE_UI` 为 true,初始化 UI 并显示启动屏。
|
||||
5. **DOM 扫描**: 监听 `DOMContentLoaded`,扫描页面中的 `<resource-share>` 标签,构建加载队列。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心机制详解
|
||||
|
||||
### 3.1 资源加载与缓存策略
|
||||
|
||||
系统采用“源驱动,缓存优先”的策略。
|
||||
|
||||
* **队列机制**: 扫描到的 `<resource-share>` 标签会被推入 `resourceExecutionQueue`。系统严格按顺序处理队列,确保依赖关系(例如 jQuery 必须在业务插件前加载)。
|
||||
* **顶级页面**:
|
||||
1. 检查本地内存缓存 (`this.cache`)。
|
||||
2. 若未缓存,发起 `fetch` 请求。
|
||||
3. **重试机制**: 内置请求失败重试逻辑(默认 3 次,间隔 1.5s),提高弱网环境下的成功率。
|
||||
4. **缓存决策**:
|
||||
* 小于 `POST_MESSAGE_LIMIT` (1MB):存储为字符串文本。
|
||||
* 大于阈值:转换为 `Blob URL` 并存储,防止 `postMessage` 数据过大导致报错。
|
||||
* **子页面**:
|
||||
1. 先通过 `postMessage` 向父页面请求资源 (`resource-request`)。
|
||||
2. 父页面若缓存,直接返回数据(文本或 Blob URL)。
|
||||
3. 父页面若未缓存,子页面回退到直接 `fetch`,并将结果同步给父页面更新缓存。
|
||||
|
||||
### 3.2 智能脚本阻塞系统
|
||||
|
||||
为了防止业务脚本在依赖库加载完成前执行,系统劫持了 DOM 操作。
|
||||
|
||||
1. **拦截目标**:
|
||||
* 页面初始化时已存在的 `<script>`。
|
||||
* 动态插入的 `<script>`(通过 `MutationObserver` 监听)。
|
||||
* `appendChild` 和 `insertBefore` 方法的调用。
|
||||
2. **拦截逻辑**:
|
||||
* 如果当前处于 `isBlockingEnabled` 状态。
|
||||
* 且脚本不是 `ResourceShare.js` 自身。
|
||||
* 且脚本不包含 `data-resource-share` 或 `DisableRS` 标记。
|
||||
* **结果**: 脚本被移除 DOM,其信息(src, text, async 等)被存入 `blockedScripts` 数组。
|
||||
3. **释放机制**:
|
||||
* 当所有 `<resource-share>` 依赖加载并执行完毕,或发生超时/错误时。
|
||||
* 调用 `releaseBlockedScripts()`,遍历 `blockedScripts`,重建 `<script>` 标签并插入 DOM 执行。
|
||||
|
||||
### 3.3 SourceURL 路径映射 (V1.3.7.4 更新)
|
||||
|
||||
为了提升开发体验,系统会对执行的脚本进行重定向,使其在 Chrome DevTools 中显示为独立的虚拟文件。
|
||||
|
||||
1. **内联脚本**:
|
||||
* 逻辑位于 `generateInlineScriptSourceURL()`。
|
||||
* 格式: `./RS_VM/VM_{页面名}_{序号}_{随机码}.js`。
|
||||
* 效果: 即使是写在 HTML 里的 JS,也会显示为一个独立的虚拟文件。
|
||||
2. **ResourceShare 远程脚本**:
|
||||
* 逻辑位于 `getResourceShareSourceURL(url)`。
|
||||
* **V1.3.7.4 修正**: 现在保留原始 URL 的目录结构,仅修改文件名。
|
||||
* 原始 URL: `https://cdn.example.com/js/3.6.0/jquery.min.js`
|
||||
* 映射后: `https://cdn.example.com/js/3.6.0/VM_jquery.min.js`
|
||||
* 效果: 开发者能清晰看到资源来源路径,同时区分出这是经过 ResourceShare 加载的版本。
|
||||
|
||||
### 3.4 跨域/跨框架通信
|
||||
|
||||
基于 `window.postMessage` 实现,实现了“父子握手”协议。
|
||||
|
||||
1. **Ping/Pong**: 子页面加载后发送 `resource-share-ping`,父页面回复 `resource-share-pong` 以建立连接。
|
||||
2. **资源请求**: 子页面发送 `resource-request` (包含 type, url, messageId)。
|
||||
3. **资源响应**: 父页面匹配缓存后,发送 `resource-response`。
|
||||
4. **缓存同步**: 若子页面自己加载了资源,会发送 `resource-cache-update` 通知父页面更新缓存。
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置说明
|
||||
|
||||
所有配置项挂载在 `window.RS_CONFIG` 下,支持在脚本加载前重写。
|
||||
|
||||
```javascript
|
||||
window.RS_CONFIG = {
|
||||
// 基础信息
|
||||
VERSION: '1.3.7.4',
|
||||
ENABLE_UI: true, // 是否显示内置 UI 面板
|
||||
|
||||
// 阈值 (单位: 字节/毫秒)
|
||||
POST_MESSAGE_LIMIT: 1048576, // 1MB,超过此大小使用 Blob URL 传递
|
||||
BLOB_LIMIT: 1073741824, // 1GB,最大缓存文件大小限制
|
||||
SCRIPT_TIMEOUT: 300000, // 5分钟,脚本强制释放超时
|
||||
REQUEST_TIMEOUT: 10000, // 10秒,跨父请求超时
|
||||
|
||||
// 重试策略
|
||||
RETRY: {
|
||||
COUNT: 3, // 失败重试次数
|
||||
DELAY: 1500 // 重试间隔
|
||||
},
|
||||
|
||||
// 日志控制
|
||||
LOG_LEVEL_UI: 'debug', // UI 面板显示级别
|
||||
LOG_LEVEL_DEVTOOLS: 'error', // 控制台显示级别
|
||||
|
||||
// Z-Index 管理
|
||||
Z_INDEX: {
|
||||
SPLASH: 2147483647,
|
||||
// ...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 错误处理与诊断 (V1.3.7.4)
|
||||
|
||||
系统包含一个快速代码诊断模块 (`DIAGNOSTIC_RULES`),当发生运行时错误时:
|
||||
|
||||
1. **捕获**: 通过 `window.onerror` 或 `try-catch` 捕获错误对象。
|
||||
2. **匹配**: 遍历诊断规则数组,检查错误信息或错误名称。
|
||||
3. **输出**:
|
||||
* **UI 层**: 弹出模态框 (`#rs-fatal-error-screen`),显示错误堆栈和修复建议。
|
||||
* **DevTools**: 以彩色格式输出诊断信息。
|
||||
4. **常见规则**:
|
||||
* `jquery_missing`: 检测 `$ is not defined`。
|
||||
* `layui_missing`: 检测 `layui is not defined`。
|
||||
* `cors_error`: 检测 `Failed to fetch` 及 CORS 相关字眼。
|
||||
|
||||
---
|
||||
|
||||
## 6. 版本更新日志 (V1.3.7.4)
|
||||
|
||||
**发布日期:** 2026
|
||||
|
||||
**重点修正: SourceURL 路径映射逻辑**
|
||||
|
||||
1. **修正 `getResourceShareSourceURL` 函数**:
|
||||
* **旧版行为**: 可能将路径完全扁平化或生成无意义的随机文件名,导致调试时难以定位真实资源路径。
|
||||
* **新版行为**:
|
||||
* 解析输入 URL 的路径部分 (`pathname`)。
|
||||
* 提取目录结构和文件名。
|
||||
* 仅在文件名前添加 `VM_` 前缀,目录结构保持不变。
|
||||
* 例如:`/lib/js/module.js` -> `/lib/js/VM_module.js`。
|
||||
* **影响**: 开发者在 Sources 面板中看到的文件树结构将与 CDN 或服务器结构一致,大幅提升调试体验。
|
||||
|
||||
2. **保留内联脚本逻辑**:
|
||||
* `generateInlineScriptSourceURL` 保持不变,继续生成 `./RS_VM/VM_{page}_{index}.js` 格式的路径,用于区分内联代码。
|
||||
|
||||
3. **稳定性优化**:
|
||||
* 增强了 `deepMerge` 函数对特殊对象结构的处理。
|
||||
* 优化了 UI 面板在暗黑模式下的色彩对比度。
|
||||
|
||||
---
|
||||
|
||||
## 7. 使用示例
|
||||
|
||||
### 7.1 基础 HTML 集成
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- 1. 引入 ResourceShare -->
|
||||
<script src="ResourceShare.js"></script>
|
||||
|
||||
<!-- 2. 声明需要加载和共享的资源 -->
|
||||
<resource-share type="script" src="https://cdn.com/jquery.min.js"></resource-share>
|
||||
<resource-share type="style" src="https://cdn.com/bootstrap.css"></resource-share>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 3. 业务脚本 (会被自动阻塞,直到 jQuery 加载完成) -->
|
||||
<script>
|
||||
// 这里可以直接使用 $,不会报错
|
||||
$(document).ready(function(){
|
||||
console.log('Ready!');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 7.2 iframe 子页面自动共享
|
||||
|
||||
**父页面**:
|
||||
```html
|
||||
<!-- 父页面加载了 Vue -->
|
||||
<resource-share type="script" src="vue.js"></resource-share>
|
||||
<iframe src="child.html"></iframe>
|
||||
```
|
||||
|
||||
**子页面**:
|
||||
```html
|
||||
<!-- 子页面只需声明依赖,不需要再次下载 -->
|
||||
<resource-share type="script" src="vue.js"></resource-share>
|
||||
<script>
|
||||
// 瞬间完成加载,直接使用父页面的 Vue 实例
|
||||
new Vue({ ... });
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 注意事项与最佳实践
|
||||
|
||||
1. **同源限制**: 资源共享主要基于 `postMessage`,在严格跨域(Top-Level Domain 不同)场景下,子页面将回退到独立加载模式,但不会报错。
|
||||
2. **Blob URL 清理**: 系统在页面卸载 (`beforeunload`) 时会自动清理生成的 Blob URL,防止内存泄漏。
|
||||
3. **脚本顺序**: 请严格按照依赖顺序在 HTML 中编写 `<resource-share>` 标签,系统不会自动分析依赖拓扑。
|
||||
4. **调试建议**: 开发环境下建议设置 `RS_CONFIG.LOG_LEVEL_DEVTOOLS = 'debug'` 以查看详细的加载链路日志。
|
||||
218
README.MD
Normal file
218
README.MD
Normal file
@@ -0,0 +1,218 @@
|
||||
# ResourceShare.js - 高性能跨资源共享系统
|
||||
|
||||
**版本:** V1.3.7.5
|
||||
**Copyright:** © 2026 GVSDS Team
|
||||
**License:** MIT License
|
||||
|
||||
## 📖 简介
|
||||
|
||||
`ResourceShare.js` 是一个专为现代 Web 应用设计的高性能资源管理与共享系统。它通过拦截脚本加载、智能缓存、跨iframe/页面通信以及虚拟源映射技术,解决了复杂 Web 应用中资源加载顺序混乱、重复请求浪费带宽以及内联代码难以调试等问题。
|
||||
|
||||
**核心优势:**
|
||||
* **跨域/跨页面资源共享**:顶级页面加载一次资源,同源子页面直接复用,极大减少流量消耗。
|
||||
* **精准的加载控制**:使用 `<resource-share>` 标签明确定义加载顺序,确保依赖库(如 jQuery)先于业务代码执行。
|
||||
* **智能调试支持**:自动生成带有 `VM_` 前缀的虚拟 SourceURL,让原本混淆或内联的代码在浏览器开发者工具中拥有清晰的文件路径。
|
||||
* **可视化管理面板**:内置splash屏、进度条、日志控制台和错误诊断界面,开发体验极佳。
|
||||
* **健壮的错误处理**:内置自动重试机制和智能诊断系统(如检测 jQuery 缺失、CORS 错误等)。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 引入脚本
|
||||
|
||||
将 `ResourceShare.js` 文件放入您的项目目录,并在 HTML 中尽可能早地引入它(建议在 `<head>` 中)。
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ResourceShare Demo</title>
|
||||
<!-- 引入 ResourceShare -->
|
||||
<script type="text/javascript" src="https://cdn.gvsds.com/ResourceShare/ResourceShare-1.3.7.2.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
...
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 2. 定义资源加载列表
|
||||
|
||||
使用自定义的 `<resource-share>` 标签来声明需要加载的资源。系统会严格按照标签出现的顺序进行加载。
|
||||
|
||||
**示例:**
|
||||
|
||||
```html
|
||||
<body>
|
||||
<script type="text/javascript" src="https://cdn.gvsds.com/ResourceShare/ResourceShare-1.3.7.2.js"></script>
|
||||
<resource-share type="script" src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></resource-share>
|
||||
<resource-share type="style" href="/assets/css/main.css"></resource-share>
|
||||
<resource-share type="script" src="/assets/js/app.js"></resource-share>
|
||||
<!-- 这些标签会被 ResourceShare 解析并接管加载过程 -->
|
||||
</body>
|
||||
```
|
||||
|
||||
### 3. 正常编写业务代码
|
||||
|
||||
除了引入资源的方式改变外,您的业务代码不需要做任何修改。系统会自动拦截 `<script>` 标签的执行,直到上述 `<resource-share>` 资源全部加载完毕。
|
||||
|
||||
```html
|
||||
<script>
|
||||
// 即使这段代码写在 resource-share 标签下面,
|
||||
// 系统也会等待 jQuery 加载完成后才执行它,
|
||||
// 确保 $ 一定是可用的。
|
||||
$(document).ready(function() {
|
||||
console.log('App is ready!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置选项
|
||||
|
||||
`ResourceShare.js` 提供了一个全局配置对象 `RS_CONFIG`。您可以在引入 `ResourceShare.js` **之前** 定义它来覆盖默认设置。
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.RS_CONFIG = {
|
||||
ENABLE_UI: false, // 关闭内置UI界面
|
||||
LOG_LEVEL_UI: 'info', // UI日志级别
|
||||
SCRIPT_TIMEOUT: 10000 // 脚本加载超时时间
|
||||
};
|
||||
</script>
|
||||
<script src="ResourceShare.js"></script>
|
||||
```
|
||||
|
||||
### 配置参数详解
|
||||
|
||||
| 参数名 | 类型 | 默认值 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **VERSION** | String | '1.3.7.4' | 系统版本号(只读) |
|
||||
| **ENABLE_UI** | Boolean | `true` | 是否显示内置的 Splash 加载屏、进度条和日志面板。生产环境建议设为 `false`。 |
|
||||
| **POST_MESSAGE_LIMIT** | Number | 1048576 (1MB) | 使用 postMessage 传输数据的最大阈值。超过此大小将自动使用 Blob URL。 |
|
||||
| **BLOB_LIMIT** | Number | 1073741824 (1GB) | 单个资源允许的最大大小。 |
|
||||
| **SCRIPT_TIMEOUT** | Number | 300000 (5分钟) | 资源加载的超时时间,超时后强制释放被阻塞的脚本。 |
|
||||
| **RETRY.COUNT** | Number | `3` | 网络请求失败时的重试次数。 |
|
||||
| **RETRY.DELAY** | Number | `1500` | 重试间隔(毫秒)。 |
|
||||
| **LOG_LEVEL_UI** | String | `'debug'` | 屏幕日志面板的显示级别 (`'debug'`, `'info'`, `'error'`, `'none'`)。 |
|
||||
| **LOG_LEVEL_DEVTOOLS** | String | `'error'` | 浏览器控制台 的日志级别。 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 高级用法
|
||||
|
||||
### 1. 跨 Iframe 资源共享
|
||||
|
||||
这是本系统的核心功能之一。当您的页面包含多个同源 iframe 时,只需在父页面(Top Level)加载一次资源,子页面即可直接读取父页面缓存。
|
||||
|
||||
**父页面:**
|
||||
```html
|
||||
<!-- 父页面正常加载资源 -->
|
||||
<script src="ResourceShare.js"></script>
|
||||
<resource-share type="script" src="large-library.js"></resource-share>
|
||||
```
|
||||
|
||||
**子页面:**
|
||||
```html
|
||||
<!-- 子页面只需要引入 ResourceShare.js -->
|
||||
<script src="ResourceShare.js"></script>
|
||||
<resource-share type="script" src="large-library.js"></resource-share>
|
||||
<!-- 子页面在加载时,会自动向父页面请求 large-library.js 的内容,而不会发起网络请求 -->
|
||||
```
|
||||
|
||||
若父页面并未加载 large-library.js,但有一个子页面加载了 large-library.js,第二个子页面再次尝试加载 large-library.js 时即可直接读取父页面缓存。
|
||||
|
||||
### 2. 虚拟文件名映射 (V1.3.7.4 更新)
|
||||
|
||||
系统会自动为加载的脚本生成 SourceURL,以便在 Chrome DevTools 的 Sources 面板中调试。
|
||||
|
||||
* **外部资源:** 如果加载 `https://example.com/libs/jquery.min.js`,在 Sources 中将显示为 `https://example.com/libs/VM_jquery.min.js`。这有助于区分这是由 ResourceShare 接管加载的文件。
|
||||
* **内联脚本:** 如果是页面内直接执行的脚本,将被映射为 `./RS_VM/VM_pageName_index_randomStr.js`。
|
||||
|
||||
### 3. 禁用特定脚本的接管
|
||||
|
||||
如果您希望某个脚本不被 ResourceShare 阻塞(即希望它立即执行),可以添加 `DisableRS` 属性。
|
||||
|
||||
```html
|
||||
<!-- 这个脚本会忽略 ResourceShare 的队列,立即执行 -->
|
||||
<script src="https://www.googletagmanager.com/gtag/js" DisableRS></script>
|
||||
```
|
||||
|
||||
### 4. 错误诊断系统
|
||||
|
||||
当发生严重错误(如 jQuery 未定义)导致加载停止时,系统会自动弹出一个诊断模态框。该模态框会根据错误堆栈提供解决建议(例如:“脚本依赖 jQuery,但似乎未加载...”)。
|
||||
|
||||
---
|
||||
|
||||
## 🐛 调试与日志
|
||||
|
||||
### 屏幕日志面板
|
||||
默认情况下,页面左下角会出现一个悬浮面板,显示资源加载的实时日志。
|
||||
* **蓝色:** 普通信息 / 请求
|
||||
* **绿色:** 加载成功 / 缓存命中
|
||||
* **黄色:** 警告 / 重试中
|
||||
* **红色:** 错误 / 加载失败
|
||||
|
||||
### 控制台日志
|
||||
在浏览器控制台中,日志以彩色前缀输出,方便过滤:
|
||||
* `[INFO]`
|
||||
* `[SUCCESS]`
|
||||
* `[WARNING]`
|
||||
* `[ERROR]`
|
||||
* `[CACHE]` - 表示命中了父页面缓存或本地缓存
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 工作原理
|
||||
|
||||
1. **初始化**: 脚本加载后,立即覆盖原生的 `appendChild` 和 `insertBefore` 方法,拦截所有 `<script>` 和 `<style>` 标签的插入。(这可能会导致类似的js无法正常发挥作用或者导致本程序失效)
|
||||
2. **阻塞**: 被拦截的标签会被暂时移除 DOM,并存入阻塞队列。
|
||||
3. **扫描**: 扫描页面中的 `<resource-share>` 标签,按顺序建立加载队列。
|
||||
4. **加载**:
|
||||
* 如果是**顶级页面**:直接发起网络请求。
|
||||
* 如果是**子页面**:先通过 `postMessage` 询问父页面是否有缓存。
|
||||
5. **执行**: 资源加载完成后,根据类型处理:
|
||||
* **Script**: 构造带有 `sourceURL` 的代码,使用 `new Function` 执行,确保调试栈清晰。
|
||||
* **Style**: 插入 `<style>` 标签或创建 Blob URL 链接。
|
||||
6. **释放**: 当所有 `<resource-share>` 资源就绪后,释放之前被阻塞的队列,触发页面的 `DOMContentLoaded` 和 `jQuery.ready` 等事件。
|
||||
|
||||
---
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
### V1.3.7.4 (Current)
|
||||
* **修正**: 修正了 `<resource-share>` 脚本的 SourceURL 路径映射规则,现在生成完整路径 + `VM_` 前缀文件名(例如 `https://.../VM_jquery.min.js`),便于在 DevTools 中识别资源来源。
|
||||
* **优化**: 保留了内联脚本的 `VM_pageName_index.js` 虚拟路径规则。
|
||||
|
||||
### V1.3.7.2
|
||||
* 修复 Blob URL 执行时的无限拦截循环问题。
|
||||
* 完善重试机制。
|
||||
|
||||
### V1.3.7
|
||||
* 重构日志系统,分离 UI 日志和 DevTools 日志。
|
||||
* 新增 `ResourceShareLogger` 类,统一管理日志输出。
|
||||
* UI 层不再判断日志级别,改为由 Logger 统一分发。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **同源策略**: 跨 iframe 共享功能目前仅适用于 **同源** 页面。跨域场景需要额外配置 CORS(安全保护,可修改)。
|
||||
2. **Inline Script 执行**: 内联脚本使用 `new Function` 执行,这意味着它无法直接访问块级作用域变量(如 `const` 声明在 `<script>` 标签顶部的变量),但全局变量(`var`, `window.xxx`)不受影响。建议将依赖全局变量的代码放在 ResourceShare 管理的队列之后。
|
||||
3. **动态插入脚本**: 系统通过 `MutationObserver` 监听 DOM 变化,动态插入的 `<script>` 也会被自动接管。
|
||||
|
||||
---
|
||||
|
||||
## 💡 常见问题 (FAQ)
|
||||
|
||||
**Q: 为什么我的页面一直显示 Loading?**
|
||||
A: 可能是某个 `<resource-share>` 资源加载失败。请查看控制台是否有红色 `[ERROR]` 日志,或查看是否触发了错误诊断弹窗。
|
||||
|
||||
**Q: 如何关闭左下角的日志面板?**
|
||||
A: 设置 `window.RS_CONFIG = { ENABLE_UI: false };` 或者在加载完成后点击面板上的“隐藏”/“关闭”按钮。
|
||||
|
||||
**Q: 这个系统能兼容所有老的 jQuery 插件吗?**
|
||||
A: 可以。系统确保了 jQuery 及其插件按照你书写的顺序依次加载,完美解决 `jQuery is not defined` 的问题。
|
||||
10
app.py
Normal file
10
app.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import flask
|
||||
|
||||
|
||||
app = flask.Flask(__name__, template_folder = "./", static_folder = "./", static_url_path = "/")
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return flask.render_template('test.html')
|
||||
|
||||
app.run(debug = True)
|
||||
1376
build/ResourceShare-1.3.7.4.js
Normal file
1376
build/ResourceShare-1.3.7.4.js
Normal file
File diff suppressed because it is too large
Load Diff
13
build/ResourceShare-1.3.7.4.min.js
vendored
Normal file
13
build/ResourceShare-1.3.7.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1421
build/ResourceShare-1.3.7.5.js
Normal file
1421
build/ResourceShare-1.3.7.5.js
Normal file
File diff suppressed because it is too large
Load Diff
13
build/ResourceShare-1.3.7.5.min.js
vendored
Normal file
13
build/ResourceShare-1.3.7.5.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
src/Copyright.js
Normal file
13
src/Copyright.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 高性能跨资源共享系统 - (架构重构 V1.3.7.4)
|
||||
*
|
||||
* 【V1.3.7.4 更新内容】
|
||||
* 1. 修正 <resource-share> 脚本 SourceURL 路径映射:
|
||||
* - 现在生成完整路径 + VM_前缀文件名 (如 https://.../3.6.0/VM_jquery.min.js)。
|
||||
* - 保留内联脚本的虚拟路径规则。
|
||||
*
|
||||
* Copyright (c) 2026 GVSDS Team
|
||||
* Licensed under MIT License
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
16
src/build.json
Normal file
16
src/build.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"project": "./src",
|
||||
"order": [
|
||||
"Copyright",
|
||||
"utils/utils",
|
||||
"utils/config",
|
||||
"modules/Diagnostics",
|
||||
"modules/ResourceShareUI",
|
||||
"modules/ResourceShareLogger",
|
||||
"modules/ResourceShareManager",
|
||||
"build/output"
|
||||
],
|
||||
"output": "build/ResourceShare-1.3.7.5.js",
|
||||
"minify": "build/ResourceShare-1.3.7.5.min.js",
|
||||
"map": "build/ResourceShare-1.3.7.5.min.js.map"
|
||||
}
|
||||
47
src/build/output.js
Normal file
47
src/build/output.js
Normal file
@@ -0,0 +1,47 @@
|
||||
if (typeof window.ResourceShareElement === 'undefined') {
|
||||
window.ResourceShareElement = class ResourceShareElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
// 确保 manager 存在,如果 window 上还没挂载则创建临时实例
|
||||
this.manager = window.resourceShareManager || new window.ResourceShareManager();
|
||||
}
|
||||
connectedCallback() {
|
||||
this.log('ResourceShare 元素已挂载到 DOM', 'info');
|
||||
}
|
||||
log(message, category = 'info') {
|
||||
// [重构] 强制使用统一的日志系统,移除 console.log 回退
|
||||
// 确保无论何种情况,日志都通过 ResourceShareManager 处理(包含颜色、UI渲染等)
|
||||
if (this.manager && this.manager.log) {
|
||||
this.manager.log(message, category);
|
||||
}
|
||||
}
|
||||
};
|
||||
customElements.define('resource-share', window.ResourceShareElement);
|
||||
}
|
||||
|
||||
// 如果全局管理器实例尚未初始化,则进行初始化
|
||||
if (typeof window.resourceShareManager === 'undefined') {
|
||||
window.resourceShareManager = new window.ResourceShareManager();
|
||||
}
|
||||
|
||||
// 全局消息监听(处理跨页面握手)
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('message', (event) => {
|
||||
// 顶级页面:响应子页面的 Ping 请求
|
||||
if (event.data.type === 'resource-share-ping' && window.resourceShareManager && window.resourceShareManager.isTopLevel) {
|
||||
event.source.postMessage({ type: 'resource-share-pong', origin: event.origin }, event.origin);
|
||||
}
|
||||
|
||||
// 子页面:收到父页面的 Pong 响应,确认连接建立
|
||||
if (event.data.type === 'resource-share-pong' && window.resourceShareManager && !window.resourceShareManager.isTopLevel) {
|
||||
// [重构] 移除手动获取颜色和 console.log,改用统一的日志系统
|
||||
// 日志系统会自动根据 'success' 类别配置颜色和时间戳
|
||||
window.resourceShareManager.log('父页面连接已建立 (Channel Ready)', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 支持 CommonJS 环境导出
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { ResourceShareManager: window.ResourceShareManager, ResourceShareElement: window.ResourceShareElement };
|
||||
}
|
||||
77
src/modules/Diagnostics.js
Normal file
77
src/modules/Diagnostics.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// ==========================================
|
||||
// 快速代码诊断系统
|
||||
// ==========================================
|
||||
const DIAGNOSTIC_RULES = [
|
||||
{
|
||||
id: 'jquery_missing',
|
||||
check: (error) => error.message && (error.message.includes('$ is not defined') || error.message.includes('jQuery is not defined')),
|
||||
diagnose: (error) => `<strong>jQuery 依赖错误:</strong><br/>脚本依赖 jQuery,但似乎未加载或加载顺序错误。<br/><em>解决方案: 请检查 resource-share 中 jQuery 是否在其他依赖库之前声明。</em>`,
|
||||
headerStyle: 'color: #ff3b30; font-weight: bold; font-size: 14px;',
|
||||
bodyStyle: 'color: #fbff00; font-size: 12px;'
|
||||
},
|
||||
{
|
||||
id: 'layui_missing',
|
||||
check: (error) => error.message && (error.message.includes('layui is not defined') || error.message.includes('Layui is not defined')),
|
||||
diagnose: (error) => `<strong>Layui 缺失:</strong><br/>脚本依赖 Layui,但 Layui 似乎未加载。<br/><em>解决方案: 确保已正确引入 Layui 相关资源。</em>`,
|
||||
headerStyle: 'color: #ff9500; font-weight: bold; font-size: 14px;',
|
||||
bodyStyle: 'color: #ffe4b5; font-size: 12px;'
|
||||
},
|
||||
{
|
||||
id: 'syntax_error',
|
||||
check: (error) => error.name === 'SyntaxError',
|
||||
diagnose: (error) => `<strong>语法错误:</strong><br/>代码中存在语法错误。<br/><em>详情: ${error.message}</em>`,
|
||||
headerStyle: 'color: #8e0000; font-weight: bold; font-size: 14px;',
|
||||
bodyStyle: 'color: #ffb3ba; font-size: 12px;'
|
||||
},
|
||||
{
|
||||
id: 'type_error',
|
||||
check: (error) => error.name === 'TypeError',
|
||||
diagnose: (error) => `<strong>类型错误:</strong><br/>尝试调用 undefined/null 的方法或访问属性。<br/><em>详情: ${error.message}</em>`,
|
||||
headerStyle: 'color: #ff3a30; font-weight: bold; font-size: 14px;',
|
||||
bodyStyle: 'color: #ffd6d6; font-size: 12px;'
|
||||
},
|
||||
{
|
||||
id: 'network_error',
|
||||
check: (error) => error.message && (error.message.includes('Failed to fetch') || error.message.includes('NetworkError') || error.message.includes('4') || error.message.includes('5')),
|
||||
diagnose: (error) => `<strong>网络错误:</strong><br/>资源无法获取,可能是断网或服务器故障。<br/><em>解决方案: 请检查网络连接。</em>`,
|
||||
headerStyle: 'color: #ff2d55; font-weight: bold; font-size: 14px;',
|
||||
bodyStyle: 'color: #ffc7d0; font-size: 12px;'
|
||||
},
|
||||
{
|
||||
id: 'cors_error',
|
||||
check: (error) => error.message && error.message.includes('CORS'),
|
||||
diagnose: (error) => `<strong>跨域错误 (CORS):</strong><br/>跨域请求被浏览器安全策略拦截。<br/><em>解决方案: 配置服务器响应头 (Access-Control-Allow-Origin) 或使用代理。</em>`,
|
||||
headerStyle: 'color: #007aff; font-weight: bold; font-size: 14px;',
|
||||
bodyStyle: 'color: #b3d4ff; font-size: 12px;'
|
||||
}
|
||||
];
|
||||
|
||||
function runDiagnostics(error) {
|
||||
let htmlOutput = '';
|
||||
DIAGNOSTIC_RULES.forEach(rule => {
|
||||
if (rule.check(error)) {
|
||||
htmlOutput += rule.diagnose(error) + '<br/><br/>';
|
||||
}
|
||||
});
|
||||
if (!htmlOutput) htmlOutput = `<span style="color:#666">未找到匹配此错误的诊断规则。</span>`;
|
||||
|
||||
let hasMatchedRule = false;
|
||||
DIAGNOSTIC_RULES.forEach(rule => {
|
||||
if (rule.check(error)) {
|
||||
try {
|
||||
const diagnosticHTML = rule.diagnose(error);
|
||||
let plainText = diagnosticHTML.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>?/gm, '').trim();
|
||||
const hStyle = rule.headerStyle || 'color: #ff3b30; font-weight: bold; font-size: 14px;';
|
||||
const bStyle = rule.bodyStyle || 'color: #fbff00; font-size: 12px;';
|
||||
console.error(`%c[ResourceShare 诊断]%c ${plainText}`, hStyle, bStyle);
|
||||
hasMatchedRule = true;
|
||||
} catch (e) {
|
||||
console.error(`[ResourceShare 诊断] 诊断逻辑自身出错: ${e.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!hasMatchedRule) {
|
||||
console.error(`%c[ResourceShare 诊断]%c 未找到匹配的诊断规则,无法定位错误原因`, 'color: #8e8e93; font-weight: bold; font-size: 14px;', 'color: #ffffff; font-size: 12px;');
|
||||
}
|
||||
return htmlOutput;
|
||||
}
|
||||
73
src/modules/ResourceShareLogger.js
Normal file
73
src/modules/ResourceShareLogger.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// ==========================================
|
||||
// [新增] 独立日志系统
|
||||
// ==========================================
|
||||
class ResourceShareLogger {
|
||||
constructor(manager) {
|
||||
this.manager = manager;
|
||||
this.uiManager = null; // 由外部注入
|
||||
}
|
||||
|
||||
setUIManager(uiManager) {
|
||||
this.uiManager = uiManager;
|
||||
}
|
||||
|
||||
// 获取主题颜色(复用原逻辑)
|
||||
getThemeColors() {
|
||||
const dark = window.isDarkTheme ? window.isDarkTheme() : false;
|
||||
return {
|
||||
info: dark ? '#60cdff' : '#0078d4',
|
||||
success: dark ? '#6ccf70' : '#107c10',
|
||||
warning: dark ? '#ffb900' : '#ff8c00',
|
||||
danger: dark ? '#f1707b' : '#d13438',
|
||||
cache: dark ? '#c48bf2' : '#8a2be2',
|
||||
request: dark ? '#4ec2b8' : '#008272',
|
||||
pageType: dark ? '#9cdcfe' : '#2b88d8',
|
||||
sourceFile: dark ? '#ce9178' : '#a1260d',
|
||||
message: dark ? '#d4d4d4' : '#323130'
|
||||
};
|
||||
}
|
||||
|
||||
log(message, category = 'info') {
|
||||
const colors = this.getThemeColors();
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const pageType = this.manager.isTopLevel ? 'Top' : 'Sub';
|
||||
const logMessage = `[${timestamp}] ${message}`;
|
||||
|
||||
// --- 1. DevTools 输出 ---
|
||||
const allowedDevTypes = getAllowedLogTypes('DEVTOOLS');
|
||||
if (allowedDevTypes.includes(category)) {
|
||||
const styles = {
|
||||
info: { prefix: '%c[INFO]', style: `color: ${colors.info}; font-weight: bold;` },
|
||||
success: { prefix: '%c[SUCCESS]', style: `color: ${colors.success}; font-weight: bold;` },
|
||||
warning: { prefix: '%c[WARNING]', style: `color: ${colors.warning}; font-weight: bold;` },
|
||||
danger: { prefix: '%c[ERROR]', style: `color: ${colors.danger}; font-weight: bold;` },
|
||||
cache: { prefix: '%c[CACHE]', style: `color: ${colors.cache}; font-weight: bold;` },
|
||||
request: { prefix: '%c[REQUEST]', style: `color: ${colors.request}; font-weight: bold;` }
|
||||
};
|
||||
const catStyle = styles[category] || styles.info;
|
||||
const fn = category === 'danger' ? console.error : console.log;
|
||||
fn.call(console,
|
||||
`${catStyle.prefix}%c[${pageType}]%c[${RS_CONFIG.SOURCE_FILE}]%c ${logMessage}`,
|
||||
catStyle.style,
|
||||
`color: ${colors.pageType}; font-weight: bold;`,
|
||||
`color: ${colors.sourceFile}; font-style: italic;`,
|
||||
`color: ${colors.message};`
|
||||
);
|
||||
}
|
||||
|
||||
// --- 2. UI 输出 ---
|
||||
// 只有启用了 UI 且日志级别允许时才输出
|
||||
if (RS_CONFIG.ENABLE_UI && this.uiManager) {
|
||||
const allowedUITypes = getAllowedLogTypes('UI');
|
||||
if (allowedUITypes.includes(category)) {
|
||||
this.uiManager.log(message, category);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. 严重错误触发 ---
|
||||
if (category === 'danger' && message.includes('Unrecoverable')) {
|
||||
const pseudoError = new Error(message);
|
||||
this.manager.handleCriticalError(pseudoError, 'SystemLog');
|
||||
}
|
||||
}
|
||||
}
|
||||
264
src/modules/ResourceShareUI.js
Normal file
264
src/modules/ResourceShareUI.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// ==========================================
|
||||
// UI 管理器类 (V1.3.7 - 移除日志级别判断逻辑)
|
||||
// ==========================================
|
||||
class ResourceShareUI {
|
||||
constructor(manager) {
|
||||
this.manager = manager;
|
||||
this.isVisible = true;
|
||||
this.consoleVisible = true;
|
||||
|
||||
// 仅当启用 UI 时才初始化
|
||||
if (RS_CONFIG.ENABLE_UI) {
|
||||
this.initStyles();
|
||||
this.initDOM();
|
||||
this.startSplashScreen();
|
||||
}
|
||||
}
|
||||
|
||||
initStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'rs-ui-styles';
|
||||
style.textContent = `
|
||||
@font-face { font-family: 'Segoe UI Variable'; src: local('Segoe UI Variable Display'), local('Segoe UI Variable Text'), local('Segoe UI'); }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
#rs-splash-screen {
|
||||
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
||||
background: #ffffff; z-index: ${RS_CONFIG.Z_INDEX.SPLASH};
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
transition: opacity 0.8s ease-in-out, transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
font-family: ${RS_CONFIG.UI_STYLE.FONT_FAMILY}; color: #1d1d1f;
|
||||
}
|
||||
#rs-splash-screen.hidden { opacity: 0; pointer-events: none; transform: scale(1.05); }
|
||||
|
||||
.rs-splash-title { font-size: 16px; font-weight: 600; letter-spacing: 1px; color: #0078d4; margin-bottom: 8px; text-transform: uppercase; display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
.rs-loader {
|
||||
width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid #0078d4; border-radius: 50%;
|
||||
animation: rs-spin 1s linear infinite; margin-bottom: 15px;
|
||||
}
|
||||
@keyframes rs-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
|
||||
#rs-progress-bar {
|
||||
position: fixed; top: 0; left: 0; width: 0%; height: 4px; background: #0078d4;
|
||||
z-index: ${RS_CONFIG.Z_INDEX.PROGRESS}; transition: width 0.4s cubic-bezier(0.33, 1, 0.68, 1);
|
||||
box-shadow: 0 2px 5px rgba(0, 120, 212, 0.3);
|
||||
}
|
||||
#rs-progress-bar.complete { width: 100% !important; transition: width 0.3s ease, opacity 0.5s ease 0.3s; opacity: 0; }
|
||||
|
||||
#rs-overlay-panel {
|
||||
position: fixed; bottom: 24px; left: 24px; right: 24px; max-height: 35vh;
|
||||
background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(${RS_CONFIG.UI_STYLE.BLUR_VALUE}) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(${RS_CONFIG.UI_STYLE.BLUR_VALUE}) saturate(120%);
|
||||
border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, ${RS_CONFIG.UI_STYLE.SHADOW_OPACITY}), 0 0 0 1px rgba(0,0,0,0.02);
|
||||
z-index: ${RS_CONFIG.Z_INDEX.PANEL}; display: flex; flex-direction: column;
|
||||
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s ease;
|
||||
font-family: ${RS_CONFIG.UI_STYLE.MONO_FONT}; overflow: hidden;
|
||||
}
|
||||
#rs-overlay-panel.rs-closed { transform: translateY(110%) scale(0.95); opacity: 0; pointer-events: none; }
|
||||
|
||||
.rs-panel-header {
|
||||
display: flex; justify-content: space-between; align-items: center; padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.5); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
cursor: grab; user-select: none; font-family: ${RS_CONFIG.UI_STYLE.FONT_FAMILY};
|
||||
}
|
||||
.rs-panel-title { color: #202020; font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
||||
.rs-status-indicator { width: 8px; height: 8px; background-color: #107c10; border-radius: 50%; box-shadow: 0 0 4px #107c10; transition: background-color 0.3s; }
|
||||
.rs-status-indicator.error { background-color: #d13438; box-shadow: 0 0 4px #d13438; }
|
||||
.rs-controls { display: flex; gap: 4px; }
|
||||
.rs-btn { background: transparent; border: 1px solid transparent; color: #5e5e5e; padding: 4px 10px; font-size: 12px; border-radius: 4px; cursor: pointer; transition: all 0.2s; font-family: ${RS_CONFIG.UI_STYLE.FONT_FAMILY}; }
|
||||
.rs-btn:hover { background: rgba(0, 0, 0, 0.04); color: #202020; }
|
||||
.rs-btn.active { background: rgba(0, 120, 212, 0.1); color: #0078d4; font-weight: 600; }
|
||||
.rs-btn-close { color: #d13438; padding: 4px 8px; }
|
||||
.rs-btn-close:hover { background: rgba(209, 52, 56, 0.1); }
|
||||
|
||||
#rs-console-output {
|
||||
flex: 1; overflow-y: auto; padding: 12px 16px; font-size: 11px; line-height: 1.5; color: #d4d4d4;
|
||||
background-color: #1e1e1e; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||
}
|
||||
#rs-console-output::-webkit-scrollbar { width: 6px; }
|
||||
#rs-console-output::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
|
||||
.rs-log-entry { margin-bottom: 3px; border-left: 2px solid transparent; padding-left: 8px; word-break: break-all; opacity: 0; animation: rsFadeIn ${RS_CONFIG.ANIMATION.LOG_FADE_IN}ms forwards; }
|
||||
@keyframes rsFadeIn { to { opacity: 1; } }
|
||||
.rs-log-info { border-color: #0078d4; color: #60cdff; background: rgba(0, 120, 212, 0.1); }
|
||||
.rs-log-success { border-color: #107c10; color: #6ccf70; background: rgba(16, 124, 16, 0.1); }
|
||||
.rs-log-warning { border-color: #ffb900; color: #ffb900; background: rgba(255, 185, 0, 0.1); }
|
||||
.rs-log-danger { border-color: #d13438; color: #f1707b; background: rgba(209, 52, 56, 0.15); }
|
||||
.rs-log-cache { border-color: #c48bf2; color: #c48bf2; background: rgba(196, 139, 242, 0.1); }
|
||||
|
||||
#rs-fatal-error-screen {
|
||||
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(32, 32, 32, 0.6); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);
|
||||
z-index: ${RS_CONFIG.Z_INDEX.ERROR}; align-items: center; justify-content: center;
|
||||
font-family: ${RS_CONFIG.UI_STYLE.FONT_FAMILY};
|
||||
}
|
||||
.rs-error-card {
|
||||
background: #fbfbfb; width: 90%; max-width: 600px; max-height: 85vh; border-radius: 8px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); display: flex; flex-direction: column;
|
||||
border: 1px solid rgba(0,0,0,0.1); animation: rsSlideUp ${RS_CONFIG.ANIMATION.ERROR_SLIDE_UP}ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
@keyframes rsSlideUp { from { opacity: 0; transform: translateY(20px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
||||
.rs-error-header { padding: 24px 24px 10px 24px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 16px; }
|
||||
.rs-error-icon { width: 32px; height: 32px; background: #d13438; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 18px; flex-shrink: 0; }
|
||||
.rs-error-title { font-size: 18px; font-weight: 600; color: #201f1e; line-height: 1.3; }
|
||||
.rs-error-desc { font-size: 13px; color: #605e5c; margin-top: 4px; line-height: 1.5; }
|
||||
.rs-error-content { padding: 16px 24px; overflow-y: auto; flex: 1; }
|
||||
.rs-section-title { font-size: 12px; font-weight: 700; text-transform: uppercase; color: #605e5c; margin-bottom: 8px; margin-top: 16px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.rs-section-title:first-child { margin-top: 0; }
|
||||
.rs-stack-trace { background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 4px; font-family: ${RS_CONFIG.UI_STYLE.MONO_FONT}; font-size: 12px; white-space: pre-wrap; word-break: break-all; max-height: 150px; overflow-y: auto; border: 1px solid #333; }
|
||||
.rs-diagnosis-box { background: #fdefeb; border-left: 3px solid #d13438; padding: 12px; border-radius: 2px; font-size: 13px; color: #3b1305; line-height: 1.6; }
|
||||
.rs-error-footer { padding: 16px 24px; background: #f3f3f3; border-top: 1px solid #e0e0e0; display: flex; justify-content: flex-end; gap: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; }
|
||||
.rs-btn-primary { background: #0078d4; color: white; border: 1px solid #0078d4; padding: 6px 20px; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; min-width: 80px; }
|
||||
.rs-btn-primary:hover { background: #106ebe; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.rs-btn-secondary { background: white; color: #323130; border: 1px solid #8a8886; padding: 6px 20px; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; min-width: 80px; }
|
||||
.rs-btn-secondary:hover { background: #f3f2f1; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// 初始化 DOM 结构
|
||||
initDOM() {
|
||||
// 1. 启动页
|
||||
const splash = document.createElement('div');
|
||||
splash.id = 'rs-splash-screen';
|
||||
splash.innerHTML = `<div class="rs-loader"></div><div class="rs-splash-title">RS-LCDN System V${RS_CONFIG.VERSION}</div>`;
|
||||
document.documentElement.appendChild(splash);
|
||||
this.splashElement = splash;
|
||||
|
||||
// 2. 进度条
|
||||
const progress = document.createElement('div');
|
||||
progress.id = 'rs-progress-bar';
|
||||
document.documentElement.appendChild(progress);
|
||||
this.progressBarElement = progress;
|
||||
|
||||
// 3. 主面板
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'rs-overlay-panel';
|
||||
panel.innerHTML = `
|
||||
<div class="rs-panel-header">
|
||||
<div class="rs-panel-title"><div class="rs-status-indicator"></div><span>ResourceShare Log</span></div>
|
||||
<div class="rs-controls">
|
||||
<button class="rs-btn active" id="rs-toggle-console">日志</button>
|
||||
<button class="rs-btn active" id="rs-toggle-overlay">隐藏</button>
|
||||
<button class="rs-btn rs-btn-close" id="rs-close-btn">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rs-console-output"></div>
|
||||
`;
|
||||
document.documentElement.appendChild(panel);
|
||||
this.overlayPanel = panel;
|
||||
this.consoleOutput = panel.querySelector('#rs-console-output');
|
||||
|
||||
// 4. 错误屏幕
|
||||
const errorScreen = document.createElement('div');
|
||||
errorScreen.id = 'rs-fatal-error-screen';
|
||||
errorScreen.innerHTML = `
|
||||
<div class="rs-error-card">
|
||||
<div class="rs-error-header">
|
||||
<div class="rs-error-icon">!</div>
|
||||
<div><div class="rs-error-title">系统严重错误</div><div class="rs-error-desc">ResourceShare 遇到不可恢复的错误。</div></div>
|
||||
</div>
|
||||
<div class="rs-error-content">
|
||||
<div class="rs-section-title"><span>智能诊断与建议</span></div>
|
||||
<div id="rs-diagnosis-content" class="rs-diagnosis-box">正在检查...</div>
|
||||
<div class="rs-section-title"><span>错误堆栈跟踪</span><button id="rs-copy-btn" style="background:none; border:none; color:#0078d4; cursor:pointer; font-size:12px;">复制信息</button></div>
|
||||
<div id="rs-stack-content" class="rs-stack-trace">等待详细信息...</div>
|
||||
</div>
|
||||
<div class="rs-error-footer"><button id="rs-refresh-btn" class="rs-btn-primary">刷新页面</button></div>
|
||||
</div>
|
||||
`;
|
||||
document.documentElement.appendChild(errorScreen);
|
||||
this.errorScreen = errorScreen;
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.errorScreen.querySelector('#rs-refresh-btn').addEventListener('click', () => window.location.reload());
|
||||
this.errorScreen.querySelector('#rs-copy-btn').addEventListener('click', () => {
|
||||
const stackText = document.getElementById('rs-stack-content').textContent;
|
||||
const diagText = document.getElementById('rs-diagnosis-content').innerText;
|
||||
navigator.clipboard.writeText(`=== 错误日志 ===\n${stackText}\n\n=== 诊断信息 ===\n${diagText}`).then(() => {
|
||||
const btn = this.errorScreen.querySelector('#rs-copy-btn');
|
||||
const originalText = btn.textContent; btn.textContent = '已复制!'; setTimeout(() => btn.textContent = originalText, 2000);
|
||||
});
|
||||
});
|
||||
document.getElementById('rs-close-btn').addEventListener('click', () => this.setOverlayVisible(false));
|
||||
const overlayBtn = document.getElementById('rs-toggle-overlay');
|
||||
overlayBtn.addEventListener('click', () => {
|
||||
this.setOverlayVisible(!this.isVisible);
|
||||
overlayBtn.classList.toggle('active', this.isVisible);
|
||||
overlayBtn.textContent = this.isVisible ? '隐藏' : '显示';
|
||||
});
|
||||
const consoleBtn = document.getElementById('rs-toggle-console');
|
||||
consoleBtn.addEventListener('click', () => {
|
||||
this.consoleVisible = !this.consoleVisible;
|
||||
consoleBtn.classList.toggle('active', this.consoleVisible);
|
||||
this.consoleOutput.style.display = this.consoleVisible ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
startSplashScreen() {
|
||||
setTimeout(() => {
|
||||
this.splashElement.classList.add('hidden');
|
||||
setTimeout(() => { if (this.splashElement.parentNode) this.splashElement.parentNode.removeChild(this.splashElement); }, RS_CONFIG.ANIMATION.SPLASH_HIDE);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
setOverlayVisible(visible) {
|
||||
this.isVisible = visible;
|
||||
if (visible) this.overlayPanel.classList.remove('rs-closed');
|
||||
else this.overlayPanel.classList.add('rs-closed');
|
||||
}
|
||||
|
||||
// V1.3.7: 此方法不再判断日志级别,仅负责渲染
|
||||
log(message, type) {
|
||||
if (!this.consoleOutput) return;
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `rs-log-entry rs-log-${type}`;
|
||||
const time = new Date().toLocaleTimeString('en-US', { hour12: false }) + '.' + new Date().getMilliseconds().toString().padStart(3, '0');
|
||||
entry.textContent = `[${time}] ${message}`;
|
||||
this.consoleOutput.appendChild(entry);
|
||||
this.consoleOutput.scrollTop = this.consoleOutput.scrollHeight;
|
||||
}
|
||||
|
||||
updateProgress(current, total) {
|
||||
if (total === 0) return;
|
||||
const percent = Math.min(100, Math.round((current / total) * 100));
|
||||
this.progressBarElement.style.width = percent + '%';
|
||||
const title = document.querySelector('.rs-panel-title span');
|
||||
if (title) title.textContent = `资源加载中... ${percent}%`;
|
||||
}
|
||||
|
||||
finishLoading() {
|
||||
this.progressBarElement.classList.add('complete');
|
||||
setTimeout(() => { if (this.progressBarElement.parentNode) this.progressBarElement.parentNode.removeChild(this.progressBarElement); }, RS_CONFIG.ANIMATION.PROGRESS_COMPLETE);
|
||||
const status = document.querySelector('.rs-status-indicator');
|
||||
if (status) status.style.backgroundColor = '#107c10';
|
||||
const title = document.querySelector('.rs-panel-title span');
|
||||
if (title) title.textContent = '系统就绪';
|
||||
setTimeout(() => {
|
||||
this.setOverlayVisible(false);
|
||||
setTimeout(() => {
|
||||
if (this.overlayPanel && this.overlayPanel.parentNode) this.overlayPanel.parentNode.removeChild(this.overlayPanel);
|
||||
if (this.errorScreen && this.errorScreen.parentNode) this.errorScreen.parentNode.removeChild(this.errorScreen);
|
||||
const style = document.getElementById('rs-ui-styles');
|
||||
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||
}, RS_CONFIG.ANIMATION.PANEL_HIDE);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showFatalError(error, diagnosticsHTML) {
|
||||
const stackEl = document.getElementById('rs-stack-content');
|
||||
const diagEl = document.getElementById('rs-diagnosis-content');
|
||||
if (error) stackEl.textContent = error.stack || error.message || String(error);
|
||||
else stackEl.textContent = "无可用的堆栈跟踪。";
|
||||
if (diagnosticsHTML) diagEl.innerHTML = diagnosticsHTML;
|
||||
else diagEl.innerHTML = "<span style='color:#666'>未找到特定的诊断信息。</span>";
|
||||
this.errorScreen.style.display = 'flex';
|
||||
const status = document.querySelector('.rs-status-indicator');
|
||||
if (status) { status.classList.add('error'); status.style.backgroundColor = '#d13438'; status.style.boxShadow = '0 0 5px #d13438'; }
|
||||
this.progressBarElement.style.opacity = '0';
|
||||
}
|
||||
}
|
||||
60
src/utils/config.js
Normal file
60
src/utils/config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
window.RS_CONFIG = deepMerge(window.RS_CONFIG || {}, {
|
||||
// 版本/版权
|
||||
VERSION: '1.3.7.5',
|
||||
COPYRIGHT: '© 2026 GVSDS Team',
|
||||
SOURCE_FILE: 'ResourceShare.js',
|
||||
|
||||
// 功能开关
|
||||
ENABLE_UI: true,
|
||||
|
||||
// 阈值配置
|
||||
POST_MESSAGE_LIMIT: 1 * 1024 * 1024,
|
||||
BLOB_LIMIT: 1024 * 1024 * 1024,
|
||||
SCRIPT_TIMEOUT: 5 * 60 * 1000,
|
||||
PING_INTERVAL: 100,
|
||||
PING_TIMEOUT: 5000,
|
||||
REQUEST_TIMEOUT: 10000,
|
||||
|
||||
// 重试机制配置
|
||||
RETRY: {
|
||||
COUNT: 3,
|
||||
DELAY: 1500
|
||||
},
|
||||
|
||||
// Z-Index层级
|
||||
Z_INDEX: {
|
||||
SPLASH: 2147483647,
|
||||
PROGRESS: 2147483646,
|
||||
PANEL: 2147483645,
|
||||
ERROR: 2147483648
|
||||
},
|
||||
|
||||
// 日志级别配置
|
||||
LOG_LEVEL_UI: 'debug',
|
||||
LOG_LEVEL_DEVTOOLS: 'error',
|
||||
|
||||
// 日志级别定义映射
|
||||
LOG_LEVEL_MAP: {
|
||||
debug: ['info', 'success', 'warning', 'danger', 'cache', 'request'],
|
||||
info: ['info', 'success', 'warning', 'danger'],
|
||||
error: ['danger'],
|
||||
none: []
|
||||
},
|
||||
|
||||
// UI动画时长
|
||||
ANIMATION: {
|
||||
SPLASH_HIDE: 800,
|
||||
PROGRESS_COMPLETE: 800,
|
||||
PANEL_HIDE: 500,
|
||||
ERROR_SLIDE_UP: 400,
|
||||
LOG_FADE_IN: 200
|
||||
},
|
||||
|
||||
// 样式常量
|
||||
UI_STYLE: {
|
||||
FONT_FAMILY: "'Segoe UI Variable', 'Segoe UI', sans-serif",
|
||||
MONO_FONT: "'Consolas', 'Monaco', monospace",
|
||||
BLUR_VALUE: '20px',
|
||||
SHADOW_OPACITY: 0.12
|
||||
}
|
||||
});
|
||||
67
src/utils/utils.js
Normal file
67
src/utils/utils.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// ======================== 文件检查 ========================
|
||||
// (V1.3.7.4)
|
||||
function getFileNameFromPath(path) {
|
||||
if (!path) return '';
|
||||
return path.split(/[?#]/)[0].replace(/\\/g, '/').split('/').pop() || '';
|
||||
}
|
||||
function getCurrentScriptFileName() {
|
||||
if (document.currentScript) { return getFileNameFromPath(document.currentScript.src); }
|
||||
const scripts = document.scripts;
|
||||
return getFileNameFromPath(scripts[scripts.length - 1].src);
|
||||
}
|
||||
const ResourceShareFileName = getCurrentScriptFileName();
|
||||
|
||||
// ======================== 全局常量配置(单文件内集中管理)========================
|
||||
/**
|
||||
* 深度合并两个对象(后者覆盖前者同名属性,嵌套对象递归合并)(V1.3.7.4)
|
||||
*/
|
||||
function deepMerge(target = {}, source = {}) {
|
||||
const merged = { ...target };
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
if (
|
||||
typeof source[key] === 'object' &&
|
||||
source[key] !== null &&
|
||||
!Array.isArray(source[key]) &&
|
||||
typeof target[key] === 'object' &&
|
||||
target[key] !== null &&
|
||||
!Array.isArray(target[key])
|
||||
) {
|
||||
merged[key] = deepMerge(target[key], source[key]);
|
||||
} else {
|
||||
merged[key] = target.hasOwnProperty(key) ? target[key] : source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// 辅助函数:获取当前配置允许的日志类型数组
|
||||
function getAllowedLogTypes(target) {
|
||||
const levelKey = target === 'UI' ? RS_CONFIG.LOG_LEVEL_UI : RS_CONFIG.LOG_LEVEL_DEVTOOLS;
|
||||
return RS_CONFIG.LOG_LEVEL_MAP[levelKey] || RS_CONFIG.LOG_LEVEL_MAP.debug;
|
||||
}
|
||||
|
||||
// 主题检测辅助函数 (挂载到 window 方便 Logger 调用)
|
||||
window.isDarkTheme = () => {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) return true;
|
||||
try {
|
||||
const bgColor = window.getComputedStyle(document.body).backgroundColor;
|
||||
const rgb = bgColor.match(/\d+/g);
|
||||
if (rgb && rgb.length >= 3) {
|
||||
const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;
|
||||
return brightness < 128;
|
||||
}
|
||||
} catch (e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
// [辅助] 生成随机字符串
|
||||
function generateRandomString(length) {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
4
test.html
Normal file
4
test.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<meta charset="UTF-8">
|
||||
<script src="./build/ResourceShare-1.3.7.5.js"></script>
|
||||
<resource-share type="style" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"></resource-share>
|
||||
<i class="fas fa-user"></i>
|
||||
46
vbuild.py
Normal file
46
vbuild.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# VBuild
|
||||
import json
|
||||
import sys
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Error: No config file path provided")
|
||||
sys.exit(1)
|
||||
cfg_path = sys.argv[1]
|
||||
print(f"Reading config file: {cfg_path}")
|
||||
try:
|
||||
with open(cfg_path, 'r', encoding='utf-8') as f:
|
||||
cfg = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Config file {cfg_path} does not exist")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error: {cfg_path} is not a valid JSON file")
|
||||
sys.exit(1)
|
||||
project = cfg['project']
|
||||
order = cfg['order']
|
||||
output = cfg['output']
|
||||
|
||||
content = []
|
||||
total_files = len(order)
|
||||
print(f"Starting JS file concatenation, {total_files} files to process")
|
||||
for idx, path in enumerate(order, 1):
|
||||
file_path = f"{project}/{path}.js"
|
||||
print(f"[{idx}/{total_files}] Reading file: {file_path}")
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content.append(f.read())
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File {file_path} not found, terminating concatenation")
|
||||
sys.exit(1)
|
||||
print(f"All files read successfully, writing to output file: {output}")
|
||||
combined_content = '\n'.join(content)
|
||||
|
||||
# 写入未压缩的文件
|
||||
with open(output, 'w', encoding='utf-8') as f:
|
||||
f.write(combined_content)
|
||||
total_size = len(combined_content) / 1024
|
||||
print(f"Concatenation completed! Output file size: {total_size:.2f} KB")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user