Node系列 · ORM:moment 与时区
时间处理是后端的常见地雷:服务器存的是 UTC,但用户在不同时区看到的"今天"不一样。本章讲清楚 UTC 与时区的关系、moment 库的正确用法、以及"服务器存 UTC、客户端按本地时区展示"的推荐架构。
一、UTC 与时区基础
1.1 UTC 是什么
UTC(Coordinated Universal Time) 是世界时间标准,以英国格林威治(GMT)为基准。
- UTC 0 点 = 英国格林威治当地时间的 0 点
- 北京时间是 UTC+8,比 UTC 早 8 小时
UTC 时间 + 8 小时 = 北京时间
1.2 为什么推荐存 UTC
| 方案 | 优点 | 缺点 |
|---|---|---|
| 存 UTC 时间戳 | 不同时区统一,跨时区协作无歧义 | 显示时需要转换 |
| 存本地时间字符串 | 显示直观 | 跨时区错乱,DST(夏令时)处理复杂 |
| 存带时区的 ISO 8601 | 信息完整 | 不同数据库支持不一致 |
TIP
存 UTC,显示按本地时区——这是业界最佳实践。数据库存 DATETIME(不带时区)或 BIGINT 时间戳,应用层负责转换。
二、moment 库
bash
npm install moment moment-timezone中文网:momentjs.cn
2.1 创建时间对象
javascript
const moment = require('moment');
// 当前时间(系统时区)
moment();
moment(); // 两次不一样
// 字符串解析
moment('2024-08-15'); // ISO 格式
moment('2024-08-15 14:30:00', 'YYYY-MM-DD HH:mm:ss'); // 自定义格式
moment('15/08/2024', 'DD/MM/YYYY'); // 显式指定格式
// 时间戳
moment(1723698600000); // ms 时间戳
moment.unix(1723698600); // s 时间戳
// 对象
moment({ year: 2024, month: 7, day: 15, hour: 14, minute: 30 });
// 注意:month 是 0-11(0=1月)2.2 格式化输出
javascript
const m = moment('2024-08-15 14:30:45');
m.format('YYYY-MM-DD HH:mm:ss'); // '2024-08-15 14:30:45'
m.format('YYYY年MM月DD日'); // '2024年08月15日'
m.format('YYYY/MM/DD dddd'); // '2024/08/15 Thursday'
m.format('a h:mm:ss'); // 'pm 2:30:45'
// 常用格式符
m.format('YYYY'); // 4 位年
m.format('MM'); // 2 位月
m.format('DD'); // 2 位日
m.format('HH'); // 24 小时制
m.format('hh'); // 12 小时制
m.format('mm'); // 分钟
m.format('ss'); // 秒
m.format('SSS'); // 毫秒
m.format('A'); // AM / PM
m.format('a'); // am / pm
m.format('Z'); // 时区偏移如 +08:00
m.format('ZZ'); // 时区偏移如 +08002.3 时区转换
javascript
const moment = require('moment-timezone');
// 系统时区时间(北京时间)
const beijingTime = moment('2024-08-15 14:30:00');
// 假设系统时区是 Asia/Shanghai
// 转为 UTC
const utcTime = beijingTime.utc();
utcTime.format(); // '2024-08-15T06:30:00Z'
// 转为纽约时间
const nyTime = beijingTime.tz('America/New_York');
nyTime.format(); // '2024-08-14T22:30:00-04:00'
// 列出所有时区
moment.tz.names().slice(0, 5);
// ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ...]| 时区 ID | 偏移 |
|---|---|
UTC | 0 |
Asia/Shanghai | +08:00(北京时间) |
Asia/Tokyo | +09:00 |
America/New_York | -05:00 / -04:00(夏令时) |
Europe/London | 0 / +01:00(夏令时) |
三、推荐架构:UTC + 客户端本地化
3.1 服务器端
javascript
// ✅ 数据库统一存 UTC(DATETIME 不带时区,按 UTC 解读)
const createdAt = moment.utc().format('YYYY-MM-DD HH:mm:ss');
// '2024-08-15 06:30:00'
// ✅ API 返回 ISO 8601 带 Z 后缀
res.json({
createdAt: moment.utc().toISOString(), // '2024-08-15T06:30:00.000Z'
});
// ❌ 错误:返回本地时间字符串
moment().format(); // '2024-08-15 14:30:00' ← 看不出时区3.2 客户端
javascript
// 拿到服务器返回的 UTC 时间字符串
const utcTime = '2024-08-15T06:30:00.000Z';
// 客户端按本地时区显示
const local = new Date(utcTime).toLocaleString('zh-CN');
// '2024/8/15 14:30:00'(北京时间用户看到的)
// 或用 Intl.DateTimeFormat 控制格式
new Intl.DateTimeFormat('zh-CN', {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'Asia/Shanghai',
}).format(new Date(utcTime));
// '2024年8月15日 14:30'TIP
服务器只负责发 UTC 时间戳,所有时区转换在客户端做。这样:
- 全球用户各自看到本地时间
- 服务器逻辑简单(永远用 UTC)
- 数据库统一(同一种格式)
- DST / 夏令时问题由浏览器/OS 处理
3.3 表单录入:接收 ISO 字符串
javascript
// 前端表单提交时,把本地时间转 UTC
const localDate = '2024-08-15 14:30:00';
const utcDate = new Date(localDate).toISOString();
await api.post('/events', { startAt: utcDate });四、moment 的局限
4.1 不可变 vs 可变
javascript
const m = moment('2024-08-15');
m.add(1, 'day');
console.log(m.format('YYYY-MM-DD')); // '2024-08-16'
// ⚠️ moment 是可变的!多次复用会出 bug
const dates = ['2024-08-15', '2024-08-16', '2024-08-17'];
const formatted = dates.map((d) => moment(d).add(1, 'day').format('YYYY-MM-DD'));
// 正确4.2 维护状态
WARNING
moment 已进入维护模式,作者建议迁移到其他库。新项目不要首选 moment。
五、替代方案
5.1 Day.js(轻量级)
bash
npm install dayjsjavascript
const dayjs = require('dayjs');
// API 与 moment 几乎一致
dayjs('2024-08-15').format('YYYY-MM-DD');
// 时区插件
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs('2024-08-15 14:30:00').tz('Asia/Shanghai').format();
// '2024-08-15T14:30:00+08:00'体积仅 7 KB(moment 是 290 KB),API 兼容 moment,是推荐的替代方案。
5.2 date-fns(函数式)
bash
npm install date-fns date-fns-tzjavascript
const { format, addDays } = require('date-fns');
const { zonedTimeToUtc, utcToZonedTime, format } = require('date-fns-tz');
format(new Date(), 'yyyy-MM-dd HH:mm:ss');
addDays(new Date(), 7);
// 时区
const shanghaiTime = utcToZonedTime(new Date(), 'Asia/Shanghai');
format(shanghaiTime, 'yyyy-MM-dd HH:mm:ssXXX', { timeZone: 'Asia/Shanghai' });函数式 API,每个功能独立 import,更利于 tree-shaking。
5.3 选型对照
| 库 | 大小 | API 风格 | 时区支持 | 推荐 |
|---|---|---|---|---|
| moment | 290 KB | 链式 + 可变对象 | 内置(moment-timezone) | ⚠️ 维护模式,老项目 |
| Day.js | 7 KB | 链式 + 不可变 | 插件 | ✅ 新项目首选 |
| date-fns | 按需 | 函数式 | date-fns-tz | ✅ 函数式偏好 |
| luxon | 22 KB | 不可变对象 | 内置 | ✅ 时区 / 国际化重需求 |
六、最佳实践
| 场景 | 推荐 |
|---|---|
| 服务器存时间 | 永远存 UTC(DATETIME 或 BIGINT 时间戳) |
| API 返回 | ISO 8601 字符串带 Z 后缀 |
| 时区转换 | 客户端按本地时区展示 |
| 库选型 | 新项目用 Day.js / date-fns |
| 解析用户输入 | new Date(input).toISOString() 转 UTC |
| 表单时间选择器 | 用户选本地时间 → 转 UTC → 提交 |
七、小结
- 存 UTC、显示本地——后端最稳的时间处理架构
- UTC 是世界时间标准;中国是 UTC+8,比 UTC 早 8 小时
- moment 库 API 友好但已进入维护模式;新项目用 Day.js 或 date-fns
- API 返回 ISO 8601 字符串(带
Z后缀),客户端按本地时区格式化 - moment 是可变对象,多次复用要
.clone()否则会出 bug
