如有延迟,点此刷新首页
本WIKI编辑权限开放,欢迎收藏起来防止迷路,也希望有爱的小伙伴和我们一起编辑哟~
编辑帮助:目录BWIKI反馈留言板

全站通知:

Widget:BLoader

来自光遇WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索

BLoader - BWiki资源加载器

对于加载了BLoader的页面,可以用模板:Load加载指定的JS、CSS文件。

源码:

<script>
// 这里用来定义 tools wiki需要加载的JS、CSS
// 预定义资源列表, path: 'wiki/file.css' , timing: 'immediate'(default) | 'dom' | 'jquery' | 'complete'
(window.BLoaderList = window.BLoaderList || []).push(
    {
        path: 'tools/MediaWiki:Main.js',
        timing: 'immediate'
    },
    {
        path: 'tools/MediaWiki:Main.css',
        timing: 'immediate'
    },
); // 务必在之后加载BLoader。如果BLoder已经加载过,就不会加载这些资源了
</script>
<script>
/**
 * BLoader.js - BWiki资源加载器
 * 
 * 用于加载CSS和JS资源
 *     控制加载时机:DOMContentLoaded、jQuery加载、window.onload、立即加载
 *     支持作为Widget放入SiteNotice中
 *     支持放入Tampermonkey中
 * 
 *     不会重复加载
 *     支持对管理员/界面管理员绕过缓存,默认对界面管理员绕过缓存
 *     支持动态加载:页面中被动态添加 .loader.js 和 .loader.css 元素时,会触发自动加载
 * 
 * 
 * 代码几乎全部由 Roo Cline + DeepSeek-V3 完成, GitHub Copilot (GPT 4o) 负责微调细节, Lu提供引导、注释和测试。
 * 
 * 
 * 使用示例:
 * 1. 加载单个CSS:
 *    在页面中添加如下元素:
 *    <div class="loader css" style="display:none">style.css</div>
 * 
 *    可以自定义模板来快速构造此HTML
 * 
 * 2. 加载单个JS:
 *    在页面中添加如下元素:
 *    <div class="loader js" data-load-timing="dom" data-debug="true" style="display:none">script.js</div>
 * 
 *    可以自定义模板来快速构造此HTML
 * 
 *    其中 data-load-timing 可选值为: dom | jquery | complete | immediate (default)
 *       * dom: DOMContentLoaded 时加载
 *       * jquery: jQuery加载后加载, 最长等待5秒后立即加载
 *       * complete: window.onload 时加载
 *       * immediate: 立即加载(默认值)
 *    如果执行加载时,已经错过了对应的时机,则立即加载
 * 
 * 3. 预定义资源列表:
 *    修改 BLoaderList 数组,每个元素包含 path、timing、debug,分别表示资源路径、加载时机和绕过缓存,加载时机如上所述
 *    path 为资源路径,如 'wiki/file.css' 或 'tools/file.js', 需要带有wiki域名作为前缀,这是为了便于加载其他wiki的资源
 *    如:
        
        // 预定义资源列表, path: 'wiki/file.css' , timing: 'immediate'(default) | 'dom' | 'jquery' | 'complete'
        (window.BLoaderList = window.BLoaderList || []).push(
            {
                path: 'tools/MediaWiki:Prism.css',
                timing: 'jquery',
                debug: true
            },
            {
                path: 'blhx/MediaWiki:xxxx.css',
                debug: true
            },
            {
                path: 'wiki/MediaWiki:Demo.js',
                timing: 'dom'
            },
            // 在此添加更多资源
        ); // 务必在之后加载BLoader。如果BLoder已经加载过,不会加载这些资源
 * 
 * 
 * 代码分为以下部分:
 * 0. 辅助函数:输出页面加载生命周期,用于调试
 * 
 * 1. BLoader - 资源加载器,加载CSS和JS资源
 *    监听页面中的 .loader.js 和 .loader.css 元素,以其标签内文本作为资源名称加载,仅支持本wiki MW空间,如 Mediawiki:resource.css
 * 
 * 2. AdminGroupCache - 缓存高权限用户的用户组(管理员、界面管理员)到 localStorage,其他程序如 BLoader 可以针对用户组免缓存加载资源
 *    定时刷新用户组信息到localStorage,供BLoader等程序使用
 * 
 * 
 */
(function() { // 辅助函数:添加日志,用于调试
    function addLog(message) {
        var timestamp = new Date().toLocaleTimeString();
        console.debug('%c[' + timestamp + '] ' + message, 'color: green');
    }

    // 脚本开始执行
    addLog('BLoader脚本开始执行');

    // DOMContentLoaded 事件:当初始的 HTML 文档被完全加载和解析完成时触发,不需要等待样式表、图像和子框架的完成加载
    document.addEventListener('DOMContentLoaded', function() {
        addLog('DOMContentLoaded 事件触发:DOM 完全加载和解析');
    });

    // window.onload 事件:当页面的所有资源(如图片、样式表)都已加载完成时触发
    window.addEventListener('load', function() {
        addLog('load 事件触发:页面及所有资源完全加载');
    });

    function waitjQuery(waitCount) { // 等待jQuery加载,最多等待10秒
        if (window.jQuery || waitCount > 200) {
            addLog('jquery 事件触发:jquery加载了!');
        } else{
            setTimeout(function() {waitjQuery(waitCount + 1)}, 50);
        }
    }
    waitjQuery(0);
})();


// BLoader
(function() {
    var noCacheForSysop = false; // 对 管理员 绕过缓存
    var noCacheForInterfaceAdmin = true; // 对 界面管理员 绕过缓存

    var cacheKey = 'BLoaderAdminGroupsCache'; //如需修改,请同步修改AdminGroupCache

    var JQ_INTERVAL = 81; // jQuery检查间隔(81毫秒)
    var MAX_WAIT_COUNT = 64; // 最大等待次数(81*64=5184毫秒)

    var bwiki_url = 'https://wiki.biligame.com/'; // bwiki域名
    var wikiName = typeof RLCONF === 'object' ? RLCONF.wgGameName : new URL(window.location.href).pathname.split('/')[1]; // 当前wiki名称

    var loadedResources = new Set(); // 已加载资源URL集合,避免重复加载
    var needNoCache = isNeedNoCache(); // 对当前用户计算是否需要禁用缓存

    function loadCSS(path, debug) { // 加载CSS资源, path, 如 'wiki/file.css'
        if (loadedResources.has(path)) {
            console.debug('此CSS已经加载过,不再重复加载: ' + path);
            return;
        }

        var link = document.createElement('link');
        link.rel = 'stylesheet';
        link.href = formatUrl(bwiki_url + path, debug);// 仅限加载bwiki资源。如果需要禁用缓存,则在URL后添加随机参数
        document.head.appendChild(link);
        console.debug('BLoader 加载 CSS: ' + link.href);
        loadedResources.add(path);
    };

    function loadJS(path, timing, debug) { // 加载JS资源, path, 如 'wiki/file.js'
        if (loadedResources.has(path)) {
            console.debug('此JS已经加载过,不再重复加载: ' + path);
            return;
        }

        var script = document.createElement('script'); // 创建script标签
        script.src = formatUrl(bwiki_url + path, debug); // 仅限加载bwiki资源。如果需要禁用缓存,则在URL后添加随机参数
        loadedResources.add(path); // 存入已加载集合,标记此资源已加载

        function appendScript() { // 添加script标签到head, 写成函数便于下方使用
            document.head.appendChild(script);
            console.debug('BLoader 加载 JS: ' + script.src);
        };

        function waitjQuery(waitCount) { // 等待jQuery加载,最多等待MAX_WAIT_COUNT次
            if (window.jQuery || waitCount > MAX_WAIT_COUNT) {
                appendScript();
            } else{
                setTimeout(function() {waitjQuery(waitCount + 1)}, JQ_INTERVAL);
            }
        }

        switch (timing) {
            case 'dom':
                document.readyState !== 'loading' ? appendScript() : document.addEventListener('DOMContentLoaded', appendScript);
                break;
            case 'jquery':
                waitjQuery(0);
                break;
            case 'complete':
                document.readyState === 'complete' ? appendScript() : window.addEventListener('load', appendScript);
                break;
            default:
                appendScript();
                break;
        }
    }

    function isNeedNoCache() { // 是否需要禁用缓存,有相关用户组,且上方用户组配置为true
        try {
            var adminGroups = JSON.parse(localStorage.getItem(cacheKey)) || {};
            var userGroups = adminGroups[wikiName] || {};
            return (userGroups['sysop'] && noCacheForSysop) || (userGroups['interface-admin'] && noCacheForInterfaceAdmin);
        } catch (error) {
            console.error('Failed to parse admin groups from localStorage:', error);
            return false;
        }
    }

    function formatUrl(url, debug) { // 如果需要禁用缓存,则在URL后添加随机参数
        if (url.toLowerCase().endsWith('.css')) {
            url = url + '?action=raw&ctype=text/css'; // 以raw形式加载css
        } else {
            url = url + '?action=raw&ctype=text/javascript'; // 以raw形式加载js
        }

        if (needNoCache || debug){
            url = url + '&nocache=1&random=' + Math.ceil(Date.now() / 1000);
        }
        return url;
    }
    
    // 处理页面中的 .loader.js 和 .loader.css 元素,以其标签内文本作为资源名称加载,仅支持本wiki MW空间,如 Mediawiki:resource.css。
    function loadFromElements() {
        function getFullPath(name, type) {
            name = 'MediaWiki:' + (name.toLowerCase().startsWith('mediawiki:') ? name.slice(10) : name); // 去除Mediawiki:前缀,下文统一加,避免大小写问题导致重定向
            name = name.toLowerCase().endsWith('.' + type) ? name : name + '.' + type; // 确保包含正确的后缀,即必须以type(js/css)结尾
            return wikiName + '/' + name;
        }
        function processElement(el, type) {
            try {
                var name = el.textContent.trim();
                if (type === 'css') {
                    loadCSS(getFullPath(name, type), el.dataset.debug === 'true'); // 仅支持wiki自身内容,避免用户有权限跨站注入js
                } else {
                    loadJS(getFullPath(name, type), el.dataset.loadTiming || 'immediate', el.dataset.debug === 'true');
                }
                el.dataset.processed = true; // 标记此标签已处理,即使重复调用也不会重复加载
            } catch (error) {
                console.error('Failed to process loader:', type, el, error);
            }
        };
        document.querySelectorAll('#content .loader.css:not([data-processed])').forEach(function (el) {
            processElement(el, 'css');
        });
        document.querySelectorAll('#content .loader.js:not([data-processed])').forEach(function (el) {
            processElement(el, 'js');
        });
    }

    // 节流函数,限制触发频率
    function throttle(func) {
        var inThrottle = false;
        return function() {
            if (!inThrottle) {
                func();
                inThrottle = true;
                setTimeout(function(){inThrottle = false;}, 100);// 100ms
            }
        };
    }

    function watchdog() {
        // 避免重复加载 watchdog,因为MutationObserver会在每次变化时调用,多次加载会浪费资源
        if (window.BLoader_IsLoaded) {
            return;
        }
        window.BLoader_IsLoaded = true;
        loadFromElements();
        var content = document.querySelector('#content');
        if (!content) {
            content = document.body;
        }
        var observer = new MutationObserver(throttle(loadFromElements));
        observer.observe(content, { childList: true, subtree: true });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', watchdog);
    } else {
        watchdog();
    }

    (window.BLoaderList = window.BLoaderList || []).forEach(function (res) { // 加载预定义资源列表中的资源
        if (res.path.endsWith('.css')) {
            loadCSS(res.path, res.debug || false);
        } else {
            loadJS(res.path, res.timing || 'immediate', res.debug || false);
        }
    });
    loadFromElements(); // 此时dom可能没有加载完全,只能加载部分资源,后续会通过MutationObserver继续加载
})();


// AdminGroupCache - 缓存高权限用户的用户组(管理员、界面管理员)到 localStorage,其他程序如 BLoader 可以针对用户组免缓存加载资源
(function() {
    function purgeAdminGroupCache() {
        var apiUrl = 'https://wiki.biligame.com/' + RLCONF.wgGameName + '/api.php?action=query&meta=userinfo&uiprop=groups&format=json';
        // localStorage 缓存配置
        var cacheKey = 'BLoaderAdminGroupsCache'; // 如需修改,请同步修改BLoader
        var cacheExpiryKey = cacheKey + '_expiry';
        var cacheExpiryTime = 5 * 60 * 1000; // 5 minutes
    
        function purgeCache() { /* 刷新localStorage的缓存 */
            fetch(apiUrl)
                .then(function(response){return response.json();})
                .then(function(data) {
                    var groups = (data && data.query && data.query.userinfo && data.query.userinfo.groups) || [];
                    var adminGroups = JSON.parse(localStorage.getItem(cacheKey)) || {};
                    adminGroups[RLCONF.wgGameName] = {
                        'sysop': groups.includes('sysop'),
                        'interface-admin': groups.includes('interface-admin')
                    };
                    localStorage.setItem(cacheKey, JSON.stringify(adminGroups));
                    localStorage.setItem(cacheExpiryKey, Date.now() + cacheExpiryTime);
                })
                .catch(function(error){
                    console.error('Failed to fetch and update admin groups:', error);
                });
        }
    
        function isCacheExpired() {
            try{
                var expiryTime = localStorage.getItem(cacheExpiryKey);
                return !expiryTime || Date.now() > expiryTime;
            } catch (error) {
                console.error('Failed to check localStorage cache expiry:', error);
                return true;
            }
        }
    
        if (isCacheExpired()) {
            purgeCache();
        }
    }

    if (document.readyState === 'loading') { // 等dom加载完再执行,避免在油猴等环境下执行过早,无法获取RLCONF
        document.addEventListener('DOMContentLoaded', purgeAdminGroupCache);
    } else {
        purgeAdminGroupCache();
    }
})();
</script>