跳到主要内容

日期格式 Bug

· 阅读需 2 分钟
1adybug
子虚伊人

最新发现一个 JavaScriptDate 对象的特性(bug),记录一下,以下是 Claude 的回答:

在JavaScript中,new Date("2025-01-01")new Date("2025/01/01") 的结果确实会不同,这是因为JavaScript解析这两种格式的日期字符串的方式不同:

  1. new Date("2025-01-01") - 这是ISO 8601格式,被解析为UTC时间(协调世界时)。

  2. new Date("2025/01/01") - 这是非标准格式,被解析为本地时间(你电脑所在的时区)。

所以,如果你在不同时区运行相同的代码,使用斜杠格式(2025/01/01)会得到根据你的本地时区调整后的日期时间,而使用连字符格式(2025-01-01)会得到相同的UTC时间,然后显示时会根据本地时区进行转换。

举个例子,如果你在东八区(北京时间):

  • new Date("2025-01-01") 会被解析为UTC的2025年1月1日0时0分0秒,但显示时会转换为东八区的2025年1月1日8时0分0秒
  • new Date("2025/01/01") 会直接被解析为东八区的2025年1月1日0时0分0秒

这种差异可能会导致日期计算出现错误,因此在处理日期时要特别注意格式和时区问题。

React Strict Mode

· 阅读需 3 分钟
1adybug
子虚伊人

以下是 React 官网的解释:

`React` 中,`Strict Mode` 是一个用于开发环境的工具,用于帮助开发者发现潜在的问题。它会在开发环境中执行额外的检查,并提供警告信息。

严格模式启用了以下仅在开发环境下有效的行为:

- 组件将 额外重新渲染一次 以查找由于非纯渲染而引起的错误。
- 组件将 额外重新运行一次 `Effect` 以查找由于缺少 `Effect` 清理而引起的错误。
- 组件将 额外重新运行一次 `refs` 回调 以查找由于缺少 `ref` 清理函数而引起的错误。
- 组件将被 检查是否使用了已弃用的 `API`

以下是测试代码:

import { FC, useEffect, useMemo, useRef, useState } from "react"

let stateCache: any
let refCache: any
let memoCache: any
let stateCache2: any
let refCache2: any
let memoCache2: any

const App: FC = () => {
const [state, setState] = useState(() => (console.log("calculate state"), {}))
const ref = useRef({})
const memo = useMemo(() => (console.log("calculate memo"), {}), [])

if (stateCache) console.log(state === stateCache)
else stateCache = state

if (refCache) console.log(ref === refCache, ref.current === refCache.current)
else refCache = ref

if (memoCache) console.log(memo === memoCache)
else memoCache = memo

useEffect(() => {
if (stateCache2) console.log(state === stateCache2, stateCache === stateCache2)
else stateCache2 = state

if (refCache2) console.log(ref === refCache2, ref.current === refCache2.current)
else refCache2 = ref

if (memoCache2) console.log(memo === memoCache2, memoCache === memoCache2)
else memoCache2 = memo
}, [])

return <div></div>
}

export default App

在 React 18 中,以下是打印结果:

calculate state
calculate memo
calculate state
calculate memo
false
false false
false
true false
true true false false
true false

可以看出,组件额外渲染了一次,staterefmemo 都额外重新获取了一次值,并且返回的第二次获取的新值。

Effect 额外运行了一次,但是获取到的 staterefmemo 的值是相同的,并且是第二次获取的新值

在 React 19 中,以下是打印结果:

calculate state
calculate state
calculate memo
calculate memo
true
true true
true
true true
true true true true
true true

可以看出,组件额外渲染了一次,staterefmemo 都额外重新获取了一次值,然而返回的却是第一次获取的旧值。

Effect 额外运行了一次,获取到的 staterefmemo 的值是依然是相同的,并且是第一次获取的旧值,

以下是 React 官网的解释:

### StrictMode changes

React 19 includes several fixes and improvements to Strict Mode.

When double rendering in Strict Mode in development, useMemo and useCallback will reuse the memoized results from the first render during the second render. Components that are already Strict Mode compatible should not notice a difference in behavior.

As with all Strict Mode behaviors, these features are designed to proactively surface bugs in your components during development so you can fix them before they are shipped to production. For example, during development, Strict Mode will double-invoke ref callback functions on initial mount, to simulate what happens when a mounted component is replaced by a Suspense fallback.
区别React 18React 19
组件额外渲染一次
staterefmemo 额外重新获取一次值
render 阶段获取到的 staterefmemo旧值/新值旧值/旧值
Effect 在首次渲染时额外运行一次并且立即执行卸载
Effect 阶段获取到的 staterefmemo新值/新值旧值/旧值

使用 wsl 版的 docker desktop

· 阅读需 1 分钟
1adybug
子虚伊人

如果在内网安装 docker desktop,那么 docker desktop 会强制要求升级 wsl,否则无法使用。只需要修改 docker desktop 的配置文件:

// C:\Users\用户名\AppData\Roaming\Docker\setting-store.json
{
// 添加这一行
"WslUpdateRequired": false
}

使用 nginx 作为 ai 应用反向代理

· 阅读需 2 分钟
1adybug
子虚伊人

很多 ai 应用都是使用流式传输,需要对 nginx 进行配置,才能正确的反向代理。

events {
worker_connections 1024;
}

http {

server {
listen 11435;
server_name 0.0.0.0;

location / {
proxy_pass http://host.docker.internal:11434;

# 关闭缓存,确保流式输出
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;

# 增加超时时间,防止长响应被中断
proxy_read_timeout 1800s;
proxy_connect_timeout 60s;
proxy_send_timeout 1800s;

# 首先清除上游服务器可能发送的 CORS 头部
proxy_hide_header 'Access-Control-Allow-Origin';
proxy_hide_header 'Access-Control-Allow-Methods';
proxy_hide_header 'Access-Control-Allow-Headers';
proxy_hide_header 'Access-Control-Expose-Headers';
proxy_hide_header 'Access-Control-Max-Age';

# 移除 CORS 限制
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

# 处理 OPTIONS 请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}

# 代理设置
proxy_set_header Host $host;
# 传递真实 IP,如果后端服务屏蔽了客户端 IP,可以注释掉下面两行
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

windows 版本的 docker desktop

· 阅读需 1 分钟
1adybug
子虚伊人

之前为了便于 wsl 中的系统直接使用 windows 的代理,我将 wsl 的网络模式都设置为了 mirrored,这样就导致了无法使用 ip 访问 docker 中的应用,所以必须将网络模式设置为 NAT,这样就可以通过 ip 访问 docker 中的应用了。

[wsl2]
# 注释掉或者改为 NAT
# networkingMode=mirrored
networkingMode=NAT

关闭 wsl,然后重启 docker desktop,这样就可以通过 ip 访问 docker 中的应用了。

wsl --shutdown

contentRect、contentBoxSize 和 borderBoxSize 之间的区别

· 阅读需 1 分钟
1adybug
子虚伊人

这些尺寸属性之间的主要区别如下:

  1. contentRect:

    • 表示元素的内容区域的尺寸
    • 不包含内边距(padding)和边框(border)
    • 是最内层的测量值
  2. contentBoxSize:

    • 与contentRect测量相同的区域
    • 同样只包含内容区域的尺寸
    • 主要区别在于返回值的格式不同:contentBoxSize返回一个包含width和height的对象
  3. borderBoxSize:

    • 测量元素的完整尺寸
    • 包含内容区域、内边距和边框
    • 是最外层的测量值
    • 计算公式:内容区域 + padding * 2 + border * 2
    • inlineSize:沿着文本方向的尺寸,一般为 width
    • blockSize:垂直于文本方向的尺寸,一般为 height

这些属性在实际应用中的使用场景:

  • contentRect/contentBoxSize:当你需要知道元素实际内容区域大小时使用,比如计算文本区域
  • borderBoxSize:当你需要知道元素在页面上占据的总空间时使用,比如布局计算

如果你对某个具体的使用场景有疑问,或者想要了解更多细节,请随时告诉我

同步 Cursor 设置

· 阅读需 1 分钟
1adybug
子虚伊人

使用办法

  1. 覆盖保存

    • 下载 settings.jsonC:\Users\用户名\AppData\Roaming\Cursor\User 目录下
    • 下载 global.code-snippetsC:\Users\用户名\AppData\Roaming\Cursor\User\snippets 目录下
  2. 使用 zixulu 进行同步

    npx zixulu sc

设置

代码片段

Excel 日期处理中的一个 bug

· 阅读需 7 分钟
1adybug
子虚伊人

最近在使用 SheetJSxlsx 库(以下用 SheetJS 代称)处理 Excel 的数据时,遇到一个非常有趣的 bug。

在读取 xlsx 文件时,我并没有设置读取的配置,所有读取出来的数据都是 Record<string, string> 类型。在处理日期数据时,我发现返回的数据是一个整数字符串,例如 44297,而不是我期望的日期字符串,例如 2024-12-30。询问 Claude 得到以下回答:


让我解释一下 Excel 中日期的存储机制。

Excel 将日期存储为序列号(serial number),这是从一个特定的起始日期开始计算的天数。具体来说:

  1. 对于 Windows 版本的 Excel,起始日期是 1900 年 1 月 1 日,这一天的序列号是 1
  2. 对于 Mac 版本的 Excel,起始日期是 1904 年 1 月 1 日,这一天的序列号是 0

你看到的 45293 就是这样的序列号。让我用代码演示一下如何处理这个数字:

从上面的演示可以看出,序列号 45293 对应的是 2024 年 1 月 2 日。这种存储方式有几个特点:

  1. 整数部分表示天数
  2. 小数部分表示时间(例如 45293.5 表示当天的中午 12 点)
  3. 这种格式便于进行日期计算,比如两个日期相减就能得到相差的天数

当你在编程中处理 Excel 文件时,可以使用像 SheetJS 这样的库来正确解析这些数值。如果你在用 Pythonpandas 库也能自动处理这种转换。如果需要,我可以给你展示更多的日期处理示例。


真是坑爹,WindowsMac 版本的 Excel 竟然表现还不一致,不过手头没有 Mac,就无法测试差异了。

了解了原理,就尝试自己处理这个问题。我使用了 dayjs 库来处理日期,代码如下:

import dayjs from "dayjs"

import "dayjs/locale/zh-cn"

dayjs.locale("zh-cn")

const start = dayjs("1900-01-01 00:00:00")

function parser(serial: number) {
return start.add(serial - 1, "day").format("YYYY-MM-DD HH:mm:ss")
}

console.log(parser(45293))

得到的结果是 2024-01-03 00:00:00

可以看到时间是正确的,但是日期是错误的,我想是不是时区转换引起的。尝试使用 UTC 时间来实现 start

// ...
const start = dayjs("1900-01-01T00:00:00.000Z")
// ...
注意

注意,dayjs 会默认进行本地化

我擦,问题又出现了,得到的结果是 2024-01-03 00:05:43!我可以理解,小时是由于时区的差异产生的错误,但是分钟和秒怎么会有偏差呢?那肯定是某个时间点出问题了:

console.log(new Date("1900-01-01T00:00:00.000Z").toString())

得到的结果是 Mon Jan 01 1900 08:05:43 GMT+0805 (中国标准时间)!再次询问 Claude,得到以下回答:


让我解释这个有趣的现象。这涉及到历史上的时区变迁和JavaScript处理日期的方式。 原因主要有两点:

1900年时的中国采用的是地方时间(Local Mean Time, LMT),而不是现在的 UTC+8 时区。具体来说,当时北京使用的是 UTC+8:05:43。 JavaScriptnew Date() 在处理历史日期时会考虑历史时区信息。当你创建一个 1900 年的日期时,JavaScript 会使用当时的历史时区偏移,而不是现代的 UTC+8。


OK,那我们采用另一种策略来处理这个问题:我们把 Excel 中的时间就当做是 UTC 时间,然后在处理时加上 8 小时:

import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"

dayjs.extend(utc)

const start = dayjs.utc("1900-01-01 00:00:00.00")

function parser(serial: number) {
return start.add(serial - 1, "day").format("YYYY-MM-DD HH:mm:ss")
}

console.log(parser(45293))

得到的结果是 2024-01-03 00:00:00,问题解决!日期仍然错误,继续询问 Claude


让我帮你分析一下这个差异。我会用 SheetJS 来测试。

啊!现在我明白为什么会有这个差异了。这是因为 ExcelSheetJS 在处理日期时有一个历史遗留问题:

  1. Excel 有一个著名的 1900 年闰年 bug:Excel 错误地认为 1900 年是闰年,在 1900 年 2 月 28 日之后多算了一天。

  2. 所以对于 2024/1/2 这个日期:

    • 不考虑 bug 时的序列号是:45292(这是实际天数)
    • 考虑 bug 后的序列号是:45293(这是 SheetJSExcel 实际使用的数字)

所以如果你使用 SheetJS 读取到的是 45293,这是完全正确的。让我给你一个正确处理这个序列号的代码:

function parser(serial: number) {
// 修正 1900 年闰年 bug:如果序列号大于 60,需要减 1
const adjustedSerial = serial > 60 ? serial - 1 : serial

const epoch = new Date("1900-01-01T00:00:00+08:00")
const millisecondsPerDay = 24 * 60 * 60 * 1000

return new Date(epoch.getTime() + (adjustedSerial - 1) * millisecondsPerDay)
}

// 使用示例
const serial = 45293 // `SheetJS` 读取的值

const date = parser(serial)
// 将输出 2024/1/2

总结一下:

  1. 你获取到 45293 是正确的,这是 SheetJSExcel 实际使用的序列号
  2. 这个数字比实际天数大 1,是因为 Excel 的 1900 年闰年 bug
  3. 在处理这类日期时,建议:
    • 如果你使用 SheetJS 的内置函数(如 XLSX.SSF.parse_date_code),直接使用 45293
    • 如果你自己写转换逻辑,记得处理这个 +1 的差异

这就是为什么我们会看到这个差异。这是 Excel 中一个著名的特性,为了保持兼容性,现代的库(包括 SheetJS)都会遵循这个行为。


算了,我还是用 SheetJS 的内置函数来处理这个问题吧,这样就不用自己处理这个 +1 了。

import xlsx from "xlsx"

const date = xlsx.SSF.parse_date_code(45293)
console.log(date)

这里又有一个坑,SheetJS 中的 SSF 类型是 any,但是它引用是 ssf 这个库,所以我们需要安装这个库:

import ssf from "ssf"

type SSFDate = {
D: number /* number of whole days since relevant epoch, 0 <= D */
y: number /* integral year portion, epoch_year <= y */
m: number /* integral month portion, 1 <= m <= 12 */
d: number /* integral day portion, subject to gregorian YMD constraints */
q: number /* integral day of week (0=Sunday .. 6=Saturday) 0 <= q <= 6 */

T: number /* number of seconds since midnight, 0 <= T < 86400 */
H: number /* integral number of hours since midnight, 0 <= H < 24 */
M: number /* integral number of minutes since the last hour, 0 <= M < 60 */
S: number /* integral number of seconds since the last minute, 0 <= S < 60 */
u: number /* sub-second part of time, 0 <= u < 1 */
}

const date = ssf.parse_date_code(45293)
console.log(date)
{
"D": 45293,
"y": 2024,
"m": 1,
"d": 2,
"q": 2,
"T": 0,
"H": 0,
"M": 0,
"S": 0,
"u": 0
}

配合 zod 实现的解析:

import { parse_date_code } from "ssf"
import { z } from "zod"

export const schema = z
.string({ message: "无效的日期" })
.trim()
.regex(/^[1-9]\d*$/, "无效的日期")
.transform(value => {
const { y, m, d } = parse_date_code(Number(value))
return new Date(y, m - 1, d)
})

使用 npm 安装最新的包

· 阅读需 1 分钟
1adybug
子虚伊人

有时候我们在更新了自己的 npm 包之后,需要立即在项目中进行更新,但是 registry 上的包并没有及时更新。这时候我们可以使用 npm i 命令的 @latest 标记配合 --registry https://registry.npmjs.com 来安装最新的包。

npm i soda-next@latest --registry https://registry.npmjs.com

使用 PowerShell,可以在 $PROFILE 中配置:

function inpm {
$package = $args | ForEach-Object { "$_@latest" }
npm i $package --registry=https://registry.npmjs.org
}

在 jsdoc 中定义和复用对象类型

· 阅读需 2 分钟
1adybug
子虚伊人

我来介绍几种在 JSDoc 中定义和复用对象类型的方法:

  1. 使用 @typedef 定义类型:

    /**
    * @typedef {Object} User
    * @property {string} name - 用户名
    * @property {number} age - 年龄
    * @property {string} [email] - 可选的邮箱
    */

    /**
    * 创建新用户
    * @param {User} userData - 用户数据
    * @returns {User}
    */
    function createUser(userData) {
    // ...
    }
  2. 使用 @typedef 定义嵌套对象:

    /**
    * @typedef {Object} Address
    * @property {string} street
    * @property {string} city
    */

    /**
    * @typedef {Object} UserWithAddress
    * @property {string} name
    * @property {Address} address - 用户地址
    */

    /**
    * @param {UserWithAddress} user
    */
    function printUserAddress(user) {
    // ...
    }
  3. 使用 @callback 定义包含函数的对象类型:

    /**
    * @typedef {Object} ConfigOptions
    * @property {string} baseUrl
    * @property {function(Error): void} onError - 错误处理回调
    */

    /**
    * @param {ConfigOptions} config
    */
    function initialize(config) {
    // ...
    }
  4. 继承和扩展已有类型:

    /**
    * @typedef {Object} BaseConfig
    * @property {string} env - 环境变量
    */

    /**
    * @typedef {BaseConfig} ProductConfig
    * @property {string} productId - 产品ID
    */

    /**
    * @param {ProductConfig} config
    */
    function setupProduct(config) {
    // ...
    }
  5. 使用命名空间组织类型:

    /**
    * @namespace API
    */

    /**
    * @typedef {Object} API.RequestConfig
    * @property {string} url
    * @property {string} method
    */

    /**
    * @typedef {Object} API.Response
    * @property {number} status
    * @property {*} data
    */

    /**
    * @param {API.RequestConfig} config
    * @returns {Promise<API.Response>}
    */
    function request(config) {
    // ...
    }

这些类型定义可以在整个项目中重复使用。一些实用建议:

  • 将常用的类型定义放在单独的文件中,例如 types.js
  • 为复杂对象添加详细的属性描述
  • 使用 @property 的可选标记 [] 来表示可选属性
  • 用命名空间避免类型名称冲突
  • IDE(如 VSCode)会根据这些类型定义提供代码提示

这样可以让你的代码更容易维护,并提供更好的开发体验。