你好 👋

欢迎来到我的博客

Vercel MIDDLEWARE_INVOCATION_FAILED

500: INTERNAL_SERVER_ERROR Code: MIDDLEWARE_INVOCATION_FAILED 清除 __vercel_toolbar 的 cookie 就好了,因此发现是 vercel 扩展的问题 后来根据新的关键字搜索,发现了类似的 issue,目前 vercel 应该是还没有解决 Vercel has a Chinese path problem · Issue #4408 · vuejs/vitepress ‍

十月 23, 2025 · Conor

使用 Docker 自部署 Supabase

背景 使用 Docker 自部署 Supabase,实现短信登录+自定义短信提供商 Supabase supabase 是一个开源的后端即服务(BaaS)平台。核心特点: 企业级 PostgreSQL 数据库,自动生成 RESTful API 和 GraphQL API 基于 JWT 的用户管理系统,行级安全策略(RLS)保护数据 边缘函数,基于 deno 的无服务器函数,可运行自定义后端逻辑 实时功能、文件存储(本次不涉及) 值得注意的是 supabase 自部署版本相比官网阉割了一些功能,例如 平台是多项目,但自部署只能单项目 平台可以在线编辑边缘函数,自部署不行 supabase 的 cli 不能给自部署的实例使用 方法 Docker 安装 官方文档 Install | Docker Docs 清华大学镜像站 docker-ce | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 安装后 Post-installation steps | Docker Docs Docker 配置 JSON File logging driver | Docker Docs Supabase 自部署 官方文档 Self-Hosting with Docker | Supabase Docs ...

九月 30, 2025 · Conor

一个比较常用的数字格式化函数

背景 整理一个常用的数字格式化函数,具备常见的四舍五入、保留数位等功能 实现 import Decimal from 'decimal.js'; export const formatDigit = ( digit?: number | string | null, options?: { nilResult?: string; emptyStringResult?: string; nanResult?: string; zeroResult?: string | false; precision?: number | false; thousandSeparator?: boolean; }, ) => { const { nilResult = '-', emptyStringResult = nilResult, nanResult = nilResult, zeroResult = false, precision = false, thousandSeparator = false, } = options ?? {}; if (digit === null || digit === undefined) { return nilResult; } if (digit === '') { return emptyStringResult; } try { const number = new Decimal(digit); if (number.isNaN()) { return nanResult; } if (number.eq(0) && zeroResult !== false) { return zeroResult; } let str: string; if (precision !== false) { str = number.toFixed(precision); } else { str = number.toString(); } if (thousandSeparator) { return formatWithThousandSeparator(str); } return str; } catch (e) { return nanResult; } }; const formatWithThousandSeparator = (str: string) => { const [intPart, decPart] = str.split('.'); const intWithComma = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); return decPart !== undefined ? `${intWithComma}.${decPart}` : intWithComma; }; ‍

六月 16, 2025 · Conor

InputEllipsis 实现

背景 需要一种在 disabled 情况下会显示 Tooltip 的输入框 实现 // index.tsx import { Form, Input, InputProps, Tooltip } from "antd"; import styles from "./index.module.less"; import React from "react"; import type { FormItemProps, InputRef } from "antd"; export const InputEllipsis = (props: InputProps) => { const { value, disabled, className, ...restProps } = props; const [isTooltipCouldVisible, setIsTooltipCouldVisible] = React.useState(false); const inputRef = React.useRef<InputRef>(null); // 检测文本是否溢出 const checkOverflow = () => { const inputElement = inputRef.current; if (inputElement) { // 获取 input 元素实际的输入框部分 const { input } = inputElement; if (input) { // 使用 clientWidth(可视区域宽度)和 scrollWidth(实际内容宽度)比较 const { clientWidth, scrollWidth } = input; setIsTooltipCouldVisible(scrollWidth > clientWidth); } } }; // 监听输入框内容变化和尺寸变化 React.useEffect(() => { checkOverflow(); }, [value]); React.useEffect(() => { window.addEventListener("resize", checkOverflow); return () => { window.removeEventListener("resize", checkOverflow); }; }, []); if (disabled) { return ( <Tooltip title={value} {...(isTooltipCouldVisible ? {} : { open: false })} > <span> <Input ref={inputRef} value={value} disabled={disabled} placeholder={"请输入"} {...restProps} className={`${styles.customEllipsisInput} ${className || ""}`} /> </span> </Tooltip> ); } else { return ( <Input value={value} disabled={disabled} className={className} placeholder={"请输入"} {...restProps} /> ); } }; interface InputEllipsisFormItemProps extends FormItemProps { fieldProps?: InputProps; } export const InputEllipsisFormItem = (props: InputEllipsisFormItemProps) => { const { fieldProps, ...restProps } = props; return ( <Form.Item {...restProps}> <InputEllipsis {...fieldProps} /> </Form.Item> ); }; // index.module.less .customEllipsisInput { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

六月 4, 2025 · Conor

React 输入法 Input 优化

背景 中文输入法触发次数过多或经过状态库异步处理后输入法无法正常使用的问题 原理 使用 composition 事件检测输入法输入 实现 首先动手确认下 compositionStart,compositionEnd,onChange 三者的顺序关系 <input onCompositionStart={() => { console.log("start"); }} onChange={() => { console.log("change"); }} onCompositionEnd={() => { console.log("end"); }} /> chrome safari firefox 可以看到,顺序都是 start、change、end 结果 支持受控 无需进行浏览器判断,目前浏览器行为均一致 import { Input, InputProps } from "antd"; import { useRef, useState, forwardRef, useEffect } from "react"; /** * https://github.com/facebook/react/issues/3926#issuecomment-1200414788 */ export const InputIME = forwardRef<any, InputProps>((props, ref) => { const { onChange, value, ...otherProps } = props; const [inputValue, setInputValue] = useState(""); useEffect(() => { setInputValue(value as string); }, [value]); const onComposition = useRef(false); return ( <Input // 其他默认值 placeholder="请输入文本" autoComplete="off" {...otherProps} ref={ref} value={inputValue} onCompositionStart={() => { onComposition.current = true; }} onChange={(e) => { if (onComposition.current) { setInputValue(e.target.value); } else { onChange?.(e); } }} onCompositionEnd={(e) => { onComposition.current = false; onChange?.(e); }} /> ); }); 参考 https://github.com/facebook/react/issues/3926#issuecomment-1200414788 personal-blog/开发遇到的小问题合集/解决使用输入法输入在 React input 框中的问题.md at master · Jacky-Summer/personal-blog react-composition-input/src/inputfield.js at master · LeoEatle/react-composition-input ‍

五月 9, 2025 · Conor

Cache Control 之 must-revalidate

启发式缓存 想让一个资源能缓存,有三种方式,按照解析优先级排序如下: HTTP 1.1 风格的Cache-Control 响应头中的 max-age指令 HTTP 1.0 风格的 Expires 响应头 Last-Modified响应头 HTTP/2 200 Date: Wed, 27 Mar 2019 22:00:00 GMT Last-Modified: Wed, 27 Mar 2019 12:00:00 GMT 没有Cache-Control,也没有 Expires,但它其实也可以被缓存。 启发式缓存:可缓存时长是用 Date响应头的时间减去Last-Modified的时间,得出的时长再除以10,用汉语描述的话,就是用这个文件最近一次更新到现在的十分之一时长作为可缓存时长,这个例子的话,计算出的可缓存时长是一小时。 规范中仅仅是推荐而已,并没有做强制要求,比如 Firefox 中就在这个算法的基础上还和 7 天时长取了一次最小值,是 min(one-week, (date_value - last_modified_value) * 0.10) 。 如果你想禁用由 Last-Modified响应头造成的启发式缓存,正确的做法是要加上 Cache-Control: no-cache,但在 Chrome 中,Cache-Control: must-revalidate也有同样的功效。但这并不是正规作用 must-revalidate revalidate:指的是当的客户端缓存过期的时候,向服务端发送条件请求,检查缓存资源是否仍然可用的过程。 客户端:浏览器/缓存服务器 服务端:源站/缓存服务器 条件请求:带有If-Modified-Since/If-None-Match请求头的请求 must-revalidate指令是用来表示在一个缓存过期之后,不能直接使用这个过期的缓存,必须校验之后才能使用。 must-revalidate生效的场景还有一个大前提,那就是 HTTP 规范是允许客户端在某些特殊情况下直接使用过期缓存的,比如校验请求发送失败的时候,还比如有配置一些特殊指令(stale-while-revalidate、stale-if-error等)的时候;带有 must-revalidate 的缓存,在任何情况下,都必须成功 revalidate 后才能使用,没有例外。 参考 可能是最被误用的 HTTP 响应头之一 Cache-Control: must-revalidate - 知乎 ‍

二月 25, 2025 · Conor

Mac 软件无法打开

sudo spctl --master-disable​ 然后打开系统设置,选择"任何来源" open -a "System Preferences" /System/Library/PreferencePanes/Security.prefPane 如果还是不行 sudo xattr -r -d com.apple.quarantine /Applications/my.app ​/Applications/my.app 替换为自己的 app 路径,一般在/Applications 下面,可以通过访达拖拽 app 到终端窗口实现自动输入 ‍

二月 10, 2025 · Conor

使用 docker-easyconnect

环境 mbp m2 15.2 在 Docker Desktop 上安装 Portainer Extension 将 docker-easyconnect 提供的命令转换成 compose 文件,可以通过一些开源网站转换Composerize 生成后稍微修改一下,$HOME 这个变量 Docker 读不到,提示我修改 File Sharing,但是看了官方文档/Users 又是默认共享的,所以我暂时改成了用户目录的绝对路径;通过 passwd 环境变量传入 vnc 密码 services: docker-easyconnect: devices: - /dev/net/tun cap_add: - NET_ADMIN tty: true stdin_open: true environment: - PASSWORD=${passwd} - URLWIN=1 - DISABLE_PKG_VERSION_XML=1 volumes: - /Users/conor/Documents/docker/easyconnect/.ecdata:/root ports: - 127.0.0.1:5901:5901 - 127.0.0.1:1080:1080 - 127.0.0.1:8888:8888 image: hagb/docker-easyconnect:7.6.7 在 Portainer 扩展的 Stack 上部署 ...

二月 8, 2025 · Conor

第二次装机

标题 列 1 列 2 列 3 列 4 机箱 机械大师 C25 1382 淘宝 带散热、电源、定制线 主板 ROG B760I DDR5 1128 闲鱼 CPU 14600KF 散片 1207 抖音 显卡 七彩虹(Colorful)iGame GeForce RTX 4060 Ultra Z OC 8GB DLSS 3 2143 京东 散热 酷里奥 FF135 纯黑 0 淘宝 风扇 利民 TL-P9 x1 / TL-C12015B x1 79 淘宝 内存 皇家戟 DDR5 16G 7200 x2 1088 闲鱼 电源 航嘉 MX760P 0 淘宝 硬盘 三星 990PRO 1TB 0 自己 ‍

十一月 16, 2024 · Conor

Valtio 原理

前言 下面所说的都是非嵌套对象的情况 分为两个部分,vanilla 和 react Vanilla 核心:为可变状态添加不变性 检测突变 使用Proxy的set来检测突变,使用版本号来标记这个对象已经发生了变化 let version = 0; const p = new Proxy( {}, { set(target, prop, value) { ++version; target[prop] = value; }, } ); p.a = 10; console.log(version); // ---> 1 ++p.a; console.log(version); // ---> 2 快照 当调用snapshot的时候,根据版本号创建快照 let version = 0; let lastVersion; let lastSnapshot; const p = new Proxy( {}, { set(target, prop, value) { ++version; target[prop] = value; }, } ); const snapshot = () => { if (lastVersion !== version) { lastVersion = version; lastSnapshot = { ...p }; } return lastSnapshot; }; p.a = 10; console.log(snapshot()); // ---> { a: 10 } p.b = 20; console.log(snapshot()); // ---> { a: 10, b: 20 } ++p.a; ++p.b; console.log(snapshot()); // ---> { a: 11, b: 21 } 事件订阅 let version = 0; const listeners = new Set(); const p = new Proxy( {}, { set(target, prop, value) { ++version; target[prop] = value; listeners.forEach((listener) => listener()); }, } ); const subscribe = (callback) => { listeners.add(callback); const unsubscribe = () => listeners.delete(callback); return unsubscribe; }; subscribe(() => { console.log("mutated!"); }); p.a = 10; // shows "mutated!" ++p.a; // shows "mutated!" p.b = 20; // shows "mutated!" React 核心:useSyncExternalStore and proxy-compare ...

四月 8, 2024 · Conor