<![CDATA[王小明博客]]>https://16px.cc/RSS for NodeMon, 30 Jun 2025 23:11:02 GMT60<![CDATA[lucide 图标设计准则]]>

下面的内容并非原文的一对一翻译,建议查看原文 Icon Design Principles

  1. 图标应该设计在 24*24 的画布上

  2. 图标内容与画布间应至少有 1px 的 padding

  3. 图标 stroke width 必须为 2

  4. 图标必须使用 round line joins

  5. 图标必须使用 round line caps

  6. 图标必须居中

  7. 转角圆弧的半径:

  • 8px (含)以上的图标,半径为 2
  • 8px 以下的图标,半径为 1
  1. 两个不同的元素间应有(至少?) 2px 的间隔

  2. 图标应具有相似的视觉尺寸(optical volume)

  3. 图标视觉中心应居中

  4. 图标应具有相似的视觉密度

  5. 连续曲线应平滑连接

  6. 图标应致力于在低 DPI 的设备上也能清晰显示

]]>
https://16px.cc/blog/7VTXNoyXoSkahttps://16px.cc/blog/7VTXNoyXoSkaMon, 30 Jun 2025 07:05:31 GMT
<![CDATA[为 Next.js 添加页面跳转动画]]>

参考 https://stackoverflow.com/a/77604347

关键代码

'use client'

import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'

const defaultProps = {
  initial: { opacity: 0 },
  animate: { opacity: 1 },
  exit: { opacity: 0 },
  transition: { ease: 'easeInOut', duration: 0.5 },
}

export function PageTransitionEffect({
  children,
}: { children: React.ReactNode }) {
  const pathname = usePathname()

  return (
    <AnimatePresence mode='popLayout'>
      <motion.main {...defaultProps} key={pathname}>
        {children}
      </motion.main>
    </AnimatePresence>
  )
}

然而这样是不行的,旧页面在执行卸载动画期间,内容会变成新页面的内容。

我们需要确保,旧页面的内容在执行卸载动画期间,内容不变。

确保旧页面内容保持不变

此时就需要用到 LayoutRouterContext

import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useContext, useRef } from 'react'

function FrozenRouter(props: { children: React.ReactNode }) {
  const context = useContext(LayoutRouterContext ?? {})
  const frozen = useRef(context).current

  if (!frozen) {
    return <>{props.children}</>
  }

  return (
    <LayoutRouterContext.Provider value={frozen}>
      {props.children}
    </LayoutRouterContext.Provider>
  )
}

用这个 FrozenRouter 包裹一下上面关键代码中的 children 就好了。

最终形态

以防我们在什么地方需要访问当前动画是否已完成,所以添加一个全局钩子

// usePageTransitionEffect.ts
import { create } from 'zustand'

export const usePageTransitionEffect = create(() => ({
  exited: true,
}))

接下来是完整代码:

// PageTransitionEffect.tsx
'use client'

import { usePageTransitionEffect } from './usePageTransitionEffect'

import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useContext, useEffect, useRef } from 'react'

import type { MotionProps } from 'framer-motion'

function FrozenRouter(props: { children: React.ReactNode }) {
  const context = useContext(LayoutRouterContext ?? {})
  const frozen = useRef(context).current

  if (!frozen) {
    return <>{props.children}</>
  }

  return (
    <LayoutRouterContext.Provider value={frozen}>
      {props.children}
    </LayoutRouterContext.Provider>
  )
}

export type PageTransitionEffectConfig = Omit<MotionProps, 'children' | 'key'>
export type PageTransitionEffectProps = PageTransitionEffectConfig & {
  children: React.ReactNode
}

const defaultProps = {
  initial: { opacity: 0 },
  animate: { opacity: 1 },
  exit: { opacity: 0 },
  transition: { ease: 'easeInOut', duration: 0.5 },
} satisfies PageTransitionEffectConfig

const onEnter = () => {
  usePageTransitionEffect.setState({
    exited: false,
  })
}

const onExitComplete = () => {
  usePageTransitionEffect.setState({
    exited: true,
  })
}

export function PageTransitionEffect({
  children,
  ...props
}: PageTransitionEffectProps) {
  const pathname = usePathname()

  useEffect(() => {
    onEnter()
  }, [pathname])

  return (
    <AnimatePresence mode='popLayout' onExitComplete={onExitComplete}>
      <motion.main {...defaultProps} {...props} key={pathname}>
        <FrozenRouter>{children}</FrozenRouter>
      </motion.main>
    </AnimatePresence>
  )
}

需要动画的地方用 PageTransitionEffect 包裹一下就可以了。

示例

// layout.tsx

export default function CustomLayout({ children }) {
  return <>
    <YourHeader />
    {/* 此时,页面切换时,只有 content 有动画,header 和 footer 没有动画 */}
    <PageTransitionEffect style={{ width: '100%', maxWidth: 1024, margin: '0 auto' }}>
      {children}
    </PageTransitionEffect>
    <YourFooter />
  </>
}

// children component

export function ChildrenComponent() {
  const { exited } = usePageTransitionEffect()

  // ...
}
]]>
https://16px.cc/blog/fXT6f-cZQcmBhttps://16px.cc/blog/fXT6f-cZQcmBSun, 29 Jun 2025 08:48:50 GMT
<![CDATA[对象类型如何同时支持 known-keys 及 unknown-keys]]>假设我们有一个 colors 对象:

type KnownKeys = 'red' | 'green' | 'blue' // and more...

const colors = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff',
  // ...
}

// 显然,这里的值是 `string`
const red = colors.red

// 如果 key 来自后端接口,它既可能是已知的 KnownKeys,也可能是任意的 string
const colorKey = await fetch('/api/color')
// 显然,这里 colorValue 应当是 `string | undefined` 类型
const colorValue = colors[colorKey]
  • 不能简单地把 colors 定义为 Record<KnownKeys, string>,因为这会导致 colors.unknownKey 跑错(key 不存在);
  • 不能简单地把 colors 定义为 Record<string, string>,因为这会导致 colors.unknownKey 的类型变为 string
  • 不能简单地把 colors 定义为 Record<string, string | undefined>,因为这会导致 colors.red 的类型变为 string | undefined

这种情况,我们应该定义 colors 的类型为:

type KnownKeys = 'red' | 'green' | 'blue' // and more...

type Colors = Record<string, string | undefined> & {
  [key in KnownKeys]: string
}

Colors['red'] // string
Colors['unknown-key'] // string | undefined

更一般地,我们可以定义一个通用类型:

type Obj<K extends string | number, V> = Record<string | number, V | undefined> & {
  [key in K]: V
}

// 那么:
const colors: Obj<KnownKeys, string> = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff',
  // ...
}
]]>
https://16px.cc/blog/PkVF1CJB8nS3https://16px.cc/blog/PkVF1CJB8nS3Wed, 25 Jun 2025 09:04:39 GMT
<![CDATA[调用 windows 主机的 explorer.exe 打开 wsl2 内的目录]]>mac 中直接就能调用 open xxx 打开文件夹,wsl2 中也可以有。

效果

你可以调用 open what-dir / open test.txt 直接打开目录。

步骤

  1. 新建文件 ~/bin/open

  2. 填入以下内容

    #!/bin/bash
    
    target="$1"
    if [ -z "$1" ]; then
      target=$(pwd)
    fi
    
    if [ ! -e "$target" ]; then
      echo "Error: '$target' does not exist."
      exit 1
    fi
    
    if [ -d "$target" ]; then
      abs_path=$(realpath "$target")
    else
      abs_path=$(dirname "$(realpath "$target")")
    fi
    
    # 转为 Windows 路径
    win_path=$(wslpath -w "$abs_path")
    
    # 打开
    explorer.exe "$win_path"
    
  3. 为文件添加执行权限

    chmod +x ~/bin/open
    
  4. 添加到系统路径 export PATH="$HOME/bin:$PATH"

]]>
https://16px.cc/blog/XX0RFmxwy4fZhttps://16px.cc/blog/XX0RFmxwy4fZSat, 14 Jun 2025 11:58:49 GMT
<![CDATA[还有高手?]]>这样的项目竟然能跑起来,59个文件有258个tsignore,77个文件有406个any

59个文件有258个tsignore

77个文件有406个any

]]>
https://16px.cc/blog/5gW4aexTk0_2https://16px.cc/blog/5gW4aexTk0_2Tue, 10 Jun 2025 02:56:29 GMT
<![CDATA[腾讯云轻量服务器使用 coturn 配置 STUN/TURN 服务]]>

coturn 官方文档: turnserverturnutils_uclient

腾讯云管理后台放行端口

轻量服务器不支持"安全组",所以放行是在"防火墙"

防火墙放行端口

  • 3478
  • 5349
  • 50000-51000

TURN 在中继时需要很多端口,所以在这需要放行一批
此处放行的端口,还需要在后续的配置中体现,见后文

安装 coturn

sudo apt update

sudo apt install coturn

配置 coturn

vim /etc/turnserver.conf

配置说明如下

# === 基本网络设置 ===
listening-port=3478
tls-listening-port=5349

# 你的公网 ip
external-ip=xxx.xxx.xxx.xxx
# 下面这两个不能开启,让 coturn 自行获取
# https://github.com/coturn/coturn/issues/1082#issuecomment-1365539726
# listening-ip=0.0.0.0
# relay-ip=0.0.0.0

# 前文说的,放行端口范围,需要在此处指定
min-port=50000
max-port=51000

fingerprint
server-name=your.domain
realm=your.domain

# 静态用户认证
# lt-cred-mech
# user=username:password

# 建议使用动态用户认证
# use-auth-secret 和 lt-cred-mech 不能同时使用
use-auth-secret
static-auth-secret=your_secret

# === TLS 证书(可选)===
# 需要确保 coturn 能访问到
cert=/path/to/your/fullchain.cer
pkey=/path/to/your/cert.key

# === 安全性建议 ===
stale-nonce
no-multicast-peers

# gpt 会说配置 no-loopback-peers
# 但是最新的 coturn 的配置是 --allow-loopback-peers
# 默认禁止 loopback-peers,需要你明确允许,一般不需要允许

测试是否可连接

在服务器上运行 coturn 测试命令(-v 便于调试)

sudo turnserver -v

在我们本地也安装 coturn, 就可以使用命令行工具 turnutils_uclient 来测试连接了。

然后在我们本地(不是服务器上)使用 ts 生成账号密码

import crypto from 'crypto'

export function getStunConfig() {
  // 1小时后过期
  const timestamp = Math.floor(Date.now() / 1000) + 3600
  const username = `${timestamp}:user`

  // TURN_SECRET 是上面的配置中 static-auth-secret 的值
  const hmac = crypto.createHmac('sha1', TURN_SECRET)
  hmac.update(username)
  const password = hmac.digest('base64')

  // turnutils_uclient 访问 your_turn_domain 时候不要带协议和端口
  // 应当是 'turn.example.com' 或者 'example.com'
  // 看你自己把哪个域名解析到 TURN 服务器
  console.log(`turnutils_uclient -u ${username} -w ${password} -r your_realm your_turn_domain`)

  return { username, password }
}

此时在本地执行 log 出来的命令 turnutils_uclient ...,成功就表示 turn 服务没问题。

继续执行 openssl s_client -connect your_turn_domain:5349,成功就表示 turns 没问题。

如果还失败,那你仔细看服务端的 coturn log 找找原因吧。

tips

gpt 说:

大多数现代浏览器(尤其是 Chrome / Firefox)只信任 TLS TURN(即 turns: 协议)
所以建议为 TURN 服务配置 TLS,使用 turns:test.cc:5349

]]>
https://16px.cc/blog/skif_Kvu2gDohttps://16px.cc/blog/skif_Kvu2gDoThu, 22 May 2025 12:33:23 GMT
<![CDATA[做了一个新玩具,在线 ffmpeg]]>地址 https://16px.cc/sh/ffmpeg

支持命令:

$ echo   
$ cd   
$ clear   
$ cp: 支持 -r 递归复制   
$ pwd   
$ cat   
$ mkdir: 支持 -r 递归创建   
$ mv   
$ touch   
$ rm: 支持 -r 递归删除   
$ ls   
$ vi/vim/edit: 编辑文件   
$ help   

$ download: 文件下载   
$ upload: 加载文件到终端当前目录   
$ ffmpeg: gif / 视频 处理   
  • 支持简单的自动补全(命令补全 + 路径补全)
  • 支持简单的历史命令

经测试,网上说的没错,在浏览器上运行的性能确实是原生的大约十分之一。
视频转换速率基本在 3500kbit/s 左右,即 0.4+MB/s
(测试视频大小约 20MB,命令 ffmpeg -i input.mp4 output.flv

命令行预览

ffmpeg 在线合成gif.gif

]]>
https://16px.cc/blog/8NTnpPbybHgEhttps://16px.cc/blog/8NTnpPbybHgESat, 17 May 2025 20:51:55 GMT
<![CDATA[做了一个图片合成 gif 的纯前端小工具]]>想起好多年前帮别人把很多图片合成 gif,那时候他把图片打包发给我,然后我用 ffmpeg 给他合成,然后再给他发回去。

最近想在前端部署 ffmpeg 玩,就想起了这个需求,就做了一个图片合成 gif 的纯前端小工具。

地址 https://16px.cc/to-gif

支持:

  • 图片排序
  • 配置循环次数(默认无限循环)
  • 配置单帧持续时间
  • 配置背景色(如果有透明 png 或者图片比例不统一时会透出背景色)
  • 配置输出的 gif 的尺寸
  • 裁切输出产物

示意图如下:

图片合成gif.gif

]]>
https://16px.cc/blog/jGxUdHG167CKhttps://16px.cc/blog/jGxUdHG167CKWed, 14 May 2025 13:53:42 GMT
<![CDATA[去韶山和张家界玩了一趟]]>韶山毛主席故居

从江西开车 300 公里到韶山😄

怪我攻略做少了,自驾到韶山才知道,离目的地(毛主席故居)很远就不让进了,要找景区内部的商家报备,每人交 20 块钱才能进去。所以临时联系饭店,报备交钱后才进去。

这里发生了一个小奇葩的事,导航去饭店的路上,中途突然过不去了,被防撞升降柱拦住了,结果百度高德都只有这一条路,问店家,老板告诉我往回走,然后右转进隧道,一直走就到了。我一路往回开了好几分钟才看到,一个右转路口几百米外有隧道。 景区在必经之路上中途拦截不让过,还没有做任何道路标牌指引,我是服气的。 防撞升降柱.jpg

吃的是离目的地不远的毛家饭店,菜倒还挺好吃,也不算贵,4 个人花了 220,还免费停车。

我们下午直接把车停在了饭店门口(免费),走路十来分钟,到毛主席故居。

在里面大概逛了半个小时,然后才发现外面的大房子不是毛主席住的,毛主席住的房子围起来了,要提前预约(免费)才能进。当时 TM 是一个工作日,结果人山人海,看数据大屏说当日已接待好几万人了,就离谱。当然就没约上,就只能在外面拍了拍照片然后走了。

毛主席故居.jpg

张家界国家森林公园

然后从韶山开车 340 公里到张家界😄

依旧是攻略做少了,半路上人家建议别去天门山国家森林公园,去张家界国家森林公园,然后我接受建议了。到酒店才发现,提前订好的酒店,离张家界国家森林公园三十几公里。。。

然后住下,第二天驱车前往张家界国家森林公园,从东门进,各种环保车(大巴)和缆车一坐,山上说实话也就那么回事,也就缆车上看石山好看点,结果我妈恐高,在缆车上全程捂着脸不敢看。。。

缆车与山-1.jpg

缆车与山-2.jpg

最后离开的时候,发生了一件巨 TM 恶心的事:

我们从东门进场,逛到南门离场,车停在了东门地下停车场,所以要从南门坐车过去。

大巴8块钱一个人,我们 4 个人,我就寻思打个车呗,在百度上打车,很快就被接单了,车子是滴滴的涂装。

结果等我过去打开车门报尾号,司机直接来一句,“订单没了”。

MD 订单没了?我看我自己的订单历史记录,MD 竟然显示“订单已取消·乘客取消”。

不是,这到底是滴滴的高科技还是百度的高科技?不乐意接就别接啊,接了直接给我取消了,还变成了“乘客取消”?

]]>
https://16px.cc/blog/q9Jygmt5RM-Lhttps://16px.cc/blog/q9Jygmt5RM-LFri, 09 May 2025 07:28:01 GMT
<![CDATA[我喜欢的 ts 新功能]]>4.9 新增 satisfies 操作符

ts 4.9 官方文档 #the-satisfies-operator

考虑如下的一个困境:

type RGB = [r: number, g: number, b: number]
type ColorNames = "red" | "green" | "blue"

const color = {
  red: [255, 0, 0],
  green: '#00FF00',
  blue: [0, 0, 255],
}

// 我们一方面希望限制 `color` 为 `Record<ColorNames, RGB | string>`,
// 另一方面又希望保留各属性的实际的值(后面可能会有用),
// 例如:
color.red.map((i) => i / 255)
color.green.toLowerCase()

过去不好实现:

// 尝试 1
const color: Record<ColorNames, RGB | string> = {
  // ...
}
color.red.map((i) => i / 255)
//        ~~~ 报错,color.red 可能是 RGB | string

// 尝试 2
const color = {
  // ...
  // 这样不仅无法实现需求,color.red 可能是 RGB | string
  // 而且还限制不了此处的多余属性名
  // 例如这儿我多写一个 gray: '#666666' 也不会报错
} as Record<ColorNames, RGB | string>

现在引入了 satisfies 操作符

const color = {
  red: [255, 0, 0],
  green: '#00FF00',
  blee: [0, 0, 255],
//~~~~ 这儿拼写错误就会抛错了
  gray: '#666666',
//~~~~ 这儿会报错 "gray" was never listed in 'ColorNames'.
} satisfies Record<ColorNames, RGB | string>

5.0 泛型声明中支持 const 修饰符

ts 5.0 官方文档 #const-type-parameters

我们在需要收紧(narrow)类型的时候,通常会使用 as const:

interface HasColors {
  colors: string[]
}

function getColors<T extends HasColors>(arg: T): T['colors'] {
  return arg.colors
}

// Inferred type: string[]
const colors = getColors({
  colors: ['RED'],
})

如果我们需要收紧类型(从 string[] 收紧到 ['RED']),我们会:

// Inferred type: ['RED']
const colors = getColors({
  colors: ['RED'],
} as const)

这样是可行的,但是有点麻烦,每次调用 getColors 的时候都要 as const 一下,还容易忘。ts 5.0 现在可以在类型参数声明中添加 const 修饰符,以使 const 推断作为默认值:

``` typescript {2,6} interface HasColors { colors: readonly string[] // ^^^^^^^^ }

function getColors(arg: T): T['colors'] { // ^^^^^ return arg.colors }

// Inferred type: ['RED'] const colors = getColors({ colors: ['RED'], })

## 5.2 引入 using 关键字

[ts 5.2 官方文档 #using-declarations-and-explicit-resource-management](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management)

> 需要 `polyfill` 以及 `tsconfig` 设置 `target` 和 `lib`, 详情见文档。

当我们需要处理文件的时候:

typescript {7,11} function doSomeWork() { const path = 'xxx' const file = fs.openSync(path, 'w+') // use file… if (someCondition()) { // Close the file fs.closeSync(file) return } // Close the file fs.closeSync(file) }

清理工作繁琐还容易遗漏。

现在 ts 引入了 `using` 关键字和全局类型 `Disposable`:

typescript {8-11,15} class TempFile implements Disposable { #path: string #handle: number constructor(path: string) { this.#path = path this.#handle = fs.openSync(path, 'w+') } [Symbol.dispose]() { // Close the file and delete it. fs.closeSync(this.#handle) } }

function doSomeWork() { using file = new TempFile('xxx') // use file… if (someCondition()) { // do some more work… return } // 会在“最后”自动执行你声明的 Symbol.dispose 方法 }

`dispose` 也支持异步(`asyncDispose`),更多请看官方文档,或者这篇博文[protogenesis: JavaScript-之-using-关键字](https://protogenesis.github.io/blogs/2023/09/05/JavaScript-%E4%B9%8B-using-%E5%85%B3%E9%94%AE%E5%AD%97.html)也写得很完善很详细(但是这篇博文介绍的是提案内容,可能不一定和 ts 当前实现完全一致)。

## 5.4 会保留闭包中的最后一次类型收紧

[ts 5.4 官方文档 #preserved-narrowing-in-closures-following-last-assignments](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html#preserved-narrowing-in-closures-following-last-assignments)

typescript {5} function formatUrl(url: string | URL) { if (typeof url === 'string') { url = new URL(url) } url.searchParams // ~~~~~~~~~~~~ } ```

过去这儿会报错,ts 认为 url 可能是 string.

现在 ts 会保留我们在 if 中对 url 的类型收紧(显然 url 一定是 URL)。

]]>
https://16px.cc/blog/T5I3LiLKGJmPhttps://16px.cc/blog/T5I3LiLKGJmPThu, 08 May 2025 17:13:56 GMT
<![CDATA[wsl 使用主机的代理]]>我们主机如果使用了代理,可能会配置 http_proxy=localhost:7890 之类的东西。此时 WSL 会报错:

wsl: 检测到 localhost 代理配置,但未镜像到 WSL。NAT 模式下的 WSL 不支持 localhost 代理。

WSL 在 NAT 模式下运行时,它自己的网络是隔离的,无法直接访问 Windows 主机上的 localhost。

此时我们需要在 WSL 中配置代理,ip 应该是主机的 ip。

配置脚本

主机 ip 可能会在网络环境变化的时候发生变化,所以需要一个脚本在 WSL 启动时获取主机 ip,并将其配置为 proxy

我们可以新建一个 set_proxy.sh 文件,填充以下内容。(保存后记得添加上可执行权限 chmod +x set_proxy.sh

#!/bin/bash

# 1. 获取 Windows 主机的 IP 地址
host_ip=$(ip route | grep default | awk '{print $3}')
# 假设你的代理端口是 1080
host_port=1080

# 检查是否获取到有效的主机 IP
if [[ -z "$host_ip" ]]; then
  echo "Error: Unable to detect the host IP. Ensure your WSL is properly configured."
  exit 1  # 如果没有找到主机 IP,就退出脚本
fi

# 2. 设置代理
export http_proxy="http://$host_ip:$host_port"
export https_proxy="http://$host_ip:$host_port"
export all_proxy="socks5h://$host_ip:$host_port"
# 大写也配置一下,提高兼容性
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"

# 3. log 当前设置的代理
echo "current proxy: $http_proxy"

# 4. 需要使用 `source ~/set_proxy.sh` 命令来使设置生效

# 5. 如果希望脚本在每次启动时自动运行,可以将上述命令放到 ~/.bashrc 或 ~/.zshrc 中
#    例如将 `[[ -f ~/set_proxy.sh ]] && source ~/set_proxy.sh` 放到 `~/.zshrc` 的恰当位置

clash for windows (以下简称 clash) 需要配置 allow LAN: true

allow LAN

遇到的问题

明明配置了 http_proxy, curl 就是不通,查看 clash 的 log 也没有。

clash 的 log

嗯嗯,请求根本就没到 host 机器。临时关闭防火墙试试,诶,通了。那我知道了,是防火墙的问题。

然后通过命令设置了允许 clash 的端口 1080 入站,但还是没用。

查看防火墙的所有入站规则,竟然发现有一条规则是禁止 clash 的所有端口,也不知道什么时候设置的。

把这条规则禁用后,就通了。

]]>
https://16px.cc/blog/mAA_Nkhitsyihttps://16px.cc/blog/mAA_NkhitsyiSat, 14 Jun 2025 05:39:30 GMT
<![CDATA[中山半日游]]>开篇吐槽

开篇就想吐槽一下,中山就跟我老家县城一样。本来以为广东这边处处繁华,没想到比惠州还拉,路上很多电瓶车,骑得很浪,从行走的车流中左右穿插,吓死个人。

缘起

上周周六(2025-03-08)又去了之前去过好几次的珠海的横琴国家湿地公园,吹着凉爽的海风很舒服,直到傍晚,直奔中山住了一夜,第二天去附近逛了半天,去了下面👇🏻这两处:

金钟水库

当时是普通的周日,可是里面停车还是停满了,车进不去,只能停路边(路边停满了车)。等我出来的时候车又能往里进了。。。

入口很长一段距离都乏善可陈,走了大概15-20分钟,到达水坝,风景才好起来,凉风习习,水面波光粼粼,不远处的山青翠茂密,很看得很。

(水坝不建议大夏天来,没有树荫遮挡,肯定晒爆。)

金钟水库山水照:站在水坝上拍的

可惜路边不知道为什么有一条大水渠,里面是黄色的水,看起来很恶心,不知道哪里来的。

我经过水坝后就往外走了,其实还能往里走,整个水库我看着实在太大了,下午要回家了,所以就没继续往里走了。

总体感觉 6 分吧。

中山市博物馆

或许是我很少逛博物馆的原因,感觉很满意,里面展品不少,但最让我感动的还是一些新石器时代的石器陶器,看着这些碎陶烂瓦,想象着古人的生活,简直浪漫到哭出来。(可惜我竟然一张照片都没拍,离谱)

启功题的字:中山市博物馆

馆藏水彩画:内容为农民赶着小毛驴

馆藏雕像:“论道”

馆藏家具陈设:“香邑人家”

]]>
https://16px.cc/blog/eorVYDLWYo_Dhttps://16px.cc/blog/eorVYDLWYo_DFri, 14 Mar 2025 11:50:06 GMT
<![CDATA[一个 bug 分析]]>逛 B 站看到某前辈的视频领导感觉这个bug在针对我【渡一教育】,分析了一个 bug, 我感觉分析的不够深入,因此在这儿简单分享一下我的看法。

bug 代码

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

/**
 * 模拟异步请求
 */
async function fetchCount(n: number) {
  await sleep(Math.random() * 1000)
  return n
}

let count = 0

async function addCount(n: number) {
  count = count + await fetchCount(n)
  console.log(count)
}

async function main() {
  // 如果此处这么调用,count 最终可能为 1 或 2,而不是 3
  addCount(1)
  addCount(2)
}

void main()

bug 描述

如果我们如上面 main 函数所述那么调用,则最终 count 可能为 1 或者 2,而不是我们期望的 3

前辈的分析

前辈说,

“在一个表达式里面,同步数据跟异步数据进行混合运算,不并发就没问题,一并发就会出问题。”

我认为这样的解释比较牵强比较浅。

我的分析

我在视频下面评论说,本质上是闭包问题。

我们按照时间顺序(代码执行顺序)来分析整个代码的执行:

第一轮,执行 addCount(1)addCount(2), 此时 count 值为 0, 因此:

  • addCount(1) 中的 count = count + await fetchCount(n) 被解释为 count = 0 + await fetchCount(1)
  • addCount(2) 中的 count = count + await fetchCount(n) 被解释为 count = 0 + await fetchCount(2)

之后就等待 fetchCount resolved

第二轮,我们假设 fetchCount(1)resolved, 此时我们观察上面可以发现 count = 0 + 1 即为 1

第三轮 此时 fetchCount(2) resolved, 此时我们观察上面可以发现 count = 0 + 2 即为 2

也就是说,由于 addCount(1)addCount(2) 是同步执行的,在他们各自的闭包(上下文)中,count 的值已经提前被“缓存”了,等到他们各自的 fetchCount resolved 的时候,count 的值已经“过期”了。

解决方案

既然我们需要避免 count 被“提前”“缓存”,那么只需要把 count 放在 await fetchCount(x) 后面就行了,有以下 2 种方案可选:

  1. count = count + await fetchCount(n) 改为 count = await fetchCount(n) + count 即可;

  2. main 函数改为:

async function main() {
  await addCount(1)
  await addCount(2)
}

不让 addCount(1)addCount(2) 同步执行,而是先执行 addCount(1), 得到结果,更新了 count 值之后,再执行 addCount(2), 这时候 addCount(2) 里面拿到的 count 就是最新的 count 了。

]]>
https://16px.cc/blog/yxhDDdzy4bmmhttps://16px.cc/blog/yxhDDdzy4bmmSat, 01 Mar 2025 00:34:47 GMT
<![CDATA[做了一个玩具:歌词编辑器]]>

不支持移动端

feature

  • 音频可以上传文件,也可以使用在线音频

  • 歌词可以上传文件,也可以自行输入

  • 支持歌词的编辑、删除、添加、插入、调整时间、调整歌词内容

  • 可以配合波形图,时间轴调整更加精准

  • 可以直接拖动时间轴来调整时间

  • 时间轴/波形图 支持缩放

跳转链接

截图

歌词编辑器.jpg

菜单.jpg

手动编辑歌词.jpg

]]>
https://16px.cc/blog/ag_JKRxNmEy9https://16px.cc/blog/ag_JKRxNmEy9Fri, 14 Feb 2025 00:50:59 GMT
<![CDATA[分享一个高速路上超越近距离并行龟速车的小技巧]]>如下图,当我们在高速路上,前方被龟速车挡住了,他们之间的距离又不够超车的时候,怎么办?

并排龟速车.jpg

你只需要行驶到他们两个中间的位置,如下图,然后开启左转向灯,等待几秒钟。

超车小技巧.jpg

80%的左车会加速超车驶离,19%的左车会减速拉开距离让你超车,只有1%的傻子才会维持现状。


警告

  1. 我们只是开启左转向灯,别直接穿插,很危险。等待左车采取措施(避让或加速驶离)之后,我们才能安全超车。

  2. 与前车控制车距,别跟太近,注意安全。

万一碰上了那1%的傻子,那就认命吧,老老实实跟在他们屁股后面,找安全的机会吧。

]]>
https://16px.cc/blog/vG-W09HXdUNnhttps://16px.cc/blog/vG-W09HXdUNnFri, 02 May 2025 05:02:49 GMT
<![CDATA[像使用本地数据一样使用远端的数据]]>

像使用 local value 一样使用 remote value

首先强调一遍,这个包是一个通信的包,不负责权限控制,需要使用者在业务层面进行恰当的处理,控制好什么该暴露,什么不该暴露。

demo code

解释

如果我们(暂且称为 甲 端)需要访问其他端(可能是服务器,也可能是其他 iframe or whatever,暂且称为 乙 端)的一些数据/对象/方法,我们通常是这么做的:

// 乙 端

const obj = {
  value: number,
  setValue: (newValue: number) => {
    obj.value = newValue
  }
}

register({
  method: 'get',
  name: 'getValue',
  handler: () => {
    return obj.value
  }
})

register({
  method: 'post',
  name: 'setValue',
  handler: (newValue: string) => {
    obj.setValue(newValue)
  }
})

// 甲 端

const value = await fetch.get('getValue')
await fetch.post('setValue', newValue)

有没有感觉在重复劳动?我 乙 端已经有了一个现成的obj,可是我还是需要在register处“封装”一下。


那么,下面这个怎么样:

// 乙 端

exposeToRemote(obj, config)

// 甲 端

import { type obj } from '/path/to/obj'

const remoteObj = remoteValue<typeof obj>(config)

// 很遗憾没能实现 await remoteObj.value 直接取值,
// 无论是值还是函数,必须在后面加一个括号调用,才能取到结果。
const value = await remoteObj.value()
const subValue = await remoteObj.deep.path.to.value()

await remoteObj.setValue(newValue)
await remoteObj.deep.path.to.func(...params)

再次强调一遍,这个包是一个通信的包,不负责权限控制,需要使用者在业务层面进行恰当的处理,控制好什么该暴露,什么不该暴露。

install

pnpm i @zimi/remote
]]>
https://16px.cc/blog/ayzpKzcTRMPjhttps://16px.cc/blog/ayzpKzcTRMPjMon, 26 May 2025 14:14:31 GMT
<![CDATA[CORS 请求不让跨域重定向哦]]>有没有试过在需要跨域的时候请求一个会重定向的请求?

请求 https://aaa.com, 响应是 307 重定向到 https://bbb.comaaa 允许跨域,bbb 也允许跨域,这 TM 也不行吗?

不行哦,还是会抛错,报跨域,看这个吧:CORS request external redirect not allowed

]]>
https://16px.cc/blog/Ii76pvPAP1CChttps://16px.cc/blog/Ii76pvPAP1CCTue, 10 Dec 2024 15:16:45 GMT
<![CDATA[珠海横琴国家湿地公园半日游]]>IMG_20241208_110739.jpg IMG_20241208_110641.jpg IMG_20241212_113513.jpg IMG_20241208_103749.jpg IMG_20241208_120109.jpg

我的目的地是这家黄焖鸡,湿地公园只是顺路捎带的,结果惊艳我了。很干净,天很蓝,海风无敌凉爽,不是那种咸湿的风,而是清爽的那种。可惜到中午要吃午饭了,不然能在那坐大半天。


PS: 中午吃完黄焖鸡,下午又过去坐了个把小时,风吹起来太爽了,可惜没有折叠床和被子,要不然我非要在那睡上一觉(我的椅子躺着不舒服睡不着T.T)

]]>
https://16px.cc/blog/rFFK2UWOAZmXhttps://16px.cc/blog/rFFK2UWOAZmXThu, 12 Dec 2024 03:37:39 GMT
<![CDATA[像调用本地函数一样调用远端的函数]]>

代码见 @zimi/remote

  • 本地可以是浏览器、服务器,甚至一些受限的 js 子集
  • 远端可以是任何终端,如 iframe / Java 服务器 等
  • 对远端响应的数据格式也不严格限制(可以集中解析)
  • 已在公司游戏前后端通信中应用,极大地降低了通信成本(简化调用)
  • ts 类型严格

install

pnpm i @zimi/remote

demo code

iframe 使用示例

http 使用示例

dao3 平台示例

examples

调用示意

// 远端
remote.register('something', async (params: Whatever) => {
  return WhatYouWant
})

// 本地
// res === WhatYouWant
const res = await remote._.something(xxx)

iframe 与父级相互调用

// 1. 声明各自能提供的函数类型
// type.d.ts

// 父级能提供的函数
export type FuncsFromParent = {
  plus: (data: [number, number]) => Promise<number>
}

// 子级能提供的函数
export type FuncsFromChild = {
  multiply: (data: [number, number]) => Promise<number>
}
// 2. 父级 remote 初始化
// parent.ts

import { Remote, createIframeAdaptor } from '@zimi/remote'

function getOpWindow() {
  return document.querySelector<HTMLIFrameElement>('#child-iframe')?.contentWindow
}

// 我们提供了生成 iframe adaptor 的工具函数
// 你也可以参考实现自己的 adaptor, 没多少代码
const adaptor = createIframeAdaptor({
  onEmit: (data) => {
    // 此处仅为示意,业务场景下应当限制对方的域名
    getOpWindow()?.postMessage(data, '*')
  },
})

const remote = new Remote<FuncsFromParent, FuncsFromChild>(adaptor, {
  deviceId: 'parent',
})

// 父级注册自己能提供的函数
remote.register('plus', async ([a, b]) => a + b)
// 3. 子级 remote 初始化
// child-iframe.ts

import { Remote, createIframeAdaptor } from '@zimi/remote'

function getOpWindow() {
  return window.top
}

// 我们提供了生成 iframe adaptor 的工具函数
// 你也可以参考实现自己的 adaptor, 没多少代码
const adaptor = createIframeAdaptor({
  onEmit: (data) => {
    // 此处仅为示意,业务场景下应当限制对方的域名
    getOpWindow()?.postMessage(data, '*')
  },
})

const remote = new Remote<FuncsFromChild, FuncsFromParent>(adaptor, {
  // 当涉及到多子级时,可以通过该 deviceId 来区分彼此,
  // 达到与不同子级通信的效果
  deviceId: 'child',
})

// 子级注册自己能提供的函数
remote.register('multiply', async ([a, b]) => a * b)
// 好了,现在你可以父子间随意通信了

// 对方所有函数都被代理到 remote._.xxx 上了

// parent.ts
// 父级中可以直接调用子级的函数
// 有严格的类型与提示
await remote._.multiply([3, 2])

await remote._.multiply([3, 2], {
  // 每个函数可以单独指定超时时间,超时后会抛出 RemoteTimeoutError
  timeoutMs: 1000,
  // 每个函数可以指定调用特定目标所有的函数(需要在 adaptor onEmit 中根据 targetDeviceId 往不同设备发送消息)
  targetDeviceId: 'child-2'
})

// 调用对方未注册的函数,会抛出 RemoteNotFoundError
await remote._.notRegisteredFunc()

// 当对方函数发生运行时错误时,会抛出 RemoteError
// 以上所有 error 都继承自 Error

浏览器与服务器通信

// 对方怎么写我们就不管了,假设对方返回的数据格式为:
interface JavaResponse {
  // 假设 code >= 300 为错误;code < 300 为成功
  code: number
  // 响应的数据
  data: unknown
  // 可能存在的错误信息
  errorMsg?: string
}

此时 adaptor 的 onEmit 函数稍微有些复杂,它需要解析服务端响应的数据,并封装为我们需要的格式:

const adaptor = createHttpAdaptor({
  onEmit: async (data) => {
    // 这里只是简单示意,使用者可以根据自己的情况构造 request
    const res = await fetch(`https://xxx.com/api/${data.name}`, {
      method: 'POST',
      body: JSON.stringify(data.data),
      headers: {
        'Content-Type': 'application/json',
      },
    })
    // 下面相当于我们代 server 端封装了一下数据
    // callbackName 是一定会有的,此处只是为了类型安全
    const callbackName = data.callbackName ?? 'IMPOSSIBLE_NO_CALLBACK_NAME'
    const adaptorData: AdaptorPackageData = {
      // 由于我们代 server 抛出事件
      // 所以这里的 deviceId 和 targetDeviceId 是相反的
      deviceId: data.targetDeviceId,
      targetDeviceId: data.deviceId,
      name: callbackName,
      // 我们在下面根据不同情况来填充 data
      data: null,
    }
    if (!res.ok) {
      adaptorData.data = response.error(new RemoteError('network error'))
    } else {
      const json = (await res.json()) as {
        code: number
        data: unknown
        errorMsg?: string
      }
      if (json.code < 300) {
        adaptorData.data = response.success(json.data)
      } else {
        const error = new RemoteError(`server error: ${json.errorMsg}`)
        // RemoteError 也接受 code, 你可以把服务端响应的错误码挂到其上,便于业务上区分处理
        error.code = json.code
        adaptorData.data = response.error(error)
      }
    }
    // 一定要抛出 every 事件,remote 包基于此处理远端的响应
    remoteEventManager.emit(remoteEventManager.EVERY_EVENT_NAME, adaptorData)
    remoteEventManager.emit(callbackName, adaptorData)
  },
})

// 由于服务端不会调用我们,所以我们无需提供函数,自然也无需调用 remote.register 注册函数
const remote = new Remote<{}, FuncsFromHttp>(adaptor, {
  deviceId: 'client',
})

// 使用方法同前
await remote._.xxx(anyData)

与其他端通信(如 websocket / web worker)略

你可以看看 iframe adaptorhttp adaptor 源码,包含空行也就 30 行,依葫芦画瓢很轻易就能写一个。

与 rpc 相比的优势

  • 不局限于与服务端的通信,无论对方是任何端,只要能与 js 通信,就能使用该包;
  • 相互通信,不存在“主从”的概念,通信双方是平等的;
  • 类型严格;
  • 包较底层,对项目整体的侵入较小,几乎不限制对方的响应的数据格式(因为可以自由解析对方的响应,即自由 emit);

协议

由于通信双方是平等的,所以 B 调用 A 的流程也是一样的

protocol.png

]]>
https://16px.cc/blog/WefATB_4YeEahttps://16px.cc/blog/WefATB_4YeEaThu, 16 Jan 2025 15:17:03 GMT
<![CDATA[docker 构建时报 403]]>问题

我的 Dockerfile 如下:

FROM ubuntu:22.04 AS base

# ...

执行 docker build -f Dockerfile -t my_custom_ubuntu_22 --progress plain . 时报错:

------
 > [internal] load metadata for docker.io/library/ubuntu:22.04:
------
failed to solve with frontend dockerfile.v0: failed to create LLB definition: unexpected status code [manifests 22.04]: 403 Forbidden

解决方案

最后解决方案是, 别管他, 先 docker pull ubuntu:22.04, 然后再执行 docker build ..., 这样就不会报错了。

注意

docker pull ubuntu:22.04 里面的 ubuntu:22.04 必须与 Dockerfile 里面声明的完全一致, 不能写成 ubuntu:jammy, 否则还是会报错。

]]>
https://16px.cc/blog/h2O7BL7AtBDzhttps://16px.cc/blog/h2O7BL7AtBDzTue, 22 Oct 2024 03:50:28 GMT
<![CDATA[前端优化之避免桶文件 barrel file]]>什么叫桶文件

我不知道你们有没有这种习惯,我是喜欢把一些相似的东西放在同一个文件里,例如:

// src/svg/index.ts
export { default as SvgA } from './a.svg'
export { default as SvgB } from './b.svg'
// ...

这种文件叫做桶文件(barrel file)。

然后用的时候就:

import { SvgA } from '@/svg'

桶文件的优点

这样 import 的时候有提示,省事;以及统一在一个地方命名,尤其多人合作的时候,能避免同一个 svg 文件导出不同的名字,如:

// 小王这么写
import HelloWorld from 'hello-world.svg'

// 小李这么写
import SvgHelloWorld from 'hello-world.svg'

// 小马这么写
import HelloWorldSvg from 'hello-world.svg'

桶文件的缺点

桶文件是损害了性能的,例如,A 页面只用到了 SvgA,但是打包的时候 SvgB 也被打包进去了,所以我们要避免这种情况。

解决办法

解决方法当然是,不要用这个桶文件,直接引用具体的文件,例如:

// src/pages/a.tsx
import SvgA from '@/svg/a.svg'

// src/pages/b.tsx
import SvgB from '@/svg/b.svg'
]]>
https://16px.cc/blog/UIbhqlBIrxdthttps://16px.cc/blog/UIbhqlBIrxdtMon, 09 Sep 2024 18:05:33 GMT
<![CDATA[对问路的老人家帮助得再深一点]]>8月19日,我遇到一个老人家问路,问我XXX怎么走。我跟她指了方向后,还跟她聊了好一阵。她的话语流利,思维清晰,看不到一丝异常。在半个月后的今天,我在群里面看到一则寻人启事,正是这个老人家,老年痴呆。

我怀着“万一有用呢”的心态,打了个电话过去问一问并说曾经见过她。幸好,对方说人找到了。

我当时怎么就没想过,这个老人家是回家,这个老人家找不到回家的路了呢?

抱歉。

]]>
https://16px.cc/blog/7-ODpHtgTaJFhttps://16px.cc/blog/7-ODpHtgTaJFWed, 04 Sep 2024 02:04:36 GMT
<![CDATA[解决 WSL 无法使用 sudo]]>背景

WSL 中使用 sudo 报错

sudo: unable to stat /etc/sudoers: Permission denied
sudo: no valid sudoers sources found, quitting
sudo: unable to initialize policy plugin

使用 wsl -u root 启动 WSL 仍报错,即,报错时,whoamiroot/etc 权限为:

drwx------   23 root     root          4096 Jul 28 03:22 etc

解决

  1. /etc 目录权限改为 755
   chmod 755 /etc
  1. 编辑 /etc/sudoers,添加如下内容:
   %sudo  ALL=(ALL) ALL

问题解决

]]>
https://16px.cc/blog/E9W7XgRXUIYuhttps://16px.cc/blog/E9W7XgRXUIYuTue, 30 Jul 2024 11:34:56 GMT
<![CDATA[ffmpeg 学习笔记]]>-f 强制格式(一般能自动识别, 无须指明)

-i 输入

-y 覆盖

-stream_loop 循环次数(0 表示不循环, -1 表示无限循环)

-c -codec 编解码器

-t duration(和 -to 互斥, 且优先级比 -to 高)

-to 到某个时间为止

-ss-to 相反, 从某个时间开始

-sseof 输入参数, 相对于文件结尾的相对位置, 负值

-r frame rate 帧率(作为输入参数时请使用 -framerate)

-s frame size 输出帧尺寸

-aspect 视频尺寸比例

-frames:v 输出帧

-codec:v 视频编码

-filter:v 视频滤镜

-vf scale=960:-1 将视频scale

-pattern_type glob 可以设置输入文件名的匹配模式为glob模式, 例如 ffmpeg -pattern_type glob -framerate 12 -i "foo-*.jpg" -s WxH foo.mp4

]]>
https://16px.cc/blog/fpID-q1NVAXWhttps://16px.cc/blog/fpID-q1NVAXWSat, 15 Jun 2024 05:57:18 GMT
<![CDATA[nginx 学习笔记]]>location

location 文档

syntax: location [ ~ | ~* | ^~ | = ]  /uri/ { ... }
  1. 无前缀

    • literal string 匹配
    • 匹配成功后仍会进行后续匹配, 如果后续存在优先级更高的匹配, 则会采用优先级高的匹配, 不存在才会采用该匹配
    • 匹配优先级见官网文档 ngxhttpcore_module #location
  2. ~~*

    • 正则匹配
    • 匹配成功后仍会进行后续匹配, 如果后续存在优先级更高的匹配, 则会采用优先级高的匹配, 不存在才会采用该匹配
    • ~: 大小写敏感
    • ~*: 大小写敏感
  3. ^~

    • 区别于 ~, 匹配成功则立即终止匹配
  4. =

    • 仅匹配 exact query
    • 匹配成功则立即终止匹配

alias

alias 文档

location /i/ {
  alias  /path/to/images/;
}

# 剩余 location 直接拼接到 alias
# request /i/top.gif
# return  /path/to/images/top.gif

alias 指令不能用在指定正则的 location 中; 指定正则的 location 中需要使用 rewrite + root;

root

root 文档

location /i/ {
  root /path/to/images;
}

# 整个 location 直接拼接到 root
# request /i/top.gif
# return  /path/to/images/i/top.gif

rewrite

rewrite 文档

  1. rewrite 位于 server 下时, 可以使用 last 标记来中止 rewrite; nginx server { # ... rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 last; rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra last; return 403; # ... }
  2. rewrite 位于 location 下时, 需要把 last 换成 break; nginx location /download/ { rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 break; rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra break; return 403; }
  3. replacement 包含新的 arguments 时, 需要在结尾添加 ?, 因为原 arguments 会直接拼接到 replacement 后面 nginx rewrite ^/users/(.*)$ /show?user=$1? last;

proxy_pass

proxy_pass 文档

  1. proxypass 最后有 "/", 则将 剩余 location 拼接到 proxypass
  2. proxy_pass 最后没有 "/":
    • 如果结尾是 host, 则将 整个 location 拼接到 proxy_pass
    • 如果结尾是 path, 则将 剩余 location 拼接到 proxy_pass

try_files

try_files 文档

location / {
  try_files index.html index.htm @whatever_fallback;
}

location @whatever_fallback {
  root /var/www/error;
  index index.html;
}
  1. @whatever_fallback 是具名location(named location)
  2. 尝试检测指定文件是否存在, 返回第一个存在的文件;
  3. 如果都不存在, 将会调用 @fallback location;

部分内置变量

部分内置变量 文档

  1. $args(等同于 $query_string) > 请求参数
  2. $request_filename > 基于 root / aliasrequest URI 的当前 file path
  3. $request_uri > 包括 arguments 的完整初始 uri(complete initial URI)
  4. $uri > 当前 uri > 不同于initial uri, 因为可能经过了内部重定向

tips

  1. It is important to know that nginx does the comparison against decoded URIs. For example, if you wish to match /images/%20/test, then you must use /images/ /test to determine the location.
]]>
https://16px.cc/blog/maRde2B9KnaQhttps://16px.cc/blog/maRde2B9KnaQWed, 17 Jul 2024 13:24:34 GMT
<![CDATA[昨天突然接到一个私人电话,说是网安大队的]]>昨天突然接到一个私人电话,说是网安大队的,单纯跟我确认我有没有备案、以及服务器在哪里。是准备万一我说了什么不合适的,直接就给我一锅烩了吗。。。我这么单纯的一个技术分享网站。。。

]]>
https://16px.cc/blog/ilYGuH_PAYxthttps://16px.cc/blog/ilYGuH_PAYxtThu, 23 May 2024 05:39:48 GMT
<![CDATA[竟然会有人,宁愿相信人,也不相信程序]]>是人就会犯错,而且可能会一遍一遍地犯同一个错误。

固然程序也会犯错,但是程序的错误是可以复现且修复的,而且,修复后,它就不会再犯了。所以能用程序的,我都会用程序实现/自动处理。我不相信人,不单他人,包括自己。

]]>
https://16px.cc/blog/nIAfNN-fpt7fhttps://16px.cc/blog/nIAfNN-fpt7fWed, 22 May 2024 11:39:15 GMT
<![CDATA[使用 scroll-margin 优化页面内滚动]]>问题

当需要某个元素平滑滚动到顶部时,我们会使用:

targetElement.scrollIntoView({ behavior: 'smooth' })

然而,当我们有一个固定的头部时,这样就有问题了——元素确实滚动到顶部了,可是,被头部遮挡了。

解决方案:scroll-margin

这时候,我们就需要 css 的 scroll-margin 来帮忙了。

/* css */
targetElement {
  /* 假设头部的高度为 40px */
  scroll-margin: 40px;
}

这时候再调用 element.scrollIntoViewelement 会自动停到距离顶部 40px(上面声明的值)的位置,问题解决。

兼容性

Can I use: scroll-margin?


扩展

以下内容需要配合 scroll-snap-type 使用,建议先了解 scroll-snap-type

下面的各个属性,建议直接浏览对应的 MDN 文档,毕竟人家还有在线 demo。

scroll-padding

看名字就知道,scroll-margin 用于目标元素上,指示元素滚动到目标位置且保留 "margin"。我们看到 margin 就会想到 padding,确实,还有 scroll-paddingscroll-padding 如其名,用于滚动容器上。

我们知道,当我们的滚动容器设置 scroll-snap-typemandatory 时[^1],滚动起来就像轮播一样,会“吸附”。这时候我们在滚动容器上定义一个 scroll-padding,效果就如同在子元素上定义了 scroll-margin 一样了。

[^1]: 子元素需要设置 scroll-snap-align

scroll-x-inline & scroll-x-block

scroll-x-inline

scroll-snap-typemandatory 且子级为表现为 inline 时,使用 scroll-x-inline,即:

  • scroll-margin-inline
  • scroll-padding-inline

scroll-x-block

scroll-snap-typemandatory 且子级为表现为 block 时,使用 scroll-x-block,即:

  • scroll-margin-block
  • scroll-padding-block

scroll-xx-direction

  • scroll-xx-start
  • scroll-xx-end
  • scroll-xx-top
  • scroll-xx-bottom
  • scroll-xx-left
  • scroll-xx-right
]]>
https://16px.cc/blog/XhMYxwOtfix5https://16px.cc/blog/XhMYxwOtfix5Mon, 06 May 2024 16:21:17 GMT
<![CDATA[[译文] React 19 Beta 中文翻译]]>

原文链接:React 19 Beta

该文在一些难翻译的细节用词上会采用意译,其中会掺杂一些译者的个人理解,建议阅读原文,以获得更准确的信息。

以下为译文


React 19 Beta 现已在 npm 上发布!这篇文章概述 React 19 中的新功能及其使用。

2024 年 4 月 25 日,React 团队

注意

该 Beta 版本是为了让包开发者(libraries)为 React 19 做准备。普通开发者(App developers)应当升级 18.3.0 并等待 React 19 稳定,因为我们会跟包开发者(libraries)合作并根据反馈进行更改。

React 19 Beta 现已在 npm 上发布!

在我们的 React 19 Beta 升级指南 中,我们分享了将应用程序升级到 React 19 Beta 的分步说明。在这篇文章中,我们将概述 React 19 中的新功能及其使用。

破坏性更新(breaking changes)列表,请查看React 19 Beta 升级指南:Breaking changes

React 19 的新增功能

Actions

React 应用程序中的一个常见用例是,数据变更(mutation),驱动状态更新[^1]。例如,当用户提交表单更新姓名时,你会发送 API 请求,然后处理响应。过去,你需要手动处理 pending、errors、乐观更新(optimistic updates)和顺序请求(sequential requests)。

[^1]: 原文 perform a data mutation and then update state in response.

例如,你可能会使用 useState 来处理 pending 和 errors:

```jsx {5,8,10} // 有 Actions 之前 function UpdateName({}) { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, setIsPending] = useState(false);

const handleSubmit = async () => { setIsPending(true); const error = await updateName(name); setIsPending(false); if (error) { setError(error); return; } redirect("/path"); };

return (

setName(event.target.value)} /> {error &&

{error}

}
); }

在 React 19 中,我们支持在 transitions 中使用异步函数,以自动处理 pending、errors、表单和乐观更新。

例如,你可以使用 `useTransition` 来处理 pending:

jsx {5,8,15} // 使用 Actions 更新 pending function UpdateName({}) { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition();

const handleSubmit = () => { startTransition(async () => { const error = await updateName(name); if (error) { setError(error); return; } redirect("/path"); }) };

return (

setName(event.target.value)} /> {error &&

{error}

}
); }

异步 transition 会立即将 `isPending` 状态设置为 true,发送异步请求,并在任何 transitions 后将 `isPending` 切换为 false。这样可以在数据更改时,保持当前 UI 的响应能力和交互性。

> 译者注:其实这个例子看不出和 `useState` 相比的优势。建议浏览 [useTransition](https://zh-hans.react.dev/reference/react/useTransition) 查看 `useTransition` 的应用。

> 注意
>
> 按照惯例,使用异步 transitions 的函数称为“Actions”。
> Actions 会自动管理提交数据(submitting data):
> - pending 状态:Actions 提供 pending 状态,该状态在请求开始时启动(`译者注:置为 true`),并在所有状态更新都提交(committed)后自动重置。
> - 乐观更新:Actions 支持新的 [useOptimistic](#user-content-新-hook-useOptimistic) hook,你可以在提交请求时给用户即时反馈。
> - 错误处理:Actions 提供错误处理,以便你可以在请求失败时展示 Error Boundaries,并自动将“乐观更新”恢复为其原始值。
> - 表单:`<form>` 的 action 和 formAction 属性现在支持函数了。将函数传递给 action 属性会默认使用 Actions,并在提交后自动重置表单。

React 19 构建于 Actions 之上,引入了 [useOptimistic](#user-content-新-hook-useOptimistic) 来管理乐观更新,并引入了新 hook [React.useActionState](#user-content-新-hook-useActionState) 来处理 Actions 的常见情况。在 `react-dom` 中,我们添加了 [\<form\> Actions](#user-content-React-DOM-form-Actions) 来自动管理表单,并添加了 [useFormStatus](#user-content-React-DOM-新-hook-useFormStatus) 来支持表单中操作的常见情况。

在 React 19 中,上面的例子可以简化为:

jsx // 使用

Actions and useActionState function ChangeName({ name, setName }) { const [error, submitAction, isPending] = useActionState( async (previousState, formData) => { const error = await updateName(formData.get("name")); if (error) { return error; } redirect("/path"); return null; }, null, );

return ( {error &&

{error}

}
); }

在下一节中,我们将详细介绍 React 19 中的每个新 Action 功能。

### 新 hook:useActionState

为了使 Actions 易于处理常见情况,我们添加了一个名为 `useActionState` 的新 hook:

jsx const [error, submitAction, isPending] = useActionState( async (previousState, newName) => { const error = await updateName(newName); if (error) { // 该 action 你可以返回任何结果 // 该示例我们只返回一个 error return error; }

// 此处可以处理成功的情形
return null;

}, null, );

`useActionState` 接受一个函数(即 “Action”),并返回一个包装过的 Action 来用于调用(译者注:即上面示例中的 `submitAction`)。`This works because Actions compose.` 当调用包装过的 Action 时, `useActionState` 将返回 Action 的最后结果作为 data ,并将 Action 的挂起状态返回作为 pending 。

> 注意
>
> `React.useActionState` 在之前的 Canary 版本中称为 `ReactDOM.useActionState`,但我们已经重命名并弃用了 `ReactDOM.useActionState`。
>
> 更多详细信息,请参阅 [#28491](https://github.com/facebook/react/pull/28491)。

### React DOM: \<form\> Actions

Actions 还集成到了 React 19 的 `react-dom` 新 `<form>`中。 `<form>` 、`<input>` 和 `<button>` 的 `action` 和 `formAction` 属性,支持传递函数,以使用 Actions 自动提交表单。

jsx

当 `<form>` Action 成功时,React 会自动重置[非受控表单组件](https://zh-hans.react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components)。如果你需要手动重置 `<form>` ,可以调用新的 `requestFormReset` React DOM API。

> 译者注:没找到 `requestFormReset` 这个 React DOM API,可能是指 [form.reset()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset)?

更多详细信息,请参阅 react-dom 文档中的 [\<form\>](https://zh-hans.react.dev/reference/react-dom/components/form)、[\<input\>](https://zh-hans.react.dev/reference/react-dom/components/input) 和 `<button>`。

### React DOM: 新 hook:useFormStatus

在系统设计时,我们通常会设计,使得组件能直接访问当前所处的 `<form>` 的信息,而无需使用 props 逐层传递。这可以通过 Context 来完成,但为了简化该常见情形,我们添加了一个新的 hook `useFormStatus`:

jsx import {useFormStatus} from 'react-dom';

function DesignButton() { const {pending} = useFormStatus(); return }

实现不复杂,直接看**全部代码**

tsx import { clamp } from 'lodash-es' import { useCallback, useState } from 'react'

const InfiniteTimeout = 2 ** 31 - 1

/**

  • 同一个 withLoading 可以包装多个函数,

  • 同一个 withLoading 包装的所有函数全部执行完毕后才会关闭 loading; *

  • @usage

  • ```tsx

  • function Comp() {

  • const [ loading, withLoading ] = useLoading() *

  • const { data } = useSWR('xxx', withLoading(asyncTask1)) *

  • return <>

  • *

  • <Button

  • onClick={withLoading(asyncTask2)}

  • >

  • async task

  • }

  • ``` */ export function useLoading() { const [flag, setFlag] = useState(0)

    /**

  • @param fn 需要包装的函数

  • @param delayMs 延迟显示 loading (!!! 而非延迟执行函数) */ const withLoading = useCallback( ( fn: (…args: Arg) => Res | Promise, delayMs = 500 ) => async (…args: Arg) => { let timer = -1 if (delayMs > 0) { timer = +setTimeout(() => { timer = -1 setFlag((prev) => prev + 1) }, clamp(delayMs, 0, InfiniteTimeout)) } else { setFlag((prev) => prev + 1) } try { return await fn(…args) } finally { clearTimeout(timer) if (timer < 0) { setFlag((prev) => prev - 1) } } }, [] )

    return [ loading: flag > 0, withLoading, ] as const }

```


后记:代码的实现很简单,但我感觉从代码风格上来说,由原来的命令式,改为了声明式,更 "react" 了,所以感觉很有意义,就此记录。~~(当然不会说是为了水一篇文了)~~

]]>
https://16px.cc/blog/rhCL8zAniBX_https://16px.cc/blog/rhCL8zAniBX_Fri, 12 Jan 2024 01:29:58 GMT
<![CDATA[极简 js 入门]]>需要下载的东西:

编辑器, 写代码用的
sublime / notepad++ 等现代编辑器都行
windows 自带的记事本不行

  • node js 的环境, 是一个命令行工具

起步

  1. 新建一个项目目录

最好起全英文名, 别带中文, 别带空格
例如 hello-worldjs-testhello (分割单词使用中划线 下划线都行, 看自己喜好)

  1. 在刚刚那个目录里面新建一个 js 文件

一般叫 index.js 或者 main.js
用上面下载的 vscode, 别用 windows 自带的记事本

  1. 在里面写些代码并保存

比如说

   console.log('hello world')
  1. 执行

在项目目录下打开命令行工具, 执行 node index.js, 应该就能看到打印出来的 hello world

进阶

  • 去网上找些案例, 跟着做一做
  • 把项目保存到 github(或 gitee), 并使用 git 管理项目
  • 使用 typescript

有用的站点

可以在 https://github.com 上面新建一个自己的账号, 代码可以保存在里面。上面说的 git 就可以用于管理 github 上的项目的。(如果实在没有梯子, https://gitee.com/ 也勉强能用)

大佬

大佬

脚手架, 能帮助我们更快、更便捷地构建项目
建议学习 js 半个月之后再接触

一个前端的框架
如果想要学习前端就可以学学
建议学习 js 半个月之后再接触

]]>
https://16px.cc/blog/9yQot2vnXJVihttps://16px.cc/blog/9yQot2vnXJViFri, 03 Nov 2023 07:54:41 GMT
<![CDATA[小计一次 CLS 优化]]>网站 CLS(Cumulative Layout Shift) 评分一直在 0.7+, lighthouse 评分一直在 80 分上下, 然后就尝试找哪里有问题。

首先运行 lighthouse 评分, 找到 performance 下的 CLS 选项, 看到主要两处影响了 CLS 评分:

  1. 第一处是所有文章的列表, 是一个 MUI Grid 组件

当初文章列表打算做响应式, 就用了 MUI 的 Grid 组件, 后来决定列表不响应了, 但是感觉 Gird 还能用于纵向的 space, 就懒得换了。 可是这是 MUI 啊, 难道 MUI 还能影响 CLS? 不, 我不相信!!!

然而看 CLS 的截图, 明确指向了 Grid 组件, 之前看过 Grid 组件实现原理就是负 margin, 难道是负 margin的问题? 一搜关键字, "mui Grid CLS", "negative margin CLS", 好几个 issue 指向了它:

  1. 第二处是所有标签(tag)的列表, 是一个 object 元素

由于我希望在任何地方, 标签(tag)都能点击跳转到标签详情页(即使在另一个大的 a 标签下面), & 我希望使用原生的 a 标签, 以保证在禁用 js 的情况下仍能跳转, 然而 a 标签不能嵌套, 查了查, 哦, 可以用一个 object 标签包裹住 Tag 组件, 就用了。

CLS 截图指示 Tag 组件影响了评分, 可是它只是一个行内文本, 带了点 padding 和 margin, 它什么也没干啊, 它也没有负 margin, 为什么说它影响了评分啊, 不, 我不相信!!!

可是突然回想起前些天处理 tailwind preflight.css 时, 发现 tailwind preflight 把 object 和 img 放在了一起, 并将他们的默认样式设置为了 display: block;, 会不会, object 和 img 有差不多的性质?

一看 MDN, 果然:

HTML <object> 元素(或者称作 HTML 嵌入对象元素)表示引入一个外部资源,这个资源可能是一张图片,一个嵌入的浏览上下文,亦或是一个插件所使用的资源。

它引用一个外部资源, 那他必然不知道那个外部资源有多大, 而它里面的文字是标签的 name, 有长有短, 就必然导致 object 产生偏移。是啦, 说干就干, 标签 name 固定一下长度, object 也设置一下宽度, 搞定。

重新跑一下 lighthouse, 99 分, 啊, 多么美丽的绿色(是不是…有哪里不对)

]]>
https://16px.cc/blog/9M7HgvNI--R3https://16px.cc/blog/9M7HgvNI--R3Fri, 28 Jul 2023 17:05:36 GMT
<![CDATA[HTML 邮件需要注意的点]]>优秀参考

XHTML 1 规范与 HTML 4 的差异

翻译自 https://www.w3.org/TR/xhtml1/

  1. 文档格式必须正确

    正确: 嵌套元素

   <p>这是强调 <em>段落</em>。</p>

错误: 重叠元素

   <p>这是强调 <em>段落</p>。</em>
  1. 元素名和属性名必须小写

    li 而非 LI

  2. 非空元素必须要结束标签

    正确

    <p>这是一段。</p><p>这是另一段。</p>
    

    错误

    <p>这是一段。<p>这是另一段。
    
  3. 所有属性值必须使用引号

    正确

    <td rowspan="3">
    

    错误

    <td rowspan=3>
    
  4. 不支持属性最小化(Attribute Minimization)

    正确

    <input checked="checked" />
    

    错误

    <input checked />
    
  5. 空元素必须有结束标签

    正确

    <br /><hr />
    

    错误

    <br><hr>
    
  6. 处理属性值中的空白

    根据 XML 属性值归一化 的标准:

    • 去除前导和尾随空白字符
    • 词间的一个或多个空白字符(包括换行符)将替换为一个 space
  7. script 和 style 元素(其实在邮件中是用不上 script 的)

    在 XML 中, <& 被视为标记的开始, 而 &lt;&amp; 会被转义为 <&. 所以如果你不希望他们被转义, 可以用 CDATA 包裹他们, 或者引用外部文档

    <script type="text/javascript">
      <![CDATA[
      ... unescaped script content ...
      ]]>
    </script>
    
  8. SGML 排除项

    SGML 禁止某些元素嵌套, XML 中没有这些禁令, 但仍表示这些元素不应嵌套

    • a 禁止包含其他 a
    • pre 禁止包含其他 img, object, big, small, sub, sup
    • button 禁止包含其他 input, select, textarea, label, button, form, fieldset, iframe, isindex
    • label 禁止包含其他 label
    • form 禁止包含其他 form
  9. 元素的 idname 属性

    HTML 4 为 a, applet, form, frame, iframe, img, map 定义了 name 属性, 同时也有 id 属性, nameid 都是用作片段标识符

    而 XML 仅使用 id 作为标识符, 且必须文档内唯一(id不能重复), 因此出于兼容性考虑, 当上述元素需要定义标识符时, 必须使用 id(而非 name)

    同时需要注意, 上述元素的 name 属性在 XHTML 1.0 中已被正式弃用, 并将在 XHTML 的后续版本中删除。

  10. 属性枚举值

    HTML 4 中, 属性枚举值不区分大小写(如 inputtype 属性, 值 TEXT 等同于 text), 但在 XML 中区分大小写, XHTML 1 中定义为小写

  11. 16 进制实体引用(Entity references)必须使用小写

    &#xnn; 而非 &#XNN;

  12. 空元素的尾随 / 前面要加一个空格

    • <br /> 而非 <br/>
    • 使用 <br /> 而非 <br></br> (因为后者, 用户代理可能给出不确定的结果, 即不同浏览器的解析结果可能不一致)
  13. 内容模型(content model)非空的元素, 别使用 minimized form

    <p> </p> 而非 <p />

  14. 如果 scripts or style sheets 脚本中有用到 <, &, ]]>, -- 时, 使用外部脚本

    XML 允许用户代理删除注释, 因此你想将他们放在注释中来做兼容也是行不通的

  15. 避免在属性值中使用换行符和多个空白字符, 用户代理处理这些情况是不一致的

  16. 同时使用 langxml:lang 时, xml:lang 的优先级更高

  17. 片段标识符

    标识符用于在 URI 结尾来引用元素, 如 #foo 会引用到 标识符的值为 foo 的元素。

    • XML 中以 id 作为标识符, HTML 4 以 name 作为标识符, 建议对支持的元素, 同时使用 idname, 以做到最大兼容。
    • 需要注意, XHTML 1.0 中 a, applet, form, frame, iframe, img, mapname 属性已弃用, 并将在 XHTML 的后续版本中删除。
  18. 在属性值(和其他地方)中使用 & 符号需要转义 (存疑)

    文档表示, 当元素 href 属性带有 & 符号时, 需要转义, 如:

    • 正确: http://my.site.dom/cgi-bin/myscript.pl?class=guest&amp;name=user
    • 错误: http://my.site.dom/cgi-bin/myscript.pl?class=guest&name=user

    我是有些存疑的, 那样还能访问到正确的地址吗? 待测试

  19. 一些在 HTML 文档中合法的字符, 在 XML 文档中是非法的。

    例如, 在 HTML 中, Formfeed 字符 (U+000C) 被视为空白, 在 XHTML 中, 由于 XML 对字符的定义, 它是非法的。

  20. 撇号 不应该使用 &apos; 而应该使用 &#39; > - 命名字符引用 &apos; (撇号 U+0027: ') 是在 XML 1.0 中引入的, 但没有出现在 HTML 中。因此, 应该使用 &#39; 而不是 &apos;, 才能在 HTML 4 用户代理中按预期工作。 > - 我就遇到过, 产品表示部分用户收到的邮件标题直接出现了 &apos;, 体验非常不好, 而 &#39; 是否可以避免这个问题? 待测试

]]>
https://16px.cc/blog/mz9sNneP9r8chttps://16px.cc/blog/mz9sNneP9r8cFri, 28 Jul 2023 17:04:18 GMT