致谢
首先感谢【 代码片段大家咋管理的啊 - 链滴 】提出了个好问题,关键的是大佬们的回答,才让我知道其实早有探索和分享,W 佬实在太强了!【分享思源代码片段调试技巧 - 链滴】,把那个 loadSnippets 代码片段一挂起来,效率提高多少先不说,但起码心态愉悦了很多。并且用这个方法相比代码片段,在控制台很直观可以找到

不由感叹论坛还是藏得太深了,作为新人,错过了很多,靠搜索也不容易发现~
W 佬在文中还提了一嘴 leolee 大佬的监听文件更新方案【[js] 代码片段实现代码块最近使用的语言置顶 - leolee 的回帖 - 链滴】,也没再纠结热重载的方案了——但我更懒些,觉得那样能热重载不是更方便?
但对于还是小白的我来说,拼起来不是那么理想。首先是测试第一个方案:
方案一:监听指定文件夹变化,自动刷新
就是在【[js] Shift+F5 刷新页面 - 链滴】基础上改的,这个不难,需要结合 loadSnippets 自动重载,也不需要纠结缓存残留的问题,
(() => {
const CONFIG = {
WATCH_PATH: '/data/plugins/myFonts/Snippets/Running/JS',
POLL_INTERVAL: 2000 // 保留文件监控轮询间隔
};
let watchStats = {};
let timers = new Set(); // 跟踪所有定时器
// 新增:加载上次变更记录
const lastChanges = localStorage.getItem('hotReloadChanges');
if (lastChanges) {
const { files, timestamp } = JSON.parse(lastChanges);
console.log(`[热更新] 上次刷新由以下文件变更触发(${new Date(timestamp).toLocaleString()}):`, files);
localStorage.removeItem('hotReloadChanges');
}
// 统一刷新方法
const forceReload = (changedFiles = []) => {
存储变更记录
if (changedFiles.length > 0) {
localStorage.setItem('hotReloadChanges', JSON.stringify({
files: changedFiles,
timestamp: Date.now()
}));
}
//window.location.search = `?t=${Date.now()}`;
const url = new URL(window.location.href);
url.searchParams.set('t', Date.now());
window.location.href = url.toString();
window.location.reload(true);
};
// 快捷键监听(精简版)
document.addEventListener('keydown', e => {
if (e.shiftKey && e.key === 'F5' && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
forceReload();
}
});
// 修改watch回调
watch(CONFIG.WATCH_PATH, (changedFiles) => {
forceReload(changedFiles); // 传递变更文件列表
});
function watch(path, callback) {
// console.info(`[热更新] 开始监控目录: ${path}`);
let changedFiles = [];
let isWalking = false
const walkDir = async (currentPath) => {
// console.debug(`[热更新] 扫描路径: ${currentPath}`);
try {
const response = await fetch('/api/file/readDir', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentPath }),
});
// 增强响应状态检查
if (!response.ok) {
console.error(`[热更新] API请求失败: ${response.status} ${response.statusText}`);
return;
}
const json = await response.json();
// console.debug('[热更新] API响应数据:', json);
if (!json?.data) {
console.error('[热更新] 无效的API响应结构:', json);
return;
}
const stats = json.data;
for (const entry of stats) {
// 修复路径拼接问题
const fullPath = [currentPath, entry.name]
.join('/')
.replace(/\/+/g, '/'); // 处理多斜杠问题
if (entry.isDir) {
await walkDir(fullPath);
} else {
if (watchStats[fullPath] && watchStats[fullPath] !== entry.updated) {
changedFiles.push(fullPath);
}
watchStats[fullPath] = entry.updated;
}
}
} catch (e) {
console.error('[热更新] 扫描异常:', e);
}
};
const timerHandler = () => {
const timer = setTimeout(idleCallBack, CONFIG.POLL_INTERVAL);
timers.add(timer);
return timer;
};
const idleCallBack = async () => {
if (isWalking) return
isWalking = true
changedFiles = [];
await walkDir(path)
if (changedFiles.length > 0) {
callback(changedFiles)
}
isWalking = false
timerHandler(); // 替换原来的 setTimeout
}
timerHandler(); // 初始化
}
// 添加卸载清理
window.addEventListener('unload', () => {
timers.forEach(t => clearTimeout(t));
timers.clear();
});
})();
/*
作者:leolee
链接:https://ld246.com/article/1723089690687/comment/1723218018823#comments
来源:链滴
协议:CC BY-SA 4.0 https://creativecommons.org/licenses/by-sa/4.0/ */
/* 作者:vincents
链接:https://ld246.com/article/1726311206127
来源:链滴
协议:CC BY-SA 4.0 https://creativecommons.org/licenses/by-sa/4.0/ */
但始终需要忍受黑屏一下,我还是希望能实现自动热重载,大佬们把方法都点破了的。
方案二:监听指定文件夹变化,自动热重载
懒人的胜利,直接上代码:
(() => {
//代码片段加载器,可监控自定义代码片段(CSS/JS),Ctrl+F5启用/禁用,默认关闭。
//感谢大佬wilsons和leolee,在大佬版本基础上修改,具体链接在底部~
/* ******************* 配置常量 ******************* */
const CONFIG = {
// 路径配置
BASE_PATH: '/data/plugins/myFonts/Snippets', // 代码片段根目录(必须存在)
WATCH_PATH: '/data/plugins/myFonts/Snippets/Running', // 实时监控目录(可动态创建)
DEFAULT_INCLUDES: ["Running"], // 默认加载的目录白名单
DEFAULT_EXCLUDES: ["禁用", "备份"], // 强制排除的目录黑名单
// 运行参数
AUTO_WATCH: true, // 是否自动启用监控
POLL_INTERVAL: 3000, // 文件监控轮询间隔(ms)(建议 2000-5000)
MAX_RETRIES: 3, // API失败重试次数
// 日志配置
LOG_LEVEL: 'debug', // 日志级别: debug | info | warn | error
LOG_STYLES: {
debug: 'color: #666;',
info: 'color: #2196F3;',
success: 'color: #4CAF50; font-weight: bold;',
warn: 'color: #FF9800;',
error: 'color: #f44336; font-weight: bold;'
},
};
// 添加清理模块,需要其他JS代码注册!
const CLEANUP_REGISTRY = {};
/* 已暴露window.__registerCleanupHandler;以下是注册清理函数的示例
// 注册清理函数(根据所在代码片段里注册或更改的dom要素或监听事件等按需修改)
const cleanup = () => {
checkFilter();
stopTabObserver(tabObservers);
observer2.disconnect();
document.querySelectorAll('.dock__item[aria-label*="筛选过滤器"]').forEach(btn => btn.remove());
document.querySelectorAll('style[data-style="filter-highlight"]').forEach(style => style.remove());
};
------------------------------------------------------------------
const currentScript = document.currentScript?.src;
if (!currentScript) {
console.error('无法确定当前脚本路径,loadSnippets后续清理函数未注册。');
return;
}
// 提取纯文件名
const currentFilename = currentScript.split('/').pop();
if (window.__registerCleanupHandler) {
window.__registerCleanupHandler(currentFilename, cleanup);
} else {
console.error('清理注册接口不可用');
}
------------------------------------------------------------------
*/
/* ****************** 文件系统-工具函数 ***************** */
/** * 带重试机制的请求封装
* @param {string} url - 请求地址
* @param {Object} options - 请求配置
* @param {number} retries - 剩余重试次数(默认取用CONFIG.MAX_RETRIES)
* @returns {Promise<Response>} 响应对象
*/
async function fetchWithRetry(url, options, retries = CONFIG.MAX_RETRIES) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error('Network response was not ok');
return response;
} catch (error) {
if (retries > 0) {
logger.warn(`Fetch failed, retrying (${CONFIG.MAX_RETRIES - retries + 1}/${CONFIG.MAX_RETRIES})...`);
return fetchWithRetry(url, options, retries - 1);
}
logger.error('Fetch failed after retries:', error);
throw error;
}
}
/** * 遍历目录并处理每个文件
* @param {string} currentPath - 当前目录路径
* @param {Function} processFileEntry - 处理文件条目的回调函数
*/
async function scanDirectory(currentPath, processFileEntry) {
const response = await fetchWithRetry('/api/file/readDir', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentPath })
});
const json = await response.json();
for (const entry of json.data) {
const fullPath = [currentPath, entry.name].join('/').replace(/\/+/g, '/');
if (entry.isDir) {
await scanDirectory(fullPath, processFileEntry);
} else {
await processFileEntry(fullPath, entry);
}
}
}
/** * 获取路径内所有文件
* @param {string} path - 根目录路径
* @param {string[]} includes - 包含的文件名或路径
* @param {string[]} excludes - 排除的文件名或路径
* @returns {Promise<string[]>} 文件路径数组
*/
async function getAllFiles(path, includes = [], excludes = []) {
let files = [];
await scanDirectory(path, (fullPath, entry) => {
const currentPath = fullPath.split('/').slice(0, -1).join('/');
const fileName = entry.name;
if (
(includes.length && !includes.some(item => fullPath.includes(item))) || // 使用完整路径判断
currentPath.startsWith(".") || // 使用实际目录路径
fileName.startsWith(".") ||
excludes.some(exclude => currentPath.includes(exclude)) || // 路径包含匹配
excludes.includes(fileName)
) return;
files.push(fullPath.replace(/^\/data/i, ''));
});
return files;
}
/* ****************** 加载代码片段模块 ***************** */
/** * 代码片段加载器 - 统一处理CSS/JS的加载和更新
* @param {'style'|'script'} type - 资源类型
* @returns {Function} 资源加载函数
*/
function snippetsLoader(type) {
return function (url) {
const encodedUrl = encodeURI(url);
// 智能缓存清理策略 ▼
const selector = type === 'style' ?
`link[href="${encodedUrl}"], link[href*="${encodedUrl}?t="]` :// CSS选择器
`script[src*="${encodedUrl}"]`;// JS选择器
// 清理旧资源
document.querySelectorAll(selector).forEach(el => el.remove());
// 带时间戳的缓存绕过方案 ▼
const el = document.createElement(type === 'style' ? 'link' : 'script');
if (type === 'style') {
el.rel = 'stylesheet';
el.href = `${encodeURI(url)}?t=${Date.now()}`;// CSS强制更新
} else {
el.src = url;// JS保持原始路径
}
el.onerror = () => logger.error(`Failed to load ${type}: ${url}`);
document.head.appendChild(el);
}
}
const loadStyle = snippetsLoader('style');
const loadScript = snippetsLoader('script');
/* ******************* 核心功能 ******************* */
async function load(names = [], excludes = []) {
const files = await getAllFiles(CONFIG.BASE_PATH, names, excludes);
await scanDirectory(CONFIG.BASE_PATH, (fullPath, entry) => {
fileMonitor.stats[fullPath] = `${entry.updated}-${entry.size}`;
});
let jsCount = 0, cssCount = 0;
files.forEach(file => {
if (file.endsWith(".css")) {
loadStyle(file);
cssCount++;
logger.debug(`✅ Loaded CSS: ${file}`); // 恢复详细加载日志
}
if (file.endsWith(".js")) {
loadScript(file);
jsCount++;
logger.debug(`✅ Loaded JS: ${file}`); // 恢复详细加载日志
}
});
const total = jsCount + cssCount;
logger.success(
`🎉 自定义代码片段已加载完毕! [${new Date().toLocaleTimeString()}]\n` +
`CSS: ${cssCount} files ` +
`JS: ${jsCount} files ` +
`Total: ${total} files`
);
}
// 重载默认配置的文件夹
async function reloadSnippets() {
return load(CONFIG.DEFAULT_INCLUDES, CONFIG.DEFAULT_EXCLUDES);
}
/* ******************* 监控模块 ******************* */
const fileMonitor = {
// 状态管理{path: '更新时间戳-文件大小'}
stats: {},
// 生命周期控制
timers: new Set(), // 活跃定时器集合
watcherDispose: null, // 停止/释放监控器
// 启动监控器
start(path, callback) {
this.stop();
this.watcherDispose = this._createWatcher(path, callback);
},
// 停止监控器
stop() {
this.watcherDispose?.();
// this.timers.forEach(t => clearTimeout(t));
// this.timers.clear();
this.watcherDispose = null;
},
// 切换监控状态
toggle() {
if (this.watcherDispose) {
this.stats = {};
this.stop();
logger.info('🔕 监控已禁用');
showNotification('监控已禁用', true);
} else {
this._cleanBeforeStart(CLEANUP_REGISTRY, reloadSnippets);
}
},
// 清理并重启监控器
_cleanBeforeStart(registry, reloadFn) {
Object.keys(registry).forEach(filename => {
registry[filename]();
unregisterCleanupHandler(filename);
});
reloadFn().then(() => {
this.start(CONFIG.WATCH_PATH, handleFileChange);
logger.info('🔔 监控已启用');
});
},
// 监控核心逻辑
_createWatcher(path, callback) {
let isActive = true;
let changedFiles = [];
let isScanning = false;
let currentTimer = null;
// 文件状态对比逻辑 ▼
const scanDirInWatcher = async (currentPath, currentFilesSet) => {
await scanDirectory(currentPath, async (fullPath, entry) => {
const newStat = `${entry.updated}-${entry.size}`;
if (!this.stats[fullPath] || this.stats[fullPath] !== newStat) {
changedFiles.push(fullPath); // 新增或变更都 reload
}
this.stats[fullPath] = newStat; // 更新状态缓存
currentFilesSet.add(fullPath);
});
};
// 定时器闭环管理 ▼
const timerHandler = () => {
if (currentTimer) {
clearTimeout(currentTimer);
this.timers.delete(currentTimer);
}
currentTimer = setTimeout(checkUpdates, CONFIG.POLL_INTERVAL);
this.timers.add(currentTimer);
};
const checkUpdates = async () => {
if (isScanning) return;
isScanning = true;
changedFiles = [];
const currentFilesSet = new Set();
await scanDirInWatcher(path, currentFilesSet);
// 检查被删除的文件
const deletedFiles = Object.keys(this.stats).filter(f => !currentFilesSet.has(f));
if (deletedFiles.length > 0) {
deletedFiles.forEach(file => {
// 执行已注册的清理函数
const encodedName = file.split('/').pop();
const filename = decodeURIComponent(encodedName);
if (CLEANUP_REGISTRY[filename]) {
CLEANUP_REGISTRY[filename]();
unregisterCleanupHandler(filename);
}
// 移除已加载的 JS/CSS 标签
if (file.endsWith('.js')) {
// 移除 script 标签
const scripts = document.querySelectorAll(`script[src*="${file}"]`);
scripts.forEach(el => el.remove());
} else if (file.endsWith('.css')) {
// 移除 link 标签
const links = document.querySelectorAll(`link[href*="${file}"]`);
links.forEach(el => el.remove());
}
delete this.stats[file];
if (!isExcluded(file) && file.startsWith(CONFIG.WATCH_PATH)) {
logger.info(`[${new Date().toLocaleTimeString()}]❌ Unloaded (deleted) ${file.split('.').pop().toUpperCase()}: ${file.replace(/^\/data/i, '')}`);
}
});
}
if (changedFiles.length > 0) {
callback(changedFiles);
}
isScanning = false;
timerHandler();
};
timerHandler();
return () => { // 返回监控停止函数
isActive = false; // ① 关闭监控循环标志
this.timers.forEach(t => clearTimeout(t)); // ② 清除所有定时器
this.timers.clear();// ③ 清空定时器集合
};
},
};
/* **************** 变更处理&清理注册 **************** */
// 注册清理函数
function registerCleanupHandler(filename, cleanupFn) {
const decodedName = decodeURIComponent(filename);
logger.debug(`♲ loadSnippets已注册清理函数: ${decodedName}`);
CLEANUP_REGISTRY[decodedName] = cleanupFn; // 使用解码后的文件名作为键
}
// 注销清理函数
function unregisterCleanupHandler(filename) {
const decodedName = decodeURIComponent(filename);
delete CLEANUP_REGISTRY[decodedName];
logger.debug(`♻ loadSnippets已注销清理函数: ${decodedName}`);
}
function isExcluded(file) {
// 判断路径中是否包含黑名单目录
return CONFIG.DEFAULT_EXCLUDES.some(ex => file.includes('/' + ex + '/'));
}
// 处理文件变更
function handleFileChange(changedFiles) {
changedFiles = Array.from(new Set(changedFiles)); // 去重,避免重复处理
changedFiles.forEach(file => {
if (isExcluded(file)) return; // 跳过黑名单
// 执行已注册的清理函数
const encodedName = file.split('/').pop();
const filename = decodeURIComponent(encodedName);
if (CLEANUP_REGISTRY[filename]) {
CLEANUP_REGISTRY[filename]();
unregisterCleanupHandler(filename);
}
const loader = file.endsWith('.js') ? loadScript :
file.endsWith('.css') ? loadStyle : null;
if (loader) {
const relativeFile = file.replace(/^\/data/i, '');
loader(relativeFile);
logger.info(`[${new Date().toLocaleTimeString()}]🔄 Reloaded ${file.split('.').pop().toUpperCase()}: ${relativeFile}`);
}
});
}
/* ****************** 事件系统 ******************* */
function initEventListeners() {
document.addEventListener('keydown', handleHotkey);
window.addEventListener('unload', handleUnload);
}
function handleHotkey(e) {
if (e.ctrlKey && e.key === 'F5') fileMonitor.toggle();
}
function handleUnload() {
fileMonitor.stop();
}
/* ******************* 消息通知 ******************* */
function showNotification(message, isError = false) {
const endpoint = `/api/notification/${isError ? 'pushErrMsg' : 'pushMsg'}`;
return fetch(endpoint, {
method: "POST",
body: JSON.stringify({
msg: message,
timeout: 5000
})
});
}
const logger = {
debug(...args) {
if (['debug'].includes(CONFIG.LOG_LEVEL)) {
console.log(`%c[DEBUG]`, CONFIG.LOG_STYLES.debug, ...args);
}
},
info(...args) {
if (['debug', 'info'].includes(CONFIG.LOG_LEVEL)) {
console.log(`%c[INFO]`, CONFIG.LOG_STYLES.info, ...args);
}
},
success(...args) {
console.log(`%c[SUCCESS]`, CONFIG.LOG_STYLES.success, ...args);
},
warn(...args) {
console.warn(`%c[WARN]`, CONFIG.LOG_STYLES.warn, ...args);
},
error(...args) {
console.error(`%c[ERROR]`, CONFIG.LOG_STYLES.error, ...args);
}
};
/* ******************* 初始化流程 ******************* */
(function init() {
initEventListeners();
load(CONFIG.DEFAULT_INCLUDES, CONFIG.DEFAULT_EXCLUDES);
if (CONFIG.AUTO_WATCH) {
fileMonitor.start(CONFIG.WATCH_PATH, handleFileChange);
}
// 暴露加载接口,方便快捷开发:window.loadSnippets("项目目录名")
window.loadSnippets = load;
// 暴露清理注册接口
window.__registerCleanupHandler = registerCleanupHandler;
// 控制台日志调节接口,__snippetsLogger.setLevel('debug')
window.__snippetsLogger = {
setLevel: (level) => CONFIG.LOG_LEVEL = level,
getLevel: () => CONFIG.LOG_LEVEL
};
})();
})();
/*
作者:wilsons
链接:https://ld246.com/article/1723287942851#%E7%BB%88%E6%9E%81%E5%A4%A7%E6%8B%9B
作者:leolee
链接:https://ld246.com/article/1723089690687/comment/1723218018823#comments
*/
需要根据自己的路径更改下配置,默认路径不通用。
需要注意的是,如果加载的插件里面涉及到对 dom 元素的修改,又需要频繁调试,最好在对应的代码片段加入清理函数,就这部分,我这边也试用了下,还行。【[js] 各种小界面的过滤器,更多功能等接力 - 链滴】
/* 已暴露window.__registerCleanupHandler;以下是注册清理函数的示例
// 注册清理函数(根据所在代码片段里注册或更改的dom要素或监听事件等按需修改)
const cleanup = () => {
checkFilter();
stopTabObserver(tabObservers);
observer2.disconnect();
document.querySelectorAll('.dock__item[aria-label*="筛选过滤器"]').forEach(btn => btn.remove());
document.querySelectorAll('style[data-style="filter-highlight"]').forEach(style => style.remove());
};
------------------------------------------------------------------
const currentScript = document.currentScript?.src;
if (!currentScript) {
console.error('无法确定当前脚本路径,loadSnippets后续清理函数未注册。');
return;
}
// 提取纯文件名
const currentFilename = currentScript.split('/').pop();
if (window.__registerCleanupHandler) {
window.__registerCleanupHandler(currentFilename, cleanup);
} else {
console.error('清理注册接口不可用');
}
------------------------------------------------------------------
*/
另外,这个方法在控制台 - 源代码/来源 那里看不到 css 的中文文件名,是为了清缓存用的方法,小瑕疵吧,不过,已经稳定的 css 也是建议在思源自带的代码片段面板添加~
方案三:直接更新\data\snippets\conf.json
我瞎说的,话说 这方法可行吗?
代码片段真好玩!
再次感谢一众大佬们的贡献,我有很多想法都是看过才知道还可以这样的,写起来都是抄作业,不一一致谢了。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于