<![CDATA[王小明博客]]>https://16px.cc/RSS for NodeSat, 19 Apr 2025 14:31:44 GMT60<![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%的傻子,那就认命吧,老老实实跟在他们屁股后面,找安全的机会吧。

]]>
https://16px.cc/blog/vG-W09HXdUNnhttps://16px.cc/blog/vG-W09HXdUNnSun, 05 Jan 2025 03:34:28 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/ayzpKzcTRMPjThu, 16 Jan 2025 15:44:54 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