1. 开始之前先搞清楚你到底要哪种“当前时间”在 Vue 项目里拿到当前日期时间表面看就是一行代码的事但实际开发里我见过太多人在这里翻车。不是拿到的格式不对就是时区差了几个小时或者列表渲染时时间不刷新甚至生产环境打包后报错。所以动手之前先把需求拆清楚。你是要一次性显示当前时间比如页面标题下的“最后更新2025-07-02 14:30:00”还是页面上有个时钟每秒钟自动走秒或者是要传给后端做参数要求 ISO 8601 格式还是只要日期不要时间要不要做时区转换不同诉求写法完全不同。下面我从最基础的new Date()讲起再到moment.js最后说项目里怎么选、怎么封装、怎么避坑。这篇东西你照着做基本不会再被时间处理绊住。先说结论如果只是取当前时间并格式化成“YYYY-MM-DD HH:mm:ss”直接用原生Date加一个手写格式化函数就够了完全没必要引moment.js。如果项目里已经有moment或者你涉及大量时间运算、国际化、时区转换那再用moment也不迟。我先把两种方案的代码都给你后面再讲原理和取舍。2. 方案一原生 new Date() 最轻量2.1 一行代码获取当前时间对象这是最核心的一步const now new Date(); console.log(now); // 输出类似Thu Jul 02 2025 14:30:00 GMT0800 (中国标准时间)new Date()不传参数时会返回一个Date对象内部存储的是“从 1970-01-01 00:00:00 UTC 到当前时刻的毫秒数”。注意这个关键点它存储的是 UTC 时刻不是本地时间。打印这个对象时浏览器会调用toString()自动把 UTC 时间转成你电脑所在时区的本地时间所以你会看到GMT0800这样的字样。如果你在中国拿到的是东八区时间。这就是为什么有些同学在后端接口里拿到的时间字段前端new Date(str)后再打印发现比预期早了 8 小时——因为后端给的是 UTC 时间而你的本地时区是 UTC8。2.2 从 Date 对象里提取年月日时分秒Date对象提供了一组 getter 方法用来提取各时间部分const now new Date(); const year now.getFullYear(); // 2025 const month now.getMonth() 1; // 注意月份从 0 开始所以要 1 const day now.getDate(); // 2 const hours now.getHours(); // 14 const minutes now.getMinutes(); // 30 const seconds now.getSeconds(); // 0 const milliseconds now.getMilliseconds(); // 0这里最坑的就是getMonth()它返回的是 0 到 11而不是 1 到 12。很多新手第一次写就卡在这// 错误示范 const month now.getMonth(); // 5月会被取成 56月会被取成 6导致月份永远少 1正确写法是const month now.getMonth() 1;然后再拼成你想要的格式const now new Date(); const year now.getFullYear(); const month String(now.getMonth() 1).padStart(2, 0); const day String(now.getDate()).padStart(2, 0); const hours String(now.getHours()).padStart(2, 0); const minutes String(now.getMinutes()).padStart(2, 0); const seconds String(now.getSeconds()).padStart(2, 0); const dateTimeString ${year}-${month}-${day} ${hours}:${minutes}:${seconds}; console.log(dateTimeString); // 2025-07-02 14:30:00padStart(2, 0)的作用是补零。如果月份是 7字符串就是7补零后变成07。这一步必须有否则你会得到2025-7-2 14:30:0这种格式前后端口径对不上排序也会出问题。2.3 封装成公共函数别每次手写如果只是偶尔用一两次直接在页面里写没问题。但我还是建议在一开始就封装成一个公共方法放src/utils/date.js里/** * 格式化日期时间 * param {Date|string|number} date 日期对象、时间字符串或时间戳可选默认为当前时间 * param {string} fmt 目标格式默认 YYYY-MM-DD HH:mm:ss * returns {string} 格式化后的字符串 */ export function formatDate(date new Date(), fmt YYYY-MM-DD HH:mm:ss) { const d date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) { console.error(Invalid date:, date); return ; } const pad (n) String(n).padStart(2, 0); const map { YYYY: d.getFullYear(), MM: pad(d.getMonth() 1), DD: pad(d.getDate()), HH: pad(d.getHours()), mm: pad(d.getMinutes()), ss: pad(d.getSeconds()) }; return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) map[match]); }用法import { formatDate } from /utils/date; // 当前时间 formatDate(); // 2025-07-02 14:30:00 // 自定义格式 formatDate(new Date(), YYYY/MM/DD); // 2025/07/02 // 传时间戳 formatDate(1719896400000, YYYY-MM-DD HH:mm); // 2025-07-02 14:30这个封装好处是以后不管哪个页面要用一行 import 就行不用到处复制粘贴。而且它还做了非法日期校验避免后端传了个undefined或null进来时页面直接崩掉。2.4 在 Vue 模板里直接用如果你只想在模板里快速打印一行时间可以这样template div当前时间{{ currentTime }}/div /template script setup import { ref, onMounted } from vue; import { formatDate } from /utils/date; const currentTime ref(formatDate()); /script这里我把formatDate()的结果赋给了响应式变量currentTime页面初始化时渲染一次。要注意这个时间不会自动更新除非你重新赋值。如果页面挂在那里一小时显示的还是进入页面那一刻的时间。2.5 动态显示实时时钟每秒更新如果你要的是一个会跳动的时钟那就需要setIntervaltemplate div{{ currentTime }}/div /template script setup import { ref, onMounted, onUnmounted } from vue; import { formatDate } from /utils/date; const currentTime ref(formatDate()); let timer null; onMounted(() { timer setInterval(() { currentTime.value formatDate(); }, 1000); }); onUnmounted(() { clearInterval(timer); }); /script这里有两个要点间隔设为 1000 毫秒也就是一秒刷新一次。如果你用setInterval做精准计时比如计算“从开始到现在过了多少秒”那误差会累积。因为 JS 是单线程的定时器可能因为主线程被占用而延后执行。如果只是显示时钟一秒刷新一次完全够用。组件销毁时一定要clearInterval否则定时器会一直跑。如果页面已经销毁但定时器还在执行就会报“内存泄漏”警告严重时页面会越来越卡。很多新手忘了清理切了几个页面后浏览器风扇就开始狂转。还有一种写法是用computed但注意computed只在响应式依赖变化时重新计算new Date()不是响应式的所以computed在这里并不会自动更新。正确做法是像上面那样用refsetInterval。2.6 原生 Date 的常见坑位原生Date写起来要自己处理好多边界我列几个最常见的问题坑 1月份从 0 开始。前面说过了getMonth()返回 0-11必须 1。坑 2new Date(2025-07-02 14:30:00)在 iOS Safari 上可能解析失败。iOS 上对带横杠的时间字符串解析兼容性有问题建议把横杠替换成斜杠或者传new Date(2025/07/02 14:30:00)或者统一传时间戳。坑 3时间戳单位是毫秒。如果你从接口拿到一个类似1719896400的 10 位数字那是秒new Date()直接传会很离谱相当于 1970 年正确的要乘 1000new Date(1719896400 * 1000);坑 4时区不一致。new Date()的getHours()返回的是本地时间如果要拿 UTC 时间得用getUTCHours()。做国际业务时别搞混。这些坑不是吓唬你都是实际开发中一行一行调出来的教训。3. 方案二moment.js 来处理复杂时间逻辑3.1 为什么很多人喜欢用 momentmoment.js是前端圈子里最老牌、最流行的时间处理库。它的核心价值不是“取当前时间”而是强大的格式化、解析、运算和国际化能力。比如你要获取“本月最后一天”计算两个时间相差多少天全局切换语言中文、英文、日文解析各种奇怪格式的日期字符串这些用原生Date自己写很痛苦moment一行就能搞定。3.2 安装与引入npm install moment在组件里引入import moment from moment; const now moment(); console.log(now.format(YYYY-MM-DD HH:mm:ss)); // 2025-07-02 14:30:00moment()不传参数表示当前时间。它返回的是一个moment对象直接打印会看到一堆内部属性比如_d、_i等不要慌你只需要用.format()转成字符串。3.3 在 Vue 2 项目里注册为全局方法如果你是 Vue 2 项目Vue 3 不推荐这样做后面会说可以在main.js里挂到原型上import Vue from vue; import moment from moment; Vue.prototype.$moment moment;然后组件里this.$moment().format(YYYY-MM-DD HH:mm:ss);这样方便是方便但也有个问题全局挂载后你很难知道哪些地方用了它。项目大了以后某个组件里直接用this.$moment如果你哪天要换库就得全局搜一遍。我个人的看法是宁可每个组件显式import moment from moment看起来多写一行但你很清楚依赖关系。3.4 在 Vue 3 项目里怎么用Vue 3 用 Composition API不推荐挂全局属性了虽然还是可以挂app.config.globalProperties更好的是把常用操作封装成 composable 或者工具函数。// utils/date.js import moment from moment; export function formatCurrent(fmt YYYY-MM-DD HH:mm:ss) { return moment().format(fmt); } export function formatWithTime(date, fmt YYYY-MM-DD HH:mm:ss) { return moment(date).format(fmt); }组件里import { formatCurrent } from /utils/date; const nowStr formatCurrent();这样和 Vue 3 的模块化风格一致测试也方便。3.5 moment 常用的几个高级操作moment真正厉害的地方在于运算。举几个实际场景场景 1获取上个月的今天const lastMonthToday moment().subtract(1, month).format(YYYY-MM-DD);场景 2判断某个时间是否在 7 天内const targetDate moment(2025-07-01); const isWithin7Days moment().diff(targetDate, days) 7;场景 3计算两个时间点之间的小时数const start moment(2025-07-02 08:00:00); const end moment(2025-07-02 14:30:00); const diffHours end.diff(start, hours); // 6这些操作如果全用原生Date写你会怀疑人生的。3.6 moment 的坑体积和模式moment最大的槽点是体积大。默认全量引入会把所有语言包都打包进去最终体积可能超过 300KBgzip 后也有 70KB 左右。对于对性能敏感的项目这确实是个负担。优化方法是按需引入语言包import moment from moment; import moment/locale/zh-cn; moment.locale(zh-cn);这样只引入中文语言包体积能小不少。另外moment本身被官方宣布进入“维护模式”了不再添加新功能只修 bug。所以新项目里我更推荐用day.js——它的 API 和moment几乎完全一致但体积只有 2KB 左右。如果你只是因为moment好用才用它换成day.js基本零成本。这里说一句从维护角度看旧项目用moment没有任何问题稳定能用。但新项目起步我真心建议先看看day.js或者直接用原生Date。4. 两种方案如何选型不花冤枉钱4.1 选型对照表我做了个对照表方便你一眼看清楚维度原生 new Date()moment.js获取当前时间支持支持格式化输出手写函数format() 一行搞定时间运算手动处理毫秒数subtract/add/diff 等时区处理手动内置 utc offset国际化自己写内置语言包体积gzip0约 70KB按需可降低bug 维护浏览器原生官方维护模式学习成本低中适合场景简单展示、格式化复杂运算、多语言、时区一句话总结能不用就别用用了就好好用。4.2 我的实际经验和建议根据我的经验项目里时间处理的复杂度可以分为三档第一档只是显示当前时间格式化一下。这种情况直接用原生Date封装个formatDate完全够用。你唯一的诉求就是“别让我手写补零”那就把工具函数写好。第二档有一些时间运算比如算倒计时、算日期差、算几个工作日后的日期。这种建议上day.jsAPI 和moment一样体积小性能好。不建议直接上moment。第三档项目里后端接口的时间字段特别乱有时是时间戳、有时是 ISO 字符串、有时是YYYY年MM月DD日这种中文格式且需要多语言切换。这种全局时间处理场景用moment或者直接上day.js都行。关键是统一入口——你必须在项目里建立一个时间处理模块所有时间格式化都走这个模块不要一个组件一套写法。我看到不少项目是“一个团队三种写法”有人用moment有人用原生还有人自己拼字符串。结果后面做时区需求时改到想哭。统一封装是长期维护的关键。4.3 关于第三方库的另一种选择前面几次提到day.js。它和moment接口高度兼容也是目前社区里很流行的轻量替代品。如果你现在要写新项目真的建议直接day.jsnpm install dayjsimport dayjs from dayjs; console.log(dayjs().format(YYYY-MM-DD HH:mm:ss));如果你要用moment请把项目中所有时间处理集中到一个小模块里这样以后就算换库也只改一个文件。5. 实战Vue 3 项目里的完整时间处理方案这一节我把前面讲的内容串起来给出一个可以直接抄进项目的完整方案。5.1 项目目录结构假设你的项目是标准 Vite Vue 3src/ utils/ date.js # 时间工具函数 composables/ useNow.js # 响应式当前时间实时时钟用 components/ ClockDemo.vue # 示例组件5.2 时间工具函数utils/date.js这个模块统一管理所有时间格式化。我基于原生Date写一套如果你打算用dayjs也很容易替换。核心目标所有页面都从这里拿格式化后的时间不再各自写。// src/utils/date.js /** * 格式化日期时间 * param {Date|string|number} [date] 默认为当前时间 * param {string} [fmtYYYY-MM-DD HH:mm:ss] * returns {string} */ export function formatDate(date new Date(), fmt YYYY-MM-DD HH:mm:ss) { const d date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return ; const pad (n) String(n).padStart(2, 0); const map { YYYY: d.getFullYear(), MM: pad(d.getMonth() 1), DD: pad(d.getDate()), HH: pad(d.getHours()), mm: pad(d.getMinutes()), ss: pad(d.getSeconds()) }; return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) map[match]); } /** * 从时间戳秒格式化 */ export function formatFromTimestamp(ts, fmt) { return formatDate(ts * 1000, fmt); }5.3 响应式当前时间composables/useNow.js如果你需要一个实时更新的“当前时间”封装成 composable 最合适。// src/composables/useNow.js import { ref, onMounted, onUnmounted } from vue; import { formatDate } from /utils/date; export function useNow(interval 1000) { const now ref(formatDate()); let timer null; onMounted(() { timer setInterval(() { now.value formatDate(); }, interval); }); onUnmounted(() { clearInterval(timer); }); return now; }组件里使用template div页面时间{{ now }}/div /template script setup import { useNow } from /composables/useNow; const now useNow(); /script这样组件销毁时自动清理定时器不需要每个页面都手写clearInterval非常省心。5.4 在渲染列表里显示时间很多场景是列表展示时后端返回createTime字段前端需要格式化后再显示。正确姿势是不要在模板里直接写格式化逻辑先处理数据再赋值给渲染变量。import { ref, onMounted } from vue; import { formatDate } from /utils/date; const list ref([]); onMounted(async () { const res await fetch(/api/list).then(r r.json()); list.value res.data.map(item ({ ...item, createTimeText: formatDate(item.createTime) })); });模板li v-foritem in list :keyitem.id {{ item.createTimeText }} /li这里把格式化放在map阶段做模板只负责渲染这样也方便以后调整格式。5.5 时间比较与倒计时比如一个活动倒计时用原生方式function getCountdown(endTime) { const end new Date(endTime).getTime(); const start new Date().getTime(); const diff end - start; if (diff 0) return 活动已结束; const days Math.floor(diff / (1000 * 60 * 60 * 24)); const hours Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds Math.floor((diff % (1000 * 60)) / 1000); return ${days}天 ${hours}小时 ${minutes}分 ${seconds}秒; }解读一下这段代码new Date(endTime).getTime()得到结束时间对应的毫秒时间戳。new Date().getTime()得到当前时间戳。两者相减得到剩余毫秒数diff。一天是1000 * 60 * 60 * 24毫秒用Math.floor取整得到整天数。后面%取余后继续算小时、分钟、秒。这个代码常用于秒杀活动、考试倒计时等场景。如果需要每秒更新结合setInterval即可。5.6 关于时区和国际化的补充如果你的项目面向全球用户那时间处理要特别小心。核心原则是存储和传输一律用 UTC通常是 ISO 字符串或时间戳。展示时再转成用户本地时区。不要在前端手动拼接 UTC 时间的年月日因为你拿到的getFullYear()已经是本地时间了。一个典型的正确示例// 后端返回2025-07-02T06:30:00.000Z const serverTime 2025-07-02T06:30:00.000Z; // 浏览器本地展示假设用户在中国UTC8 const localDate new Date(serverTime); console.log(localDate.toString()); // Thu Jul 02 2025 14:30:00 GMT0800如果你想把某个时间按指定时区展示原生Date做不到这时moment/dayjs的utcOffset就有用了。6. 常见问题速查表与排查技巧我在实际开发中总结了一些高频问题直接列成表格遇到问题先对照查。问题现象根本原因解决办法页面显示7/2/2025 2:30 PM这种奇怪格式你可能直接绑定了 Date 对象浏览器调用了默认 toString用格式化函数先转成字符串月份总是比实际少 1getMonth()从 0 开始getMonth() 1iOS 上new Date(2025-07-02)返回 Invalid DateSafari 对-分隔的字符串解析有兼容问题推荐用2025/07/02或时间戳列表复杂时间时插入了ss显示0而不是00没有补零padStart(2, 0)秒数不更新组件没有定时器只在创建时取了一次值用setInterval配合onUnmounted清理组件销毁后控制台报“Cant perform a React state update”之类错误Vue 类似定时器未清除组件销毁后还修改数据clearInterval(timer)接口传了1719896400秒格式化后是 1970 年单位搞错需要乘 1000 转毫秒new Date(ts * 1000)moment打包后体积巨大默认全量引入所有语言包按需引入moment/locale/zh-cn时间格式化函数formatDate传了null或undefined直接报错没有做进入校验在函数内判断isNaN(d.getTime())返回空字符串使用moment()后页面显示不出来检查是否忘记import moment from moment显式引入还有一个我踩过的坑setInterval里不能用computed的值来重建定时器。有人这样写// 错误示例 const currentTime computed(() formatDate()); setInterval(() { currentTime.value formatDate(); // 报错computed 只读 }, 1000);computed的返回值是只读的不能直接赋值。需要可变的值用ref。另外如果你用了类似vue-router的keep-alive缓存组件onUnmounted不会立刻触发。这种情况下时间组件可能不会在页面切换时销毁显示的时间会“卡住”。处理办法是在onActivated里刷新一次时间或者在onDeactivated里清理定时器。这是keep-alive和普通组件的差异很多人不知道。7. 性能与体验优化细节时间处理这块看着简单实际上也会影响性能尤其是列表页。1. 大列表里不要每行都格式化如果列表有一千行后端每行都带时间字符串你做个map格式化倒还好但如果在模板里写formatDate(item.time)每次渲染都会重新执行。Vue 的模板表达式会在每次渲染时求值二次渲染时即使时间没变也会重新格式化。优化方法就是我在 5.4 里写的数据进列表前先格式化好存到单独的字段。2. 定时器数量控制假设一个页面有 10 个倒计时组件每个都开一个setInterval就是 10 个定时器在跑。性能上不至于崩溃但浪费。更好的做法是只开一个定时器用事件总线或者状态管理库统一分发时间更新。不过这个优化在时间显示这种轻量场景下收益不大。我一般按“组件内够用就行”的原则来。3. 避免直接修改 Date 对象的内部值Date对象的方法都是返回新值或修改自身比如setHours会改原对象。所以如果你要基于一个时间做多步操作先new Date(original)复制一份避免影响原变量。const original new Date(); const copy new Date(original); copy.setHours(copy.getHours() 1); // 原对象不受影响这个细节在处理“开始时间 偏移时长”这种业务时特别有用。4. 关于浏览器兼容性padStart是 ES2017 特性现代浏览器都支持。如果你们的项目要兼容特别老的环境比如某个内网系统用的旧版浏览器内核可以换成手动补零const pad (n) (n 10 ? 0 n : n);这个兼容性更稳。8. 结合 Vue 生态的常见扩展场景时间处理后端不只是“取当前时间”这一步。几个我经常遇到的关联场景一起说。8.1 路由跳转时记录时间在router.beforeEach里记录用户访问时间router.beforeEach((to, from, next) { to.meta.enterTime new Date().getTime(); next(); });然后在页面里计算停留时长const stayDuration new Date().getTime() - to.meta.enterTime;这在做用户行为分析、页面停留时长统计时很有用。8.2 在 Pinia / Vuex 里放一个 lastUpdated很多管理后台会显示“数据最后更新时间”状态变化时顺手更新// store.js import { defineStore } from pinia; import { formatDate } from /utils/date; export const useAppStore defineStore(app, { state: () ({ lastUpdated: , }), actions: { refreshData() { // 模拟拉数据 this.lastUpdated formatDate(); }, }, });这样所有页面拿到的都是同一次全局刷新时间不会各页面各取各的展示不一致。8.3 结合 Spring Boot 后端接口的时间字段现在很多项目是前后端分离后端用 JavaSpring Boot、前端用 Vue。这类项目里时间字段很容易出问题。Java 后端返回的时间格式通常是createTime: 2025-07-02 06:30:00如果后端配置了 Jackson 的时区为GMT8那前端拿到后直接new Date(2025-07-02 06:30:00)解析成的是本地时区假设也是东八区那没问题。但如果后端返回了createTime: 2025-07-02T06:30:00.00000:00这种 ISO 字符串前端解析时就得注意时区转换了。我建议前后端约定好统一用yyyy-MM-dd HH:mm:ss格式传输本地时间或者统一传时间戳毫秒前端自己格式化。两种约定选一种坚决执行。混用的项目后期维护成本极高。8.4 数据可视化项目里的时间刻度如果你在做大屏、数据报表时间轴刻度是常见需求。比如生成最近 7 天的日期数组import { formatDate } from /utils/date; function getRecentDays(days 7) { const result []; for (let i days - 1; i 0; i--) { const d new Date(); d.setDate(d.getDate() - i); result.push(formatDate(d, YYYY-MM-DD)); } return result; } console.log(getRecentDays()); // [2025-06-26, 2025-06-27, ..., 2025-07-02]这种数组可以直接作为图表的 X 轴数据。注意循环里每次都要新建Date对象因为setDate会修改原对象。8.5 倒计时与“xx分钟前”这类相对时间很多社区、社交类应用会有“3 分钟前”“昨天”这类相对时间显示。你可以封装export function timeAgo(date) { const nowTs new Date().getTime(); const targetTs new Date(date).getTime(); const diff Math.floor((nowTs - targetTs) / 1000); if (diff 60) return 刚刚; if (diff 3600) return ${Math.floor(diff / 60)}分钟前; if (diff 86400) return ${Math.floor(diff / 3600)}小时前; if (diff 172800) return 昨天; return formatDate(date, YYYY-MM-DD); }这里返回的中文相对时间交互上更贴近用户习惯。但要注意这种相对时间在页面停留久了会“过期”比如刚显示“1 分钟前”过了一小时还是“1 分钟前”。如果你需要自动刷新仍然要配setInterval。我在实际项目里通常只在初次渲染时算一次因为这类信息一般不需要精确到秒。9. 最后再分享一个小技巧写 Vue 时间处理核心就一句话先选方案再写工具最后统一入口。别想着“这次先简单拼一下以后再说”——时间处理这个坑拖到后期改起来特别疼。如果让我推荐一个最适合大多数 Vue 项目的组合我会选简单格式化场景手写formatDate工具函数用原生Date复杂时间运算场景day.js几乎 0 成本上手实时时钟封装成useNowcomposable全局时区统一在工具层做转换禁止页面各自处理。我个人的体会是很多“时间 bug”不是技术难而是你根本没定义清楚“时间数据从哪来、要到哪去”。把这一层理顺了用什么库反而是次要的。另外一个小提醒如果你在团队协作项目里涉及时间格式和时区问题一定要在代码评审时留意因为不同同事的写法差异真的很大。统一工具文件里写好注释和示例能省掉后面特别多扯皮的事。如果你目前只是要“先跑起来一个展示当前时间的页面”那就直接用第二节的代码连依赖都不用装三分钟搞定。