[js] 代码片段管理 - 懒人 & 缝合 & 狗尾续貂版

致谢

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

image.png

不由感叹论坛还是藏得太深了,作为新人,错过了很多,靠搜索也不容易发现~

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

我瞎说的,话说 这方法可行吗?

代码片段真好玩!

再次感谢一众大佬们的贡献,我有很多想法都是看过才知道还可以这样的,写起来都是抄作业,不一一致谢了。

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119786 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1986 回帖
3 操作
chuchen 在 2025-07-17 16:29:01 更新了该帖
chuchen 在 2025-07-15 17:38:56 更新了该帖
chuchen 在 2025-07-15 17:37:26 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...