UNPKG

hexo-theme-volantis

Version:

Elegant and powerful theme for Hexo.

761 lines (714 loc) 26.3 kB
// global.js 此文件硬编码到head中 /************这个文件存放不需要重载的全局变量和全局函数*********/ /** * Volantis 主题全局变量 * 定义 window.volantis 全局对象和基础工具函数,需在所有其他脚本之前同步加载。 * * 模块结构: * volantis.dom - DOM 选择与封装 * volantis.EventListener - 事件监听器管理(Pjax 清理) * volantis.pjax - Pjax 生命周期回调 * volantis.dark - 暗色模式切换 * volantis.js/css - 动态脚本/样式加载(按需加载用) * volantis.scroll - 滚动事件引擎 * volantis.layoutHelper - 布局注入辅助 * volantis.requestAnimationFrame - rAF 兼容封装 */ // 全局变量和函数 // window.volantis = {}; // volantis 全局变量 /** * 内存安全的全局 volantis 对象实现 * 特性: * 1. 支持 window.volantis.xxx = yyy 直观赋值语法 * 2. 基于 WeakMap 存储数据,自动回收内存 * 3. 支持属性枚举(如 Object.keys(window.volantis)) * 4. 支持删除属性(delete window.volantis.xxx) */ (function initializeVolantis() { // 1. 初始化 WeakMap 存储实际数据(键:volantisProxy 实例,值:属性键值对) const volantisDataStore = new WeakMap(); // 2. 创建 Proxy 代理处理属性访问 const volantisProxy = new Proxy({}, { /** * 拦截属性读取(如 window.volantis.dom) * @param {Object} target - 代理目标对象(仅作为占位符,不存储数据) * @param {string} prop - 访问的属性名 * @returns {any} 属性值或 undefined */ get(target, prop) { const data = volantisDataStore.get(volantisProxy); return data?.[prop]; // 使用可选链避免数据未初始化时的错误 }, /** * 拦截属性赋值(如 window.volantis.dom = "a") * @param {Object} target - 代理目标对象 * @param {string} prop - 赋值的属性名 * @param {any} value - 赋值的属性值 * @returns {boolean} 赋值是否成功 */ set(target, prop, value) { // 初始化数据容器(若不存在) if (!volantisDataStore.has(volantisProxy)) { volantisDataStore.set(volantisProxy, Object.create(null)); // 使用无原型对象避免原型污染 } // 存储属性值 const data = volantisDataStore.get(volantisProxy); data[prop] = value; return true; // 符合 Proxy 规范,返回成功标志 }, /** * 拦截属性删除(如 delete window.volantis.dom) * @param {Object} target - 代理目标对象 * @param {string} prop - 要删除的属性名 * @returns {boolean} 删除是否成功 */ deleteProperty(target, prop) { const data = volantisDataStore.get(volantisProxy); if (!data || !Object.prototype.hasOwnProperty.call(data, prop)) { return false; // 属性不存在,删除失败 } delete data[prop]; // 若数据为空,清理 WeakMap 条目(优化内存回收) if (Object.keys(data).length === 0) { volantisDataStore.delete(volantisProxy); } return true; }, /** * 拦截属性枚举(如 Object.keys(window.volantis)) * @param {Object} target - 代理目标对象 * @returns {string[]} 可枚举的属性名数组 */ ownKeys(target) { const data = volantisDataStore.get(volantisProxy); return data ? Object.keys(data) : []; }, /** * 拦截属性描述符查询(如 Object.getOwnPropertyDescriptor) * @param {Object} target - 代理目标对象 * @param {string} prop - 属性名 * @returns {PropertyDescriptor|undefined} 属性描述符 */ getOwnPropertyDescriptor(target, prop) { const data = volantisDataStore.get(volantisProxy); if (!data || !Object.prototype.hasOwnProperty.call(data, prop)) { return undefined; } return { value: data[prop], writable: true, enumerable: true, configurable: true }; } }); // 3. 挂载到 window 对象,暴露全局访问入口 Object.defineProperty(window, 'volantis', { value: volantisProxy, writable: true, // 允许后续手动设置为 null 触发清理 configurable: true, enumerable: true }); })(); // 页面DOM操作模块 volantis.dom = {}; // 页面Dom see: /source/js/app.js etc. /******************** volantis.getFunctionHash ********************************/ // 简易文本哈希 volantis.simpleTextHash= (str) => { // 初始哈希值:选择质数5381(DJB2算法推荐的起始值) let hash = 5381; // 遍历每个字符,更新哈希值 for (let i = 0; i < str.length; i++) { // 等价于 hash = hash * 33 + charCode(位移运算更高效) hash = (hash << 5) + hash + str.charCodeAt(i); // 避免数值过大溢出(可选:取模2^32,模拟32位无符号整数) hash = hash & hash; // 或 hash >>> 0(转换为无符号) } // 转换为十六进制字符串,确保至少8位(补前导零) return hash.toString(16).padStart(8, '0'); } // 获取函数内容的哈希 volantis.getFunctionHash= (func) => { // 提取函数源码字符串 const funcStr = func.toString(); // 计算哈希值 return volantis.simpleTextHash(funcStr); } // 测试示例 //function sampleFunc(a, b) { // return a * b + 10; //} //console.log(volantis.getFunctionHash(sampleFunc)); // 输出函数源码的哈希 /******************** volantis.EventListener ********************************/ // 事件监听器 see: /source/js/app.js // 事件监听器模块 volantis.EventListener = { list: [], // 移除所有需要清理的事件监听器 remove() { this.list.forEach(listener => { listener.ele.removeEventListener(listener.type, listener.f, false); }); this.list = []; } }; // 事件监听器构造函数 class VolantisEventListener { constructor(type, handler, element) { this.type = type; this.f = handler; this.ele = element; } } /******************** volantis.dom.$ ********************************/ // 注:这里没有选择器,也没有forEach一次只处理一个dom,这里重新封装主题常用的dom方法,返回的是dom对象,对象包含了以下方法,同时保留dom的原生API // DOM 操作封装 class VolantisDom { constructor(ele) { this._ele = ele || document.createElement('div'); return new Proxy(this, { get(target, prop) { if (target[prop] !== undefined) { return target[prop]; } if (target._ele[prop] !== undefined) { if (typeof target._ele[prop] === 'function') { return target._ele[prop].bind(target._ele); } return target._ele[prop]; } return undefined; }, set(target, prop, value) { if (target._ele[prop] !== undefined) { target._ele[prop] = value; return true; } target[prop] = value; return true; } }); } // 自定义方法 find(selector) { const found = this._ele.querySelector(selector); return found ? new VolantisDom(found) : null; } hasClass(className) { return this._ele.classList.contains(className); } addClass(classNames) { if (typeof classNames !== 'string') return this; classNames.split(' ').forEach(className => { className && this._ele.classList.add(className); }); return this; } removeClass(classNames) { if (typeof classNames !== 'string') return this; classNames.split(' ').forEach(className => { className && this._ele.classList.remove(className); }); return this; } toggleClass(classNames) { if (typeof classNames !== 'string') return this; classNames.split(' ').forEach(className => { className && this._ele.classList.toggle(className); }); return this; } on(event, handler, removeOnPjax = true) { if (typeof handler !== 'function') return this; this._ele.addEventListener(event, handler, false); if (removeOnPjax) { volantis.EventListener.list.push( new VolantisEventListener(event, handler, this._ele) ); } return this; } click(handler, removeOnPjax) { return this.on('click', handler, removeOnPjax); } scroll(handler, removeOnPjax) { return this.on('scroll', handler, removeOnPjax); } html(content) { if (content === undefined) { return this._ele.innerHTML; } this._ele.innerHTML = content; return this; } hide() { this._ele.classList.remove('show'); this._ele.style.removeProperty('display'); return this; } show() { this._ele.classList.add('show'); this._ele.style.removeProperty('display'); return this; } } // DOM 选择工厂函数 volantis.dom.$ = (ele) => { if (!ele) return null; if (ele instanceof VolantisDom) { return ele; } if (ele instanceof NodeList || ele instanceof HTMLCollection) { return Array.from(ele).map(item => new VolantisDom(item)); } if (ele instanceof Node) { return new VolantisDom(ele); } return null; }; /******************** RunItem ********************************/ // 任务执行管理器 class RunItem { constructor() { this.list = []; } start() { this.list.forEach(item => item.run()); } // name 存在时同名替换,否则追加 push(fn, name, useRequestAnimationFrame = true) { if (typeof fn !== 'function') return; let taskFn = fn; if (useRequestAnimationFrame) { taskFn = () => { volantis.requestAnimationFrame(fn); }; } name = name || volantis.getFunctionHash(fn) const idx = this.list.findIndex(item => item.name === name); if (idx !== -1) { this.list[idx] = new TaskItem(taskFn, name); return; } this.list.push(new TaskItem(taskFn, name)); } remove(name) { if (typeof name !== 'string') return; this.list = this.list.filter(item => item.name !== name); } } class TaskItem { constructor(fn, name) { this.name = name || volantis.getFunctionHash(fn); this.fn = fn; } run() { try { this.fn(); } catch (error) { console.error(`Error executing task ${this.name}:`, error); } } } /******************** Pjax ********************************/ // /layout/_plugins/pjax/index.ejs // volantis.pjax.send(callBack[,"callBackName"]) 传入pjax:send回调函数 // volantis.pjax.push(callBack[,"callBackName"]) 传入pjax:complete回调函数 // volantis.pjax.error(callBack[,"callBackName"]) 传入pjax:error回调函数 volantis.pjax = {}; volantis.pjax.method = { complete: new RunItem(), error: new RunItem(), send: new RunItem() }; volantis.pjax = Object.assign(volantis.pjax, { push: volantis.pjax.method.complete.push.bind(volantis.pjax.method.complete), error: volantis.pjax.method.error.push.bind(volantis.pjax.method.error), send: volantis.pjax.method.send.push.bind(volantis.pjax.method.send) }); /******************** RightMenu ********************************/ // volantis.rightmenu.handle(callBack[,"callBackName"]) 外部菜单项控制 // 可在 volantis.mouseEvent 处获取右键事件 volantis.rightmenu = {}; volantis.rightmenu.method = { handle: new RunItem(), } volantis.rightmenu = Object.assign(volantis.rightmenu, { handle: volantis.rightmenu.method.handle.push.bind(volantis.rightmenu.method.handle), }); /******************** Dark Mode ********************************/ // /layout/_partial/scripts/darkmode.ejs // volantis.dark.mode 当前模式 dark or light // volantis.dark.toggle() 暗黑模式触发器 // volantis.dark.push(callBack[,"callBackName"]) 传入触发器回调函数 volantis.dark = {}; volantis.dark.method = { toggle: new RunItem() }; volantis.dark = Object.assign(volantis.dark, { push: volantis.dark.method.toggle.push.bind(volantis.dark.method.toggle) }); /******************** Message ********************************/ // VolantisApp.message /******************** isMobile ********************************/ // /source/js/app.js // volantis.isMobile // volantis.isMobileOld /********************脚本动态加载函数********************************/ // volantis.js(src, cb) cb 可以传入onload回调函数 或者 JSON对象 例如: volantis.js("src", ()=>{}) 或 volantis.js("src", {defer:true,onload:()=>{}}) // volantis.css(src) // 返回Promise对象,如下方法同步加载资源,这利于处理文件资源之间的依赖关系,例如:APlayer 需要在 MetingJS 之前加载 // (async () => { // await volantis.js("...theme.plugins.aplayer.js.aplayer...") // await volantis.js("...theme.plugins.aplayer.js.meting...") // })(); // 脚本动态加载(按需加载用) // volantis.js(src, cb) cb: onload 回调或属性对象 {defer:true, onload:()=>{}} // volantis.css(src) volantis.js = (src, cb) => { const escapeSelector = str => str.replace(/[#".'()[\]]/g, '\\$&'); return new Promise((resolve, reject) => { setTimeout(function () { const existingScript = document.querySelector(`script[src="${escapeSelector(src)}"]`); if (existingScript) { resolve(); return; } const script = document.createElement('script'); script.src = src; const handleLoad = () => { if (typeof cb === 'function') cb(); resolve(); }; script.onload = handleLoad; script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); if (cb && typeof cb === 'object' && !Array.isArray(cb)) { for (const p in cb) { if (!cb.hasOwnProperty(p)) continue; if (p === 'onload') { script.onload = () => { cb[p](); resolve(); }; } else if (p === 'pjax') { script.setAttribute('pjax', ''); script.setAttribute('data-pjax', ''); } else if (cb[p] === true) { script.setAttribute(p, ''); } else { script.setAttribute(p, cb[p]); } } } (document.head || document.documentElement).appendChild(script); }); }); }; volantis.css = (src) => { const escapeSelector = str => str.replace(/[#".'()[\]]/g, '\\$&'); return new Promise((resolve, reject) => { setTimeout(function () { const existingLink = document.querySelector(`link[href="${escapeSelector(src)}"]`); if (existingLink) { resolve(); return; } const link = document.createElement('link'); Object.assign(link, { rel: 'stylesheet', href: src, onload: () => resolve(), onerror: () => reject(new Error(`Failed to load CSS: ${src}`)) }); document.head.appendChild(link); }); }); }; /********************按需加载的插件********************************/ // volantis.import.jQuery().then(()=>{}) volantis.import = { jQuery: () => { if (typeof jQuery == "undefined") { return volantis.js(volantis.GLOBAL_CONFIG.cdn.jquery) } else { return new Promise(resolve => { resolve() }); } } } /********************** 节流和防抖 ********************************/ // 节流函数:确保指定时间内仅执行一次 时间戳版本的节流实现 volantis.throttle = (func, delay = 200) => { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= delay) { func.apply(this, args); lastTime = now; } }; }; // 防抖函数 setTimeout实现 volantis.debounce = (func, wait = 200) => { let timer = null; return function () { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { func.apply(this, arguments) timer = null }, wait) }; }; /********************** requestAnimationFrame ********************************/ // 1、requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。 // 2、在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的的 cpu,gpu 和内存使用量。 volantis.requestAnimationFrame = (fn) => { if (!window.requestAnimationFrame) { window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || function (callback) { return window.setTimeout(callback, 1000 / 60); }; } return window.requestAnimationFrame(fn); }; // 3. 基于 requestAnimationFrame 的节流函数 volantis.rafThrottle = (fn) => { let rafId = null return (...args) => { if (rafId) return rafId = volantis.requestAnimationFrame(() => { fn(...args) rafId = null }) } }; /************************ layoutHelper *****************************************/ // Layout Helper volantis.layoutHelper = (helper, html, opt = {}) => { const { clean = false, pjax = true } = { ...opt }; const handleLayout = () => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; const layoutHelper = document.querySelector(`#layoutHelper-${helper}`); if (layoutHelper) { if (clean) layoutHelper.innerHTML = ''; layoutHelper.append(tempDiv); } }; handleLayout(); if (pjax) { volantis.pjax.push(handleLayout, `layoutHelper-${helper}`); } }; /****************************** 滚动事件处理 ****************************************/ // 滚动事件处理 volantis.scroll = { engine: new RunItem(), unengine: new RunItem(), }; volantis.scroll = Object.assign(volantis.scroll, { push: volantis.scroll.engine.push.bind(volantis.scroll.engine), }); // 滚动条距离顶部的距离 volantis.scroll.getScrollTop = () => { let scrollPos; if (window.pageYOffset) { scrollPos = window.pageYOffset; } else if (document.compatMode && document.compatMode != 'BackCompat') { scrollPos = document.documentElement.scrollTop; } else if (document.body) { scrollPos = document.body.scrollTop; } return scrollPos; } volantis.scroll.scrollHeight = function () { return Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, ) } volantis.scroll.offsetHeight = function () { return Math.min( document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ) } volantis.scroll.progress = function () { return volantis.scroll.getScrollTop() / (volantis.scroll.scrollHeight() - volantis.scroll.offsetHeight()) } // 使用 requestAnimationFrame 处理滚动事件 // `volantis.scroll.del` 中存储了一个数值, 该数值检测一定时间间隔内滚动条滚动的位移, 数值的检测频率是浏览器的刷新频率. 数值为正数时, 表示向下滚动. 数值为负数时, 表示向上滚动. volantis.scroll.handleScrollEvents = () => { volantis.scroll.lastScrollTop = volantis.scroll.getScrollTop() function loop() { const scrollTop = volantis.scroll.getScrollTop(); if (volantis.scroll.lastScrollTop !== scrollTop) { volantis.scroll.del = scrollTop - volantis.scroll.lastScrollTop; volantis.scroll.lastScrollTop = scrollTop; // if (volantis.scroll.del > 0) { // console.log("向下滚动"); // } else { // console.log("向上滚动"); // } // 注销过期的unengine未滚动事件 volantis.scroll.unengine.list = [] volantis.scroll.engine.start(); } else { volantis.scroll.unengine.start(); } volantis.requestAnimationFrame(loop) } volantis.requestAnimationFrame(loop) } volantis.scroll.handleScrollEvents() volantis.scroll.ele = null; // 触发页面滚动至目标元素位置 volantis.scroll.to = (ele, option = {}) => { if (!ele) return; volantis.scroll.ele = ele; // 默认配置 opt = { top: ele.getBoundingClientRect().top + document.documentElement.scrollTop, behavior: volantis.GLOBAL_CONFIG.scroll_behavior } // 定义配置 if ("top" in option) { opt.top = option.top } if ("behavior" in option) { opt.behavior = option.behavior } if ("addTop" in option) { opt.top += option.addTop } if (!("observerDic" in option)) { option.observerDic = 100 } // 滚动 window.scrollTo(opt); // 监视器 // 监视并矫正元素滚动到指定位置 // 用于处理 lazyload 引起的 cls 导致的定位失败问题 // option.observer = false if (option.observer) { setTimeout(() => { if (volantis.scroll.ele != ele) { return } volantis.scroll.unengine.push(() => { let me = ele.getBoundingClientRect().top if (!(me >= -option.observerDic && me <= option.observerDic)) { volantis.scroll.to(ele, option) } volantis.scroll.unengine.remove("unengineObserver") }, "unengineObserver") }, 1000) } } /********************** Content Visibility ********************************/ // 见 source/css/first.styl 如果遇到任何问题 删除 .post-story 即可 // 一个元素被声明 content-visibility 属性后 如果元素不在 viewport 中 浏览器不会计算其后代元素样式和属性 从而节省 Style & Layout 耗时 // content-visibility 的副作用: 锚点失效 等等(实验初期 暂不明确), 使用此方法清除样式 volantis.cleanContentVisibility = () => { if (document.querySelector(".post-story")) { console.log("cleanContentVisibility"); document.querySelectorAll(".post-story").forEach(e => { e.classList.remove("post-story") }) } } /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ //图像加载出错时的处理 // 图片错误降级 function errorImgAvatar(img) { img.src = volantis.GLOBAL_CONFIG.default.avatar; img.srcset = volantis.GLOBAL_CONFIG.default.avatar; img.onerror = null; } function errorImgCover(img) { img.src = volantis.GLOBAL_CONFIG.default.cover; img.srcset = volantis.GLOBAL_CONFIG.default.cover; img.onerror = null; } /******************************************************************************/ // Hack Interval // 保存原生定时器方法 const nativeSetTimeout = window.setTimeout; const nativeSetInterval = window.setInterval; const nativeClearTimeout = window.clearTimeout; const nativeClearInterval = window.clearInterval; // 存储活动定时器ID的数组 volantis.activeTimeout = []; volantis.activeInterval = []; // 重写setTimeout:执行后自动移除ID window.setTimeout = function(callback, delay, ...args) { // 包装回调函数,执行后从列表移除ID const wrappedCallback = function(...innerArgs) { try { // 执行原始回调(保持this指向和参数传递) return callback.apply(this, innerArgs); } finally { // 无论回调是否报错,均移除ID const index = volantis.activeTimeout.indexOf(timerId); if (index !== -1) volantis.activeTimeout.splice(index, 1); } }; // 调用原生setTimeout获取ID const timerId = nativeSetTimeout(wrappedCallback, delay, ...args); // 添加到活动列表 volantis.activeTimeout.push(timerId); return timerId; }; // 重写setInterval:需显式清除才移除ID window.setInterval = function(callback, delay, ...args) { const timerId = nativeSetInterval(callback, delay, ...args); volantis.activeInterval.push(timerId); return timerId; }; // 重写清除方法:同步移除ID列表记录 window.clearTimeout = function(timerId) { nativeClearTimeout(timerId); const index = volantis.activeTimeout.indexOf(timerId); if (index !== -1) volantis.activeTimeout.splice(index, 1); }; window.clearInterval = function(timerId) { nativeClearInterval(timerId); const index = volantis.activeInterval.indexOf(timerId); if (index !== -1) volantis.activeInterval.splice(index, 1); }; volantis.getActiveInterval = function () { return [...volantis.activeInterval]; } volantis.getActiveTimeout = function () { return [...volantis.activeTimeout]; } volantis.clearAllTimers = function (){ volantis.getActiveInterval().forEach(function(i) { clearInterval(i); }); volantis.getActiveTimeout().forEach(function(i) { clearTimeout(i); }); } volantis.pjax.send(volantis.clearAllTimers,"clearAllTimers"); /******************************************************************************/ // DOMContentLoaded:缓存 DOM 引用 document.addEventListener('DOMContentLoaded', function () { /******************** volantis.dom ********************************/ // 页面选择器 将dom对象缓存起来 see: /source/js/app.js etc. volantis.dom.bodyAnchor = volantis.dom.$(document.getElementById("safearea")); // 页面主体 volantis.dom.topBtn = volantis.dom.$(document.getElementById('s-top')); // 向上 volantis.dom.wrapper = volantis.dom.$(document.getElementById('wrapper')); // 整个导航栏 volantis.dom.coverAnchor = volantis.dom.$(document.querySelector('#l_cover .cover-wrapper')); // 1个 volantis.dom.switcher = volantis.dom.$(document.querySelector('#l_header .switcher .s-search')); // 搜索按钮 移动端 1个 volantis.dom.header = volantis.dom.$(document.getElementById('l_header')); // 移动端导航栏 volantis.dom.search = volantis.dom.$(document.querySelector('#l_header .m_search')); // 搜索框 桌面端 移动端 1个 volantis.dom.mPhoneList = volantis.dom.$(document.querySelectorAll('#l_header .m-phone .list-v')); // 手机端 子菜单 多个 }); /******************************************************************************/