Day.js 实战指南:2kB 不可变日期库的核心 API、国际化与插件体系
2026/9/18 11:58:34 网站建设 项目流程

Day.js 实战指南:2kB 不可变日期库的核心 API、国际化与插件体系

【免费下载链接】dayjs⏰ Day.js 2kB immutable date-time library alternative to Moment.js with the same modern API项目地址: https://gitcode.com/gh_mirrors/da/dayjs

本文以 Day.js 仓库的 README.md 为主体,系统讲解这个约 2kB、不可变、与 Moment.js 用法高度对齐的日期时间库:从安装接入、解析/格式化/增删/查询等核心 API,到按需加载的国际化(I18n)体系与插件机制。读完本文,你将能够独立完成 Day.js 的引入,理解其不可变性与链式调用的底层实现,并能按需组合 locale 与插件来扩展日期处理能力。

一、Day.js 定位:Moment.js 的轻量化替代

README 对项目的一句话定位是:

Day.js is a minimalist JavaScript library that parses, validates, manipulates, and displays dates and times for modern browsers with a largely Moment.js-compatible API. If you use Moment.js, you already know how to use Day.js.

它围绕六个核心特性组织:与 Moment.js 一致的 API 和模式不可变(Immutable)可链式调用(Chainable)国际化支持(I18n)2kB 微型体积全浏览器支持

其中“2kB 体积”并不是宣传口号,而是被写进了构建流程的硬性约束。package.json 中配置了size-limit检查,产物dayjs.min.js的 gzip 体积上限被设定为2.99 KBnpm run build会执行size-limit && gzip-size dayjs.min.js(见 package.json)。这意味着任何核心功能的膨胀都会在构建阶段被拦截。

仓库内同时提供简体中文、日语、葡萄牙语、韩语、西班牙语、俄语、土耳其语等多语言 README,例如 docs/zh-cn/README.zh-CN.md,内容与主 README 保持一致。需要说明的是,仓库docs/目录下更细粒度的 API、安装、I18n、插件文档(如 docs/en/API-reference.md)目前仅保留了“文档已迁移至官方站点”的跳转说明,完整的 API 细节需结合官方文档与本文的源码分析来掌握。

二、安装与快速上手

2.1 安装

npm install dayjs --save

安装后即可通过import dayjs from 'dayjs'引入。README 开篇给出的最小可运行示例即展示了 Day.js 的典型调用风格——一段完整的链式表达式:

dayjs().startOf('month').add(1, 'day').set('year', 2018).format('YYYY-MM-DD HH:mm:ss');

这条链的含义是:取当前时刻 → 截断到本月第一天 → 加一天(即次月 1 日)→ 把年份替换为 2018 → 按YYYY-MM-DD HH:mm:ss格式输出。仓库的 docs/demo/index.js 提供了更多基础用法,如addsubtractdiffisBeforestartOf等,可直接作为示例集参考。

2.2 入口实现:dayjs 函数与 Dayjs 类

从源码结构看,整个库的入口非常精炼,核心只有三块:src/index.js(约 470 行的核心类)、src/constant.js(常量与正则)和 src/utils.js(填充、单位归一化等工具函数)。

src/index.js 中的dayjs工厂函数负责实例创建,并顺带实现了跨实例安全克隆:如果传入的已经是 Day.js 对象,直接返回其clone(),避免重复解析:

const dayjs = function (date, c) { if (isDayjs(date)) { return date.clone() } const cfg = typeof c === 'object' ? c : {} cfg.date = date cfg.args = arguments return new Dayjs(cfg) }

其中isDayjs通过instanceof Dayjs或标记字段$isDayjsObject双重判断(见 src/index.js),这样即使对象来自不同打包版本也能被识别。test/constructor.test.js 中对instanceof$isDayjsObject的测试印证了这一设计。

Dayjs类的构造流程见 src/index.js:先解析 locale,再调用parse(cfg)完成日期解析,随后init()把原生Date的 year/month/date/weekday/hour/minute/second/millisecond 拆分缓存到$y/$M/$D/$W/$H/$m/$s/$ms字段上,后续所有 getter 都直接读这些缓存,避免反复调用原生方法。

三、核心 API:解析、展示、取值/赋值、增减、查询

README 将 API 分为 parse / display / get & set / manipulate / query 五类,下面逐类展开,并对照源码说明行为细节。

3.1 解析(parse)

dayjs('2018-08-08') // parse

解析逻辑集中在 src/index.js 的parseDate中,其规则依次是:

  1. null直接判为无效日期(new Date(NaN));
  2. 空参数(undefined)返回今天;
  3. Date实例按原生日期复制;
  4. 字符串若匹配预置正则,则按结构化字段构造,不再经过浏览器字符串解析,保证行为跨环境一致;
  5. 其余情况回退到new Date(date)

关键正则定义在 src/constant.js:

REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/

从正则结构看,2018-08-082018/8/82018-08-08T08:00:002018-08-08 08:00:00.123等形态都能被结构化解析;分隔符(-/)和可选的时间部分都由字符类放宽处理。而带Z结尾的字符串会跳过正则分支(!/Z$/i.test(date)条件,见 src/index.js),直接交给原生Date解析 ISO 格式——时区相关的精细处理则留给utc插件(src/plugin/utc/index.js)。

3.2 展示/格式化(display)

dayjs().format('{YYYY} MM-DDTHH:mm:ss SSS [Z] A') // display

不传参数时format()使用默认格式YYYY-MM-DDTHH:mm:ssZ(ISO 风格,见 src/constant.js)。format的实现见 src/index.js,它先用正则REGEX_FORMAT扫描格式串中的每个 token(YYYYMMHHSSSZA等),逐个替换为对应字段值:

  • [...]方括号内的文本被原样输出(正则中\[([^\]]+)]分支,src/constant.js)。这就是 README 示例中[Z]能输出字面量 "Z" 的原因;
  • 两位补零由padStart工具完成(src/utils.js),如MMHH
  • A/a(AM/PM)默认输出英文,若当前 locale 定义了meridiem函数则优先使用 locale 的版本(见 src/index.js);
  • Z输出时区偏移(如+08:00),由 src/utils.js 的padZoneStr基于utcOffset()计算;
  • 无效实例(isValid()为 false)会直接返回 locale 的invalidDateInvalid Date(src/index.js)。

格式串解析所用的完整正则(src/constant.js):

REGEX_FORMAT = /\[([^\]]+)]|YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g

它覆盖了年、月、日、星期、小时(24 小时制H与 12 小时制h)、上下午、分、秒、时区与毫秒等基础 token;更丰富的 token(如季度Q、ISO 周WW、时间戳X/x)则由插件扩展,见第五节。

3.3 取值与赋值(get & set)

dayjs().set('month', 3).month() // get & set

get/set是一对对称操作:不传参数时是 getter,传参数时是 setter。README 示例dayjs().set('month', 3).month()的返回值是从 0 开始的月份序号(即3表示四月),这一点与源码一致——month()最终返回内部缓存$M,而非本地化的“四月”字样。

源码上有两个值得注意的实现细节:

  • 不可变赋值:公开方法set(string, int)的第一件事是this.clone()(src/index.js),所有修改只发生在克隆体上,原实例保持不变;
  • 月末钳制:修改年或月时,实现会先把日期设为当月 1 号再改年月,最后把日期钳制到目标月/年的实际天数内(Math.min(this.$D, date.daysInMonth()),见 src/index.js)。因此dayjs('2020-01-31').set('month', 1)会得到 2 月 29 日(2020 闰年)而非 3 月 2 日,行为与 Moment.js 对齐。

getter 的批量生成见 src/index.js:millisecond/second/minute/hour/date/month/year八个 getter 统一由$g(input, get, set)模式生成——有参走 set,无参走 get,与 README“get & set 一个 API 两用”的描述完全一致。

3.4 时间增减(manipulate)

dayjs().add(1, 'year') // manipulate

add(number, units)见 src/index.js,内部按单位分三条路径:

单位实现策略原因
month、年yearsetthis.set(C.M, this.$M + number)复用月末钳制逻辑,2 月 31 日 + 1 月得到 3 月 31 日而不是溢出到下下月
day、周week在“日期序号”上整数相加(instanceFactorySetdate + n的整日步长,避免跨夏令时时按毫秒加 24 小时可能少/多算 1 小时
时/分/秒/毫秒换算成毫秒后加时间戳时间单位换算精确(毫秒常量见 src/constant.js)

subtract(number, units)只是add(number * -1, units)的薄封装(src/index.js)。

单位字符串支持year/month/week/day/hour/minute/second等完整名称,归一化逻辑在 src/utils.js 的prettyUnit:先查别名表(y→year、M→month、w→week、h→hour、m→minute、s→second、Q→quarter),未命中则转小写并去掉尾部s,所以'years''year'等价。

3.5 比较查询(query)

dayjs().isBefore(dayjs()) // query

isBefore/isAfter/isSame的实现见 src/index.js,其设计很巧妙:不比较时间戳本身,而是用startOf/endOf构造区间再比较:

isSame(that, units) { const other = dayjs(that); return this.startOf(units) <= other && other <= this.endOf(units) } isAfter(that, units) { return dayjs(that) < this.startOf(units) } isBefore(that, units) { return this.endOf(units) < dayjs(that) }

这意味着dayjs().isSame(other, 'day')判断的是“同一自然日”,isBefore(other, 'hour')判断的是“更早的整点区间之前”,与units参数精确对应。不带units时则退化为毫秒级全量比较。区间端点的计算在startOf(src/index.js)中按年/月/周/日/时/分/秒分别取极值,其中“周”的起点会读取 locale 的weekStart字段(默认 0,即周日;中文 locale 定义为 1,即周一),这也是 I18n 影响核心 API 行为的一个实例。

3.6 不可变与链式调用

README 把 Immutable 与 Chainable 列为两大卖点,源码上体现为:所有产生新时间的公开方法都返回新实例setaddsubtractlocale等内部均先clone()),链式调用的安全性由这一约定保证。clone()本身通过保留 locale、UTC 标记等上下文的wrapper重建实例(src/index.js、src/index.js),保证克隆体与原实例“同源”。

四、国际化 I18n:按需加载与双层作用域

README 对 I18n 的表述有两层含义:功能强大,以及默认不打包——“none of them will be included in your build unless you use them”。

import 'dayjs/locale/es' // load on demand dayjs.locale('es') // use Spanish locale globally dayjs('2018-05-05').locale('zh-cn').format() // use Chinese Simplified locale in a specific instance

4.1 按需加载的机制

locale 模块不是被核心引用,而是被使用方显式 import。以 src/locale/zh-cn.js 为例,文件末尾调用dayjs.locale(locale, null, true)完成自注册——副作用是把它挂进全局注册表,之后才能通过名字加载。核心侧的注册表与解析逻辑见 src/index.js 和 src/index.js:

let L = 'en' // global locale const Ls = {} // global loaded locale Ls[L] = en

parseLocale(挂到dayjs.locale上)的行为规则:

  1. undefined时返回/保持当前全局 locale;
  2. 传字符串时按小写名字查注册表,找不到再尝试把zh-cn这类带连字符的名字回退到主语言zh(src/index.js);
  3. 传 locale 对象时以对象的name字段注册;
  4. 第三个参数为true(实例级调用)时只解析、不修改全局L;否则调用即切换全局 locale

仓库 src/locale/ 目录内置了 150+ 语言包(enzh-cnzh-twjakoesfrde等),每个文件结构一致。以 src/locale/zh-cn.js 为例,一个 locale 对象包含以下字段:

字段说明zh-cn 实例
weekdays/weekdaysShort/weekdaysMindddd/ddd/dd输出的星期全称/简称/缩写星期日… / 周日… / 日、一、二…
months/monthsShortMMMM/MMM输出的月份一月…十二月 / 1月…12月
ordinal(number, period)Dowo等序数格式化的函数W时输出1周,默认输出1日
weekStart一周的第一天(影响startOf('week')1(周一)
yearStart周历中年份切换的参考周4
formatsLT/L/LL等快捷格式(配合localizedFormat插件)LL: 'YYYY年M月D日'
relativeTime相对时间文案(配合relativeTime插件)past: '%s前'
meridiem(hour, minute)A/atoken 的本地化输出凌晨/早上/上午/中午/下午/晚上

4.2 全局 locale 与实例 locale

README 示例演示了两种作用域:

  • dayjs.locale('es')修改全局变量L,此后所有新建实例默认使用西班牙语;
  • dayjs('2018-05-05').locale('zh-cn')走实例方法(src/index.js),同样返回一个克隆体,只有该实例使用简体中文,原实例与全局不受影响——与不可变原则一脉相承。

locale 对输出行为的影响是立竿见影的:中文 locale 定义了meridiemdayjs('2018-05-05 07:30').locale('zh-cn').format('Ah:mm')会输出早上07:30;未定义meridiem的 locale 则回退到内置的AM/PM(src/index.js)。同理,weekStart会改变startOf('week')的结果、weekOfYear插件的周号计算。这些“locale 影响核心行为”的点在 test/locale/zh-cn.test.js 等语言测试中有覆盖。

五、插件体系:按需扩展的官方能力

README 对插件的定义:A plugin is an independent module that can be added to Day.js to extend functionality or add new features.

import advancedFormat from 'dayjs/plugin/advancedFormat' // load on demand dayjs.extend(advancedFormat) // use plugin dayjs().format('Q Do k kk X x') // more available formats

5.1 dayjs.extend 的安装机制

extend的实现只有寥寥数行(src/index.js),但设计关键:

dayjs.extend = (plugin, option) => { if (!plugin.$i) { // install plugin only once plugin(option, Dayjs, dayjs) plugin.$i = true } return dayjs }
  • 插件签名是plugin(option, DayjsClass, dayjsFn):拿到可配置项、Dayjs类(用于修改原型)和dayjs函数(用于构造实例),能力相当完整;
  • $i标记实现幂等安装:重复extend同一插件只会执行一次,多模块各自extend也不会冲突;
  • extend返回dayjs本身,支持dayjs.extend(a).extend(b)的链式注册风格。

5.2 以 advancedFormat 为例看插件如何改写核心行为

README 示例中的Q Do k kk X x正是 src/plugin/advancedFormat/index.js 提供的能力。从源码看,插件采用的典型手法是原型方法装饰:保存旧的proto.format,替换为“先扩展新 token、再委托旧实现”的包装器(src/plugin/advancedFormat/index.js):

const oldFormat = proto.format proto.format = function (formatStr) { // 先把 Q / Do / gggg / WW / k / X / x / z 等新 token 替换成具体值 const result = str.replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g, (match) => { ... }) return oldFormat.bind(this)(result) // 剩余的基础 token 交给核心 format 处理 }

它新增的 token 与实现对应关系(见 src/plugin/advancedFormat/index.js):

token输出实现要点
Q季度(1–4)Math.ceil(($M + 1) / 3)
Do本地化序数日调用 locale 的ordinal($D),中文下输出如5日
gggg/GGGG周年 / ISO 周年委托weekYear()/isoWeekYear()(依赖 weekOfYear / isoWeek 插件)
w/ww/wo周年 / 补零周年 / 序数周依赖 weekOfYear 插件
W/WWISO 周号依赖 isoWeek 插件
k/kk24 小时制小时(0 点显示为 24)$H === 0 ? 24 : $H
X/x秒级 / 毫秒级时间戳Math.floor(getTime() / 1000)/getTime()
z/zzz时区缩写 / 全称依赖 timezone 插件的offsetName()

注意其中weekYear/isoWeek/offsetName等方法是其他插件注入的——这体现了插件生态的协作关系:advancedFormat 只负责 token 解析与委托,具体能力由对应插件提供。test/plugin/advancedFormat.test.js 验证了Q Do k kk X x等输出结果。

5.3 仓库内置插件全景

src/plugin/ 目录内置了 35 个官方插件,每个插件一个目录、一个index.js,并配有同名 TypeScript 声明与测试文件(types/plugin/ 与test/plugin/),可按需挑选:

类别插件
解析/构造customParseFormat、objectSupport、arraySupport、preParsePostFormat
格式化advancedFormat、localizedFormat(LT/LL 等快捷格式)、buddhistEra
查询/判断isBetween、isSameOrAfter、isSameOrBefore、isToday、isTomorrow、isYesterday、isLeapYear、isMoment
周/季度weekOfYear、weekYear、weekday、isoWeek、isoWeeksInYear、quarterOfYear、dayOfYear
时区/UTCutc、timezone
时间差/相对时间duration、relativeTime、calendar
集合/边界minMax、localeData、toArray、toObject
兼容性/辅助pluralGetSet、negativeYear、bigIntSupport、badMutable、devHelper、updateLocale

使用方式与 README 示例完全一致:import xxx from 'dayjs/plugin/xxx'dayjs.extend(xxx),部分插件支持在extend时传入第二个 option 参数(如 relativeTime 的thresholds)。

六、工程化保障:测试与体积约束

README 徽章区的“Build Status”与“Codecov”背后,是仓库 package.json 中定义的测试流水线,可作为选型参考:

  • 100% 行覆盖率门槛jest --coverage --coverageThreshold="{ \"global\": { \"lines\": 100} }",未达标则测试任务失败(package.json);
  • 多时区回归test脚本在Pacific/AucklandEurope/LondonAmerica/Whitehorse三个TZ环境各跑一轮 test/timezone.test.js,专门覆盖夏令时与偏移计算类场景(package.json);
  • 体积门禁size-limit将 gzip 产物限制在 2.99 KB(package.json)。

此外,仓库自带完整 TypeScript 类型定义:入口 types/index.d.ts、locale 类型 types/locale/、各插件声明 types/plugin/,import dayjs from 'dayjs'在 TS 工程下开箱即有类型提示。

七、小结

回到 README.md 的主线:Day.js 用约 2kB 的体积提供了“会 Moment 就会 Day.js”的完整能力面——

  1. 解析与格式化:结构化正则解析(REGEX_PARSE)+ 默认 ISO 格式输出,token 体系完整且支持字面量转义;
  2. 不可变 + 链式set/add/locale均返回新实例,startOf/endOf/diff/isBefore等构建在其上,调用链安全可组合;
  3. I18n 按需加载:locale 自注册 + 全局/实例双层作用域,locale 字段(weekStartmeridiemordinal等)真实参与核心行为;
  4. 插件化扩展dayjs.extend幂等安装,35 个官方插件覆盖解析、周历、时区、相对时间等进阶场景,且插件之间可以互相依赖组合。

配套代码可继续深入:核心实现 src/index.js、单位与正则 src/constant.js、工具函数 src/utils.js、示例 docs/demo/index.js,以及test/目录下与src/plugin/一一对应的测试文件。项目遵循 MIT License(LICENSE)。

【免费下载链接】dayjs⏰ Day.js 2kB immutable date-time library alternative to Moment.js with the same modern API项目地址: https://gitcode.com/gh_mirrors/da/dayjs

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询