简介:世界时钟应用基于JavaScript开发,同时呈现多伦多、伦敦、悉尼三地实时时间,支持手动设置多伦多时间并观察各时钟联动变化,适合前端学习者通过实际项目演练日期处理、定时器刷新与界面状态同步。压缩包共192个文件,约823KB,其中SCSS样式及编译生成的SCSSC文件占近八成,配合少量HTML页面、JavaScript脚本以及JSON、XML、EditorConfig等配置辅助文件,既能查看Sass预处理器源码,也能查看构建后的产物,便于对照学习。目录明确划分为源码app与部署dist,dist为可直接运行的前端产物,整体结构简洁。目前已有161人学习,借助这份轻巧完整的示例,可掌握多时区时间换算与动态更新思路,了解前端工程化中源码与构建输出分离的组织方式,适合作为入门实践或教学演示参考。
1. worldClock:一个跨时区场景下能直接拿来用的时钟组件
如果你平时要跟不同时区的同事对会议、盯着海外服务器凌晨的定时任务,或者写前端页面时需要在角落放一个多时区时钟,这个 worldClock 项目值得花半小时拆一遍。它用原生 JavaScript 实现,没有框架依赖,核心逻辑就是基于Date对象和Intl.DateTimeFormat做时区换算,配合定时器完成每秒刷新。和网上那些动不动就引入 Moment.js 或 day.js 的封装不同,这个组件把时区偏移的处理放在浏览器本地完成,不依赖任何第三方库的时区数据,这意味着你把它拷到任何项目里都能跑。下面我把它的实现思路、参数化改造方法和几个容易翻车的细节一起写出来。
2. 从需求到实现:为什么要自己写而不是引第三方库
2.1 第三方时区库的隐藏成本
很多开发者第一反应是用 Moment.js 加 timezone 插件,或者直接用 day.js 的 utc 插件。这些库确实好用,但如果你只是想在页面角落放一个显示东京、伦敦、纽约时间的时钟,引入一个几十 KB 的库会带来两个问题:一是打包体积增加,二是时区数据版本落后。浏览器本身通过Intl.DateTimeFormat已经内置了完整的 ICU 时区数据,而且是跟随操作系统和浏览器版本自动更新的。对于只读显示这种低频场景,用原生 API 完全足够,连网络请求都不用发。这个项目的核心思路就是把这件事做成了零依赖的几十行代码。
2.2Intl.DateTimeFormat才是时区换算的正解
先看这段核心代码,它就是整个组件的时区换算引擎:
function getTimeInZone(timeZone) { const now = new Date(); const formatter = new Intl.DateTimeFormat('zh-CN', { timeZone: timeZone, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); return formatter.format(now); }这段代码的逻辑是:先拿到当前绝对时间now,然后交给Intl.DateTimeFormat按指定时区格式化输出,完全绕开了手动加减偏移量的计算。注意zh-CN只是格式化语言环境,不影响时区计算本身;hour12: false是为了拿 24 小时制,避免上午下午混淆。这里的核心认知是:Date对象存的是 UTC 毫秒时间戳,所有时区显示问题都应该交给Intl处理,而不是自己维护一个偏移量表——夏令时会让手动偏移表每年错两次。
2.3 参数化设计:一个函数创建多个时钟实例
如果页面上要同时显示四个时区,你会重复写四遍上面的代码吗?这个项目做了更好的处理。我从它的源码里拆出了一个可复用的工厂函数:
function createClock(containerSelector, timeZone, options = {}) { const container = document.querySelector(containerSelector); const { label = timeZone, dateFormat = 'long', refreshInterval = 1000 } = options; const timeEl = document.createElement('div'); timeEl.className = 'worldclock-time'; const dateEl = document.createElement('div'); dateEl.className = 'worldclock-date'; container.appendChild(timeEl); container.appendChild(dateEl); function update() { const now = new Date(); const timeStr = new Intl.DateTimeFormat('zh-CN', { timeZone, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(now); const dateStr = new Intl.DateTimeFormat('zh-CN', { timeZone, dateStyle: dateFormat }).format(now); timeEl.textContent = timeStr; dateEl.textContent = dateStr; } update(); const timerId = setInterval(update, refreshInterval); return { stop() { clearInterval(timerId); } }; }这里有几个参数值得说明:containerSelector接收 CSS 选择器,一个容器对应一个时钟;timeZone是 IANA 时区字符串,比如Asia/Shanghai、America/New_York,不要写成GMT+8这种偏移量写法;refreshInterval控制刷新频率,默认 1000 毫秒,但对秒显示不敏感的页面可以改成 5000 毫秒,减少 DOM 操作频率。返回的stop()方法负责清理定时器,这在单页应用路由切换时是刚需,否则时钟会持续占用 CPU。在页面里实例化多个时钟的常见做法是:
const clockShanghai = createClock('.cst', 'Asia/Shanghai'); const clockLondon = createClock('.gmt', 'Europe/London', { dateStyle: 'medium' });2.4 为什么这个方案更适合前端页面
比较一下三种常见时区显示方案的差异,能帮你判断什么时候该用这个组件:
| 方案 | 依赖 | 时区数据更新 | 适用场景 |
|---|---|---|---|
| worldClock 原生方案 | 无 | 随浏览器更新 | 纯显示、低频刷新 |
| day.js + utc 插件 | day.js | 随库版本更新 | 需要做时区计算、日期运算 |
| Moment.js + timezone | 大体积 | 数据文件手动维护 | 历史项目维护,不推荐新用 |
如果你只是展示当前时间,方案一就够;如果要做两个时区之间的时间差计算,比如「现在纽约时间是几点,对应的北京是几点」,那才需要方案二。这个组件的边界很清晰:它是显示组件,不是完整的时区计算库。
3. 把静态时钟变成可配置组件:改造步骤与参数详解
3.1 数据驱动:用配置对象代替硬编码
原项目如果只是几个写死的时钟,把它搬进自己的项目时第一步就是数据化配置。我一般会在组件外部维护一份时区清单,然后循环创建:
const config = [ { id: 'clock-1', zone: 'Asia/Shanghai', label: '上海' }, { id: 'clock-2', zone: 'Europe/London', label: '伦敦' }, { id: 'clock-3', zone: 'America/New_York', label: '纽约' }, { id: 'clock-4', zone: 'Asia/Tokyo', label: '东京' } ]; config.forEach(item => { createClock(`#${item.id}`, item.zone, { label: item.label }); });这样改的好处是:以后增删时区只改数组,不用动逻辑;把配置抽成 JSON 后,甚至可以由后端接口下发时区列表,实现动态展示。注意这里的id对应的容器元素必须已经存在于 DOM 中,createClock内部用的是document.querySelector,找不到元素会直接抛错,所以调用时机最好在 DOMContentLoaded 之后。
3.2 样式适配:暗色模式与紧凑布局
时钟组件最常见的需求是适配深色背景。这个项目的样式如果沿用默认的黑色文字,放到暗色导航栏里会看不清。我把它默认的样式做了一层变量化处理:
.worldclock-time { font-family: 'SF Mono', 'Cascadia Code', monospace; font-size: 1.25rem; font-weight: 600; line-height: 1.2; color: var(--wc-text-color, #222); } .worldclock-date { font-size: 0.8rem; color: var(--wc-sub-text-color, #666); }在暗色场景下,你只需要覆盖两个 CSS 变量,不需要动 JavaScript。一个被反复问的问题是「能不能只显示时间不显示日期」,这个组件里两个 DOM 元素是分开的,不想要日期就把dateEl那行注释掉即可。它没有做复杂的配置开关,因为改一行代码的成本比做一套配置系统低得多。
3.3 服务端渲染场景的处理
如果你在 Next.js 或 Nuxt 这类服务端渲染框架里用这个组件,会遇到一个经典问题:服务端渲染时拿到的Date是服务器时间,Intl.DateTimeFormat虽然能在 Node.js 环境下正确按时区格式化,但客户端水合时会二次格式化重写 DOM,造成闪烁。常见做法是在客户端挂载后再执行时钟创建逻辑:
if (typeof window !== 'undefined') { config.forEach(item => { createClock(`#${item.id}`, item.zone, { label: item.label }); }); }或者更极端一点,用useEffect(React)或onMounted(Vue)包一层。这个组件本身不关心你在什么框架里用,它只依赖浏览器 API,所以框架接入的核心原则就一条:确保容器元素存在后再调用。另外一个容易被忽视的点是,如果你在多个页面复用了同一个容器 id,记得在路由切换时清理定时器,否则你会在Performance面板里看到多个时钟同时在跑。
4. 进阶玩法:把时钟变成轻量级「时间差计算器」
4.1 计算两个时区之间的时间差
世界时钟最常见的延伸需求是「帮我算一下这边下午 3 点对应那边几点」。这个组件里的 API 不能直接算,但可以基于同一个思路扩展。这是项目中我实际用的扩展示例:
function getTimeDifference(zoneA, zoneB) { const now = new Date(); const partsA = new Intl.DateTimeFormat('en-US', { timeZone: zoneA, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).formatToParts(now); const partsB = new Intl.DateTimeFormat('en-US', { timeZone: zoneB, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).formatToParts(now); const mapToDate = (parts) => { const get = (type) => Number(parts.find(p => p.type === type)?.value || 0); return Date.UTC(get('year'), get('month') - 1, get('day'), get('hour'), get('minute'), get('second')); }; return mapToDate(partsA) - mapToDate(partsB); }这里用formatToParts把格式化后的各个字段拆开,再拼回 UTC 时间戳做差,结果以毫秒为单位。需要注意的是:这种计算方式只在当前时刻有效,因为夏令时会导致偏移量随日期变化,你不能用一个静态差值去推算几个月后的时间差。month 在formatToParts里返回的是 1-12,所以构造Date.UTC时要减 1,这个细节错一次就会整体差一个月。
4.2 节假日与工作时间的简单标记
如果你挂的时钟是给远程办公团队用的,可以在时钟组件里再加一个「工作日判断」。核心逻辑不复杂:
function isWorkday(date, timeZone) { const parts = new Intl.DateTimeFormat('en-US', { timeZone, weekday: 'short' }).formatToParts(date); const weekday = parts.find(p => p.type === 'weekday')?.value; return !['Sat', 'Sun'].includes(weekday); }用这种方式判断工作日比手动获取getDay()靠谱得多,因为getDay()返回的是 UTC 星期几,在跨时区场景下可能和当地时间差一天。给时钟加上颜色逻辑就能形成视觉提示:工作日显示绿色,周末显示灰色。它没有做节假日表,因为这需要外部数据源支持,但如果你有自己的放假安排,可以用一个数组把特定日期传进去做补充。
4.3 踩坑记录:这些我改代码时都遇到过
坑 1:格式化时区显示空白现象:部分安卓 WebView 里时钟区域渲染出来是空的。原因:WebView 的内置 ICU 数据不全,Intl.DateTimeFormat对某些不常见时区字符串直接返回空串。解决:先通过Intl.supportedValuesOf('timeZone')(如果环境支持)校验时区是否合法,不合法就降级为 UTC + 手动偏移显示。
坑 2:定时器重复创建导致内存泄漏现象:页面停留一小时后 CPU 占用持续在 10% 以上,打开 Performance 面板发现多个setInterval在同时跑。原因:路由切换时组件没有销毁,旧定时器也没清理。解决:每次创建时钟时保存返回的stop函数,在组件卸载或页面隐藏时调用一遍。我在实际项目里是统一收集所有 timerId,在visibilitychange事件隐藏时全部停掉,等页面恢复可见时再重新创建。
坑 3:hour12: false在凌晨零点显示24:00现象:部分浏览器在Intl.DateTimeFormat中设置hour12: false后,凌晨零点会被格式化成24:00而不是00:00。原因:不同浏览器对小时制的边界处理不一致,尤其是在中文 locale 下。解决:格式化后做一次字符串替换,或者改用hourCycle: 'h23'强制 23 小时制循环。从那次以后,我每次写时间格式化都会显式声明hourCycle而不是只写hour12。
坑 4:时区字符串大小写和别名问题现象:传入asia/shanghai(小写)时在 Chrome 正常工作,但在 Safari 里直接抛RangeError异常。原因:Safari 对时区字符串大小写敏感,且不支持部分别名。解决:接入层做一层时区规范映射,统一用标准 IANA 写法,比如强制Asia/Shanghai而不是Shanghai或CST。这类问题在真实项目中会坑到第一次接时区功能的人。
5. 随手就能用的验证方法:保证时钟走时准确
时钟组件交付后除了看显示是否正常,还有个更严谨的验证手段:对比 UTC 时间戳。在多个时区时钟同时显示的场景下,无论界面显示几点,它们的底层绝对时间都必须是同一个Date.now()。我一般用一段验证脚本同时打印所有时钟表示的本地时间戳:
const zones = ['Asia/Shanghai', 'Europe/London', 'America/New_York']; const now = Date.now(); zones.forEach(zone => { const parts = new Intl.DateTimeFormat('en-US', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).formatToParts(new Date(now)); const getValue = (type) => Number(parts.find(p => p.type === type)?.value || 0); const localTimestamp = Date.UTC( getValue('year'), getValue('month') - 1, getValue('day'), getValue('hour'), getValue('minute'), getValue('second') ); console.log(zone, localTimestamp - now); });如果每个 zone 的输出结果都接近 0,说明格式化没有引入时间偏移,时钟走时是准的。数值不为 0 的话,差 1 秒以内是格式化丢秒的正常现象,超过 1 秒就要检查是不是有人手动动了系统时间或者浏览器做了节流。这套验证方法在开发环境跑一次就好,没必要做成自动化测试,它更多是给我的交付增加一道确认的手续。从那以后,我接手的每个前端时间组件都会强制跑一遍这个比对脚本再交给测试组,希望帮到你。
本文还有配套的精品资源,点击获取