news 2026/9/19 16:51:25

Bilibili-Evolved 夜间模式计划时段:时间调度机制与源码实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bilibili-Evolved 夜间模式计划时段:时间调度机制与源码实现解析

Bilibili-Evolved 夜间模式计划时段:时间调度机制与源码实现解析

【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved

Bilibili-Evolved 是一款开源的哔哩哔哩增强脚本,其中的「夜间模式计划时段」组件允许用户设定一个时间范围,让夜间模式在该时间段内自动开启、离开该时间段后自动关闭,从而避免手动反复切换主题。本文以该组件的说明文档(schedule/index.md)为主体,结合其 TypeScript 实现 与「夜间模式」主组件源码,完整讲解配置方法、跨天时间判断逻辑、自动切换调度原理,以及与其他夜间模式组件的配合与注意事项。

功能概述:按时间段自动开关夜间模式

「夜间模式计划时段」(darkModeSchedule)是 Bilibili-Evolved 夜间模式体系下的一个调度型组件。它的核心能力是:

  • 用户指定一个时间段(如18:006:00);
  • 当系统时间进入该时间段时,自动开启夜间模式;
  • 当系统时间离开该时间段时,自动关闭夜间模式。

从源码中的组件定义可以看出,该组件归属于stylegeneral两个标签分类,注册名为darkModeSchedule,显示名为「夜间模式计划时段」:

export const component = defineComponentMetadata({ name: 'darkModeSchedule', displayName: '夜间模式计划时段', tags: [componentsTags.style, componentsTags.general], entry: ({ settings }) => fullyLoaded(() => checkTime(settings)), urlExclude: darkExcludes, options, })

(见 schedule/index.ts)

其中entry通过fullyLoaded包装调度函数,确保页面核心内容完全加载后才开始首次时间检查;urlExclude则复用了夜间模式统一的排除页面列表darkExcludes,在这些页面上组件不会生效。

配置说明:时间段参数的格式与默认值

该组件的唯一配置项是range(时间段),其元数据定义如下:

const options = defineOptionsMetadata({ range: { defaultValue: { start: '18:00', end: '6:00', }, displayName: '时间段', validator: (range: Range<string>) => { const { start, end } = range const regex = /^(\d{1,2}):(\d{1,2})$/ if (!regex.test(start) || !regex.test(end)) { return null } const startTime = new ScheduleTime(range.start) const endTime = new ScheduleTime(range.end) return { start: startTime.toString(), end: endTime.toString(), } }, }, })

(见 schedule/index.ts)

由此可以得到以下配置要点:

要点说明
默认时间段18:006:00(晚上 18 点到次日早上 6 点)
时间格式HH:mmH:mm,由正则/^(\d{1,2}):(\d{1,2})$/校验,小时允许 1~2 位数字、分钟允许 1~2 位数字
格式校验不符合正则的输入会返回null,校验失败,无法保存
输入规范化校验通过后,时间会被解析并重新格式化(如6:0会规范化为06:00)返回,保证配置存储格式统一

需要注意的是,校验正则本身并不严格限制小时在 0~23、分钟在 0~59 的范围内,但ScheduleTime的解析逻辑会对越界值进行规范化处理(详见下一节),因此即使输入了如25:70这样的值,也会被折算为合法的时钟时间。

时间解析与规范化:ScheduleTime 类

组件实现的核心是一个轻量的时间工具类ScheduleTime,负责完成字符串解析、数值规范化与时间比较。其构造函数支持三种调用方式:

class ScheduleTime { constructor(...args: [] | [string] | [number, number]) { if (args.length === 0) { const now = new Date() this.hour = now.getHours() this.minute = now.getMinutes() } else if (args.length === 1) { const [text] = args ;[this.hour, this.minute] = text .split(':') .slice(0, 2) .map(it => ScheduleTime.validatePart(it)) this.normalize() } else if (args.length === 2) { ;[this.hour, this.minute] = args } } }

(见 schedule/index.ts)

  • 无参数:取当前系统时间(小时 + 分钟),用于"现在"的判断;
  • 一个字符串:形如18:00,先按冒号分割,用validatePart逐段解析为数字,再调用normalize()规范化;
  • 两个数字:直接以(hour, minute)赋值。

validatePart只接受 0~59 之间的数字,其余返回null。而normalize()会把越界的分钟折算进位/借位到小时、把越界的小时折算到 0~23 区间:

normalize() { while (this.minute < 0) { this.minute += 60; this.hour -= 1 } while (this.minute >= 60) { this.minute -= 60; this.hour += 1 } while (this.hour < 0) { this.hour += 24 } while (this.hour >= 24) { this.hour -= 24 } }

(见 schedule/index.ts)

此外,toString()会将时间统一输出为补零的HH:mm格式,这正是配置项校验时用于规范化返回值的依据。

跨天时间段判断:结束时间小于起始时间视为次日

文档中特别强调:结束时间小于起始时间时将视为次日。例如18:006:00表示晚上 18:00 到次日 6:00。这一语义由isInRange方法实现:

isInRange(start: ScheduleTime, end: ScheduleTime) { if (start.equals(end)) { return false } let inRange = this.greaterThan(start) && this.lessThan(end) if (start.greaterThan(end)) { inRange = this.greaterThan(start) || this.lessThan(end) } const result = inRange || this.equals(start) return result }

(见 schedule/index.ts)

其判断逻辑可以拆解为三种情况:

场景判断方式示例
起始时间等于结束时间直接判定为不在范围内(false),避免全天恒开18:00~18:00
起始时间小于结束时间(当天内)当前时间必须同时大于起始、小于结束6:00~18:00
起始时间大于结束时间(跨天)当前时间大于起始小于结束,二者满足其一即可18:00~6:00

另外,无论哪种情况,只要当前时间恰好等于起始时间(this.equals(start)),也会被判定为在范围内,保证进入时间点能即时生效。这种"起始点闭区间、结束点开区间"的语义让跨天场景(如夜晚睡眠时段)无需区分日期即可正确工作。

自动切换的调度机制:checkTime 与定时器

组件的运行核心是checkTime函数,它完成三件事:判断当前是否处于时段内、同步夜间模式开关状态、安排下一次检查的定时器:

const checkTime = (settings: ComponentSettings<Options>) => { const start = new ScheduleTime(settings.options.range.start) const end = new ScheduleTime(settings.options.range.end) const now = new ScheduleTime() const useDarkMode = now.isInRange(start, end) const darkModeSettings = getComponentSettings('darkMode') if (darkModeSettings.enabled !== useDarkMode) { darkModeSettings.enabled = useDarkMode } let timeout = 0 if (useDarkMode) { timeout = ScheduleTime.millisecondsBefore(end) } else { timeout = ScheduleTime.millisecondsBefore(start) } if (timeout !== 0) { setTimeout(() => checkTime(settings), timeout) } }

(见 schedule/index.ts)

整个调度流程如下:

  1. 构造startendnow三个时间点,调用isInRange判断当前是否应处于夜间模式;
  2. 通过getComponentSettings('darkMode')获取「夜间模式」主组件的设置对象,若其enabled状态与判断结果不一致,则直接写入新的状态——这一写操作会触发夜间模式主组件的启用/卸载,进而完成主题切换;
  3. 依据当前所处状态,用millisecondsBefore计算出距离下一个切换点(结束时点或开始时点)的毫秒数,用setTimeout安排下一次checkTime调用,形成"检查—休眠—再检查"的循环;
  4. timeout === 0时不再递归调度(例如时间已精确到达切换点且无后续剩余时间),避免空转。

millisecondsBefore负责精确计算"距离目标时间还有多少毫秒",并在跨天时自动加上一整天:

static millisecondsBefore(time: ScheduleTime) { const now = new ScheduleTime() const nowSeconds = new Date().getSeconds() const currentMilliseconds = 1000 * (now.hour * 3600 + now.minute * 60 + nowSeconds) const targetMilliseconds = 1000 * (time.hour * 3600 + time.minute * 60) let result = targetMilliseconds - currentMilliseconds if (now.greaterThan(time) || (now.equals(time) && nowSeconds !== 0)) { result += 24 * 3600 * 1000 } return result }

(见 schedule/index.ts)

这里将时间统一折算为当天零时起的秒数再求差,若目标时间已过(或恰好等于但秒数不为零),则补上 24 小时,从而保证定时器总是指向"下一次"切换点而非过去的某个时刻。这种基于setTimeout而非轮询的设计,使组件在页面运行期间几乎不消耗额外资源。

与夜间模式主组件的协作关系

「夜间模式计划时段」本身并不直接修改页面样式,它只是调度器:真正的主题切换由「夜间模式」主组件(darkMode)完成。主组件的add/remove逻辑会执行以下动作(见 dark-mode/index.ts):

  • document.body添加/移除darkclass,这是所有暗色样式生效的总开关;
  • 写入localStoragepbp_theme_v4键(值为'b'表示暗色),保证主题状态可持久化;
  • 同步维护页面<meta name="theme-color"><meta name="color-scheme">标签,让浏览器地址栏配色与页面渲染模式与夜间模式保持一致。

主组件还提供了一项「提前注入」插件能力:在页面内容加载阶段就依据设置提前添加darkclass,减少首屏短暂白屏的闪烁问题(见 dark-mode/index.ts)。因此当计划时段组件通过写darkMode.enabled触发切换时,整个主题系统会按既有链路平滑生效。

使用注意事项

文档明确指出:请勿和「夜间模式跟随系统」一同使用。其原因是两个组件都以"自动决定darkMode.enabled"为职责,会互相覆盖对方的状态。对比两者实现:

  • 「夜间模式跟随系统」(darkModeFollowSystem)通过matchMedia('(prefers-color-scheme: dark)')监听操作系统的亮/暗主题,并在系统主题变化时同步切换(见 follow-system/index.ts);
  • 「夜间模式计划时段」则完全依据用户设定的时钟区间判断。

若同时启用,系统主题变化与时间区间会争夺开关控制权,导致夜间模式状态在两个来源间反复横跳。此外文档还提醒,在某些浏览器(如 Microsoft Edge)中「跟随系统」仅同步浏览器自身的亮/暗主题而非操作系统设置。因此实际使用时应二选一:追求"跟光线走"选跟随系统,追求"固定作息"选计划时段。

另一个需要留意的点是排除页面:darkExcludes列表(见 dark-urls.ts)排除了投稿中心、创作平台、直播剪辑、安全中心等页面,这些页面要么更新频繁难以及时适配,要么使用频率低,计划时段组件在这些页面不会执行自动切换。

实战配置示例

在 Bilibili-Evolved 的设置面板中启用「夜间模式计划时段」组件后,只需配置一个「时间段」选项即可:

  • 夜间作息(默认):起始18:00,结束6:00——适合默认配置,覆盖整个夜间;
  • 白天禁用:例如起始22:00,结束8:00,让深夜到清晨自动保持暗色;
  • 全天候亮色:将起始与结束设为同一时间(如0:00~0:00),isInRange会判定为恒不在范围内,夜间模式始终保持关闭。

配置输入会经过正则校验与ScheduleTime规范化,即使输入6:0也会被标准化为06:00后保存,无需担心格式问题。若想进一步了解夜间模式样式的编写规范、配色建议与排除列表维护方式,可阅读夜间模式模块的 README 及其对应的 index.md。

【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 16:51:19

第一性原理:技术架构决策与性能调优的底层思维

简介&#xff1a;《第一性原理思维模型与应用思考》是一份系统讲解第一性原理思维方法的 Word 文档&#xff0c;适合互联网从业者、产品经理、技术管理者&#xff0c;以及渴望突破传统思维框架的学习者。内容从量子力学与亚里士多德的原始定义出发&#xff0c;清晰对比演绎法与…

作者头像 李华
网站建设 2026/9/19 16:50:23

C语言循环结构调试与优化实战指南

简介&#xff1a;本资源是一份面向C语言初学者与编程入门学习者的循环结构专项训练题集&#xff0c;聚焦for、while、do...while三大循环语句的综合应用与典型场景实战。文档系统梳理10道经典编程题&#xff0c;涵盖奇数求和、阶乘计算、字符统计、闰年判断、乘法表打印、等腰三…

作者头像 李华
网站建设 2026/9/19 16:50:19

如何快速构建qmcdump解密工具:makefile与make install编译完整教程

如何快速构建qmcdump解密工具&#xff1a;makefile与make install编译完整教程 【免费下载链接】qmcdump 一个简单的QQ音乐解码&#xff08;qmcflac/qmc0/qmc3 转 flac/mp3&#xff09;&#xff0c;仅为个人学习参考用。 项目地址: https://gitcode.com/gh_mirrors/qm/qmcdum…

作者头像 李华
网站建设 2026/9/19 16:43:59

微信小程序接入TDesign:解决NPM packages not found报错全指南

说实话&#xff0c;我第一次在小程序项目里接入 TDesign 时&#xff0c;对着控制台里这行NPM packages not found愣是折腾了一整晚。工具是最新版&#xff0c;npm install也显示装好了&#xff0c;node_modules里明明躺着tdesign-miniprogram的目录&#xff0c;可编译跑起来就是…

作者头像 李华