微信小程序世界时间:moment-timezone时区转换实战
2026/9/14 22:22:44 网站建设 项目流程

简介:微信小程序世界时间设置示例源码是一套面向初中级小程序开发者的实践项目,旨在演示如何构建全球多城市时间查询与设置功能,从而掌握小程序从页面搭建到逻辑处理的基本流程。压缩包共9个文件,含5个js、2个json、1个wxml、1个wxss,大小仅39KB,其中js负责业务逻辑与工具函数,json配置全局与页面参数,wxml和wxss分别完成界面结构与样式。项目基于moment及moment-timezone实现时区换算,并演示了数据绑定、生命周期函数、点击事件等核心知识,也适合作为学习模块化与工具库用法的参考。目前已有202人学习,代码体量小、结构清晰,可帮助开发者快速理解并改造出自己的世界时钟小程序。

1. 世界时间小程序:为什么我放弃了系统 API 而选择 moment-timezone

做微信小程序时遇到一个真实需求:界面要同时显示纽约、东京、伦敦的实时时间,并且不同设备看到的结果必须一致。设备本地时间拿到的是“这一刻”,但用户手机可能是北京时间、美东时间或 UTC,直接用new Date()再算时区偏移,代码立刻变成一团浆糊。尝试用微信自带的wx.getSystemInfoIntl.DateTimeFormat,发现小程序的 JavaScript 内核在不同平台上对Intl支持不一致,有的直接报错,有的返回结果不正确。后来拆这个“世界时间设置 demo”的源码,发现它采用了moment.jsmoment-timezone.js这两个库,把所有时区转换问题全部变成了数据查询,逻辑清晰很多。这篇文章不是泛泛介绍小程序目录结构,而是围绕时区数据如何在小程序里加载、计算和刷新,把 demo 的核心代码逐一拆开,适合想在小程序里做多时区、排期、日历功能的开发者。

2. 先拆项目骨架:app.json、pages 和依赖库的组织方式

2.1 小程序全局配置与页面注册

打开 demo 源码,先看根目录的app.json,这是小程序的入口配置。所有页面必须在pages字段中注册,否则无法访问。同时,window字段定义了导航栏样式和背景色。在这个 demo 里,核心页面是pages/index,其余页面未必很多,但app.json中的配置决定了后续代码的加载方式。

{ "pages": [ "pages/index/index" ], "window": { "navigationBarTitleText": "世界时间", "navigationBarBackgroundColor": "#1f1f1f", "navigationBarTextStyle": "white" } }

pages数组中的第一项就是小程序启动后展示的第一页。这里只有一个index页面,说明 demo 的业务逻辑全部集中在pages/index下。navigationBarTitleText是顶部导航栏的标题,navigationBarBackgroundColornavigationBarTextStyle控制标题栏颜色,把背景设为深色、文字设为白色,是为了配合世界时间这种跨时区场景的视觉效果。如果后续要增加“城市管理”页面,需要手动在pages里追加路径,否则跳转时会提示页面不存在。

2.2 moment-timezone 在小程序里的引入方式

微信小程序不像普通 Node.js 项目,直接把moment-timezone安装到node_modules还不够。常见做法有两种:一种是通过开发者工具的“构建 npm”功能,然后使用importrequire;另一种是直接把moment.jsmoment-timezone.js文件复制到utils目录,再用相对路径引入。demo 采用的是后一种方式,因为在小程序包上传时必须显式看到文件,而且依赖版本可控,不需要每次在开发者工具里点“构建 npm”。

引入方式优点缺点适用场景
开发者工具构建 npm版本管理清晰,更新方便每次改依赖要重新构建,代码压缩后调试栈不友好团队项目、依赖较多
直接复制 js 文件到 utils代码一目了然,打包体积可控手动更新库文件小型 demo、单个页面
从插件市场引入封装库开箱即用黑盒,出现问题难定位原型验证、非核心逻辑

我一般建议先把moment.min.jsmoment-timezone-with-data.min.js放进utils,因为moment-timezone的完整数据文件体积不小,直接引用完整文件会让小程序主包接近 2MB,但只要不涉及分包,demo 场景还能接受。引入方式如下:

const moment = require('../../utils/moment.min.js'); const momentTZ = require('../../utils/moment-timezone-with-data.min.js');

require的路径要从当前文件出发,pages/index/index.jsutils目录要回退两级,所以是../../utils。第一个require加载moment核心库,第二个require加载时区数据,并且它也会在内部绑定到同一个moment对象上,所以后面直接用moment.tz()即可。这里有个容易被忽略的细节:必须先引入moment.min.js,再引入moment-timezone-with-data.min.js,顺序反了会抛出moment.tz is not a function

2.3 封装时区查询工具

为了让页面代码不直接依赖moment-timezone,demo 一般会在utils下单独放一个timezone.js,把“根据时区名称获取格式化时间”的逻辑集中管理。这样页面里只需要调用一个函数,后续改时间格式也只需改一处。

const momentTZ = require('./moment-timezone-with-data.min.js'); function formatTimeByZone(timeZone, pattern) { if (!momentTZ.tz.zone(timeZone)) { throw new Error('Unsupported time zone: ' + timeZone); } return momentTZ().tz(timeZone).format(pattern || 'YYYY-MM-DD HH:mm:ss'); } module.exports = { formatTimeByZone: formatTimeByZone };

formatTimeByZone接收两个参数:timeZone是 IANA 时区名称,比如America/New_Yorkpatternmoment的格式化模板,默认输出“年-月-日 时:分:秒”。momentTZ.tz.zone(timeZone)用来检查时区名是否真实存在,避免拼写错误导致运行时异常。注意这里调用的是momentTZ(),也就是当前时刻,没有传时间戳,因为显示世界时间必须永远基于“现在的瞬间”,而不是某个写死的日期。如果需要按下某个城市后固定某一时刻的时间,可以传入时间戳或日期字符串,但 demo 里的核心场景是实时刷新,所以默认取当前时刻。

2.4 时区数据裁剪的取舍

moment-timezone-with-data.min.js包含了全球上千个时区,但真实业务用到的可能只有十几个城市。如果执着于包体大小,可以只保留一部分时区数据,用moment.tz.add手动添加城市定义。但这个 demo 没有这么做,原因是“世界时间”天然需要覆盖全世界的主要城市,一旦裁剪掉某个冷门城市,用户切换时就会直接报错。折中方案是:把城市列表限制在 20 个以内,并只加载这些城市对应的时区数据,体积能从几百 KB 降到几十 KB。如果后续要发布正式版,建议单独抽一个timezone-data.js,在构建时按城市列表动态生成。这里我常用 Node 脚本读取一个 JSON 城市表,再调用moment.tz.zone采集数据,最后拼接成一个精简版时区数据文件,但 demo 中的完整数据文件更适合作为学习时的对照基准。

3. 核心逻辑:时区数据加载、时间格式与数据绑定

3.1 城市列表与时区映射

打开pages/index/index.js,数据源不再是硬编码的UTC+8这种偏移量,而是一个城市对象数组。每个对象包含三个字段:显示名称、IANA 时区名、以及初始化时用到的默认城市标记。城市名称用于界面展示,时区名交给moment.tz计算。

const timezoneUtil = require('../../utils/timezone.js'); Page({ data: { cityList: [ { name: '北京', zone: 'Asia/Shanghai' }, { name: '东京', zone: 'Asia/Tokyo' }, { name: '纽约', zone: 'America/New_York' }, { name: '伦敦', zone: 'Europe/London' } ], timeList: [], currentZone: 'Asia/Shanghai', currentTime: '' }, onLoad() { this.updateTimes(); this.timer = setInterval(() => { this.updateTimes(); }, 1000); }, updateTimes() { const now = Date.now(); const timeList = this.data.cityList.map(item => { return { name: item.name, zone: item.zone, time: timezoneUtil.formatTimeByZone(item.zone, 'YYYY-MM-DD HH:mm:ss') }; }); this.setData({ timeList: timeList, currentTime: timezoneUtil.formatTimeByZone(this.data.currentZone, 'HH:mm:ss') }); } });

now变量虽然在这个代码片段中没有直接传给formatTimeByZone,但它代表了一个关键设计:同一颗时间戳必须在一次刷新中共享,避免不同城市之间因为调用时间差出现秒级偏差。实际项目中,我会把formatTimeByZone改成接收第二个参数timestamp,然后统一用这一毫秒值去格式化所有城市。setInterval设定为 1 秒一次,是因为界面要展示“秒针”,如果只显示到分钟,把间隔改成 30000 毫秒即可,省电且减少setData开销。currentTime是单独给页面顶部大号数字用的,它和列表用的格式化模板不同,列表需要完整日期,顶部只需要时分秒。

3.2 wxml 渲染与 wxs 格式化边界

index.wxml负责把timeList渲染成一组卡片。微信小程序的插值表达式做不了复杂函数调用,所以不能在{{}}里写moment(),只能把格式化结果预先放在timeListtime字段中。这是 iOS 和 Android 端统一表现的关键,因为不同端的 JavaScript 引擎对Date.prototype.toLocaleString生成结果不一致,但字符串拼接永远不会出错。

<view class="container"> <view class="current-time">{{currentTime}}</view> <view class="city-grid"> <block wx:for="{{timeList}}" wx:key="zone"> <view class="city-card" bindtap="onSelectCity">onHide() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }, onUnload() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }

清理定时器后,如果小程序只是切到后台再回到前台,onShow阶段要重新启动定时器并立刻更新一次时间,否则界面时间会停留在后台时刻。这一块很多新手会漏掉,我一般会抽成一个公共mixin,给所有需要实时时间的页面复用。微信小程序支持Behavior,把定时器管理逻辑放进去就够了。

4. 实战:动态切换城市、自动刷新与夏令时处理

4.1 点击卡片切换当前城市

demo 的交互不复杂,点击任意城市卡片后,顶部大号时间切换成该城市的当前时间。实现方法是在 3.1 节预留的onSelectCity事件处理函数中处理。

onSelectCity(e) { const zone = e.currentTarget.dataset.zone; if (!zone) return; this.setData({ currentZone: zone, currentTime: timezoneUtil.formatTimeByZone(zone, 'HH:mm:ss') }); }

e.currentTarget.dataset.zone能取到 wxml 里>const momentTZ = require('../../utils/moment-timezone-with-data.min.js'); const winterDate = '2024-01-15 12:00:00'; const summerDate = '2024-07-15 12:00:00'; const offsetWinter = momentTZ.tz(winterDate, 'America/New_York').utcOffset(); const offsetSummer = momentTZ.tz(summerDate, 'America/New_York').utcOffset(); console.log(offsetWinter); // 300 分钟,即 UTC-5 console.log(offsetSummer); // 240 分钟,即 UTC-4

utcOffset()返回的是分钟数,不是小时数,这点极容易把人绕晕。300 分钟等于 5 小时,也就是说 1 月时纽约比 UTC 慢 5 小时,7 月慢 4 小时。不要自己去写+8-5这种便宜逻辑,否则一份维护两年的代码,总有一天会遇到“欧洲在夏令时切换当天少了一小时”的线上问题。moment.tz还支持zone.abbr(timestamp)来获取缩写名,比如ESTEDT,在 UI 上展示会比单纯偏移量更友好。需要注意zone.abbr在不同环境下可能返回不同格式,紧凑型数据甚至可能返回空字符串,所以我对abbr的惯例是优先展示城市名,把缩写作为次要信息。

4.3 最小化 setData 与请求频控

一秒一次的setData会不断触发视图层 diff,如果页面存在地图组件,频繁更新会造成明显卡顿。更合理的做法是把秒级刷新只应用到顶部currentTime,城市列表的时间改为每 30 秒到 60 秒刷新一次。

updateListTimes() { const timeList = this.data.cityList.map(item => ({ name: item.name, zone: item.zone, time: timezoneUtil.formatTimeByZone(item.zone, 'HH:mm:ss') })); this.setData({ timeList }); }, updateCurrentTime() { this.setData({ currentTime: timezoneUtil.formatTimeByZone(this.data.currentZone, 'HH:mm:ss') }); }

这里拆成两个函数后,定时器里可以分别设置:一个每秒调用updateCurrentTime,另一个每隔 30 秒调用updateListTimes。如果城市列表只有三四个城市,每秒全量更新其实也不明显,但往往 demo 会预留几十个城市,这时候每秒钟做几十次时间格式化,再通过setData传几十个对象,CPU 占用会直线上升。另一个优化是硬件时钟校准:小程序的Date.now()来自设备系统,用户手动改系统时间会直接影响结果。如果需要可信时间,可以在onShow时调用一款时间校准接口,把本地时间与服务器时间的偏移量缓存起来,后续计算都用“本地时间 + 偏移量”合成伪时间戳。这种方案我不建议在 demo 中使用,因为引入网络请求会掩盖时区库本身的学习重点,但正式项目里几乎必做。

5. 进阶验证:用时间戳快照校验 demo 的时区计算结果

5.1 用 Node.js 对拍关键时区

demo 在小程序里运行逻辑没问题,但每次都要打开开发者工具肉眼观察,效率太低。我习惯在命令行用 Node.js 跑一份同样的moment-timezone,把固定时间戳下的格式化结果和小程序端打印的日志做对拍。先在项目根目录创建verify.js

const momentTZ = require('./utils/moment-timezone-with-data.min.js'); const timestamps = [ { label: 'DST开始前', time: '2024-03-10 06:59:00' }, { label: 'DST开始后', time: '2024-03-10 07:00:00' } ]; timestamps.forEach(({ label, time }) => { const ny = momentTZ.tz(time, 'America/New_York').format('YYYY-MM-DD HH:mm:ss'); const bj = momentTZ.tz(time, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss'); console.log(`${label}: 纽约=${ny}, 北京=${bj}`); });

运行命令node verify.js,观察两个边界时刻的换算结果。2024 年 3 月 10 日是美国转入夏令时的时刻,纽约本地时间从 2 点直接跳到 3 点,所以 07:00 UTC 时纽约已经是 3 点,用 Node 跑出的结果应该是 03:00,而非 02:00。小程序端如果也用同一份数据文件,结果必然一致,因此一旦有误差,问题就出在小程序环境对Date的解析上。

5.2 在开发者工具中验证设备时区无关性

这个 demo 的核心目标是“无论手机设置成哪个时区,显示的城市时间都不变”,所以验证时必须切换设备时区。在微信开发者工具的“普通编译”下拉菜单里,可以自定义设备参数,把系统时区改为“美国东部”,再编译预览,观察顶部北京时间的卡片是否依然按Asia/Shanghai计算。如果时间显示变成了设备当地时间,说明代码里混入了new Date()而没有走moment.tz去显式指定时区。另一种更快的验证是直接修改电脑系统时间,再重进小程序,看currentTime是否等于城市时区的时间。这个方法只建议在 demo 阶段使用,因为小程序在真机上读取的是手机系统时间,开发者工具模拟器与实际手机在时区数据上完全一致,但Intl的行为有差异,所以重点验证moment.tz路径即可。

最后补充一个可复用的小技巧:在utils/timezone.js中增加一个getTimeStamp函数,统一提供当前可信时间戳,所有计算都改用它。getTimeStamp内部定义一个本地模块级变量,在onShow时同步一次系统时间,之后每秒增加 1000,而不是每次调用Date.now()。这样做的好处是,即使某些安卓机型在低功耗模式下Date.now()出现跳变,显示的世界时间依然保持平滑递增,不会出现明显回拨或跳秒。一个小改动,能让 demo 从“能跑”变成“真机体验良好”。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询