<![CDATA[王小明博客]]>https://16px.cc/RSS for NodeWed, 11 Dec 2024 22:09:22 GMT60<![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_20241208_110305.jpg IMG_20241208_103749.jpg IMG_20241208_120109.jpg

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


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

]]>
https://16px.cc/blog/rFFK2UWOAZmXhttps://16px.cc/blog/rFFK2UWOAZmXSun, 08 Dec 2024 15:52:04 GMT
<![CDATA[像调用本地函数一样调用远端的函数]]>

代码见 @zimi/remote

  • 本地可以是浏览器、服务器,甚至一些受限的 js 子集
  • 远端可以是任何终端,如 iframe / Java 服务器 等
  • 对远端响应的数据格式也不严格限制(可以集中解析)

install

pnpm i @zimi/remote

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_4YeEaWed, 11 Dec 2024 02:07:37 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