Trước đây, để tính thời gian đã trôi qua hoặc thời gian còn lại giữa các sự kiện (chẳng hạn như thời gian hết hạn dùng thử, thời gian đếm ngược của gói thuê bao hoặc khoảng thời gian SLA) và trình bày các khoảng thời gian đó ở định dạng đã bản địa hoá, con người có thể đọc được, bạn phải tính toán thủ công hoặc sử dụng thư viện bên ngoài. Đối tượng Date cũ chỉ theo dõi mili giây của kỷ nguyên, khiến cho phép tính tháng theo lịch, chuyển đổi Giờ mùa hè (DST) và cân bằng đơn vị dễ xảy ra lỗi.
API Temporal kết hợp Temporal.ZonedDateTime với Temporal.Duration và Intl.DurationFormat để cung cấp chênh lệch thời gian an toàn theo giờ mùa hè (DST), cân bằng đơn vị có thể định cấu hình, so sánh thời lượng theo lịch và định dạng được bản địa hoá.
Cách triển khai
- Lấy điểm cuối
Temporal.ZonedDateTime: Sử dụngTemporal.Now.zonedDateTimeISO()cho thời gian hiện tại theo múi giờ của hệ thống hoặc truyền một chuỗi múi giờ IANA rõ ràng (Temporal.Now.zonedDateTimeISO(timeZone)) khi múi giờ của sự kiện mục tiêu khác với múi giờ mặc định của hệ thống người dùng. - Tính toán vi phân bằng
.since()và.until():- Sử dụng
now.since(start, options)để tính thời gian đã trôi qua kể từ một sự kiện trong quá khứ. - Sử dụng
now.until(end, options)để tính thời gian còn lại cho đến khi một sự kiện trong tương lai diễn ra. - Truyền
largestUnit(ví dụ:'year','month','day','hour') vàsmallestUnit(ví dụ:'minute') để cân bằng và làm trònTemporal.Durationthu được.
- Sử dụng
- Cân bằng hoặc so sánh các đối tượng
Temporal.Durationđộc lập:- Tạo thời lượng độc lập bằng
Temporal.Duration.from({ minutes: 90 })và cân bằng lại đơn vị bằngduration.round({ largestUnit: 'hour' })(cung cấprelativeTokhi cân bằng giữa các tháng, năm theo lịch hoặc chuyển đổi DST). - So sánh hai khoảng thời gian bằng
Temporal.Duration.compare(d1, d2, { relativeTo })– truyềnPlainDatehoặcZonedDateTimedưới dạngrelativeTolà bắt buộc bất cứ khi nào một trong hai khoảng thời gian chứa đơn vị lịch (years,months,weeks) hoặc vượt qua ranh giới DST.
- Tạo thời lượng độc lập bằng
- Định dạng cho người dùng và cho máy:
- Sử dụng
duration.toLocaleString(locale, options)(uỷ quyền choIntl.DurationFormat) cho văn bản đã bản địa hoá dành cho người dùng. - Chỉ sử dụng
duration.toString()cho các chuỗi thời lượng ISO 8601 có thể đọc được bằng máy (chẳng hạn nhưP1M2DT4H).
- Sử dụng
Ví dụ: Sự khác biệt về thời gian hết hạn của bản dùng thử và định dạng được bản địa hoá
// 1. Get the current time in an explicit target IANA time zone (or omit the
// argument to use the system's current time zone).
const tz = 'America/New_York';
const now = Temporal.Now.zonedDateTimeISO(tz);
// 2. Combine separate date and time strings directly.
// Do not build an ISO string with template literals; chain Temporal methods instead.
const startDateStr = '2025-01-01';
const startTimeStr = '12:00:00';
const endDateStr = '2025-01-31';
const endTimeStr = '12:00:00';
const start = Temporal.PlainDate.from(startDateStr).toPlainDateTime(startTimeStr).toZonedDateTime(tz);
const end = Temporal.PlainDate.from(endDateStr).toPlainDateTime(endTimeStr).toZonedDateTime(tz);
// 3. Calculate differences using .since() and .until()
// ZonedDateTime differences default to largestUnit: 'hour' (days are not a fixed
// length across DST, so Temporal won't balance into days unless asked). Pass
// largestUnit: 'day' or a calendar unit ('week', 'month', 'year') when you want a
// human-scale breakdown instead of e.g. PT720H. smallestUnit rounds away
// seconds and sub-second noise so the formatted string stays readable.
const timeActive = now.since(start, { largestUnit: 'year', smallestUnit: 'minute' });
const timeRemaining = now.until(end, { largestUnit: 'year', smallestUnit: 'minute' });
// 4. Format durations for display with .toLocaleString() (Intl.DurationFormat)
// instead of manually concatenating unit numbers and suffixes. 'en-US' is used
// here for deterministic output; pass undefined to use the user's locale.
console.log(`Active: ${timeActive.toLocaleString('en-US', { style: 'narrow' })}`);
console.log(`Remaining: ${timeRemaining.toLocaleString('en-US', { style: 'long' })}`);
// Use .toString() for ISO 8601 serialization (e.g., API payloads or <time datetime="...">)
console.log(`ISO 8601: ${timeRemaining.toString()}`);
// 5. Check status via .sign, and compare durations with Temporal.Duration.compare()
const isExpired = timeRemaining.sign < 0;
// Pass relativeTo when comparing durations that may include calendar months/years or DST shifts
const isPastHalfway = Temporal.Duration.compare(timeActive, timeRemaining, { relativeTo: now }) > 0;
Ví dụ: Cân bằng thời lượng độc lập (round và Intl.DurationFormat)
// Convert a raw unit count (e.g., 90 minutes) into a balanced breakdown (1 hour, 30 minutes)
const rawDuration = Temporal.Duration.from({ minutes: 90 });
const balanced = rawDuration.round({ largestUnit: 'hour' });
// Format via duration.toLocaleString() or directly with Intl.DurationFormat
const formatter = new Intl.DurationFormat('en-US', { style: 'long' });
console.log(formatter.format(balanced)); // "1 hour, 30 minutes"
Triển khai chiến lược và các phương pháp hay nhất
- NÊN sử dụng
Temporal.ZonedDateTimecho các phép tính liên quan đến sự kiện thực tế xảy ra ở các múi giờ cụ thể (như gia hạn gói thuê bao hoặc đếm ngược sự kiện) và truyềntimeZonerõ ràng đếnTemporal.Now.zonedDateTimeISO(timeZone)khi tính toán theo múi giờ khác với múi giờ mặc định của hệ thống. - NÊN kết hợp các chuỗi ngày và giờ riêng biệt bằng các phương thức Temporal, ví dụ:
Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)hoặcTemporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), thay vì nội suy một chuỗi ISO như`${dateStr}T${timeStr}`. - NÊN truyền
largestUnit(ví dụ:'day','month','year') trong.since(),.until()hoặc.round()khi bạn muốn cân bằng thời lượng ngoài giờ; các phương thức chênh lệchZonedDateTimemặc định là'hour'. - NÊN sử dụng
Temporal.Duration.compare(d1, d2, { relativeTo })khi so sánh hai khoảng thời gian, cung cấp một điểm neorelativeTobất cứ khi nào một trong hai khoảng thời gian liên quan đến đơn vị lịch (months,years) hoặc ngày theo múi giờ. - NÊN định dạng thời lượng cho màn hình của người dùng bằng
duration.toLocaleString()hoặcnew Intl.DurationFormat(locale, { style })('long','short','narrow'hoặc'digital') thay vì nội suy${duration.years}y ${duration.months}m...theo cách thủ công. - NÊN sử dụng
duration.toString()cho chuỗi thời lượng ISO 8601 có thể đọc được bằng máy (ví dụ:PT1H30M), không bao giờ dùng làm nội dung dành cho người dùng. - NÊN hiển thị
PlainTimedưới dạngHH:MM(ví dụ: cho<input type="time">) bằng cách sử dụng.toPlainTime().toString({ smallestUnit: 'minute' })thay vì chuỗi.slice(0, 5). - KHÔNG sửa đổi trực tiếp các thực thể; các đối tượng
Temporallà bất biến. Các thao tác như.add(),.subtract(),.round()hoặc.with()sẽ trả về một thực thể mới. - KHÔNG quay lại phép toán
Date/.getTime()cho chính phép vi phân;Datekhông có phép toán lịch nhận biết múi giờ và chỉ có độ chính xác đến mili giây. - NÊN sử dụng thuộc tính
.signgốc củaTemporal.Duration(hoặcTemporal.ZonedDateTime.compare()) được tính toán để kiểm tra xem thời lượng có đại diện cho một thời điểm trong quá khứ/đã hết hạn (sign < 0) hay thời điểm bắt đầu đang chờ xử lý trong tương lai hay không.
Chiến lược dự phòng
Phương án dự phòng và hỗ trợ trình duyệt cho Temporal
Đối với các trình duyệt chưa hỗ trợ API Temporal gốc, hãy sử dụng tính năng phát hiện tính năng và tải động temporal-polyfill (~15 KB min+gz).
Xin lưu ý rằng việc nhập temporal-polyfill không tự động chỉ định đối tượng Temporal cho phạm vi toàn cục. Hãy chỉ định rõ ràng nếu mã của bạn dựa vào globalThis.Temporal.
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();