Skip to content

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');     // 时区偏移如 +0800

2.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偏移
UTC0
Asia/Shanghai+08:00(北京时间)
Asia/Tokyo+09:00
America/New_York-05:00 / -04:00(夏令时)
Europe/London0 / +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 dayjs
javascript
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-tz
javascript
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 风格时区支持推荐
moment290 KB链式 + 可变对象内置(moment-timezone)⚠️ 维护模式,老项目
Day.js7 KB链式 + 不可变插件✅ 新项目首选
date-fns按需函数式date-fns-tz✅ 函数式偏好
luxon22 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.jsdate-fns
  • API 返回 ISO 8601 字符串(带 Z 后缀),客户端按本地时区格式化
  • moment 是可变对象,多次复用要 .clone() 否则会出 bug