Bilibili-Evolved 夜间模式计划时段:时间调度机制与源码实现解析
【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved
Bilibili-Evolved 是一款开源的哔哩哔哩增强脚本,其中的「夜间模式计划时段」组件允许用户设定一个时间范围,让夜间模式在该时间段内自动开启、离开该时间段后自动关闭,从而避免手动反复切换主题。本文以该组件的说明文档(schedule/index.md)为主体,结合其 TypeScript 实现 与「夜间模式」主组件源码,完整讲解配置方法、跨天时间判断逻辑、自动切换调度原理,以及与其他夜间模式组件的配合与注意事项。
功能概述:按时间段自动开关夜间模式
「夜间模式计划时段」(darkModeSchedule)是 Bilibili-Evolved 夜间模式体系下的一个调度型组件。它的核心能力是:
- 用户指定一个时间段(如
18:00至6:00); - 当系统时间进入该时间段时,自动开启夜间模式;
- 当系统时间离开该时间段时,自动关闭夜间模式。
从源码中的组件定义可以看出,该组件归属于style与general两个标签分类,注册名为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:00至6:00(晚上 18 点到次日早上 6 点) |
| 时间格式 | HH:mm或H: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:00至6: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)
整个调度流程如下:
- 构造
start、end、now三个时间点,调用isInRange判断当前是否应处于夜间模式; - 通过
getComponentSettings('darkMode')获取「夜间模式」主组件的设置对象,若其enabled状态与判断结果不一致,则直接写入新的状态——这一写操作会触发夜间模式主组件的启用/卸载,进而完成主题切换; - 依据当前所处状态,用
millisecondsBefore计算出距离下一个切换点(结束时点或开始时点)的毫秒数,用setTimeout安排下一次checkTime调用,形成"检查—休眠—再检查"的循环; 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,这是所有暗色样式生效的总开关; - 写入
localStorage的pbp_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),仅供参考