ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue项目日期时间获取全攻略:原生Date与moment.js深度解析

Vue项目日期时间获取全攻略:原生Date与moment.js深度解析 做Vue项目的人迟早会遇到一个绕不开的基本需求把当前日期时间拿到页面上来。不管你是做后台管理系统的“创建时间”还是做移动端H5的“最后更新于”最终都要落到同一行代码上——获取当前时间。这里最常用的两条路一条是直接用原生JavaScript的new Date()另一条是引入moment.js这个老牌日期处理库。这个需求看起来人畜无害但真到了写代码的时候你会发现里面藏了不少坑月份为什么要加1、时间字符串为什么在iOS上显示Invalid Date、moment打包体积怎么这么大、Vue 2和Vue 3里挂载日期方法的方式为什么不一样。这篇文章我把这两套方案从头到尾捋一遍包括基础的写法、封装思路、Vue 2/Vue 3的适配以及我实际开发中踩过的那些坑帮你一次性把这些细节都理明白。1. 先想清楚需求再动手日期时间在Vue里到底是什么角色1.1 同一个时间三种完全不同的用法“获取当前日期时间”这句话听起来简单但它背后至少有三种完全不同的使用场景处理方式也不同。第一种是直接展示。比如页面上显示“今天是2025年1月15日”或者列表里的创建时间戳这种纯粹给人看的只需要把时间转成固定格式的字符串放进data或者computed里就行。第二种是提交给后端。前端传时间给后端的时候格式通常要严格对齐比如2025-01-15 10:30:00多一个空格少一个空格都可能导致后端解析失败这时候格式化逻辑必须固定在一处不能每个页面各写各的。第三种是参与计算。比如算两个时间差、判断是否超时、倒计时这种场景还得分清楚你拿到的到底是本地时间还是UTC时间时间戳是秒还是毫秒稍不留神算出来的结果就是错的。很多初学者一上来就写new Date()然后到处用代码里散落着各种重复的格式化逻辑等需求一变更比如要从2025-01-15改成2025年1月15日满项目找人肉替换这就是典型的需求没想清楚就动手。1.2 为什么“当前时间”在JavaScript里没你想的那么简单new Date()拿到的其实是运行代码的那台设备的本地时间注意是“本地时间”不是全球统一的时间。如果你的服务器部署在国外用户在国内你直接用服务器时间显示页面上就可能差8个小时。反过来如果你们的产品面向全球用户你就得考虑是显示用户本地时间还是统一显示某个时区的时间。更隐蔽的一个点在于客户端时间是可以被用户手动改的。用户的手机时间不准你拿new Date()算出来的时间自然也不准。所以如果你做的是考试系统、抢购系统这类对时间一致性要求高的功能绝对不能信任用户设备的时间必须以后端下发的服务器时间为准。这也是我在项目里反复强调的一件事new Date()只是拿时间的手段但时间准不准取决于你从哪里拿。2. new Date()原生方案最基础但也最容易出错2.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 hour String(now.getHours()).padStart(2, 0); const minute String(now.getMinutes()).padStart(2, 0); const second String(now.getSeconds()).padStart(2, 0); const formatted ${year}-${month}-${day} ${hour}:${minute}:${second}; console.log(formatted); // 输出类似2025-01-15 10:30:00这段代码里有三个必须记住的坑。第一个坑getMonth()返回的是从0开始的月份也就是0代表1月、1代表2月以此类推所以拿到后必须加1。我见过不少新人在这个上面翻车页面上生生显示成“2025年0月15日”排查半天发现是月份没加1。第二个坑getDate()是拿日getDay()是拿星期几两者名字长得像作用完全不一样。记住要“日”就用getDate()要“星期几”就用getDay()。第三个坑数字补零。如果你直接输出2025-1-5 8:5:3这个格式虽然也能看但不符合绝大多数后端接口的格式要求也不利于排序比较。用String().padStart(2, 0)可以统一补成两位数。除了这几个getter方法原生Date对象还有几个取时间的快捷方式但各有讲究toISOString()返回的是2025-01-15T02:30:00.000Z这种格式末尾的Z表示UTC时间中国用户看到它实际上要加8小时才是本地时间。toLocaleString()返回的是本地化格式比如2025/1/15 10:30:00这种格式受浏览器语言环境影响同一个时间在不同浏览器上可能显示不一样所以它适合给人看不适合直接存库或者提交接口。getTime()返回的是毫秒级时间戳这个是计算机之间交流的统一语言不受时区影响后端存时间戳、前端转时间都是通过它。2.2 Vue组件里到底该把时间放在哪拿到时间之后怎么在Vue组件里使用这是很多新人容易搞混的地方。如果这个时间只在页面初始化时取一次之后就固定不变那你应该放在data里data() { return { currentTime: this.getNow() }; }, methods: { getNow() { const now new Date(); return ${now.getFullYear()}-${String(now.getMonth() 1).padStart(2, 0)}-${String(now.getDate()).padStart(2, 0)}; } }注意我这里在data里直接调了this.getNow()。Vue 2的官方文档明确说过data里不应该尝试访问this但实际上在data的方法里通过this调用组件实例的methods在很多业务项目里是能用的不过为了规范起见我更推荐把getNow抽成独立的工具函数在data里直接调用这个工具函数完全避开this的指向问题。Vue 3的组合式写法就更简洁了import { ref } from vue; const currentTime ref(formatNow()); function formatNow() { const now new Date(); return ... }如果这个时间是“当前”的时间而且需要跟着时钟实时跳动那就要用computed配合定时器或者在一段生命周期里定时更新。这里有个常见的误区很多人把new Date()直接写在模板里比如{{ new Date().toLocaleString() }}然后发现页面时间不更新。原因很简单模板渲染是基于响应式数据的new Date()不是响应式数据它不会触发重新渲染所以你看到的永远是页面首次渲染时的时间。这个问题的标准解法我在后面封装章节里详细讲。3. moment.js在Vue项目里的正确打开方式3.1 引入moment到底图什么moment.js这个库的历史非常悠久2011年发布在JavaScript日期处理领域垄断了很长一段时间。它的核心价值就一句话把日期处理的复杂度打包封装让你用一行代码完成格式化、解析、比较、加减、差值的所有操作。拿格式化来说原生写法要写六七行moment只需要一行import moment from moment; moment().format(YYYY-MM-DD HH:mm:ss); // 2025-01-15 10:30:00 moment().format(YYYY年MM月DD日); // 2025年01月15日 moment().format(dddd); // 星期三format方法里的占位符是有固定规则的YYYY代表四位年份MM代表两位月份DD代表两位日期HH代表24小时制的两位小时mm代表两位分钟ss代表两位秒。这些规则必须记住因为你阅读别人的代码时几乎必定会碰到这一套占位符系统。moment还提供了很多原生Date没有的便捷方法。比如相对时间moment(2025-01-15 10:00:00).fromNow(); // 2小时前再比如日期的加减moment().add(7, days).format(YYYY-MM-DD); // 七天后的日期 moment().subtract(1, months).format(YYYY-MM-DD); // 一个月前的日期这些操作如果用原生Date自己算你得先搞清楚一个月有多少天、闰年怎么处理、跨年怎么进位非常容易出错。moment把这些都处理好了这就是它最吸引人的地方。3.2 安装、引入以及中文语言包配置安装moment很简单npm或者yarn都行npm install moment --save装完之后如果你的项目是中文界面第一件事是配置中文语言包否则fromNow()这些方法返回的是英文的“2 hours ago”体验很差import moment from moment; import moment/locale/zh-cn; moment.locale(zh-cn);这里有个坑要单独拎出来说。moment的完整包体积很大压缩后大约60多KB因为它的核心代码加所有语言包都包含在内了。很多项目组为了优化体积会做按需加载语言包的配置但在Vue CLI的webpack构建环境中moment.locale(zh-cn)配合import moment/locale/zh-cn一般能正常工作。真正要注意的是如果你在业务代码里对moment做了各种奇怪的动态引入webpack可能把整个语言包目录全打进去体积不减反增。我的建议是项目组做体积优化时直接用dayjs替代momentAPI几乎一样体积只有2KB这个问题就彻底不存在了。但如果你接手的是一个历史项目里面moment已经用得铺天盖地那就先别动保持现状最稳妥。3.3 在Vue 2老项目里全局挂载momentVue 2项目里最省事的做法是把moment挂到Vue的原型上这样所有组件都能通过this.$moment访问到不用每个文件都import一遍。在main.js里import Vue from vue; import moment from moment; import moment/locale/zh-cn; moment.locale(zh-cn); Vue.prototype.$moment moment;组件里使用// 某个Vue组件内部 data() { return { createdAt: this.$moment().format(YYYY-MM-DD HH:mm:ss) }; }, methods: { parseTime(timeStr) { return this.$moment(timeStr).format(YYYY年MM月DD日); } }为什么挂到原型上而不是每个组件里import主要图个方便。如果你的项目里日期处理几乎处处要用全局挂载能少写很多行import而且团队里风格统一。尤其是parseTime这类收时间字符串、吐格式化结果的小函数如果有几十个页面都在用抽成全局方法再挂到原型上日常开发的效率会高不少。3.4 Vue 3组合式API里怎么用moment到了Vue 3情况有变化。Vue.prototype这个写法没了取而代之的是app.config.globalProperties。写法如下// main.js import { createApp } from vue; import App from ./App.vue; import moment from moment; const app createApp(App); app.config.globalProperties.$moment moment; app.mount(#app);在组合式API的模板里如果你想通过this的方式访问需要在setup函数里用getCurrentInstance()取实例import { getCurrentInstance } from vue; export default { setup() { const { proxy } getCurrentInstance(); const now proxy.$moment().format(YYYY-MM-DD HH:mm:ss); return { now }; } };不过说实话Vue 3时代我更推荐直接用组合式函数useNow把moment或者原生Date的格式化逻辑封装成一个可复用的函数在需要的地方调用。这样依赖关系更清晰也方便做单元测试。具体封装代码我在下一章给出。4. 项目里的统一封装实践从工具函数到组合式函数4.1 通用格式化函数一个函数搞定所有场景不管是原生方案还是moment方案实际项目里都不能把格式化逻辑写得到处都是。我的做法是维护一个独立的utils/date.js文件集中放日期处理相关的方法整个项目统一引用。原生方案封装一个通用格式化函数// utils/date.js /** * 格式化日期时间 * param {Date|string|number} input - 日期对象、日期字符串或时间戳 * param {string} pattern - 格式化模式例如 YYYY-MM-DD HH:mm:ss * returns {string} */ export function formatDate(input new Date(), pattern YYYY-MM-DD HH:mm:ss) { const map { YYYY: getFullYear, MM: getMonth, DD: getDate, HH: getHours, mm: getMinutes, ss: getSeconds }; const date input instanceof Date ? input : new Date(input); if (isNaN(date.getTime())) { return ; } return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) { let value date[map[match]](); if (match MM) value 1; return match YYYY ? String(value) : String(value).padStart(2, 0); }); }如果是moment方案封装更简单因为moment自己就有format能力// utils/date.js import moment from moment; export function formatTime(input new Date(), pattern YYYY-MM-DD HH:mm:ss) { return moment(input).format(pattern); } export function fromNow(input) { return moment(input).fromNow(); }这个封装的价值在于整个项目的格式化逻辑收口到一处以后要调整格式比如统一改成2025/01/15只需要改这一个文件。而且formatDate里有个细节——如果传入的日期无效返回空字符串而不是返回Invalid Date这样页面上的日期即使坏了也不会把一坨红色的报错信息直接扔给用户看。4.2 秒级刷新的“当前时间”组件定时器的正确用法前面提到直接{{ new Date() }}不会自动更新因为模板依赖的不是响应式数据。如果页面上要显示一个每秒跳动的实时时钟正确做法是配合setInterval定时刷新。在Vue 2里我一般这样写template div{{ now }}/div /template script import { formatDate } from /utils/date; export default { data() { return { now: }; }, created() { this.updateNow(); this.timer setInterval(this.updateNow, 1000); }, beforeDestroy() { clearInterval(this.timer); }, methods: { updateNow() { this.now formatDate(new Date(), YYYY-MM-DD HH:mm:ss); } } }; /scriptVue 3里用组合式API封装成一个useNow的组合式函数// composables/useNow.js import { ref, onMounted, onBeforeUnmount } from vue; import { formatDate } from /utils/date; export function useNow(pattern YYYY-MM-DD HH:mm:ss) { const now ref(); let timer null; const update () { now.value formatDate(new Date(), pattern); }; onMounted(() { update(); timer setInterval(update, 1000); }); onBeforeUnmount(() { clearInterval(timer); }); return { now }; }组件里使用import { useNow } from /composables/useNow; export default { setup() { const { now } useNow(); return { now }; } };这里有一个我踩过几次的坑定时器一定要在组件销毁时清理否则组件销毁了定时器还在跑不仅浪费性能还可能造成内存泄漏。Vue 2用beforeDestroyVue 3用onBeforeUnmount位置写对组件生命周期结束就把定时器清掉。另外如果刷新频率不需要每秒一次比如只是“登录后显示最近登录时间”那完全不需要定时器初始化时取一次就够了。不要为了一个固定不变的时间开着定时器白耗性能。5. 项目实战中的常见问题与排查技巧5.1 问题速查表踩过的坑都在这了日期时间处理虽然基础但坑是真多。我把这几年来遇到的高频问题整理成了一张表每一条都是血泪教训问题现象根本原因解决方案显示出来的月份比实际少1个月getMonth()返回的月份从0开始计数拿到结果后加1iOS浏览器显示Invalid DateiOS对2025-01-15 10:30:00这种带空格的字符串解析兼容性差把字符串中的-替换成/或者new Date(2025/01/15 10:30:00)时间差8个小时toISOString()返回的是UTC时间中国在东八区展示前根据时区做转换或者统一用时间戳计算moment输出的语言全是英文没有配置中文语言包import moment/locale/zh-cn; moment.locale(zh-cn)时间戳转换后日期不对接口返回的是秒级时间戳new Date()需要毫秒级秒级时间戳乘以1000打包后体积暴涨moment整个语言包目录被打包考虑替换dayjs或者做语言包按需裁剪定时更新时间不跳定时器更新时间了但没触发视图刷新确保更新的字段是响应式的比如在data里初始化好getDay()返回的结果不是日期getDay()是获取星期几不是获取“日”获取日期用getDate()这里重点说一下第二个问题。new Date(2025-01-15 10:30:00)在Chrome和Firefox上能正常解析但在部分iOS Safari版本上会返回Invalid Date。原因在于ECMAScript规范里对日期字符串的格式要求比较严格2025-01-15T10:30:00这种带T的ISO格式是规范明确支持的而2025-01-15 10:30:00这种带空格的格式在不同浏览器上的支持度不一致。最稳妥的写法是把字符串里的横杠替换成斜杠new Date(2025/01/15 10:30:00)这个格式在iOS和Android上都能解析。如果你用的是momentmoment(2025-01-15 10:30:00)内部已经做了兼容处理一般不容易踩这个坑。5.2 排查思路遇到日期问题该从哪里入手日期相关的Bug有个特点本地测试很难复现用户那边偶尔报一次复现步骤还不清晰。遇到这种问题我的排查思路一般分三步。第一步先确认时间来源。这个时间到底是前端new Date()直接生成的还是后端接口返回的字符串如果是后端返回的先看接口原始数据确认是不是后端时间本身就是错的。这一步能排除一半的冤枉Bug。第二步再确认时间格式。把前端收到的时间原样打印出来看是2025-01-15T02:30:00.000Z这种带时区的格式还是2025-01-15 10:30:00这种不带时区的格式还是1736901000这种时间戳。不同格式的解析路径完全不同格式没对齐后面全是白费功夫。第三步最后看显示层。确认格式对了之后再看展示时用的是什么方法、经过了什么转换。最常见的就是把时间字符串又new Date()了一遍结果在iOS上炸了。按照这个思路排查大部分日期问题都能在10分钟之内定位。另外还有一个忠告跨时区的业务场景前后端约定统一用时间戳交互是坠吼的没有之一。时间戳不涉及时区、不存在格式差异、排序比较也方便前端拿到后按需转成展示格式后端存库也干净这是最省心的一条路。5.3 我的选择什么时候用原生什么时候用moment最后聊聊工具选型的心得。如果你在写一个全新的项目日期处理只有格式化这种基础需求我建议直接用原生new Date()加自己封装的formatDate函数能不引依赖就不引依赖项目轻装上阵。如果你的项目里日期处理特别重需要大量做日期的加减、比较、多格式转换那就上moment或者dayjs。我个人现在的倾向是新项目首选dayjsAPI兼容moment体积小很多团队上手成本也低。如果是维护老项目原项目里已经大量使用moment那没有必要为了“技术升级”去全局替换找一个边缘模块试点用dayjs验证没问题再逐步替换不要一上来就全局换容易引发各种隐藏的解析差异问题。毕竟moment的format和dayjs的format虽然99%的情况下输出一致但极少数边界情况可能会有细微差别在做秒杀、订单这类对时间精度敏感的功能时一点点差别都可能引发线上问题。我在实际项目中最常用的组合是原生new Date()拿时间戳自己封装一个轻量格式化函数处理常规展示万一遇到复杂日期计算再按需引入dayjs这样项目里的日期处理既轻量又灵活基本覆盖了我遇到的所有业务场景。
返回列表