<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ray-D-Song&apos;s Blog</title><description>我正在和日常战斗</description><link>https://ray-d-song.com/</link><item><title>C 与 Go 的编译期控制</title><link>https://ray-d-song.com/blog/2026/02/c-and-go-compile-controll</link><guid isPermaLink="true">https://ray-d-song.com/blog/2026/02/c-and-go-compile-controll</guid><pubDate>Wed, 04 Mar 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;编写软件时一个常见的需求，就是 Debug 版本打印更多调试信息，而正式版不包含。&lt;/p&gt;
&lt;p&gt;实现这个效果有两种做法，一种是&lt;strong&gt;运行时控制&lt;/strong&gt;，通过环境变量，配置文件来决定是否要调用调试函数。&lt;/p&gt;
&lt;p&gt;另一种是在编译时就根据编译的条件来生成两份代码，这种叫做&lt;strong&gt;编译时控制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;编译时控制的好处是，不需要的代码一开始就不会出现在可执行文件中，产物体积更小，性能更好，除非运行时需要动态打开开关，不然大多数时候是最优解。&lt;/p&gt;
&lt;h3&gt;C 语言的编译期控制&lt;/h3&gt;
&lt;p&gt;C 语言的编译期控制主要依赖编译宏。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个名为 DEBUG 宏，值为 1
#define DEBUG 1

// 如果 DEBUG 宏被定义（无论值是多少），则编译下面代码
#ifdef DEBUG
printf(&quot;value=%d\n&quot;, x);
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;可以看到，通过编译宏，C 语言可以做到代码块级别的编译控制。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一般 C 项目还会搭配 Makefile 和 CMake，使用这类构建系统时，就不需要在代码中硬编码宏 define。&lt;/p&gt;
&lt;p&gt;Makefile 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CC = gcc
CFLAGS = -Wall

TARGET = app
SRC = main.c

# build 命令的产物会包含 Debug 代码
build:
	$(CC) $(CFLAGS) -DDEBUG $(SRC) -o $(TARGET)        # 这里的 -DDEBUG 等价于 #define DEBUG 1，这时代码中就不需要再写 `#define DEBUG 1`

# buildprod 命令的产物不包含 Debug 代码
buildprod:
	$(CC) $(CFLAGS) $(SRC) -o $(TARGET)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CMakeLists 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cmake_minimum_required(VERSION 3.16)
project(app C)

add_executable(app main.c)

# Debug 构建时定义 DEBUG（等价于 -DDEBUG 或 -DDEBUG=1）
target_compile_definitions(app PRIVATE $&amp;lt;$&amp;lt;CONFIG:Debug&amp;gt;:DEBUG=1&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Debug（有 DEBUG）
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build

# Release（无 DEBUG）
cmake -S . -B buildprod -DCMAKE_BUILD_TYPE=Release
cmake --build buildprod
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 语言的编译期控制&lt;/h3&gt;
&lt;p&gt;Go 语言的&lt;strong&gt;编译期控制&lt;/strong&gt;主要通过 &lt;strong&gt;Build Tag&lt;/strong&gt;（也称为 &lt;strong&gt;构建约束，Build Constraints&lt;/strong&gt;）来实现。&lt;/p&gt;
&lt;p&gt;最常见的使用场景是：&lt;strong&gt;针对不同的操作系统平台提供不同的实现代码&lt;/strong&gt;。在这种情况下，通常会为同一功能创建多个平台对应的源码文件，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file_linux.go
file_windows.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go 在编译时会根据目标平台，自动选择符合条件的文件进行编译。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//go:build linux

//该文件仅在构建目标为 Linux 平台时才会被编译。如果编译目标是 Windows 或其他平台，这个文件会被自动忽略。
package main

func platform() string {
    return &quot;Linux&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，Go 的 &lt;strong&gt;build tag 只能控制“文件是否参与编译”&lt;/strong&gt; ，而 &lt;strong&gt;C 的&lt;/strong&gt;  &lt;strong&gt;&lt;code&gt;#ifdef&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;可以控制代码块，&lt;/strong&gt; Go 的控制粒度要粗的多。&lt;/p&gt;
&lt;p&gt;所以 Go不能像 C 那样在函数内部写条件编译，但可以通过 文件拆分 + build tag 达到类似效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//go:build debug

package main

func DebugPrint(x int) {
    fmt.Printf(&quot;value=%d\n&quot;, x)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//go:build !debug

package main

func DebugPrint(x int) {} //这里只有空实现
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;go build -tags debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在第二个文件中，我们只留了一个 stub implementation（空实现），空实现本身不会被排除编译，但通常可以做到接近 0 运行时开销。是否完全 0 开销取决于代码是否被 内联（inline）和死代码消除（dead code elimination）。&lt;/p&gt;
&lt;p&gt;如果编译器没有 inline &lt;strong&gt;，&lt;/strong&gt; 调用代码会变成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CALL Debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Debug 里面什么都不做然后 return，开销是一次函数调用和一次 return，很小但不是 0。&lt;/p&gt;
&lt;p&gt;不过 Go 编译器通常会自动 inline 很小的函数，这时配合死代码消除会让函数调用直接消失，最终生成的汇编完全没有这部分代码。&lt;/p&gt;
&lt;p&gt;为了检测函数是否被内联，可以用以下编译命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go build -gcflags\=&quot;-m&quot;

# 如果输出
# inlining call to Debug
# 说明编译器把调用内联了。
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Go 逃逸分析</title><link>https://ray-d-song.com/blog/2026/02/go-escape-analysis</link><guid isPermaLink="true">https://ray-d-song.com/blog/2026/02/go-escape-analysis</guid><pubDate>Wed, 18 Feb 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;什么是逃逸分析&lt;/h3&gt;
&lt;p&gt;逃逸分析（Escape Analysis） 是编译器在编译阶段执行的一种静态分析技术，它的核心目的是判断一个变量（或对象）会不会“逃逸”到堆上。&lt;/p&gt;
&lt;p&gt;之所以要判断会不会逃逸到堆，主要是因为栈上分配内存非常快，函数返回就自动回收，不需要垃圾回收器介入。&lt;/p&gt;
&lt;p&gt;堆上分配需要经过内存分配器 + 垃圾回收器管理，成本明显更高。&lt;/p&gt;
&lt;p&gt;而编程语言之所以要区分堆还是栈，主要是因为栈大小必须在编译期（或函数开始时）确定，牺牲了灵活性。&lt;/p&gt;
&lt;p&gt;由此我们也可以得出，一个对象会被分配到堆还是栈，由编译器在编译时决定。&lt;/p&gt;
&lt;p&gt;决定的因素由很多，除了需要在编译期确定大小外，因为每个栈帧有大小限制，也不能存放太大的对象。&lt;/p&gt;
&lt;p&gt;发生逃逸主要是以下 6 种场景：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;是否逃逸&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;对外部指针赋值&lt;/td&gt;
&lt;td&gt;逃逸&lt;/td&gt;
&lt;td&gt;&amp;amp;x 被赋给了全局变量、结构体字段、闭包捕获、map/slice元素等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;返回局部变量的指针&lt;/td&gt;
&lt;td&gt;逃逸&lt;/td&gt;
&lt;td&gt;return &amp;amp;localVar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;局部变量被闭包捕获，且闭包逃逸&lt;/td&gt;
&lt;td&gt;逃逸&lt;/td&gt;
&lt;td&gt;闭包本身逃逸 → 捕获的变量也要跟着逃逸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;动态大小的对象（如切片、map）&lt;/td&gt;
&lt;td&gt;几乎必逃&lt;/td&gt;
&lt;td&gt;切片底层数组、map 本身通常都要逃逸（除非极小且完全可内联的情况）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;interface{} 装箱&lt;/td&gt;
&lt;td&gt;逃逸&lt;/td&gt;
&lt;td&gt;任何值转成 interface{} 后几乎都会逃逸（因为接口里存的是指针）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;太大、无法确定大小的对象&lt;/td&gt;
&lt;td&gt;逃逸&lt;/td&gt;
&lt;td&gt;栈空间有限（通常几KB），编译器认为放栈上不安全时直接给堆&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;还有一点，容器类型的各个部分并不一定位于同一处，以切片为例，切片由三部分组成：指针 + 长度 + 容量，这三部分统称为 slice header，在 64 位系统上占用 24 个字节。slice header 和底层存储数据的数组，内存分配位置是分开考虑的。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;部分&lt;/th&gt;
&lt;th&gt;常见情况&lt;/th&gt;
&lt;th&gt;能否栈分配？&lt;/th&gt;
&lt;th&gt;主要决定因素&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;slice header&lt;/td&gt;
&lt;td&gt;局部变量&lt;/td&gt;
&lt;td&gt;几乎总是可以&lt;/td&gt;
&lt;td&gt;除非 header 本身逃逸（返回 &amp;amp;s、给 interface{} 等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;底层数组（data）&lt;/td&gt;
&lt;td&gt;make 时长度/容量已知且较小&lt;/td&gt;
&lt;td&gt;可以栈分配&lt;/td&gt;
&lt;td&gt;编译期知道确切大小 + 不逃逸 + 不太大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;底层数组（data）&lt;/td&gt;
&lt;td&gt;长度/容量运行时决定&lt;/td&gt;
&lt;td&gt;通常堆分配&lt;/td&gt;
&lt;td&gt;大小未知，编译器无法预留栈空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;底层数组（data）&lt;/td&gt;
&lt;td&gt;长度 ≥ ~64KB（8192 个 int 左右）&lt;/td&gt;
&lt;td&gt;强制堆分配&lt;/td&gt;
&lt;td&gt;即使不逃逸，栈太小（默认栈帧有限制）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;底层数组（data）&lt;/td&gt;
&lt;td&gt;发生 append 扩容&lt;/td&gt;
&lt;td&gt;几乎必堆&lt;/td&gt;
&lt;td&gt;可能需要重新分配，更难留在栈上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h3&gt;为什么需要逃逸分析&lt;/h3&gt;
&lt;p&gt;逃逸分析主要用于分析和优化内存分配，尤其是搞清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些变量本来可以放在栈上，却被逃逸到了堆上？&lt;/li&gt;
&lt;li&gt;为什么这个结构体/闭包/切片逃逸了？&lt;/li&gt;
&lt;li&gt;能不能通过改写代码让更多变量留在栈上？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;逃逸分析命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;go build -gcflags\=&quot;-m&quot; .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令的核心参数是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-gcflags：把后面的参数透传给底层编译器（go tool compile）&lt;/li&gt;
&lt;li&gt;&quot;-m&quot;：m = more escape analysis info&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见的用法变体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 最常用写法（推荐加 all= 显示所有包的逃逸信息）
go build -gcflags=&quot;all=-m&quot; .

# 只看当前包
go build -gcflags=&quot;-m&quot; .

# 只看不编译（最干净，只看逃逸分析，不生成可执行文件）
go build -gcflags=&quot;all=-m&quot; -n .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./main.go:12:6: x escapes to heap
./main.go:15:17: []int literal escapes to heap
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Go 可执行文件体积分析与裁剪</title><link>https://ray-d-song.com/blog/2026/02/how-to-cut-go-bin-size</link><guid isPermaLink="true">https://ray-d-song.com/blog/2026/02/how-to-cut-go-bin-size</guid><pubDate>Wed, 18 Feb 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;为什么要裁剪？&lt;/h3&gt;
&lt;p&gt;Go 相较于 Java、Python 等托管语言和脚本语言的一大优势，就是原生支持编译成可执行文件，这一特性在云原生、容器、CLI 工具等场景下极具吸引力。&lt;/p&gt;
&lt;p&gt;但不管是容器镜像，还是 CLI 工具，都对产物体积较为敏感，因此对 Go 可执行文件进行裁剪，就是一个值得研究的工程问题。&lt;/p&gt;
&lt;h3&gt;如何分析产物&lt;/h3&gt;
&lt;p&gt;最简单的分析方法就是使用 go-size-analyzer 库，它会分析可执行文件内容，并以可视化图表的形式展示出来。&lt;/p&gt;
&lt;p&gt;使用方式也非常简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装
go install github.com/Zxilly/go-size-analyzer/cmd/gsa@latest
# 编译产物
go build -o app
# 分析产物
gsa --web app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过图表我们可以直观看出哪些包占用了较多的体积。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://sy.ray-d-song.com/assets/image-20260219231404-8fsuzom.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来我们可以通过 go mod why 命令确认为什么需要这个库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go mod why github.com/goccy/go-yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们以一个 Go Gin  框架的 Web 项目为例，Gin 的 binding 包导入了 YAML、TOML、XML 等格式支持，并提供了 &lt;code&gt;c.TOML&lt;/code&gt;​、&lt;code&gt;c.XML&lt;/code&gt;​ 等渲染方法。同时提供了绑定方法 &lt;code&gt;c.BindTOML&lt;/code&gt;​、&lt;code&gt;c.BindXML&lt;/code&gt; 等。即使你不调用，它们也会被链接进二进制。&lt;/p&gt;
&lt;p&gt;例如 YAML 包占用了 571.14kb。但大多数 Http Web 项目并不需要支持这些格式。&lt;/p&gt;
&lt;p&gt;替换为更轻量的框架（例如 0 依赖的 chi）体积会减少非常多。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;h3&gt;反射对产物体积的影响&lt;/h3&gt;
&lt;p&gt;最后我们要谈谈反射。&lt;/p&gt;
&lt;p&gt;现代编译器都包含一种名为 Dead-code elimination（DCE，死代码删除）的技术，顾名思义就是将源码中没用到的部分剔除，不出现在可执行文件中。&lt;/p&gt;
&lt;p&gt;在 Go 中，大规模死码删除主要发生在链接期（linker），因此最好的观察方式就是检查二进制中是否存在该符号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go build -o app
go tool nm app | grep YourFunc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而反射会严重影响 Go 编译器的死代码消除。最极端的例子就是匿名导入 + 反射，导致整个包无法 DCE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	_ &quot;example.com/hugepkg&quot;
	&quot;reflect&quot;
)

func main() {
	var x any
	reflect.TypeOf(x)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方案就是采用&lt;code&gt;go generate&lt;/code&gt; + 自定义生成器，接口 + 显式注册，泛型等工具来替代反射。&lt;/p&gt;
</content:encoded></item><item><title>uni-app 编译分析</title><link>https://ray-d-song.com/blog/2025/07/uniapp-compiler</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/07/uniapp-compiler</guid><pubDate>Tue, 01 Jul 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;uni-app 编译分析&lt;/h1&gt;
&lt;p&gt;目前已经有不少文章对 uni-app 的编译流程进行了分析，但大多数都是关于 Vue SFC 如何编译为微信小程序的 WXML/WXSS/JS 文件。很少有文章会从源码角度解析其整个流程。&lt;br /&gt;
最近越发难以忍受基于 WebPack 的 Vue2 uni-app，因此决定基于 RsPack 重新实现 uni-app（以插件形式），分析其整个流程就非常重要了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们以微信小程序为例，这也是 uni-app 最常用的平台。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当执行 &lt;code&gt;pnpm run build:mp-weixin&lt;/code&gt; 命令时，实际发生了以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;环境变量设置&lt;/strong&gt;: &lt;code&gt;cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin&lt;/code&gt; 会设置两个关键环境变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NODE_ENV=production&lt;/code&gt;: 确保构建在生产模式下进行，启用相应的优化和压缩。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UNI_PLATFORM=mp-weixin&lt;/code&gt;: 明确指定目标平台为微信小程序。这是 &lt;code&gt;uni-app&lt;/code&gt; 插件识别并应用小程序特有配置的关键。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Vue CLI 服务启动&lt;/strong&gt;: &lt;code&gt;vue-cli-service uni-build&lt;/code&gt; 命令启动 Vue CLI 服务，并指示其执行 &lt;code&gt;uni-build&lt;/code&gt; 命令。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里可以展开讲讲，vue-cli 有一套&lt;a href=&quot;https://cli.vuejs.org/zh/dev-guide/plugin-dev.html&quot;&gt;插件机制&lt;/a&gt;，其中有一类&lt;a href=&quot;https://cli.vuejs.org/zh/dev-guide/plugin-dev.html#service-%E6%8F%92%E4%BB%B6&quot;&gt;service&lt;/a&gt;插件，可以通过 &lt;code&gt;vue-cli-service&lt;/code&gt; 命令来启动，uni-app 插件就是这样实现的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;加载 &lt;code&gt;vue-cli-plugin-uni&lt;/code&gt; 插件&lt;/strong&gt;: Vue CLI 服务会加载 &lt;code&gt;vue-cli-plugin-uni&lt;/code&gt; 插件。该插件的入口文件 &lt;code&gt;packages/vue-cli-plugin-uni/index.js&lt;/code&gt; 会被执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注册 &lt;code&gt;uni-build&lt;/code&gt; 命令&lt;/strong&gt;: 在 &lt;code&gt;index.js&lt;/code&gt; 中，&lt;code&gt;api.registerCommand(&apos;uni-build&apos;, ...)&lt;/code&gt; 会注册 &lt;code&gt;uni-build&lt;/code&gt; 命令的回调函数。当 &lt;code&gt;vue-cli-service uni-build&lt;/code&gt; 被调用时，该回调函数会被触发。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;执行 &lt;code&gt;build.js&lt;/code&gt; 中的 &lt;code&gt;build&lt;/code&gt; 函数&lt;/strong&gt;: &lt;code&gt;uni-build&lt;/code&gt; 命令的回调函数（位于 &lt;code&gt;packages/vue-cli-plugin-uni/commands/build.js&lt;/code&gt;）是核心执行逻辑。它会调用 &lt;code&gt;async function build(args, api, options)&lt;/code&gt; 函数来启动实际的构建过程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;获取基础 Webpack 配置&lt;/strong&gt;: 在 &lt;code&gt;build&lt;/code&gt; 函数内部，会调用 &lt;code&gt;getWebpackConfigs&lt;/code&gt;（最终调用 &lt;code&gt;getWebpackConfig&lt;/code&gt;）来获取一个基础的 Webpack 配置。这个基础配置是基于 Vue CLI 的默认配置和项目的 &lt;code&gt;vue.config.js&lt;/code&gt; 文件生成的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;应用微信小程序平台特有配置&lt;/strong&gt;: 这是最关键的一步。&lt;code&gt;vue-cli-plugin-uni&lt;/code&gt; 插件通过 &lt;code&gt;api.configureWebpack&lt;/code&gt; 和 &lt;code&gt;api.chainWebpack&lt;/code&gt; 方法，加载并应用 &lt;code&gt;packages/vue-cli-plugin-uni/lib/mp/index.js&lt;/code&gt; 中定义的针对微信小程序平台的 Webpack 配置和插件。这些配置包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;入口和输出&lt;/strong&gt;: 设置小程序特有的入口文件（如 &lt;code&gt;app.json&lt;/code&gt; 和 &lt;code&gt;pages.json&lt;/code&gt; 解析出的页面/组件入口）和输出路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块解析&lt;/strong&gt;: 配置 &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.vue&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt; 等文件的解析规则，以及路径别名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loader 配置&lt;/strong&gt;: 为 &lt;code&gt;.vue&lt;/code&gt; 文件配置 &lt;code&gt;vue-loader&lt;/code&gt;，并传入小程序特有的 &lt;code&gt;compilerOptions&lt;/code&gt;；为样式文件配置 &lt;code&gt;mini-css-extract-plugin&lt;/code&gt;、&lt;code&gt;css-loader&lt;/code&gt;、&lt;code&gt;postcss-loader&lt;/code&gt; 等；为 JavaScript 文件配置 &lt;code&gt;babel-loader&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件集成&lt;/strong&gt;: 引入一系列核心插件，例如：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WebpackUniAppPlugin&lt;/code&gt;: 处理 &lt;code&gt;uni-app&lt;/code&gt; 特有逻辑，如页面和组件的转换。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebpackUniMPPlugin&lt;/code&gt; (通过 &lt;code&gt;createUniMPPlugin()&lt;/code&gt;): 针对小程序平台的 Webpack 插件，负责将 Vue/uni-app 代码转换为微信小程序原生代码结构（&lt;code&gt;wxml&lt;/code&gt;, &lt;code&gt;wxss&lt;/code&gt;, &lt;code&gt;js&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webpack.DefinePlugin&lt;/code&gt;: 定义全局常量，注入 &lt;code&gt;process.env.NODE_ENV&lt;/code&gt; 和 &lt;code&gt;process.env.UNI_PLATFORM&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CopyWebpackPlugin&lt;/code&gt;: 复制静态资源。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MiniCssExtractPlugin&lt;/code&gt;: 提取 CSS 到单独文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webpack.ProvidePlugin&lt;/code&gt;: 自动注入 &lt;code&gt;uni&lt;/code&gt; 和 &lt;code&gt;uniCloud&lt;/code&gt; 等全局对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PreprocessAssetsPlugin&lt;/code&gt;: 处理构建产物中的 &lt;code&gt;wxss&lt;/code&gt; 和 &lt;code&gt;js&lt;/code&gt; 文件，例如添加公共 &lt;code&gt;runtime&lt;/code&gt; 和 &lt;code&gt;vendor&lt;/code&gt; 引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化和压缩&lt;/strong&gt;: 在生产环境下，对 CSS 和 JavaScript 进行优化和压缩。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;执行 Webpack 编译&lt;/strong&gt;: &lt;code&gt;build&lt;/code&gt; 函数使用最终合并了基础配置和微信小程序特有配置的 Webpack 配置对象，调用 &lt;code&gt;webpack&lt;/code&gt; 包进行实际的代码编译和打包。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输出构建产物&lt;/strong&gt;: Webpack 将编译后的微信小程序代码（包含 &lt;code&gt;wxml&lt;/code&gt;, &lt;code&gt;wxss&lt;/code&gt;, &lt;code&gt;js&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt; 文件以及其他资源）输出到指定的构建目录（通常是 &lt;code&gt;dist/build/mp-weixin&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;简而言之，&lt;code&gt;build:mp-weixin&lt;/code&gt; 命令是一个高度自动化的构建流程，它通过 Vue CLI 和 &lt;code&gt;uni-app&lt;/code&gt; 插件的协同工作，将 &lt;code&gt;uni-app&lt;/code&gt; 项目的源代码转换并优化为符合微信小程序运行规范的最终代码。&lt;/p&gt;
&lt;h1&gt;其他配置&lt;/h1&gt;
&lt;p&gt;uni-app 项目相较于标准的 Vue Web 项目，还有一些特殊的文件，这些也影响了构建：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pages.json&lt;/code&gt;：页面配置文件，用于配置路由、窗口表现、底部 tab 等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;manifest.json&lt;/code&gt;：应用配置文件，用于配置应用名称、版本、权限等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postcss.config.js&lt;/code&gt;：PostCSS 配置文件，用于配置 PostCSS 插件，这个在 Web 项目中也有，但 uni-app 还有一个 &lt;code&gt;@dcloudio/vue-cli-plugin-uni/packages/postcss&lt;/code&gt;。主要用于处理各平台的样式兼容问题。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;babel.config.js&lt;/code&gt;：Babel 配置文件，用于配置 Babel 插件，这个在 Web 项目中也有，但 uni-app 还有一个 &lt;code&gt;@dcloudio/vue-cli-plugin-uni/packages/babel&lt;/code&gt;。主要也是处理兼容性问题（真坑啊）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uni.promisify.adaptor.js&lt;/code&gt;：这玩意用来把 uni-app 早期设计的那些基于回调的 API 转换为 Promise 风格的 API。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;uni-app 虽然对 Vue 的运行时也进行了魔改，但核心还是这套编译机制。&lt;br /&gt;
这大概会是个系列文章，逐步分析这套编译机制和我的移植过程。&lt;/p&gt;
</content:encoded></item><item><title>Node.js 计算性能瓶颈和发展</title><link>https://ray-d-song.com/blog/2025/04/nodejs-performance-issue</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/04/nodejs-performance-issue</guid><pubDate>Sat, 26 Apr 2025 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;每当提到 Node.js 的计算性能，一定会提到 cluster 和 worker_threads，但这俩并不是什么好的解决方案，只是临时的、打补丁似的思路。&lt;br /&gt;
最大的问题就是开销，主要来自于两方面:&lt;/p&gt;
&lt;p&gt;首先是通信的损耗，如果用 worker_threads ，主线程和工作线程之间的通信主要通过消息传递。对于大量或复杂数据的传输，数据的序列化、反序列化和复制会带来显著的性能开销。虽然可以使用 SharedArrayBuffer 实现内存共享，但这需要更复杂的同步机制来避免竞态条件。&lt;br /&gt;
如果使用 cluster，这个问题更严重，每个工作进程都是一个独立的 Node.js 进程，拥有独立的内存空间。这意味着工作进程之间无法直接共享内存中的数据。如果在应用中需要共享状态（例如用户会话、缓存数据等），需要借助外部存储（如数据库、缓存服务器 Redis 等）或通过 IPC 进行进程间通信来同步数据。&lt;br /&gt;
这意味着，即便你构建的是一个巨石架构的单体项目，也需要以大型分布式系统的方式来思考。&lt;/p&gt;
&lt;p&gt;第二是 cluster 和 worker_threads 都需要启动独立的 VM 实例，这会带来一定的启动开销和资源消耗。&lt;br /&gt;
如果频繁地创建和销毁 Worker，其开销可能会抵消并行带来的好处，为了避免这样的事情发生，最终又需要类似于piscina的池化方案。&lt;/p&gt;
&lt;p&gt;那么，有没有办法解决这些问题？&lt;br /&gt;
目前有希望的是 Struct 提案。&lt;/p&gt;
&lt;h3&gt;Struct 提案&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;proposal-structs（结构体提案）&lt;/code&gt; 是 TC39 目前处于 Stage2 的提案，该提案引入了 3 个重要的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;结构体
结构体是更严格的对象。它们的行为类似于 class 实例，但有更多限制，有利于优化和分析。&lt;/li&gt;
&lt;li&gt;共享结构体
进一步受限的结构体，可以被多个实例并行共享和访问。它们支持共享内存多线程。&lt;/li&gt;
&lt;li&gt;互斥锁和条件变量
是用于同步访问共享内存的更高级别的抽象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个提案解决了我们的第一个问题，那就是通信间的损耗和并发时的内存安全问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享结构体可以在多个实例间共享，这意味着我们不需要再担心进程间通信的损耗。&lt;/li&gt;
&lt;li&gt;互斥锁和条件变量可以让我们在多个线程间安全地共享数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你对 Struct 提案感兴趣，可以看&lt;a href=&quot;https://github.com/tc39/proposal-structs&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>为脚本语言创建可执行文件的方法</title><link>https://ray-d-song.com/blog/2025/04/create-exe-for-script-lang</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/04/create-exe-for-script-lang</guid><pubDate>Tue, 15 Apr 2025 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Python&lt;/code&gt;、&lt;code&gt;JavaScript&lt;/code&gt;等脚本语言，都需要一个运行时（runtime）来执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python foo.py # 运行 Python 脚本
node foo.js # 运行 JavaScript 脚本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以脚本语言编写的程序基本都是源码分发，比如 Node.js 的 npx，但这要求用户的电脑上也有对应的运行环境。&lt;br /&gt;
又因为语言的API会发生改变，所以有时候需要安装多个版本。&lt;br /&gt;
比如工具A依赖Python 3.10，工具B依赖Python 3.11，这时候就需要安装两个版本的Python，这时又要引入一个管理多个版本的工具，比如 pyenv。&lt;/p&gt;
&lt;p&gt;更好的方法是直接把脚本打包成可执行文件，这样用户就不需要安装运行时了。&lt;/p&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;目前已经有很多打包工具，比如 JavaScript 的 &lt;a href=&quot;https://deno.land/&quot;&gt;Deno&lt;/a&gt;、&lt;a href=&quot;https://github.com/vercel/pkg&quot;&gt;pkg&lt;/a&gt; 和 Python 的 &lt;a href=&quot;https://pyinstaller.org/&quot;&gt;PyInstaller&lt;/a&gt;。&lt;br /&gt;
其思路基本都是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;提取一个干净的语言运行时，去掉不必要的功能以减少体积&lt;/li&gt;
&lt;li&gt;将源码压缩，并嵌入到运行时的可执行文件中，并添加一个魔数，用于识别源码是否存在&lt;/li&gt;
&lt;li&gt;修改运行时的执行逻辑，在启动时先通过魔数检查源码是否存在，如果存在则解压并执行，否则进入 REPL 或者直接报错&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;为 LLRT 添加构建可执行文件的功能&lt;/h2&gt;
&lt;p&gt;LLRT 是一个轻量级的 JavaScript 运行时，提供了不少 Node.js 的 API 的同时，体积只有 8MB 左右，相比 Node.js 的 100MB 小了很多，非常适合打包轻量级服务和CLI工具。&lt;br /&gt;
为了验证上面的思路，我为 LLRT 添加了构建可执行文件的功能。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Ray-D-Song/lexe&quot;&gt;lexe 是一个基于 LLRT 的打包工具，支持将 Node.js 项目打包成可执行文件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面是具体的实现过程：&lt;/p&gt;
&lt;h3&gt;提取干净的语言运行时&lt;/h3&gt;
&lt;p&gt;对于 LLRT，这部分可以省略，因为由JavaScript实现的标准库代码量并不多。&lt;/p&gt;
&lt;h3&gt;将源码压缩，并嵌入到运行时的可执行文件中&lt;/h3&gt;
&lt;p&gt;这里我借助了 Deno 团队编写的 &lt;a href=&quot;https://github.com/denoland/libsui&quot;&gt;libsui&lt;/a&gt; 来实现。&lt;br /&gt;
libsui 是一个基于 Rust 语言的数据嵌入工具，支持将任意数据嵌入到可执行文件中，并在运行时提取出来。&lt;br /&gt;
对于不同的系统，libsui 会执行不同的操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mac（Macho-O文件）：资源被添加为一个新Segment中的Section，并更新加载命令（Load Command）。&lt;/li&gt;
&lt;li&gt;Linux（ELF文件）：资源直接被添加到文件末尾，还包含一个标签和魔数&lt;/li&gt;
&lt;li&gt;Windows（PE文件）：资源被添加到资源段（Resource Section），运行时使用 &lt;code&gt;FindResource&lt;/code&gt; 函数查找并使用 &lt;code&gt;LoadResource&lt;/code&gt; 函数加载&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if platform == &amp;amp;Platform::WindowsX64 {
    PortableExecutable::from(&amp;amp;llrt_binary)?
        .write_resource(SECTION_NAME, compiled.clone())?
        .build(&amp;amp;mut output)?;
    output.write_all(MAGIC_NUMBER.as_bytes())?;
} else if platform == &amp;amp;Platform::LinuxX64 || platform == &amp;amp;Platform::LinuxArm64 {
    Elf::new(&amp;amp;llrt_binary)
        .append(SECTION_NAME, &amp;amp;compiled, &amp;amp;mut output)?;
} else if platform == &amp;amp;Platform::DarwinX64 || platform == &amp;amp;Platform::DarwinArm64 {
    Macho::from(llrt_binary)?
        .write_section(SECTION_NAME, compiled.clone())?
        .build_and_sign(&amp;amp;mut output)?;
    output.write_all(MAGIC_NUMBER.as_bytes())?;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改运行时的执行逻辑&lt;/h3&gt;
&lt;p&gt;在上一步中，对于Windows和macOS系统，我们在可执行文件的最后追加了一个魔数，用于识别源码是否存在。&lt;br /&gt;
对于Linux系统，libsui 已经添加了一个魔数，如果再添加一个，会导致 libsui 无法识别，所以直接使用 libsui 的魔数。&lt;/p&gt;
&lt;p&gt;首先编写一个函数，用于检查魔数是否存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn has_magic_number(platform: &amp;amp;Platform) -&amp;gt; std::io::Result&amp;lt;bool&amp;gt; {
    let path = env::current_exe()?;
    let mut file = File::open(path)?;

    let file_size = file.metadata()?.len() as usize;
    if file_size &amp;lt; 16 {  // 如果文件太小则可以认为没有源码
        return Ok(false);
    }

    // 根据不同的平台，使用不同的方法检查魔数
    match platform {
        Platform::LinuxX64 | Platform::LinuxArm64 =&amp;gt; {
            // 对于 Linux 平台，libsui 在文件末尾添加了 16 字节的数据：魔数（4 字节）+ 哈希（4 字节）+ 大小（8 字节）
            const TRAILER_LEN: i64 = 16;
            file.seek(SeekFrom::End(-TRAILER_LEN))?;
            let mut buf = [0; 4]; // 只读取魔数部分
            file.read_exact(&amp;amp;mut buf)?;
            
            // 检查魔数（小端序）
            let magic = u32::from_le_bytes(buf);
            Ok(magic == LIBSUI_MAGIC_NUMBER)
        },
        _ =&amp;gt; {
            // 其他平台使用字符串魔数检测方法
            let search_area_size = 1024.min(file_size);
            let search_start = file_size.saturating_sub(search_area_size);

            file.seek(SeekFrom::Start(search_start as u64))?;

            let mut buffer = vec![0; search_area_size];
            file.read_exact(&amp;amp;mut buffer)?;

            // 搜索字符串魔数
            // 从后往前搜索，因为魔数在文件末尾
            for i in (0..search_area_size - MAGIC_NUMBER.len() + 1).rev() {
                if &amp;amp;buffer[i..i + MAGIC_NUMBER.len()] == MAGIC_NUMBER.as_bytes() {
                    return Ok(true);
                }
            }
            Ok(false)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得注意的一点是，采用从后向前搜索，而不是对整个文件进行匹配，这样可以降低误匹配的概率并提高效率。&lt;/p&gt;
&lt;h3&gt;提取源码并执行&lt;/h3&gt;
&lt;p&gt;如果魔数存在，则提取源码并执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if has_magic_number {
    // 提取源码
    let code_binary = extract_code_binary();
    if let Some(code_binary) = code_binary {
        // 执行源码
        vm.run_with(|ctx| {
            let module = llrt_core::modules::require::loader::CustomLoader::load_bytecode_module(ctx.clone(), &amp;amp;code_binary)?;
            module.eval()?;
            Ok(())
        }).await;
    }
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里执行源码的方式是调用虚拟机实例，不同运行时暴露的 API 不同。&lt;br /&gt;
比如 Node.js 的虚拟机实例暴露的 API 是 &lt;code&gt;vm.run_with&lt;/code&gt;，而 Python 的虚拟机实例暴露的 API 是 &lt;code&gt;vm.run&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;通过上面的步骤，我们成功为 LLRT 添加了构建可执行文件的功能。&lt;br /&gt;
剩下的工作就是添加 CLI 选项再编写一个 JS 包装器并发布。&lt;/p&gt;
&lt;p&gt;因为 LLRT 的代码是基于 Rust 编写的，所以可以蹭 Deno 生态的工具，如果你希望为其他语言编写可执行文件，可以参考这个思路的同时，用别的工具替换 libsui。&lt;br /&gt;
或者你也可以参考 pyinstaller 的思路，在运行时将源码先解压到临时目录，然后执行。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://deno.land/&quot;&gt;Deno&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pyinstaller.org/&quot;&gt;PyInstaller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://littledivy.com/sui.html&quot;&gt;Inject RO data into existing executables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Valibot 进行类型检查和类型守卫</title><link>https://ray-d-song.com/blog/2025/03/zod-type-guard</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/03/zod-type-guard</guid><pubDate>Wed, 19 Mar 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 Valibot&lt;/h2&gt;
&lt;p&gt;Valibot 是一个基于 TypeScript 的类型检查库，它可以帮助我们在运行时进行类型检查。&lt;/p&gt;
&lt;p&gt;当我们讨论 TS 类型安全时，这种&quot;安全&quot;其实只存在于&lt;code&gt;编译时&lt;/code&gt;，而&lt;code&gt;运行时&lt;/code&gt;的类型安全需要我们自己来保证。&lt;br /&gt;
也就是说，即便我们定义了大量的&lt;code&gt;interface&lt;/code&gt;、&lt;code&gt;type&lt;/code&gt;，在运行时，我们依旧要编写&lt;code&gt;typeof&lt;/code&gt;、&lt;code&gt;instanceof&lt;/code&gt;、&lt;code&gt;in&lt;/code&gt;等判断类型的代码。&lt;/p&gt;
&lt;p&gt;Valibot 可以帮助我们减少这部分代码量，并提供更安全的类型检查。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;另一个更流行的校验库是 Zod，二者的 API 非常相似，Valibot 作为后发者做了一些优化，比如体积更小，且支持 tree-shaking。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install valibot
# 或者
yarn add valibot
# 或者
pnpm add valibot
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://valibot.dev/playground&quot;&gt;这里&lt;/a&gt;在线体验 Valibot 的类型检查。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Valibot 的核心是&lt;code&gt;模式定义&lt;/code&gt;，所谓模式，可以理解你的变量&quot;应该是什么样&quot;。&lt;/p&gt;
&lt;p&gt;比如，我们需要一个&lt;code&gt;user&lt;/code&gt;对象，它应该有&lt;code&gt;name&lt;/code&gt;、&lt;code&gt;age&lt;/code&gt;、&lt;code&gt;pwd&lt;/code&gt;三个属性。&lt;br /&gt;
&lt;code&gt;name&lt;/code&gt;和&lt;code&gt;pwd&lt;/code&gt;是字符串，&lt;code&gt;age&lt;/code&gt;是数字。&lt;br /&gt;
密码长度需要大于 6 位。&lt;/p&gt;
&lt;p&gt;那么 User 模式的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as v from &apos;valibot&apos;

const UserSchema = v.object({
  name: v.string(),
  age: v.number(),
  pwd: v.pipe(v.string(), v.minLength(6)),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们就可以使用这个模式来检查我们的数据了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const result = v.safeParse(UserSchema, {
  name: &apos;Ray&apos;,
  age: 24,
  pwd: &apos;1234&apos;,
})

console.log(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，我的pwd字段只写了4位，但在schema中我们定义了最少6位，所以会报错。&lt;br /&gt;
此时第7行的console.log会输出如下的报错信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[log]: {
  typed: true,
  success: false,
  output: {
    name: &quot;Ray&quot;,
    age: 24,
    pwd: &quot;1234&quot;
  },
  issues: [
    {
      kind: &quot;validation&quot;,
      type: &quot;min_length&quot;,
      input: &quot;1234&quot;,
      expected: &quot;&amp;gt;=6&quot;,
      received: &quot;4&quot;,
      message: &quot;Invalid length: Expected &amp;gt;=6 but received 4&quot;,
      requirement: 6,
      path: [
        {
          type: &quot;object&quot;,
          origin: &quot;value&quot;,
          input: {
            name: &quot;Ray&quot;,
            age: 24,
            pwd: &quot;1234&quot;
          },
          key: &quot;pwd&quot;,
          value: &quot;1234&quot;
        }
      ],
      issues: undefined,
      lang: undefined,
      abortEarly: undefined,
      abortPipeEarly: undefined
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，报错信息中，我们得到了&lt;code&gt;success&lt;/code&gt;，表明校验失败。&lt;br /&gt;
还有&lt;code&gt;issues&lt;/code&gt;字段，它表示失败的具体原因。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kind&lt;/code&gt;：错误类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：错误类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;input&lt;/code&gt;：输入的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expected&lt;/code&gt;：预期的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;received&lt;/code&gt;：接收的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt;：错误信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requirement&lt;/code&gt;：要求&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;价值&lt;/h2&gt;
&lt;p&gt;上面的例子不能直观感受到 Valibot 的价值，下面我们来看一个更复杂的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const UserSchema = v.object({
  // 名字，最少2个字符，最多50个字符
  name: v.pipe(v.string(), v.minLength(2), v.maxLength(50)),
  // 年龄，最小1岁，最大124岁，必须是整数
  age: v.pipe(v.number(), v.minValue(1), v.maxValue(124), v.integer()),
  // 密码，最少6个字符
  pwd: v.pipe(v.string(), v.minLength(6)),
  // 邮箱，符合邮箱格式
  email: v.pipe(v.string(), v.email())
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要编写和上面等价的校验器，我们至少需要以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 类型声明部分
interface User {
  name: string;
  age: number;
  pwd: string;
  email: string;
}

// 运行时校验部分
function validateUser(data: unknown): { valid: boolean; errors: string[]; value: User | null } {
  const errors: string[] = [];
  
  // 首先检查是否为对象
  if (!data || typeof data !== &apos;object&apos;) {
    return { valid: false, errors: [&apos;输入必须是一个对象&apos;], value: null };
  }
  
  const input = data as Record&amp;lt;string, unknown&amp;gt;;
  const result: Partial&amp;lt;User&amp;gt; = {};
  
  // 检查 name 字段
  if (typeof input.name !== &apos;string&apos;) {
    errors.push(&apos;name 必须是字符串&apos;);
  } else if (input.name.length &amp;lt; 2) {
    errors.push(&apos;name 长度不能小于2个字符&apos;);
  } else if (input.name.length &amp;gt; 50) {
    errors.push(&apos;name 长度不能超过50个字符&apos;);
  } else {
    result.name = input.name;
  }
  
  // 检查 age 字段
  if (typeof input.age !== &apos;number&apos;) {
    errors.push(&apos;age 必须是数字&apos;);
  } else if (input.age &amp;lt; 1) {
    errors.push(&apos;age 不能小于1&apos;);
  } else if (input.age &amp;gt; 124) {
    errors.push(&apos;age 不能大于124&apos;);
  } else if (!Number.isInteger(input.age)) {
    errors.push(&apos;age 必须是整数&apos;);
  } else {
    result.age = input.age;
  }
  
  // 检查 pwd 字段
  if (typeof input.pwd !== &apos;string&apos;) {
    errors.push(&apos;pwd 必须是字符串&apos;);
  } else if (input.pwd.length &amp;lt; 6) {
    errors.push(&apos;pwd 长度不能小于6个字符&apos;);
  } else {
    result.pwd = input.pwd;
  }
  
  // 检查 email 字段
  if (typeof input.email !== &apos;string&apos;) {
    errors.push(&apos;email 必须是字符串&apos;);
  } else {
    // 简单的邮箱格式验证
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(input.email)) {
      errors.push(&apos;email 格式不正确&apos;);
    } else {
      result.email = input.email;
    }
  }
  
  return { 
    valid: errors.length === 0, 
    errors, 
    value: errors.length === 0 ? result as User : null 
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，手动编写的验证代码非常冗长，而且容易出错。&lt;br /&gt;
使用 Valibot，我们只需要几行代码就能完成同样的功能，并且获得更好的类型推导和错误提示。&lt;/p&gt;
&lt;h2&gt;类型守卫&lt;/h2&gt;
&lt;p&gt;Valibot 提供了&lt;code&gt;is&lt;/code&gt;函数，可以帮助我们进行类型守卫。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as v from &apos;valibot&apos;;

// 定义用户模式
const UserSchema = v.object({
  name: v.pipe(v.string(), v.minLength(2), v.maxLength(50)),
  age: v.pipe(v.number(), v.minValue(1), v.maxValue(124), v.integer()),
  pwd: v.pipe(v.string(), v.minLength(6)),
  email: v.pipe(v.string(), v.email())
});

// 从模式中提取类型
type User = v.InferInput&amp;lt;typeof UserSchema&amp;gt;;

// 类型守卫函数
function isUser(value: unknown): value is User {
  return v.is(UserSchema, value);
}

// 使用类型守卫
function processUserData(data: unknown) {
  if (isUser(data)) {
    // 在这个作用域内，TypeScript 知道 data 是 User 类型
    console.log(`处理用户: ${data.name}, 年龄: ${data.age}`);
    return true;
  } else {
    console.error(&apos;无效的用户数据&apos;);
    return false;
  }
}

// 示例
const validUser = {
  name: &quot;张三&quot;,
  age: 30,
  pwd: &quot;password123&quot;,
  email: &quot;zhangsan@example.com&quot;
};

const invalidUser = {
  name: &quot;李&quot;,  // 名字太短
  age: 150,    // 年龄超出范围
  pwd: &quot;123&quot;,  // 密码太短
  email: &quot;invalid-email&quot;  // 邮箱格式不正确
};

processUserData(validUser);    // 输出: 处理用户: 张三, 年龄: 30
processUserData(invalidUser);  // 输出: 无效的用户数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过使用 Valibot 的 &lt;code&gt;is&lt;/code&gt; 函数，我们可以轻松创建类型守卫。&lt;/p&gt;
&lt;h2&gt;与 API 集成&lt;/h2&gt;
&lt;p&gt;Valibot 特别适合用于验证 API 请求和响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as v from &apos;valibot&apos;;
import axios from &apos;axios&apos;;

// API 响应模式
const UserResponseSchema = v.object({
  id: v.string(),
  name: v.pipe(v.string(), v.minLength(2)),
  email: v.pipe(v.string(), v.email()),
  createdAt: v.string() // ISO 日期字符串
});

type UserResponse = v.InferOutput&amp;lt;typeof UserResponseSchema&amp;gt;;

// 安全地获取用户数据
async function fetchUser(userId: string): Promise&amp;lt;UserResponse | null&amp;gt; {
  try {
    const response = await axios.get(`/api/users/${userId}`);
    const result = v.safeParse(UserResponseSchema, response.data);
    
    if (result.success) {
      return result.output;
    } else {
      console.error(&apos;API 返回了无效的用户数据:&apos;, result.issues);
      return null;
    }
  } catch (error) {
    console.error(&apos;获取用户数据失败:&apos;, error);
    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式确保了即使 API 返回了意外的数据格式，我们的应用也能优雅地处理，而不会崩溃。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Valibot 为 TypeScript 项目提供了强大的运行时类型验证能力，它的优势包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;简洁的 API&lt;/strong&gt;：相比手动编写验证代码，Valibot 的 API 更加简洁明了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型安全&lt;/strong&gt;：自动从模式中推导出 TypeScript 类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;详细的错误信息&lt;/strong&gt;：提供清晰的错误信息，便于调试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型守卫支持&lt;/strong&gt;：通过 &lt;code&gt;is&lt;/code&gt; 函数轻松创建类型守卫&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可组合性&lt;/strong&gt;：使用 &lt;code&gt;pipe&lt;/code&gt; 等函数组合多个验证规则&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>LLM还不如低代码</title><link>https://ray-d-song.com/blog/2025/03/tired-of-llm</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/03/tired-of-llm</guid><pubDate>Fri, 07 Mar 2025 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;先说结论，大模型编程已经逼近极限，最终会和上一代的热点（低代码平台）一样，回归它应有的地位。&lt;/p&gt;
&lt;p&gt;叠个甲，我自己就是LLM编程的用户，从最早的 Copilot 到现在的 Cursor、Windsurf、Devin 都使用过。很多人还在聊天窗口给gpt3.5粘代码的时候，我就已经在用AI自动补全了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.ray-d-song.com/2025/03/f3dfc193662a5a6197c9f0f81cd68e72.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我不否认LLM对于专业的程序员来说很有帮助，但我不认为LLM能帮助人们实现“不用写代码”的愿景。&lt;/p&gt;
&lt;h2&gt;炒作周期&lt;/h2&gt;
&lt;p&gt;从云计算、低代码、AR到LLM，似乎每过一段时间就会有新的概念爆火，技术人和资本一股脑的涌上去，然后过了一段新的概念冒出来，热点又转移了。&lt;/p&gt;
&lt;p&gt;咨询机构Gartner早就发现了这个规律，提出了Hype Cycle （直接翻译成中文是“炒作周期”，文雅一点是“技术成熟度曲线”）的理论：任何新的技术在出现后会迅速炒作并达到一个峰值，而在过高的期望达到时，一切会重归幻灭，回到低谷期。&lt;/p&gt;
&lt;p&gt;我们现在就处于大模型过度炒作的阶段。&lt;/p&gt;
&lt;h2&gt;软件的本质&lt;/h2&gt;
&lt;p&gt;当我们在讨论软件时，实际在说的是什么？&lt;/p&gt;
&lt;p&gt;不是某种技术，也不是某个想法，而是产品和解决方案。&lt;/p&gt;
&lt;p&gt;当用户遇到了问题，解决方案怎么可能是 &lt;strong&gt;TA自己用 Cursor 编写了一个新的软件&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;这是我在LLM时代听到最逆天的逻辑。无数营销号让 Cursor 生成一个早就成熟的工具和小游戏，以此证明大模型是未来，甚至是通往AGI的钥匙。&lt;/p&gt;
&lt;p&gt;如果你想证明AGI即将到来，那你应该告诉大模型&quot;为我解决xxx问题&quot;，而不是让它为你生成玩具。&lt;/p&gt;
&lt;p&gt;让普通用户通过Cursor生成代码并不能帮助他们获得解决方案，反而增加了复杂性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果老妈最近想学英语，你应该给她找一个学英文的App，而不是让她用Cursor写个学英语的App。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;真正的技术进步应该是让复杂的事情变得简单，而不是把简单的事情变得看似高深。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原本不需要代码的需求变得需要编写代码，这又怎么能是技术进步了！？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.ray-d-song.com/2025/03/d33ae369399e2890b5618d1c8030b444.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;过度炒作的 Devin 和 Manus&lt;/h2&gt;
&lt;p&gt;最近有个团队做了一款号称通用的AI Agent，也就是Manus。&lt;/p&gt;
&lt;p&gt;营销号开始发测评的当天晚上就闹出个乐子，Manus的官网有bug，可以绕过邀请码登录进入主菜单（虽然无法执行操作）。&lt;/p&gt;
&lt;p&gt;有人在Discord群发了这个问题，结果Discord的交流频道直接关掉，直到现在也是禁言状态。&lt;/p&gt;
&lt;p&gt;话说，Manus团队为什么不让Manus给他们修Bug呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.ray-d-song.com/2025/03/af20a1bff8291fd6566a2df00e26102b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上一个牛皮吹这么大的还是 Devin，自称是&lt;em&gt;&lt;strong&gt;全球首位AI软件工程师。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;去年我实打实的套了100刀租了个号体验了一把，评价是&lt;strong&gt;不如Claude3.7，甚至不如Cursor + R1 API&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;现在一年过去了，Devin 已然熄火，几乎没人讨论了。&lt;/p&gt;
&lt;p&gt;话说，Manus 和 Devin 都是华人团队的作品，也许他们在下一盘大棋，等着收割老美的韭菜也说不定？&lt;/p&gt;
&lt;h2&gt;LLM 和低代码&lt;/h2&gt;
&lt;p&gt;为什么我的标题会把LLM和低代码扯上关系。&lt;/p&gt;
&lt;p&gt;因为低代码是少有的被一遍又一遍被拉出来炒作的概念，而且低代码确实解决了一些问题。&lt;/p&gt;
&lt;p&gt;90年代VB的拖放界面，到后来的Winform，PowerApps，OutSystems。&lt;/p&gt;
&lt;p&gt;事实上，大多数的大模型Showcase都&lt;strong&gt;完全没有必要编写代码&lt;/strong&gt;，市场上有大量的开源和低代码方案。&lt;/p&gt;
&lt;p&gt;疫情时期，很多社区团购都使用了某赞的小程序低代码平台，我认为这是普通人不用代码去解决代码问题的很好例子。&lt;/p&gt;
&lt;p&gt;我知道很多人会说，用低代码的话，你怎么让我的网站和App有自己的审美风格呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你不会认为你让Vercel V0用Shadcn 写的那些玩意很有风格吧。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;明明大家都是Shadcn，你有个蛋的风格。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;软件行业 = 西西弗斯&lt;/h2&gt;
&lt;p&gt;很多人都听过西西弗斯的故事，这位苦命的老哥每天要把巨石推到山顶，而每次到达山顶后巨石又滚回山下，如此永无止境地重复下去。&lt;/p&gt;
&lt;p&gt;举个例子，有赞有低代码团购，帆软有低代码报表，大家还是去做自己的团购和报表。即便知道自己搓的东西和大厂比烂的跟shi一样，老板还是会让你去做。&lt;/p&gt;
&lt;p&gt;拼多多、抖音的活动页一个月甚至是一周变一次。&lt;/p&gt;
&lt;p&gt;软件行业就是这样，每个从业者不厌其烦的去推那块巨石。&lt;/p&gt;
&lt;p&gt;每种编程语言，处理同一个任务，都有无数个工具和框架，甚至每天还在诞生新的。&lt;/p&gt;
&lt;p&gt;LLM没有解决这些问题，甚至LLM也只是这一循环的一部分。&lt;/p&gt;
&lt;h2&gt;Manus们 葫芦里在卖什么药？&lt;/h2&gt;
&lt;p&gt;Manus 实际上在贩卖的是一种“概念”，而不是新的工具。&lt;/p&gt;
&lt;p&gt;他们包装的所谓&quot;智能代理&quot;，本质上是将已有的API调用和简单自动化流程重新包装。这些功能在许多现有工具中早已存在，只是被赋予了更炫目的AI光环。&lt;/p&gt;
&lt;p&gt;不只是Manus，这波风口中大量的营销材料都是&quot;未来将会实现&quot;的模糊承诺。这是软件行业炒作周期的常见饵料：先卖概念和愿景，实际功能则被推迟到&quot;即将到来&quot;的更新中。&lt;/p&gt;
&lt;h2&gt;LLM 没有质变&lt;/h2&gt;
&lt;p&gt;事实上，从GPT3.5到现在的Claude3.7，已经过去了4 年时间。这4年来LLM在沿着一个可以预期的曲线发生量变，而没有质变。&lt;/p&gt;
&lt;p&gt;甚至我觉得早期未降智的GPT3.5要比现在的千问啥的强一点（也比现在降智后的 4o mini 强）。&lt;/p&gt;
&lt;p&gt;甚至有时候我有点恍惚，OpenAI是不是每次发布新模型之前，就把老模型降智。&lt;/p&gt;
&lt;p&gt;就像有些店铺每次双十一之前就先涨价，让你觉得它便宜了。&lt;/p&gt;
&lt;p&gt;Anyway，也许未来真的有技术能实现AGI，但应该不是LLM。&lt;/p&gt;
&lt;p&gt;不知道有多少人还记得Siri刚发布的时候，Google、微软、亚马逊都紧随其步伐，发布自己的语音人工智能助理。&lt;/p&gt;
&lt;p&gt;可谓是勃勃生机，万物进发的境界。&lt;/p&gt;
&lt;p&gt;资本一起涌入，当时大家也认为技术会稳步推进，最终达到真正的人工智能。&lt;/p&gt;
&lt;p&gt;甚至Siri也引发了关于AI伦理、隐私安全和技术依赖等社会讨论，和LLM如出一辙。（事实证明都多虑了&lt;/p&gt;
&lt;p&gt;最后是截然不同的技术（LLM）革了Siri的命，我想未来也会是和LLM截然不同的技术实现AGI。&lt;/p&gt;
</content:encoded></item><item><title>虚拟机类型和发展历史考古</title><link>https://ray-d-song.com/blog/2025/02/vm-type-and-nest-vm</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/02/vm-type-and-nest-vm</guid><pubDate>Sat, 08 Feb 2025 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在虚拟化技术领域，虚拟机（Virtual Machine，VM）是一个革命性的技术创新。通过虚拟化技术，我们可以在同一台物理机器上运行多个相互隔离的操作系统环境。根据实现方式的不同，虚拟机主要可以分为 Type-1 和 Type-2 两种类型，它们各自具有独特的特点和应用场景。&lt;/p&gt;
&lt;h2&gt;第一类虚拟机（Type-1 Hypervisor）&lt;/h2&gt;
&lt;p&gt;第一类虚拟机也被称为裸机虚拟机（Bare-metal Hypervisor），它是最早出现的虚拟化技术实现。&lt;/p&gt;
&lt;h3&gt;技术特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;*直接运行在物理硬件之上，无需宿主操作系统&lt;/li&gt;
&lt;li&gt;具有接近原生硬件的性能表现&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;典型代表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;VMware ESXi：最成熟的企业级虚拟化解决方案&lt;/li&gt;
&lt;li&gt;Microsoft Hyper-V：Windows Server的原生虚拟化平台&lt;/li&gt;
&lt;li&gt;Citrix XenServer：源自开源项目Xen的企业级产品&lt;/li&gt;
&lt;li&gt;KVM（特例）：Linux内核的原生虚拟化模块，具有混合特性&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;主要应用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;企业级数据中心的服务器整合&lt;/li&gt;
&lt;li&gt;大规模云计算平台&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二类虚拟机（Type-2 Hypervisor）&lt;/h2&gt;
&lt;p&gt;第二类虚拟机是在个人计算机时代发展起来的虚拟化解决方案，主要面向开发者和个人用户。&lt;/p&gt;
&lt;h3&gt;技术特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;*运行在宿主操作系统之上，作为普通应用程序运行&lt;/li&gt;
&lt;li&gt;可以方便地与宿主系统共享资源&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;典型代表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;VMware Workstation：最早的成熟桌面虚拟化产品&lt;/li&gt;
&lt;li&gt;Oracle VirtualBox：开源免费，跨平台支持&lt;/li&gt;
&lt;li&gt;Parallels Desktop：Mac平台最优秀的虚拟化解决方案&lt;/li&gt;
&lt;li&gt;QEMU：灵活的开源模拟器和虚拟化工具&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;主要应用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;软件开发和测试环境&lt;/li&gt;
&lt;li&gt;个人桌面虚拟化需求&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;虚拟机的发展历程&lt;/h2&gt;
&lt;h3&gt;早期阶段（1960年代）&lt;/h3&gt;
&lt;p&gt;虚拟化技术最早可以追溯到1960年代的IBM CP-40和CP-67系统。这些系统是第一类虚拟机的雏形，主要解决了当时昂贵的大型主机资源共享问题。IBM通过开发虚拟机技术，成功实现了硬件资源的更高效利用，为现代虚拟化技术奠定了基础。&lt;/p&gt;
&lt;h3&gt;发展转折（1970-1990年代）&lt;/h3&gt;
&lt;p&gt;随着个人计算机的普及和硬件成本的下降，虚拟化技术经历了一段相对沉寂的时期。但到了1990年代末期，由于服务器数量激增导致管理成本上升，虚拟化技术重新获得关注。&lt;/p&gt;
&lt;h3&gt;第二类虚拟机的诞生（1990年代末）&lt;/h3&gt;
&lt;p&gt;为了适应新的市场需求，特别是个人用户和开发者的需求，第二类虚拟机应运而生。1999年发布的VMware Workstation开创了桌面虚拟化的新纪元，它完美解决了以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发者需要在同一台机器上测试不同操作系统&lt;/li&gt;
&lt;li&gt;用户需要在现有系统上运行其他操作系统&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;现代发展（2000年至今）&lt;/h3&gt;
&lt;p&gt;虚拟化技术在21世纪迎来了爆发式发展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2001年：VMware发布ESX Server（现为ESXi），开创了现代企业级虚拟化新纪元&lt;/li&gt;
&lt;li&gt;2003年：Xen开源虚拟机监控器发布，推动了开源虚拟化的发展&lt;/li&gt;
&lt;li&gt;2006年：KVM被整合进Linux内核，为Linux原生虚拟化提供了强大支持&lt;/li&gt;
&lt;li&gt;2008年：Microsoft发布Hyper-V，丰富了企业级虚拟化解决方案&lt;/li&gt;
&lt;li&gt;2010年后：云计算的兴起进一步推动了虚拟化技术的普及和创新&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>没有显示器如何初始化并连接 Linux 服务器和开发板</title><link>https://ray-d-song.com/blog/2025/01/pi-without-monitor</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/01/pi-without-monitor</guid><pubDate>Wed, 29 Jan 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;系统烧录&lt;/h2&gt;
&lt;p&gt;最近入手了一个香橙派zero3，但手头没有可以连接显示器的mini HDMI线材，所以需要在烧录系统后开启 ssh 功能并直接写入客户端设备的密钥。&lt;/p&gt;
&lt;p&gt;首先正常烧录系统，&lt;a href=&quot;https://etcher.balena.io/&quot;&gt;balenaEtcher&lt;/a&gt; 和&lt;a href=&quot;https://www.raspberrypi.com/software/&quot;&gt;Raspberry Pi Imager&lt;/a&gt;都可以烧录自定义镜像。&lt;br /&gt;
我选择Raspberry Pi Imager，因为他比balenaEtcher 更轻量。&lt;/p&gt;
&lt;h2&gt;macOS 独有的问题&lt;/h2&gt;
&lt;p&gt;烧录成功后，如果你是 macOS 系统，会提示「此电脑不能读取你连接的磁盘。」，这是因为 macOS 原生不支持 ext4 文件格式，先点击忽略。&lt;br /&gt;
虽然你可能会惊讶，但这个问题在 Mac 上一直没有好的解决方案。&lt;br /&gt;
可用的方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PD、VMWare、UTM 等虚拟机运行 Linux&lt;/li&gt;
&lt;li&gt;使用 Fuse 拓展让 macOS 支持 ext4，目前只有 extFS for Mac 这样一个付费的软件方案&lt;/li&gt;
&lt;li&gt;你有一台额外的服务器（非macOS系统），将ext4的存储器作为samba暴露&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三种方案都不是很优雅，我推荐第一个，直接使用开源且免费的 UTM 虚拟机跑 Linux。&lt;/p&gt;
&lt;h2&gt;配置开机自动连接 WiFi&lt;/h2&gt;
&lt;p&gt;首先配置WiFi连接，需要编辑&lt;code&gt;/etc/wpa_supplicant/wpa_supplicant.conf&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=CN

network={
    ssid=&quot;你的WiFi名称&quot;
    psk=&quot;你的WiFi密码&quot;
    key_mgmt=WPA-PSK
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来确保WiFi服务开机自启，编辑 /etc/network/interfaces 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto lo
iface lo inet loopback

auto wlan0
allow-hotplug wlan0
iface wlan0 inet dhcp
    wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;确保将上面配置中的 你的WiFi名称 和 你的WiFi密码 替换为实际的值&lt;/li&gt;
&lt;li&gt;WiFi接口名可能不是 wlan0，某些设备可能是 wlan1 或其他名称，你需要确认实际的接口名&lt;/li&gt;
&lt;li&gt;country=CN 是设置国家代码，这会影响到可用的WiFi频道&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;配置 ssh&lt;/h2&gt;
&lt;p&gt;大多数 Linux 发行版默认已经安装了 ssh 服务。&lt;br /&gt;
首先，确保SSH服务开机自启动，检查并编辑&lt;code&gt;/etc/systemd/system/multi-user.target.wants/&lt;/code&gt;目录下是否有 ssh.service 的软链接。如果没有，你可以手动创建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在SD卡中找到这个目录，创建软链接
ln -s /lib/systemd/system/ssh.service /etc/systemd/system/multi-user.target.wants/ssh.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查&lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;文件，确保以下设置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Port 22
PermitRootLogin yes
PasswordAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你还可以在SD卡的根目录创建一个名为&lt;code&gt;ssh&lt;/code&gt;的空文件，在很多嵌入式Linux系统中这代表开机启动ssh服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ssh连接&lt;/h2&gt;
&lt;p&gt;接下来你可以通过路由器管理界面、命令行工具（arp -a)、Angry Ip Scanner 等方法获取开发板的内网ip，使用ssh命令连接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh root@xxx.xxx.xxx.xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;root 用户的密码一般会有一个默认值，比如armbian的默认密码是&lt;code&gt;1234&lt;/code&gt;，香橙派是&lt;code&gt;orangepi&lt;/code&gt;，树莓派的默认用户是pi密码raspberry。&lt;br /&gt;
具体可以参照系统的官方文档。&lt;/p&gt;
</content:encoded></item><item><title>如何在 Vue 中动态绑定组件名称 (option name)</title><link>https://ray-d-song.com/blog/2025/01/how-to-attach-component-name-dynamic</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/01/how-to-attach-component-name-dynamic</guid><pubDate>Tue, 21 Jan 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;Vue 的缓存组件 &lt;code&gt;KeepAlive&lt;/code&gt; 以组件的名称（也就是 &lt;code&gt;option name&lt;/code&gt;）作为缓存的 key。&lt;br /&gt;
在 Vue 中定义 name 有两种方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
{/* setup 中定义 */}
defineOptions({
  name: &apos;MyComponent&apos;
})
&amp;lt;/script&amp;gt;

&amp;lt;script&amp;gt;
{/* 在 script 中定义 */}
export default {
  name: &apos;MyComponent&apos;
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这两种方式都是静态的，无法根据不同参数，动态绑定组件名称。&lt;br /&gt;
比如，如果你想根据路由的参数来动态指定名称的话会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
const route = useRoute()
defineOptions({
  name: route.params.name
})
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt; × ModuleError: [@vue/compiler-sfc] `defineOptions()` in &amp;lt;script setup&amp;gt; cannot reference locally declared variables because it will be hoisted outside of the setup() function.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个错误的意思是 defineOptions 在编译后会被提升到 setup 函数外，所以无法访问到 route.params.name。&lt;/p&gt;
&lt;p&gt;&amp;lt;strong&amp;gt;那么如果多个页面使用同一个 Page 组件，他们要么都被缓存，要么都不被缓存。&amp;lt;/strong&amp;gt;&lt;br /&gt;
这种行为肯定不是我们想要的。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;解决方案就是给组件再包一层。&lt;br /&gt;
定义一个函数，通过函数参数返回被包装的组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Component } from &apos;vue&apos;

function createPage(name: string, component: Component) {
  return {
    // 组件名称
    name,
    data () {
      return { component: null }
    },
    async created () {
      if (component instanceof Promise) {
        try {
          const module = await component
          this.component = module?.default
        } catch (error) {
          console.error(`can not resolve component ${name}, error:`, error)
        }

        return
      }
      this.component = component
    },
    render () {
      return this.component ? h(this.component) : null
    },
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数接收两个参数：组件名称和组件本身。&lt;br /&gt;
这样，我们就可以根据不同参数，动态指定组件名称了。&lt;/p&gt;
</content:encoded></item><item><title>分享我的 Vim 键位，以及在多个编辑器中配置键位的方法</title><link>https://ray-d-song.com/blog/2025/01/my-vim-keymap</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/01/my-vim-keymap</guid><pubDate>Sun, 19 Jan 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;键位列表&lt;/h2&gt;
&lt;p&gt;秉持少即是多的原则，我只会配置最需要的那些快捷键。&lt;/p&gt;
&lt;h3&gt;输入模式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;缩写原意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;退出编辑&lt;/td&gt;
&lt;td&gt;jj&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;命令模式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;缩写原意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;保存+格式化&lt;/td&gt;
&lt;td&gt;sa&lt;/td&gt;
&lt;td&gt;save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保存+关闭当前文件&lt;/td&gt;
&lt;td&gt;wq&lt;/td&gt;
&lt;td&gt;write quit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;快速修复&lt;/td&gt;
&lt;td&gt;qf&lt;/td&gt;
&lt;td&gt;quick fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前往定义&lt;/td&gt;
&lt;td&gt;gd&lt;/td&gt;
&lt;td&gt;go to definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查找引用&lt;/td&gt;
&lt;td&gt;gr&lt;/td&gt;
&lt;td&gt;go to reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全局搜索&lt;/td&gt;
&lt;td&gt;gs&lt;/td&gt;
&lt;td&gt;global search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;当前文件搜索&lt;/td&gt;
&lt;td&gt;cs&lt;/td&gt;
&lt;td&gt;current search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关闭当前文件&lt;/td&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向上移动 10 行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&lt;/code&gt;k&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向下移动 10 行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&lt;/code&gt;j&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向左移动 10 个字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&lt;/code&gt;h&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向右移动 10 个字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&lt;/code&gt;l&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;其他&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;缩写原意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;向上快速移动&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;&lt;/code&gt;k&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向下&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;&lt;/code&gt;j&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向左&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;&lt;/code&gt;h&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向右&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;&lt;/code&gt;l&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;分屏操作&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;缩写原意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;水平分屏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;sx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;split x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;垂直分屏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;sy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;split y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关闭分屏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;sc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;split close&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在多个窗口间切换&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;缩写原意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;切换到左边窗口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;screen h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换到右边窗口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;sl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换到上边窗口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;sk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换到下边窗口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;space&amp;gt;sj&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;VSCode 和 NeoVim 配置&lt;/h2&gt;
&lt;p&gt;VSCode 有 &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=asvetliakov.vscode-neovim&quot;&gt;VSCode-NeoVim 插件&lt;/a&gt;，可以在 VSCode 中使用 NeoVim 作为后端。&lt;br /&gt;
因此 VSCode 可以直接使用 NeoVim 的配置。&lt;/p&gt;
&lt;p&gt;NeoVim 使用&lt;code&gt;~/.config/nvim/init.vim&lt;/code&gt;作为配置文件。&lt;/p&gt;
&lt;p&gt;需要注意的是，关闭文件、lsp 修复提示等，在两个编辑器中执行不同的操作，我们可以使用 Lua 的条件语句来做判断。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if vim.g.vscode
then
  -- vscode 的配置
else
  -- neovim 的配置
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的配置文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 命令模式键位配置
if vim.g.vscode then
  -- VSCode 特定配置
  vim.keymap.set(&apos;n&apos;, &apos;sa&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.files.save&quot;)&amp;lt;CR&amp;gt;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;editor.action.formatDocument&quot;)&amp;lt;CR&amp;gt;&apos;)          -- 保存文件并格式化
  vim.keymap.set(&apos;n&apos;, &apos;wq&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.files.save&quot;)&amp;lt;CR&amp;gt;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.closeActiveEditor&quot;)&amp;lt;CR&amp;gt;&apos;)  -- 保存并关闭文件
  vim.keymap.set(&apos;n&apos;, &apos;qf&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;editor.action.quickFix&quot;)&amp;lt;CR&amp;gt;&apos;)              -- 显示快速修复建议
  vim.keymap.set(&apos;n&apos;, &apos;gd&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;editor.action.revealDefinition&quot;)&amp;lt;CR&amp;gt;&apos;)      -- 跳转到定义
  vim.keymap.set(&apos;n&apos;, &apos;gr&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;editor.action.goToReferences&quot;)&amp;lt;CR&amp;gt;&apos;)        -- 查看引用
  vim.keymap.set(&apos;n&apos;, &apos;gs&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.findInFiles&quot;)&amp;lt;CR&amp;gt;&apos;)        -- 全局搜索
  vim.keymap.set(&apos;n&apos;, &apos;cs&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;actions.find&quot;)&amp;lt;CR&amp;gt;&apos;)                        -- 当前文件内搜索
  vim.keymap.set(&apos;n&apos;, &apos;x&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.closeActiveEditor&quot;)&amp;lt;CR&amp;gt;&apos;)   -- 关闭当前文件件

  -- 分屏操作键位
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sx&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.splitEditorDown&quot;)&amp;lt;CR&amp;gt;&apos;)    -- 水平分屏
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sy&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.splitEditorRight&quot;)&amp;lt;CR&amp;gt;&apos;)   -- 垂直分屏
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sc&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.closeActiveEditor&quot;)&amp;lt;CR&amp;gt;&apos;)  -- 关闭当前分屏

  -- 窗口切换键位
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sh&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.focusLeftGroup&quot;)&amp;lt;CR&amp;gt;&apos;)     -- 切换到左边窗口
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sl&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.focusRightGroup&quot;)&amp;lt;CR&amp;gt;&apos;)    -- 切换到右边窗口
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sk&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.focusAboveGroup&quot;)&amp;lt;CR&amp;gt;&apos;)    -- 切换到上边窗口
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sj&apos;, &apos;&amp;lt;Cmd&amp;gt;call VSCodeCall(&quot;workbench.action.focusBelowGroup&quot;)&amp;lt;CR&amp;gt;&apos;)    -- 切换到下边窗口
else
  -- NeoVim 原生配置

  -- 输入模式键位配置
  vim.keymap.set(&apos;i&apos;, &apos;jj&apos;, &apos;&amp;lt;Esc&amp;gt;&apos;)  -- 使用 jj 快速退出输入模式
  vim.keymap.set(&apos;n&apos;, &apos;sa&apos;, &apos;:w&amp;lt;CR&amp;gt;&apos;)        -- 保存文件
  vim.keymap.set(&apos;n&apos;, &apos;wq&apos;, &apos;:wq&amp;lt;CR&amp;gt;&apos;)       -- 保存并关闭文件
  vim.keymap.set(&apos;n&apos;, &apos;qf&apos;, &apos;:lua vim.lsp.buf.code_action()&amp;lt;CR&amp;gt;&apos;)      -- 显示 LSP 快速修复建议
  vim.keymap.set(&apos;n&apos;, &apos;gd&apos;, &apos;:lua vim.lsp.buf.definition()&amp;lt;CR&amp;gt;&apos;)       -- 跳转到定义
  vim.keymap.set(&apos;n&apos;, &apos;gr&apos;, &apos;:lua vim.lsp.buf.references()&amp;lt;CR&amp;gt;&apos;)       -- 查看引用
  vim.keymap.set(&apos;n&apos;, &apos;gs&apos;, &apos;:lua require(&quot;telescope.builtin&quot;).live_grep()&amp;lt;CR&amp;gt;&apos;)              -- 使用 telescope 进行全局搜索
  vim.keymap.set(&apos;n&apos;, &apos;cs&apos;, &apos;:lua require(&quot;telescope.builtin&quot;).current_buffer_fuzzy_find()&amp;lt;CR&amp;gt;&apos;) -- 使用 telescope 在当前文件中搜索
  vim.keymap.set(&apos;n&apos;, &apos;x&apos;, &apos;:q&amp;lt;CR&amp;gt;&apos;)         -- 关闭当前文件

  -- 分屏操作键位
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sx&apos;, &apos;:split&amp;lt;CR&amp;gt;&apos;)   -- 水平分屏
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sy&apos;, &apos;:vsplit&amp;lt;CR&amp;gt;&apos;)  -- 垂直分屏
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sc&apos;, &apos;:close&amp;lt;CR&amp;gt;&apos;)   -- 关闭当前分屏
  
  -- 窗口切换键位
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sh&apos;, &apos;&amp;lt;C-w&amp;gt;h&apos;)  -- 切换到左边窗口
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sl&apos;, &apos;&amp;lt;C-w&amp;gt;l&apos;)  -- 切换到右边窗口
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sk&apos;, &apos;&amp;lt;C-w&amp;gt;k&apos;)  -- 切换到上边窗口
  vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;sj&apos;, &apos;&amp;lt;C-w&amp;gt;j&apos;)  -- 切换到下边窗口
end

-- 移动相关键位配置
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;k&apos;, &apos;10k&apos;)  -- 向上移动 10 行
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;j&apos;, &apos;10j&apos;)  -- 向下移动 10 行
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;h&apos;, &apos;10h&apos;)  -- 向左移动 10 个字符
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;l&apos;, &apos;10l&apos;)  -- 向右移动 10 个字符

-- 快速移动键位（使用 Hop）
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;k&apos;, &apos;:HopLineStartBC&amp;lt;CR&amp;gt;&apos;)  -- 向上移动
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;j&apos;, &apos;:HopLineStartAC&amp;lt;CR&amp;gt;&apos;)  -- 向下移动
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;h&apos;, &apos;:HopWordBC&amp;lt;CR&amp;gt;&apos;)  -- 向左移动
vim.keymap.set(&apos;n&apos;, &apos;&amp;lt;space&amp;gt;&amp;lt;space&amp;gt;l&apos;, &apos;:HopWordAC&amp;lt;CR&amp;gt;&apos;)  -- 向右移动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，使用 jj 或 jk 映射退出编辑模式，需要在 VSCode 配置文件中添加如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;vscode-neovim.compositeKeys&quot;: {
    &quot;jj&quot;: {
      &quot;command&quot;: &quot;vscode-neovim.escape&quot;,
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NeoVim 相关配置涉及到 Lua 语言、Hop、LSP 插件等内容，因为这篇文章主要讲快捷键，所以不作展开。&lt;/p&gt;
</content:encoded></item><item><title>奇怪的警告：Vue 避免枚举组件 key</title><link>https://ray-d-song.com/blog/2025/01/vue-avoid-enumerating-component-keys</link><guid isPermaLink="true">https://ray-d-song.com/blog/2025/01/vue-avoid-enumerating-component-keys</guid><pubDate>Tue, 14 Jan 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;Vue 项目打包后，在浏览器中运行，会发生如下警告：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Vue warn]: Avoid app logic that relies on enumerating keys on a component instance.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题原因&lt;/h2&gt;
&lt;p&gt;这个警告的含义是：避免在组件实例上枚举键。&lt;br /&gt;
Vue 允许在开发环境下通过 for 或 Object.keys 等方法枚举组件实例上的键，生产环境下则不允许。&lt;/p&gt;
&lt;p&gt;抽象之处在于，出现该警告时，warn trace 并不会给出具体的代码行数，而是提示在 &lt;code&gt;vue.runtime.esm-bundler.js&lt;/code&gt; 文件中。&lt;br /&gt;
同时出现该警告的原因稀奇古怪，并不一定是代码中真的有枚举组件实例的键。&lt;/p&gt;
&lt;h2&gt;可能的原因和解决方案&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;vue-devtools&lt;/code&gt; 调试工具的 Bug&lt;/h3&gt;
&lt;p&gt;Vue Devtools 会监听组件实例，引发这个问题。可以先将 &lt;code&gt;vue-devtools&lt;/code&gt; 卸载，判断是否是工具原因。&lt;br /&gt;
通过升级 &lt;code&gt;vue-devtools&lt;/code&gt; 或 Vue 版本（也许）可以解决。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;watch&lt;/code&gt; 监听了 route 对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const route = useRoute()
/**
 * 监听了整个 route 对象
 * watch 默认是深层响应式，会触发该警告
 */
watch(() =&amp;gt; route, () =&amp;gt; {
  // ...
})

// 修改为监听具体的 route 的属性
watch(() =&amp;gt; route.path, () =&amp;gt; {
  // ...
})

// 或者使用默认非深层响应式的 watchEffect
watchEffect(() =&amp;gt; {
  console.log(route.path)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由守卫中传递了完整的 to 对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;router.beforeEach((to, from, next) =&amp;gt; {
  addTab(to)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是我实际遇到的情况，在路由守卫中传递了完整的 to 对象，包括 &lt;code&gt;component&lt;/code&gt; 属性，导致触发该警告。&lt;br /&gt;
正确的做法是按需传递 to 对象的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;router.beforeEach((to, from, next) =&amp;gt; {
  addTab(to.path)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;参考&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/68100114/avoid-app-logic-that-relies-on-enumerating-keys-on-a-component-instance&quot;&gt;vue.js - Avoid app logic that relies on enumerating keys on a component instance - Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[Vue warn]: Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead. · Issue #2027 · vuejs/core](https://github.com/vuejs/core/issues/2027)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nuxt-modules/i18n/issues/2856&quot;&gt;Warning: Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead. · Issue #2856 · nuxt-modules/i18n&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>自部署 WebDAV 快速上手</title><link>https://ray-d-song.com/blog/2024/12/webdav-quick-start</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/12/webdav-quick-start</guid><pubDate>Mon, 16 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 WebDAV&lt;/h2&gt;
&lt;p&gt;简单来说，WebDAV 就是基于 HTTP 协议的文件系统，可以挂载后像本地文件一样使用。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;有许多开源的 WebDAV 服务，我使用的是 Go 编写的 &lt;a href=&quot;https://github.com/hacdias/webdav&quot;&gt;webdav&lt;/a&gt;。&lt;br /&gt;
优点是使用简单，直接下载编译好的二进制文件，然后运行即可。&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://github.com/hacdias/webdav/releases&quot;&gt;Releases&lt;/a&gt; 页面下载对应平台的二进制文件，然后执行后续操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar -zxvf webdav-linux-amd64.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解压后包含以下文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;webdav&lt;/code&gt; 二进制文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LICENSE&lt;/code&gt; 许可证&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt; 描述文件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;webdav&lt;/code&gt;所在目录下创建&lt;code&gt;config.yml&lt;/code&gt;文件，参考配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 监听地址
address: 0.0.0.0
# 监听端口
port: 6065
# 如果您想直接启用 TLS，相关的 TLS 设置。
# TLS 相关设置
tls: false
cert: cert.pem
key: key.pem

# 应用于 WebDAV 路径的前缀。默认为 &apos;/&apos;。
prefix: /

# 启用或禁用调试日志记录。默认为 &apos;false&apos;。
debug: false

# 禁用嗅探文件以检测其内容类型。默认为 &apos;false&apos;。
noSniff: false

# 服务器是否在受信任的代理后运行。如果为 true，
# 将使用 X-Forwarded-For 头来记录远程地址
# （如果可用）。
behindProxy: false

# 用户连接时可以访问的目录。
# 该目录将被用户使用，除非他们定义了自己的&apos;directory&apos;。
# 默认为 &apos;.&apos;（当前目录）。
directory: .

# [!code highlight:5]
# 用户的默认权限。此选项不区分大小写。可能的
# 权限：C（创建）、R（读取）、U（更新）、D（删除）。您可以组合多个
# 权限。例如，要允许读取和创建，请设置 &quot;RC&quot;。默认为 &quot;R&quot;。
permissions: R

# 用户的默认权限规则。默认为无。规则从最后到第一个应用，
# 即第一个匹配请求的规则将被应用。
rules: []

# 重新定义用户规则的行为。可以是：
# - overwrite：当用户定义了规则时，这些规则将覆盖任何全局
#   已定义的规则。也就是说，全局规则对用户不适用。
# - append：当用户定义了规则时，这些规则将附加到全局
#   已定义的规则。对于该用户，他们自己的特定规则将首先被检查，
#   然后是全局规则。默认为 &apos;overwrite&apos;。
rulesBehavior: overwrite

# 日志配置
log:
  # 日志格式（&apos;console&apos;，&apos;json&apos;）。默认为 &apos;console&apos;。
  format: console
  # 启用或禁用颜色。默认为 &apos;true&apos;。仅在格式为 &apos;console&apos; 时应用。
  colors: true
  # 日志输出。您可以有多个输出。默认为仅 &apos;stderr&apos;。
  outputs:
  - stderr

# CORS 配置
cors:
  # 是否应用 CORS 配置。默认为 &apos;false&apos;。
  enabled: true
  credentials: true
  allowed_headers:
    - Depth
  allowed_hosts:
    - http://localhost:8080
  allowed_methods:
    - GET
  exposed_headers:
    - Content-Length
    - Content-Range

# [!code highlight:18]
# 用户列表。如果列表为空，则不会有身份验证。
# 否则，将自动配置基本身份验证。
#
# 如果您将身份验证委托给其他服务，您可以使用基本身份验证代理
# 用户名，然后使用选项禁用 webdav 的密码检查：
#
# noPassword: true
users:
  # 示例 &apos;admin&apos; 用户，使用明文密码。
  - username: admin
    password: admin
  # 示例 &apos;john&apos; 用户，使用 bcrypt 加密密码，并具有自定义目录。
  - username: john
    password: &quot;{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi&quot;
    directory: /another/path
  # 示例用户，其详细信息将从环境中获取。
  - username: &quot;{env}ENV_USERNAME&quot;
    password: &quot;{env}ENV_PASSWORD&quot;
  - username: basic
    password: basic
    # 覆盖默认权限
    permissions: CRUD
    rules:
      # 使用此规则，用户无法访问 /some/files。
      - path: /some/file
        permissions: none
      # 使用此规则，用户可以在 /public/access/ 中创建、读取、更新和删除。
      - path: /public/access/
        permissions: CRUD
      # 使用此规则，用户可以读取和更新所有以 .js 结尾的文件。它使用正则表达式。
      - regex: &quot;^.+.js$&quot;
        permissions: RU
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 直接运行
./webdav

# 后台运行
./webdav &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WebDAV 服务会自动读取同目录下的&lt;code&gt;config.yml&lt;/code&gt;文件，默认的数据目录就是当前目录。&lt;/p&gt;
&lt;h3&gt;设置为开机自启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建 systemd 服务文件
sudo vim /etc/systemd/system/webdav.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=WebDAV

[Service]
ExecStart=/path/to/webdav
Restart=always

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用服务：&lt;br /&gt;
记得将&lt;code&gt;/path/to/webdav&lt;/code&gt;替换为实际路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable webdav.service
sudo systemctl start webdav.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;nginx 反代和 https 配置&lt;/h3&gt;
&lt;p&gt;如果你不熟悉 nginx，可以先参考我写的 &lt;a href=&quot;/blog/nginx-config-https&quot;&gt;nginx 配置 HTTPS 教程&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在 nginx 配置中新增一个 server 配置，参考如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 443 ssl;
    # [!code highlight:5]
    server_name your_domain.com;

    ssl_certificate /path/to/your_certificate.crt;
    ssl_certificate_key /path/to/your_private_key.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://localhost:6065; # [!code highlight]
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中需要关注的高亮行:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;your_domain.com&lt;/code&gt; 替换为你的域名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/path/to/your_certificate.crt&lt;/code&gt; 替换为你的证书路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/path/to/your_private_key.key&lt;/code&gt; 替换为你的私钥路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://localhost:6065&lt;/code&gt; 替换为你的服务地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保存配置文件后, 重启 Nginx 服务.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;使用 WebDAV 的方式很多，这里介绍用 Mac 默认的 Finder 客户端使用 WebDAV。&lt;br /&gt;
在 Finder 中，选择&lt;code&gt;前往&lt;/code&gt; -&amp;gt; &lt;code&gt;连接服务器&lt;/code&gt;，输入你的域名和端口，然后输入用户名和密码，就可以像本地文件一样使用 WebDAV 了。&lt;/p&gt;
&lt;p&gt;如果增改删的时候提示权限不足，请检查你的配置文件的高亮行部分，确保用户有相应的权限。&lt;/p&gt;
</content:encoded></item><item><title>vite-plugin-vue 实现分析</title><link>https://ray-d-song.com/blog/2024/12/vite-plugin-vue</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/12/vite-plugin-vue</guid><pubDate>Fri, 13 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在编写&lt;a href=&quot;https://github.com/ray-d-song/vue-jit&quot;&gt;vue-jit&lt;/a&gt;项目，直接在浏览器中编译 Vue 组件并运行，因此需要了解在 Vite 中，Vue 插件是如何工作的。&lt;/p&gt;
&lt;h1&gt;插件架构&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph vite-plugin-vue
        A[plugin-vue]
        B[plugin-vue-jsx]
    end

    subgraph plugin-vue核心功能
        A --&amp;gt; C[Vue SFC处理]
        A --&amp;gt; D[HMR热更新]
        A --&amp;gt; E[SSR支持]

        C --&amp;gt; C1[template编译]
        C --&amp;gt; C2[script处理]
        C --&amp;gt; C3[style处理]
        C --&amp;gt; C4[custom blocks]

        D --&amp;gt; D1[文件变更监听]
        D --&amp;gt; D2[组件重载]

        E --&amp;gt; E1[SSR模块注册]
        E --&amp;gt; E2[SSR上下文]
    end

    subgraph plugin-vue-jsx核心功能
        B --&amp;gt; F[JSX/TSX转换]
        B --&amp;gt; G[HMR支持]
        B --&amp;gt; H[SSR支持]

        F --&amp;gt; F1[Babel转换]
        F --&amp;gt; F2[TypeScript支持]

        G --&amp;gt; G1[组件热重载]

        H --&amp;gt; H1[SSR模块注册]
    end

    subgraph 公共功能
        I[源码映射]
        J[自定义元素支持]
        K[开发工具集成]
    end

    A --&amp;gt; I
    A --&amp;gt; J
    A --&amp;gt; K

    B --&amp;gt; I
    B --&amp;gt; K
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只提取 SFC 部分的架构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    A[plugin-vue]
    A --&amp;gt; C[Vue SFC处理]
    C --&amp;gt; C1[template编译]
    C --&amp;gt; C2[script处理]
    C --&amp;gt; C3[style处理]
    C --&amp;gt; C4[custom blocks]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Vue SFC 处理&lt;/h2&gt;
&lt;p&gt;入口位于&lt;code&gt;/packages/plugin-vue/src/main.ts&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;流程如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Vite 通过 plugin-vue 插件拦截 .vue 文件&lt;/li&gt;
&lt;li&gt;将 SFC 解析为 descriptor (描述符)&lt;/li&gt;
&lt;li&gt;分别处理 template/script/style 块&lt;/li&gt;
&lt;li&gt;生成最终的 JavaScript 代码&lt;/li&gt;
&lt;li&gt;处理 HMR、Sourcemap 等&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来试试编译后的结果长什么样，以下面这个 .vue 文件为例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;span&amp;gt;{{ name }}&amp;lt;/span&amp;gt;
    &amp;lt;span&amp;gt;{{ age }}&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
import { ref } from &apos;vue&apos;

const props = defineProps({
  name: {
    type: String,
    default: &apos;Ray&apos;,
  },
})

const age = ref(18)
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.container {
  color: red;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入 VDOM API
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from &quot;vue&quot;

const _hoisted_1 = { class: &quot;container&quot; }

import { ref } from &apos;vue&apos;

// 主模块
const _sfc_main = {
  __name: &apos;test&apos;,
  props: {
  name: {
    type: String,
    default: &apos;Ray&apos;,
  },
},
  setup(__props) {

const props = __props

const age = ref(18)

// 返回一个函数，用于渲染组件
return (_ctx, _cache) =&amp;gt; {
  return (_openBlock(), _createElementBlock(&quot;div&quot;, _hoisted_1, [
    _createElementVNode(&quot;span&quot;, null, _toDisplayString(__props.name), 1 /* TEXT */),
    _createElementVNode(&quot;span&quot;, null, _toDisplayString(age.value), 1 /* TEXT */)
  ]))
}
}

}


// 样式模块
import &quot;test.vue?vue&amp;amp;type=style&amp;amp;index=0&amp;amp;scoped=5e63b55d&amp;amp;lang.css&quot;

import _export_sfc from &apos;�plugin-vue:export-helper&apos;
export default /*#__PURE__*/_export_sfc(_sfc_main, [[&apos;__scopeId&apos;,&quot;data-v-5e63b55d&quot;]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后的 import 和 export default 是插件自动生成的，用于转换主模块后导出。
转化相关的代码在 &lt;code&gt;helper.ts&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const EXPORT_HELPER_ID = &apos;\0plugin-vue:export-helper&apos;

export const helperCode = `
export default (sfc, props) =&amp;gt; {
  const target = sfc.__vccOpts || sfc;
  for (const [key, val] of props) {
    target[key] = val;
  }
  return target;
}
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的样例代码，通过 &lt;code&gt;_export_sfc&lt;/code&gt; 函数为组件添加了 __scopeId 属性,值为 &quot;data-v-5e63b55d&quot;。这个 ID 用于 scoped CSS 的实现。
这样确保样式只影响当前组件的元素。
比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
  color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会被编译为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container[data-v-5e63b55d] {
  color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体分析每一步的实现&lt;/p&gt;
&lt;h3&gt;解析 SFC&lt;/h3&gt;
&lt;p&gt;为了防止对同一个.vue文件重复解析，Vite 插件包含缓存机制，在没有缓存的情况下，会调用 &lt;code&gt;createDescriptor&lt;/code&gt; 函数解析 SFC。
createDescriptor 函数内部调用 compiler.parse(@vue/compiler-sfc) 函数，解析 SFC 文件。&lt;/p&gt;
&lt;p&gt;解析后的 descriptor 的主要数据结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface SFCDescriptor {
  template: SFCTemplateBlock | null;
  script: SFCScriptBlock | null;
  // &amp;lt;script setup&amp;gt;
  scriptSetup: SFCScriptBlock | null;
  styles: SFCStyleBlock[];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;descriptor 会作为后续步骤的参数。&lt;/p&gt;
&lt;h3&gt;脚本处理&lt;/h3&gt;
&lt;p&gt;脚本处理通过 main.ts 中的&lt;code&gt;genScriptCode&lt;/code&gt; 函数完成，主要流程如下：&lt;/p&gt;
&lt;h4&gt;初始化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let scriptCode = `const ${scriptIdentifier} = {}`
let map: RawSourceMap | undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;解析脚本&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const script = resolveScript(descriptor, options, ssr, customElement)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 resolveScript 内部，调用了 compiler.compileScript 方法(@vue/compiler-sfc)，将脚本编译为 JavaScript 代码后返回。&lt;/p&gt;
&lt;p&gt;解析脚本后，有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当脚本包含导入内容时：
&lt;ul&gt;
&lt;li&gt;处理外部源文件&lt;/li&gt;
&lt;li&gt;生成查询参数&lt;/li&gt;
&lt;li&gt;通过 import 语句导入脚本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当脚本不包含导入内容，可以直接使用时：
&lt;ul&gt;
&lt;li&gt;判断编译器版本&lt;/li&gt;
&lt;li&gt;处理 TypeScript 和装饰器&lt;/li&gt;
&lt;li&gt;使用 rewriteDefault 重写默认导出&lt;/li&gt;
&lt;li&gt;或直接使用脚本内容&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一些主要的判断逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;是否可以内联（canInlineMain）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (canInlineMain(descriptor, options)) {
  // 内联处理
} else {
  // 外部处理
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;编译器版本&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (!options.compiler.version) {
  // 旧版本编译器处理
  scriptCode = options.compiler.rewriteDefault(...)
} else {
  // 新版本编译器处理
  scriptCode = script.content
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;TypeScript 支持&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const defaultPlugins =
  script.lang === &apos;ts&apos;
    ? userPlugins.includes(&apos;decorators&apos;)
      ? ([&apos;typescript&apos;] as const)
      : ([&apos;typescript&apos;, &apos;decorators-legacy&apos;] as const)
    : []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终，scriptCode 会传入&lt;code&gt;output&lt;/code&gt;数组，作为主模块的一部分。&lt;/p&gt;
&lt;h3&gt;模板编译&lt;/h3&gt;
&lt;p&gt;main.ts 通过 &lt;code&gt;genTemplateCode&lt;/code&gt; 函数处理模板编译。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;({ code: templateCode, map: templateMap } = await genTemplateCode(
  descriptor,
  options,
  pluginContext,
  ssr,
  customElement,
))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 genTemplateCode 内部，调用了位于&lt;code&gt;packages/plugin-vue/src/template.ts&lt;/code&gt;的两个方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;transformTemplateAsModule&lt;/code&gt; 将模板编译为独立模块&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transformTemplateInMain&lt;/code&gt; 将模板直接编译到主模块中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个方法本质都是调用 compiler.compileTemplate 方法(@vue/compiler-sfc)，将模板编译为 JavaScript 代码。&lt;/p&gt;
&lt;p&gt;最终编译完成的 templateCode 会传入&lt;code&gt;output&lt;/code&gt;数组，作为主模块的一部分。&lt;/p&gt;
&lt;h3&gt;样式处理&lt;/h3&gt;
&lt;p&gt;样式处理通过&lt;code&gt;genStyleCode&lt;/code&gt; 函数完成，主要内容是遍历所有的样式块，根据不同的情况进行处理。&lt;/p&gt;
&lt;p&gt;最终生成的是一堆 import 语句，如果使用了 CSS Modules，还会生成一个 cssModulesMap 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 普通样式
import &quot;style.css?vue&amp;amp;type=style&amp;amp;index=0&amp;amp;scoped=xxx&quot;
// CSS Modules
import style0 from &quot;style.css?vue&amp;amp;type=style&amp;amp;index=0&amp;amp;module&quot;
const cssModules = {
  &quot;className&quot;: style0[&quot;className&quot;]
}
// 包含作用域
import &quot;style.css?vue&amp;amp;type=style&amp;amp;index=0&amp;amp;scoped=data-v-xxx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些包含查询条件的导入会交由 Vite 处理。&lt;/p&gt;
&lt;h4&gt;外部样式流程：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;链接外部文件到 descriptor&lt;/li&gt;
&lt;li&gt;生成带有作用域的查询参数&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;CSS Modules 流程：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;检查是否支持（自定义元素模式不支持）&lt;/li&gt;
&lt;li&gt;生成导入代码和名称映射&lt;/li&gt;
&lt;li&gt;将样式代码添加到 stylesCode&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终，stylesCode 会传入&lt;code&gt;output&lt;/code&gt;数组，作为主模块的一部分。&lt;/p&gt;
&lt;h3&gt;output 数组&lt;/h3&gt;
&lt;p&gt;output 数组是插件处理后的结果，包含以下几个部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const output: string[] = [
  // 脚本
  scriptCode,
  // 模板
  templateCode,
  // 样式
  stylesCode,
  // 自定义块 (在本文中被忽略)
  customBlocksCode,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 output 数组构建完成后，会进一步处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;处理作用域样式&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (hasScoped) {
  attachedProps.push([`__scopeId`, JSON.stringify(`data-v-${descriptor.id}`)])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;添加开发工具相关信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (devToolsEnabled || (devServer &amp;amp;&amp;amp; !isProduction)) {
  attachedProps.push([
    `__file`,
    JSON.stringify(isProduction ? path.basename(filename) : filename),
  ])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;处理热更新
&lt;ul&gt;
&lt;li&gt;添加 HMR ID&lt;/li&gt;
&lt;li&gt;创建 HMR 记录&lt;/li&gt;
&lt;li&gt;监听文件变化&lt;/li&gt;
&lt;li&gt;处理模板更新&lt;/li&gt;
&lt;li&gt;实现热重载逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;处理 SSR
&lt;ul&gt;
&lt;li&gt;包装用户的 setup 函数&lt;/li&gt;
&lt;li&gt;注册 SSR 模块&lt;/li&gt;
&lt;li&gt;处理 SSR 上下文&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;导出组件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 如果没有任何附加属性，则直接导出主模块
if (!attachedProps.length) {
  output.push(`export default _sfc_main`)
} else {
  // 否则，导出带有附加属性的主模块
  output.push(
    `import _export_sfc from &apos;${EXPORT_HELPER_ID}&apos;`,
    `export default /*#__PURE__*/_export_sfc(_sfc_main, [${attachedProps
      .map(([key, val]) =&amp;gt; `[&apos;${key}&apos;,${val}]`)
      .join(&apos;,&apos;)}])`,
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 output 数组拼接后，就得到开头的编译结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入 VDOM API
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from &quot;vue&quot;

const _hoisted_1 = { class: &quot;container&quot; }

import { ref } from &apos;vue&apos;

// 主模块
const _sfc_main = {
  __name: &apos;test&apos;,
  props: {
  name: {
    type: String,
    default: &apos;Ray&apos;,
  },
},
  setup(__props) {

const props = __props

const age = ref(18)

// 返回一个函数，用于渲染组件
return (_ctx, _cache) =&amp;gt; {
  return (_openBlock(), _createElementBlock(&quot;div&quot;, _hoisted_1, [
    _createElementVNode(&quot;span&quot;, null, _toDisplayString(__props.name), 1 /* TEXT */),
    _createElementVNode(&quot;span&quot;, null, _toDisplayString(age.value), 1 /* TEXT */)
  ]))
}
}

}


// 样式模块
import &quot;test.vue?vue&amp;amp;type=style&amp;amp;index=0&amp;amp;scoped=5e63b55d&amp;amp;lang.css&quot;

import _export_sfc from &apos;�plugin-vue:export-helper&apos;
export default /*#__PURE__*/_export_sfc(_sfc_main, [[&apos;__scopeId&apos;,&quot;data-v-5e63b55d&quot;]])
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;一些额外内容&lt;/h1&gt;
&lt;h2&gt;生成实际的 css 文件&lt;/h2&gt;
&lt;p&gt;先前提到，插件会为组件生成类似下面的样式导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;style.css?vue&amp;amp;type=style&amp;amp;index=0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这个文件实际上并不存在，当 Vite 遇到这些带查询参数的导入时，会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将样式提取到单独的文件&lt;/li&gt;
&lt;li&gt;应用必要的转换（如 scoped、CSS Modules）&lt;/li&gt;
&lt;li&gt;在开发时通过 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 标签注入&lt;/li&gt;
&lt;li&gt;在生产构建时通过 CSS 提取插件合并&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>如何在浏览器中检测代码语言</title><link>https://ray-d-song.com/blog/2024/11/detect-language-in-browser</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/11/detect-language-in-browser</guid><pubDate>Thu, 21 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;仓库：https://github.com/ray-d-song/guesslang-js&lt;br /&gt;
效果展示：https://ray-d-song.github.io/guesslang-js/&lt;/p&gt;
&lt;p&gt;最近我正在完成一个叫 &lt;a href=&quot;https://github.com/Ray-D-Song/EchoRSS.git&quot;&gt;EchoRSS&lt;/a&gt; 的阅读器项目，有一个我非常想要的功能，就是拦截订阅中的外链跳转（阅读全文、引用啥的），直接在当前页内显示。&lt;/p&gt;
&lt;p&gt;有一个问题是返回的HTML代码块失去了语言标注（或者原先在 pre 和 code 标签上就没有标注语言），这样没法用 shiki 或者 prism.js 之类的工具进行代码高亮。&lt;/p&gt;
&lt;p&gt;我找到了三个检测代码语言的方案：&lt;/p&gt;
&lt;h2&gt;1. &lt;a href=&quot;https://github.com/github-linguist/linguist&quot;&gt;linguist&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这是一个部署在服务器上的 Ruby 项目，Github 用它来检测仓库的语言构成，如果你需要极高的准确度且可以在服务端运算的话，这是最优解。&lt;/p&gt;
&lt;h2&gt;2. &lt;a href=&quot;https://github.com/highlightjs/highlight.js&quot;&gt;hljs&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;highlight.js 是一个非常知名的网页代码高亮库，也是唯一一个提供自动代码检测的高亮库。&lt;br /&gt;
原理很简单，就是枚举语言的关键词，用这些关键词去一个个匹配文本，最后看哪个的匹配度最高。&lt;/p&gt;
&lt;p&gt;hljs 有四个问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对代码长度的要求很高，大多数语言至少要 300 个字符才能达到比较合格的准确度。&lt;/li&gt;
&lt;li&gt;检测语言的部分并不是单独的模块，而是跟 parser、render 紧紧的耦合在一起，代码写的也非常命令式，很难提取有用的部分&lt;/li&gt;
&lt;li&gt;如果不提取检测模块，直接用 hljs 高亮，会丢失代码原有的一些格式（换行和缩进）&lt;/li&gt;
&lt;li&gt;需要进行大量的正则匹配，性能较差，又因为原因 2，没法放到 web worker 中运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. &lt;a href=&quot;https://github.com/yoeo/guesslang&quot;&gt;guesslang&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;guesslang 是一个基于 tensorflow.js 的机器学习项目。&lt;br /&gt;
Microsoft 在 2021 年用 tensorflow.js 将这个项目移植到 node.js 上，为 vscode增加了&lt;a href=&quot;https://code.visualstudio.com/updates/v1_60#_automatic-language-detection&quot;&gt;自动语言检测&lt;/a&gt;的功能。&lt;/p&gt;
&lt;p&gt;一个越南小哥&lt;a href=&quot;https://github.com/hieplpvip&quot;&gt;hieplpvip&lt;/a&gt;三年前又将这个项目移植到了&lt;a href=&quot;https://github.com/hieplpvip/guesslang-js&quot;&gt;浏览器上&lt;/a&gt;，不过也有三个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;漏内存，疯狂漏内存...&lt;/li&gt;
&lt;li&gt;只支持 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 标签引入 umd 格式，不支持 esm，不支持 bundle&lt;/li&gt;
&lt;li&gt;同样又因为原因 2，不支持 web worker&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而且这位小哥已经不再维护这个项目，3 月份有个支持 esm 的 feat request 一直没有回复。&lt;/p&gt;
&lt;p&gt;所以我提取了 hljs 中的检测模块，又 fork guesslang-js 修复了上面那些问题，对比了一下两种方案，最终 guesslang 胜出，产物就是这个：
https://github.com/ray-d-song/guesslang-js&lt;/p&gt;
&lt;p&gt;貌似叨太多了，也许未来会有人需要这个，所以 po 一下。&lt;/p&gt;
&lt;p&gt;如果有人了解 tensorflow.js 的话，希望可以推荐一些学习材料，我想进一步改为 web gpu 计算来提升效率。&lt;/p&gt;
</content:encoded></item><item><title>用 Proxy 和 WeakMap 实现一个类似 @vueuse 的 util 方法</title><link>https://ray-d-song.com/blog/2024/11/vue-like-util</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/11/vue-like-util</guid><pubDate>Mon, 18 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在使用 Vueuse 时，我发现一个很好用的方法&lt;a href=&quot;https://vueuse.org/shared/until/&quot;&gt;util&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;该方法的作用是监听某个响应式变量，阻塞当前方法直到该变量符合要求。&lt;/p&gt;
&lt;p&gt;调用形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 阻塞当前方法直到 ref.value === true 或 1000ms 后
await until(ref).toBe(true, { timeout: 1000 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法可以解决许多工程难题，比如双 token 方案中，用户一次触发了多个请求，但是在第一个请求时 token 就过期了，此时应该发送刷新 token 的请求，并阻塞其他的请求，直到 token 刷新成功。&lt;/p&gt;
&lt;p&gt;调用方式类似于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const status = reactive(&apos;idle&apos;)

async function fetcher() {
  // 阻塞其他请求的执行，直到 status 变为 idle
  await util(status, &apos;idle&apos;)

  // ...

  if (res.status === 401) {
    // 401 未授权时阻塞其他请求的执行
    status.value = &apos;blocked&apos;
    await refreshToken()
    // 刷新成功后修改状态
    status.value = &apos;idele&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vue 的 util 依赖于 Vue 的响应式系统，我希望实现一个框架无关的 util 方案，实现一个简单的值监听效果。&lt;br /&gt;
因为 util 是一个阻塞操作，所以应该返回一个 Promise。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function util(reactive: unknown, targetValue: unknown) {
  return new Promise(resolve =&amp;gt; {
    // 直到 reactive 的值和 targetValue 相等时 resolve
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题来了，JavaScript 不包含&lt;code&gt;直到&lt;/code&gt;这样的语义或 API，但我们可以监听 reactive 值的变化，将 resolve 作为回调传递，每次变化时对比新值和 targetValue 是否相等，相等的话就执行回调。&lt;/p&gt;
&lt;p&gt;为了实现这样一个监听器，我们可以参考 Vue，Vue 的响应式库 &lt;code&gt;@vue/reactivity&lt;/code&gt; 的核心是基于 Proxy 的写入和读取代理。&lt;br /&gt;
首先编写一个 reactive 包装函数，接收初始值，返回一个被代理的对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reactive&amp;lt;T&amp;gt;(value: T): Reactive&amp;lt;T&amp;gt; {
  const proxyed = new Proxy({ value }, {
    set(target, prop, newValue) {
      target[prop as keyof typeof target] = newValue
      return true
    }
  })
  return proxyed
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在修改&lt;code&gt;proxyed.value&lt;/code&gt;的时候就会触发 set 方法。
接下来我们需要一个存储桶来存储所有的回调。&lt;br /&gt;
使用 WeakMap 来新建存储桶：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const effectMap = new WeakMap&amp;lt;object, Array&amp;lt;() =&amp;gt; void&amp;gt;&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 util 函数中将回调函数写入存储桶：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function util&amp;lt;T&amp;gt;(reactive: Reactive&amp;lt;T&amp;gt;, targetValue: unknown) {
  return new Promise(resolve =&amp;gt; {
    // 如果初始值就相同，直接 resolve
    if (reactive.value === targetValue) {
      resolve(true)
      return;
    }
    // 获取已有的 callback
    const cbs = effectMap.get(reactive) || []
    // 写入新的 callback
    cbs.push(() =&amp;gt; {
      // 当值等于目标值时 resolve
      if (reactive.value === targetValue) {
        resolve(true)
      }
    })
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来就只需要在每次 set 操作时获取 effectMap 中的回调并执行即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set(target, prop, newValue) {
  target[prop as keyof typeof target] = newValue
  const cbs = effectMap.get(raw)
  if (cbs) cbs.forEach(cb =&amp;gt; cb())
  return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码可以参考：https://github/ray-d-song/EchoRSS/web/src/lib/util.ts&lt;/p&gt;
</content:encoded></item><item><title>错误和异常</title><link>https://ray-d-song.com/blog/2024/11/error-and-exception</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/11/error-and-exception</guid><pubDate>Sun, 17 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;错误和异常&lt;/h1&gt;
&lt;p&gt;错误和异常是实践中诞生的概念，目的都是为了处理「程序无法正常运行的情况」。&lt;/p&gt;
&lt;h2&gt;错误&lt;/h2&gt;
&lt;p&gt;从代码形式上讲，错误倾向于精确的手动处理。&lt;br /&gt;
比如 fnA 方法调用了 fnB 和 fnC。这两个方法都有可能发生错误，处理代码大概就是下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fnA() {
  const { err: bErr, res: bRes } = fnB()
  if (bErr) {
    // ...
    // 错误处理
  }

  const { err: cErr, res: cRes } = fnC()
  if (cErr) {
    // ...
    // 错误处理
  }
  // 正常逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;「错误」的关键在于将函数的返回值设为对象或数组，其中一个字段代表「发生错误」。只要这个字段不为空，程序员就知道正常的流程被中断了。&lt;/p&gt;
&lt;p&gt;同时，JavaScript 存在一个内置&lt;code&gt;Error&lt;/code&gt;对象和构造方法，但代表错误的字段并不强制要求是Error对象。Error对象反而更多用在异常处理中。&lt;/p&gt;
&lt;h2&gt;异常&lt;/h2&gt;
&lt;p&gt;既然已经有了错误，为什么还需要异常？&lt;/p&gt;
&lt;p&gt;想象这样一个场景，你的软件有一个按钮，点击按钮会触发 function A，经过层层调用（也许是 10 层），在 function X 报错。你并不想提示用户&quot;未知错误&quot;，而是想具体的告知到底是哪里发生了什么错。&lt;/p&gt;
&lt;p&gt;用错误可以实现这种效果，但是你要写十次这种代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fnA() {
  const { err, res } = fnB()
  if (err) {
    // 向用户展示错误
    showErr(err)
  }
}

function fnB() {
  const { err, res } = fnC()
  if (err)
    // 向上传递
    return { err, null }
}

// ... 经过 10 个类似的传递

function fnY() {
  const { err, res } = fnX()
  if (err)
    // 向上传递
    return { err, null }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种层层传递的样板代码非常低效，更好的方法就是使用&lt;code&gt;异常（exception）&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你要做的就只是在 fnY 发生错误时 throw，在顶层 catch 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fnA() {
  try {
    fnB()
  } catch (e) {
    showErr(e)
  }
}

// ...

function fnY() {
  const { err, res } = fnX()
  if (err)
    // 抛出
    throw err
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样不管在哪层发生了错误，都可以在顶层捕捉，其他层的代码不受影响。&lt;br /&gt;
避免了某处错误对整个代码结构的&quot;污染&quot;。&lt;/p&gt;
&lt;h2&gt;为什么要区别两者？&lt;/h2&gt;
&lt;p&gt;刚刚解释了为什么有了错误还得有异常，似乎异常是错误的上位替代，错误显得有些多余。&lt;/p&gt;
&lt;p&gt;但最佳实践是严格区分两者，不需要层层向上传递的，应该在本层直接处理。比如 fnC 的错误，如果不需要在 fnA 层用到，就应该在 B 层作为错误直接处理掉，而不是向上抛出。&lt;/p&gt;
&lt;p&gt;假设所有的错误都在顶层处理，那逻辑全都堆在顶层的 catch 中，难以维护&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function main() {
  try {
    task1()
    task2()
    task3()
  } catch(e) {
    switch(e) {
      case &quot;type A&quot;:
        //...
        break;
      case &quot;type B&quot;:
        //...
        break;
      case &quot;type C&quot;:
        //...
        break;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>如何在 React 中使用 async-await</title><link>https://ray-d-song.com/blog/2024/10/async-in-react-fc</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/10/async-in-react-fc</guid><pubDate>Mon, 21 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我一直是个 Vue 用户，最近才开始使用 React，希望下面的内容可以帮到和我一样对函数式组件感到困惑的人。&lt;/p&gt;
&lt;h2&gt;Suspanse&lt;/h2&gt;
&lt;p&gt;首先，在 React 中以下写法是无效的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function Child() {
  const data = await fetchData()
  
  return &amp;lt;div&amp;gt;{data}&amp;lt;/div&amp;gt;
}

function Parent() {
  return (
    &amp;lt;Suspanse fallback={&amp;lt;div&amp;gt;loading&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;Child /&amp;gt;
    &amp;lt;/Suspanse&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是第一个误区, 在 Vue 中，使用&lt;code&gt;&amp;lt;Suspanse&amp;gt;&lt;/code&gt;包裹子组件，就可以在子组件的顶层使用 await。&lt;br /&gt;
但是 React 每个组件都是同步组件，Suspanse 只支持以下几种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持 Suspense 的框架如 Relay 和 Next.js。&lt;/li&gt;
&lt;li&gt;使用 lazy 懒加载组件代码。&lt;/li&gt;
&lt;li&gt;使用 use 读取 Promise 的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中&lt;code&gt;use API&lt;/code&gt;可以实现和 Vue 顶层 await 类似的效果，但目前还是实验阶段。&lt;/p&gt;
&lt;h2&gt;正确方式&lt;/h2&gt;
&lt;h3&gt;useEffect&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function AsyncComponent() {
  const [data, setData] = useState(null)

  useEffect(() =&amp;gt; {
    async function fetchData() {
      const result = await someAsyncOperation()
      setData(result)
    }
    fetchData()
  }, [])

  if (!data) return &amp;lt;div&amp;gt;loading&amp;lt;/div&amp;gt;
  return &amp;lt;div&amp;gt;{data}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;useEffect&lt;/code&gt;钩子的第二个参数为空数组，就会在组件首次渲染时，执行 Effect 函数。&lt;br /&gt;
在上面的例子中，就是执行 fetchData，请求成功后使用 setData 更新数据，刷新视图。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：useEffect 的第二个参数不能省略，省略=每次组件更新都会触发，这会导致无限刷新&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;自定义 hooks&lt;/h3&gt;
&lt;p&gt;每一次请求都这么写，会让组件的表达非常啰嗦。&lt;br /&gt;
常用的做法是将请求封装成一个自定义 hooks。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function useAsyncData() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() =&amp;gt; {
    async function fetchData() {
      try {
        const result = await someAsyncOperation()
        setData(result);
      } catch (error) {
        console.error(error)
      } finally {
        setLoading(false)
      }
    }
    fetchData()
  }, [])

  return { data, loading }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function AsyncComponent() {
  const { data, loading } = useAsyncData()

  if (loading) return &amp;lt;div&amp;gt;loading...&amp;lt;/div&amp;gt;
  if (!data) return &amp;lt;div&amp;gt;no data&amp;lt;/div&amp;gt;
  return &amp;lt;div&amp;gt;{data}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也有一些库可以帮我们完成这个过程，比如react-query、swr、ahooks。&lt;/p&gt;
&lt;p&gt;使用 swr：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function AsyncComponent() {
  const { data, isLoading } = useSWR(&apos;key&apos;, fetcher)

  if (isLoading) return &amp;lt;div&amp;gt;loading...&amp;lt;/div&amp;gt;
  if (!data) return &amp;lt;div&amp;gt;no data&amp;lt;/div&amp;gt;
  return &amp;lt;div&amp;gt;{data}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;React async 的核心在于理解「React 组件都是同步组件」。&lt;br /&gt;
组件内部可以维护一个异步状态，等到这个异步状态 fulfilled 的时候再用 useState 去更新视图。&lt;/p&gt;
</content:encoded></item><item><title>如何实现一个在线 mock 服务</title><link>https://ray-d-song.com/blog/2024/10/build-a-mock-server</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/10/build-a-mock-server</guid><pubDate>Sun, 13 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;有不少 SaaS 服务提供了在线 mock 服务，比如 Apifox、Insomnia，不过他们首先是一个 API 设计工具，其次才是 mock 服务。&lt;/p&gt;
&lt;p&gt;如果你只是要为 demo 提供接口，或者在后端完成工作前创建假数据，那他们的工具太重了。&lt;br /&gt;
更重要的是他们只会为商务用户提供 self-host 服务，对于普通开发者并不是很友好。&lt;/p&gt;
&lt;p&gt;接下来我会介绍如何使用 Node.js、MongoDB、Faker.js 实现一个自己的在线 mock 服务。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/Ray-D-Song/faker-server&quot;&gt;https://github.com/Ray-D-Song/faker-server&lt;/a&gt;找到项目的代码，这是一个比较完整的实现&lt;br /&gt;
在线演示：https://faker-preview.jenrays.com/ 只读权限密钥： 1234&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;功能目标&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建、编辑、删除接口数据&lt;/li&gt;
&lt;li&gt;基础鉴权能力&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;数据结构&lt;/h2&gt;
&lt;p&gt;首先我们要实现的并不是一个 json server，你不能让用户输入一个 json 内容，然后直接返回，这样没有任何意义。&lt;/p&gt;
&lt;p&gt;设计参考应该类似 Apifox，用户可以在界面上输入接口路径、方法、描述，再创建结构化的接口返回值定义。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://r2.ray-d-song.com/2024/10/d5a32b26a5d7fd54947eded8cb89b5f2.png&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;以 RESTful 接口为基准，数据结构应该是一个树，存在一个根节点&lt;code&gt;root&lt;/code&gt;，根节点的类型可以是&lt;code&gt;object&lt;/code&gt;或者&lt;code&gt;array&lt;/code&gt;。&lt;br /&gt;
如果是 array，那需要有个字段记录数组的目标长度，这在mock分页接口时非常有用。&lt;/p&gt;
&lt;p&gt;接下来要思考如何定义每个子节点，也就是 json 中的 key-value 结构。&lt;/p&gt;
&lt;p&gt;比如用户需要这样一个用户信息接口&lt;code&gt;/user&lt;/code&gt;，返回一个用户信息对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;Ray&quot;,
  &quot;age&quot;: 20,
  &quot;grade&quot;: &quot;junior&quot;,
  &quot;hobbies&quot;: [&quot;reading&quot;, &quot;traveling&quot;, &quot;coding&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;json 的 key 由用户直接输入，value 会通过 faker.js 生成，那就需要记录每个 key 对应的 faker 方法。&lt;br /&gt;
比如生成人名对应的方法是&lt;code&gt;faker.name.fullName&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;grade 对应的值是&lt;code&gt;junior&lt;/code&gt;、&lt;code&gt;middle&lt;/code&gt;、&lt;code&gt;high&lt;/code&gt;，这是一个枚举值，需要将用户输入的值转换为规范的值。&lt;br /&gt;
那我们就需要记录可选值列表，以及返回值本身的数据类型。因为有些时候需要的是数字 1、2、3，而不是字符串&quot;1&quot;、&quot;2&quot;、&quot;3&quot;。&lt;/p&gt;
&lt;p&gt;既然有数字，那就会有最大值和最小值，比如年龄，总不能生成一个 1000 岁的用户。&lt;/p&gt;
&lt;p&gt;基于以上分析，我们可以定义树的每个节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 树的每个节点
interface Node {
  // 属性名, 比如 name: &apos;Ray&apos; 中的 name
  key: string
  // 类型, 比如 string
  type: string
  // mock 方法, 比如 faker.name.fullName
  mock: string
  // 子节点
  children?: JsonNode[]
  // 数组长度(仅在 type 为 array 时有效)
  length?: number
  // 固定值
  value?: string
  // 最小值(仅在 type 为 number 时有效)
  min?: number
  // 最大值(仅在 type 为 number 时有效)
  max?: number
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;功能实现&lt;/h2&gt;
&lt;p&gt;设计完数据结构，就已经成功一半了。&lt;br /&gt;
接下来要考虑的是如何实现这些功能。&lt;/p&gt;
&lt;p&gt;服务实际上分为两部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据管理，包括创建、编辑、删除接口数据 &lt;code&gt;/api/*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;接口 mock，根据接口路径、方法，返回相应的 mock 数据 &lt;code&gt;/mock/*&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;数据管理 CRUD&lt;/h3&gt;
&lt;p&gt;最终我们存到数据库里是以接口为单位，每个接口包含路径、方法、描述、返回值定义（前面的节点树结构）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Api {
  // 接口 id
  id: string
  // 接口路径
  path: string
  // 接口方法
  method: string
  // 接口描述
  description: string
  // 接口返回值定义
  data: Node[]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以使用任何你喜欢的数据库，如果用 sqlite 或者 pg、mysql 等关系型数据库，那树形的&lt;code&gt;data&lt;/code&gt;字段就需要序列化成字符串/jsonb存储。&lt;br /&gt;
所以我推荐使用 MongoDB，它原生支持树形结构。&lt;/p&gt;
&lt;p&gt;我选用的 web 框架是 Hono，一个支持多个 serverless 平台的轻量级框架。&lt;br /&gt;
以创建接口为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const crudApp = new Hono&amp;lt;{ Bindings: Bindings }&amp;gt;()

// 匹配 POST /create
crudApp.post(&apos;/create&apos;, async (c) =&amp;gt; {
  // 读取请求体中的接口数据
  const api = await c.req.json&amp;lt;Api&amp;gt;()
  // 从ctx中读取数据库
  const db = c.get(&apos;db&apos;)
  const collection = db.collection&amp;lt;Api&amp;gt;(&apos;apis&apos;)

  // 检查接口是否已存在
  const existingApi = await collection.findOne({
      path: formatPath(api.path),
      method: api.method,
      deleted: { $ne: true },
  })
  // 如果接口已存在，返回错误
  if (existingApi) {
    return c.json({ error: &apos;API already exists&apos; }, 400)
  }

  // 插入接口
  const result = await collection.insertOne({
    ...api,
    path: formatPath(api.path),
    createdAt: new Date(),
    updatedAt: new Date(),
  })

  // 如果插入成功，返回创建的接口
  if (result.acknowledged) {
    const createdApi = await collection.findOne({ _id: result.insertedId })
    return c.json(createdApi, 201)
  }
  // 插入失败，返回错误
  return c.json({ error: &apos;Failed to create API&apos; }, 500)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你用&lt;code&gt;express&lt;/code&gt;，那核心代码一模一样，只是路由组织方式不同，不再赘述。&lt;/p&gt;
&lt;h3&gt;接口 mock&lt;/h3&gt;
&lt;p&gt;mock 接口的处理流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;接收&lt;code&gt;/mock&lt;/code&gt;开头的请求, 比如 &lt;code&gt;GET /mock/user&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;以请求方法和路径作为查询条件，查询数据库中对应的接口数据&lt;/li&gt;
&lt;li&gt;根据接口定义中的数据结构，生成 mock 数据并返回&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中最关键的是第三步，如何根据接口定义中的数据结构，生成 mock 数据。&lt;br /&gt;
因为是树形结构，所以需要递归处理。&lt;br /&gt;
同时又有许多细节需要兼顾，比如枚举类型需要随机选择一个值，如果 type 是&apos;number&apos;那需要在返回前进行转化，数字类型需要考虑最大最小值。&lt;/p&gt;
&lt;p&gt;我们从底向上来处理这个问题，首先是根据 mock 字段的值调用 faker.js 生成数据。&lt;br /&gt;
编写一个 fake 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface FakeOptions {
  min?: number
  max?: number
}

/**
 * @param key - 比如 faker.name.fullName
 * @param options - 比如 { min: 18, max: 60 }
 */
function fake(key: string, options?: FakeOptions) {
  // 根据.分割字符串
  const parts = key.split(&apos;.&apos;)
  let currentObject: any = faker

  for (const part of parts) {
    // 检查当前对象是否存在该方法
    if (typeof currentObject[part] === &apos;function&apos;) {
      let result: any
      // 如果有额外选项，则调用方法时传入
      if (options) {
        result = currentObject[part](options)
      } else {
        result = currentObject[part]()
      }
      // 检查结果是否为 BigInt, 如果是则转换为字符串, 不然 JSON.stringify 会报错
      return typeof result === &apos;bigint&apos; ? result.toString() : result
    } else if (currentObject[part] !== undefined) {
      // 继续处理下一层
      currentObject = currentObject[part]
    } else {
      throw new Error(`生成模拟数据失败: ${key}`)
    }
  }

  throw new Error(`生成模拟数据失败: ${key}`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们就可以根据节点树生成 mock 数据了。&lt;br /&gt;
编写&lt;code&gt;processResBody&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 处理返回值
 * @param body - 节点树
 */
function processResBody(body: Node[]): any {
  // 处理单个节点的逻辑
  function processNode(node: Node): any {
    if (node.value !== undefined &amp;amp;&amp;amp; node.value.trim() !== &apos;&apos;) {
      // 如果设置了非空的特定值，将其分割成可选值数组
      const options = node.value.split(&apos;^&apos;).map((v) =&amp;gt; v.trim())
      // 随机选择一个值
      const selectedValue = options[Math.floor(Math.random() * options.length)]

      // 根据类型进行转换
      switch (node.type) {
        case &apos;number&apos;:
          return Number(selectedValue)
        case &apos;boolean&apos;:
          return selectedValue.toLowerCase() === &apos;true&apos;
        case &apos;null&apos;:
          return null
        case &apos;object&apos;:
        case &apos;array&apos;:
          try {
            return JSON.parse(selectedValue)
          } catch {
            console.warn(
              `无法解析 ${node.key} 的值为 ${node.type}，使用原始字符串`,
            )
            return selectedValue
          }
        default:
          return selectedValue
      }
    }

    // 处理不同类型节点
    switch (node.type) {
      // 对象类型，递归处理每个子节点
      case &apos;object&apos;:
        const obj: { [key: string]: any } = {}
        node.children?.forEach((child) =&amp;gt; {
          obj[child.key] = processNode(child)
        })
        return obj
      // 数组类型，根据长度生成对应数量的子节点
      case &apos;array&apos;:
        const arr: any[] = []
        const length = node.length || 1
        for (let i = 0; i &amp;lt; length; i++) {
          arr.push(
            processNode(
              node.children?.[0] || {
                key: &apos;item&apos;,
                type: &apos;string&apos;,
                mock: &apos;string.alphanumeric&apos;,
              },
            ),
          )
        }
        return arr

      // 其余类型调用 fake 生成数据
      case &apos;number&apos;:
        let options: FakeOptions = {}
        if (node.min) options.min = Number(node.min)
        if (node.max) options.max = Number(node.max)
        return fake(node.mock, options)
      case &apos;string&apos;:
      case &apos;boolean&apos;:
        return fake(node.mock)
      case &apos;null&apos;:
        return null
      case &apos;any&apos;:
        return fake(&apos;datatype.json&apos;)
      default:
        return undefined
    }
  }

  // 从根节点开始处理
  return processNode(body[0])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不难看出，&lt;code&gt;processResBody&lt;/code&gt;方法的核心就是自递归，根据不同的类型调用不同的处理逻辑。&lt;/p&gt;
&lt;p&gt;接下来就是将&lt;code&gt;processResBody&lt;/code&gt;方法集成到 mock 接口的路由中，和前面的数据管理路由类似，不再赘述。&lt;/p&gt;
&lt;h3&gt;基础鉴权&lt;/h3&gt;
&lt;p&gt;因为是 web 服务，所以不能完全开放，总不能谁都能创建、删除、修改接口。&lt;/p&gt;
&lt;p&gt;这里也不再引入额外的复杂度，做多租户，直接用一个&lt;code&gt;KEY&lt;/code&gt;字段来鉴权。&lt;/p&gt;
&lt;p&gt;具体来说，在服务首次启动时，生成一个配置文件&lt;code&gt;.env&lt;/code&gt;，其中包含一个&lt;code&gt;KEY&lt;/code&gt;字段，其值为一个uuid。&lt;br /&gt;
客户端设置一个自定义的&lt;code&gt;Faker-Server-Key&lt;/code&gt;请求头，在每次请求时带上该请求头，然后在路由处理中验证其值是否等于配置的&lt;code&gt;KEY&lt;/code&gt;。&lt;br /&gt;
服务端每次启动时都会读取这个文件，所以每次修改&lt;code&gt;KEY&lt;/code&gt;后需要重启服务。&lt;/p&gt;
&lt;p&gt;由此还可以衍生出&lt;code&gt;只读 KEY&lt;/code&gt;，&lt;code&gt;ADMIN KEY&lt;/code&gt;，前者只能用于查询，后者则可以用于所有操作。&lt;br /&gt;
以及访问 mock 服务专用的&lt;code&gt;ACCESS KEY&lt;/code&gt;，可以设置过期时间，或者调用频率限制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 生成默认配置
function generateDefaultConfig() {
  // 检查配置目录是否存在
  if (!fs.existsSync(configDir)) {
    fs.mkdirSync(configDir, { recursive: true })
  }

  // 检查配置文件是否存在, 不存在则创建
  // 生成三个 UUID 作为 KEY
  if (!fs.existsSync(configFile)) {
    const accessKey = crypto.randomUUID()
    const adminKey = crypto.randomUUID()
    const readonlyKey = crypto.randomUUID()
    const defaultConfig = `
# Server Port
PORT=3000

# Access /mock/* API
ACCESS_KEY=${accessKey}

# If true, the server will allow public access to the /mock/* API
# /api/* will continue to require authentication
PUBLIC_ACCESS=false

# ADMIN_KEY is used to access the web page and modify the data
ADMIN_KEY=${adminKey}

# READONLY_KEY can access the web page, but cannot modify the data
READONLY_KEY=${readonlyKey}

# MongoDB URL
MONGO_URL=mongodb://admin:password@localhost:27017?authSource=admin
`.trim()

    // 写入配置文件
    fs.writeFileSync(configFile, defaultConfig)
    console.log(&apos;Created default configuration file at:&apos;, configFile)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;这并不是一个手把手的教程，只是提供一个思路，具体的实现可以参考&lt;a href=&quot;https://github.com/Ray-D-Song/faker-server&quot;&gt;https://github.com/Ray-D-Song/faker-server&lt;/a&gt;。&lt;br /&gt;
尤其是前端的部分没有涉及，项目使用的是 React 和 MUI，可以看&lt;code&gt;packages/client&lt;/code&gt;目录。&lt;/p&gt;
</content:encoded></item><item><title>TypeScript interface 隐式索引签名丢失问题</title><link>https://ray-d-song.com/blog/2024/09/interface-lose-index-signature</link><guid isPermaLink="true">https://ray-d-song.com/blog/2024/09/interface-lose-index-signature</guid><pubDate>Fri, 20 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近遇到一个奇怪的问题，interface 定义了一个符合要求的类型，却无法作为泛型参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo = Record&amp;lt;string, unknown&amp;gt;

type Test&amp;lt;K extends Foo&amp;gt; = K

interface Bar {
  &apos;a&apos;: string
}

/**
 * error:
 * Type &apos;Bar&apos; does not satisfy the constraint &apos;Foo&apos;.
 * Index signature for type &apos;string&apos; is missing in type &apos;Bar&apos;.
 */
type Res = Test&amp;lt;Bar&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TS 提示 Bar 类型不满足 Foo 的定义，这本质是一个隐式索引签名丢失的问题。&lt;/p&gt;
&lt;h2&gt;什么是隐式索引签名&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;隐式索引签名(implicit index signatures)&lt;/code&gt;是 TypeScript 的一个特性，允许你定义一个对象类型，可以使用任意字符串作为键。&lt;br /&gt;
这意味着你可以创建一个对象，在这个对象中，键的名称是动态的，而值的类型是统一的。&lt;/p&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Bar = {
  [key: string]: number
}
const bar: Bar = {
  &apos;a&apos;: 1,
  &apos;b&apos;: 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;隐式索引丢失的问题仅限 interface，type 没有这个问题。&lt;br /&gt;
也就是说开头的例子，将 Bar 的定义修改为 type 即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo = Record&amp;lt;string, unknown&amp;gt;

type Test&amp;lt;K extends Foo&amp;gt; = K

type Bar = { // [!code highlight]
  a: string
}

type Res = Test&amp;lt;Bar&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的类型不便修改，例如它是定义在 node_modules 中的库类型，你也可以使用&lt;code&gt;Pick&lt;/code&gt;将 interface 转换为 type。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo = Record&amp;lt;string, unknown&amp;gt;

type Test&amp;lt;K extends Foo&amp;gt; = K

interface Bar {
  a: string
}

type Res = Test&amp;lt;Pick&amp;lt;Bar, keyof Bar&amp;gt;&amp;gt; // [!code highlight]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;interface 这种行为其实符合 TypeScript team 的预期，相关讨论在 2017 年就已经有了。&lt;/p&gt;
&lt;p&gt;https://github.com/microsoft/TypeScript/issues/15300&lt;/p&gt;
&lt;p&gt;官方解释是：
「interface 可以通过同名接口合并进行增强，而 type 不可以，所以为 type 进行隐式索引推断会更安全」&lt;/p&gt;
</content:encoded></item><item><title>使用Enigma Virtual Box将 exe 和 dll 打包成单个可执行文件</title><link>https://ray-d-song.com/blog/enigma-bundle-one-exe</link><guid isPermaLink="true">https://ray-d-song.com/blog/enigma-bundle-one-exe</guid><pubDate>Thu, 12 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 macOS 和 Linux 上，大多数工具都会以单个可执行文件的形式分发。Windows 平台有时会附带&lt;code&gt;.dll&lt;/code&gt;文件。&lt;/p&gt;
&lt;h2&gt;dll 是什么&lt;/h2&gt;
&lt;p&gt;DLL文件是“动态链接库”（Dynamic Link Library）的缩写，主要用于Windows操作系统。&lt;/p&gt;
&lt;p&gt;DLL文件包含了一些可以被多个程序共享的代码和数据。当一个程序需要使用这些代码和数据时，它会动态地加载相应的DLL文件，而不是将这些代码和数据复制到每个程序中。这样可以节省内存空间，并且可以确保多个程序使用的是同一份代码和数据，从而提高系统的稳定性和安全性。&lt;/p&gt;
&lt;p&gt;可以将 DLL 简单理解为&lt;code&gt;库&lt;/code&gt;或者&lt;code&gt;包&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;举个例子，多线程应用移植到 Windows 上会依赖&lt;code&gt;libwinpthread-1.dll&lt;/code&gt;。&lt;br /&gt;
这是一个实现了 POSIX 线程（pthreads）的 DLL 文件，它提供了与 Linux 和 macOS 上的 pthreads 库相同的接口，使得多线程应用可以在 Windows 上运行。&lt;/p&gt;
&lt;p&gt;如果没有这样一个 dll 文件，那每个移植到 Windows 上的多线程应用都需要自己实现一套线程管理机制，大大提高了开发难度。&lt;/p&gt;
&lt;p&gt;你可以在&lt;code&gt;dll-files.com&lt;/code&gt;上找到可用的 dll 文件。&lt;/p&gt;
&lt;p&gt;https://www.dll-files.com/libwinpthread-1.dll.html&lt;/p&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;大多数必要的 DLL 会被集成到系统中，在 Windows 的&lt;code&gt;system32&lt;/code&gt;目录下。&lt;/p&gt;
&lt;p&gt;如果系统缺少某个 DLL，那程序就会报以下错误：&lt;br /&gt;
&lt;code&gt;Couldn&apos;t find ****.dll...&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;一个常见的做法是将 DLL 文件和可执行文件放在同一个目录下，但这样增加了分发的难度。&lt;/p&gt;
&lt;h2&gt;Engima Virtual Box&lt;/h2&gt;
&lt;p&gt;Engima Virtual Box 是一个文件封装工具，将可执行文件和配套文件（例如 dll）封装进一个可执行文件中。&lt;/p&gt;
&lt;p&gt;你可以在官网上找到 Engima Virtual Box 的下载卡片。&lt;/p&gt;
&lt;p&gt;https://enigmaprotector.com/en/downloads.html&lt;/p&gt;
&lt;p&gt;安装完成后，打开 Engima Virtual Box：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/18d11169fb1fe10b658ac0c9294c710b.png&quot; alt=&quot;EVB&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;#1&lt;/code&gt;处填写要打包的 exe 文件路径&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;#2&lt;/code&gt;处填写输出文件路径&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;#3&lt;/code&gt;处点击&lt;code&gt;Add&lt;/code&gt;，选择要打包的其他文件或文件夹&lt;/li&gt;
&lt;li&gt;点击右下角的&lt;code&gt;process&lt;/code&gt;，开始打包&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;原理上其实不难实现，将所有文件全都打包成二进制，运行时塞到内存或者拷贝进临时文件中。&lt;/p&gt;
&lt;p&gt;使用 Golang 的 embed 可以轻松实现一个简单的 demo。&lt;/p&gt;
&lt;p&gt;https://github.com/Ray-D-Song/go-embed-exec&lt;/p&gt;
&lt;p&gt;当然，Engima Virtual Box 为了适配各种情况，肯定包含复杂度更高的操作。&lt;/p&gt;
</content:encoded></item><item><title>软链接(符号链接)应用入门</title><link>https://ray-d-song.com/blog/symlink</link><guid isPermaLink="true">https://ray-d-song.com/blog/symlink</guid><pubDate>Sun, 08 Sep 2024 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近有一个需求，需要研究实现一个简化的包管理器。&lt;/p&gt;
&lt;p&gt;现代的包管理器，比如&lt;code&gt;Go Modules&lt;/code&gt;，&lt;code&gt;Maven&lt;/code&gt;都支持&lt;code&gt;soft link（软链接）&lt;/code&gt;，也称为&lt;code&gt;symbolic link（符号链接）&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;软链接的就是将文件或文件夹映射到另一个位置，对映射文件的操作会同步到源文件上。&lt;/p&gt;
&lt;p&gt;对包管理来说，这样做的好处是，不用多次拉取同一个依赖。&lt;br /&gt;
如果项目 A 和 B 都依赖 package alpha，那只需要拉取 alpha 到一个公共位置，再在 A 和 B 中分别创建一个软链接到这个位置即可。&lt;/p&gt;
&lt;h3&gt;shell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Linux macOS
ln -s &amp;lt;target&amp;gt; &amp;lt;link name&amp;gt;

# Windows
mklink &amp;lt;link name&amp;gt; &amp;lt;target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Go 语言控制软链接&lt;/h2&gt;
&lt;p&gt;Go 语言提供了&lt;code&gt;os.Symlink&lt;/code&gt;来操作软链接。&lt;/p&gt;
&lt;p&gt;https://pkg.go.dev/os#Symlink&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;err := Symlink(oldname, newname)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用 Continue 部署免费的 AI 代码助手</title><link>https://ray-d-song.com/blog/continue-free-ai-code-assistant</link><guid isPermaLink="true">https://ray-d-song.com/blog/continue-free-ai-code-assistant</guid><pubDate>Wed, 04 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;过去的一年, 我一直在使用 GitHub Copilot 作为代码助手, 它提供了补全、问答等功能, 付费订阅 10 美元/月.&lt;/p&gt;
&lt;p&gt;说实话这个价格并不算贵, 但最近 Copilot 对问答范围进行了限制, 导致很多与开发相关的问题无法得到解答.&lt;/p&gt;
&lt;p&gt;后续我也尝试了 Codium AI, 响应速度不尽人意.&lt;/p&gt;
&lt;p&gt;最近发现了一个开源的代码助手 &lt;a href=&quot;https://www.continue.dev/&quot;&gt;Continue&lt;/a&gt;, 支持调用本地和云端的模型, 包括 Cloudflare 的 worker AI.&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;Continue 本质是个帮你向大模型发起请求的客户端.&lt;br /&gt;
当你编写代码的时候, 它会获取代码的上下文, 向模型发起请求, 获取结果后处理等待你的命令(tab), 再拼接到代码中去.&lt;/p&gt;
&lt;h2&gt;本地部署&lt;/h2&gt;
&lt;p&gt;本地部署可以使用 &lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt; 或 &lt;a href=&quot;https://docs.continue.dev/reference/Model%20Providers/lmstudio&quot;&gt;lmstudio&lt;/a&gt;.&lt;br /&gt;
这是两个非常方便的大模型集成工具, 同时支持 Mac、windows 以及 Linux, 帮助你快速在本地运行大模型.&lt;/p&gt;
&lt;p&gt;这里我选择在 Mac 上运行 Ollama, 在官网Ollama可以下载到安装包, 安装过程中记得勾选命令行工具.&lt;/p&gt;
&lt;p&gt;https://ollama.com/&lt;/p&gt;
&lt;h3&gt;配置要求&lt;/h3&gt;
&lt;p&gt;配置要求取决于你要使用的模型, 这里推荐 llama3.&lt;br /&gt;
llama3 是 meta 开源的模型, 也是目前最强的开源模型, 可以获得比较好的补全效果.&lt;br /&gt;
模型有两个变体, 8B 需要至少 16g 的内存和 8g 的显存, 70B 则需要 64g 以上的内存.&lt;br /&gt;
默认运行的 8B 版本即可满足正常需要.&lt;/p&gt;
&lt;h3&gt;运行模型&lt;/h3&gt;
&lt;p&gt;安装完成后进入terminal, 输入以下命令获取 llama3 模型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama run llama3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模型拉取完成后就会自动运行.&lt;br /&gt;
测试用中文让他编写一个 python 版的 hello-world.&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/6d3a74f153d36136f7ae81586e8315fb.png&quot; alt=&quot;test&quot; /&gt;&lt;/p&gt;
&lt;p&gt;输入&lt;code&gt;/bye&lt;/code&gt;可以退出运行.&lt;/p&gt;
&lt;h3&gt;配置 Continue 插件&lt;/h3&gt;
&lt;p&gt;安装 &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=Continue.continue&quot;&gt;vscode plugin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;安装完成左侧边栏会展示 Continue 的对话框:&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/ea18b185908bda96720414fcad005f10.png&quot; alt=&quot;chat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个时候是无法对话的, 因为还没有选择模型, 点击 &lt;code&gt;Select model&lt;/code&gt;-&amp;gt;&lt;code&gt;Add Model&lt;/code&gt;, 向下拉, 找到:&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/58d456bc4ba21cacbb2f2c2b076722de.png&quot; alt=&quot;Ollama&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后再选择第一个&lt;code&gt;Autodetect&lt;/code&gt;选项:&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/05191d0410e4f23c30285563b15c54dc.png&quot; alt=&quot;model&quot; /&gt;&lt;/p&gt;
&lt;p&gt;返回对话, 选择 Ollama 选项:
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/f82f1758e1ea1f85cf5cd9db3913e11d.png&quot; alt=&quot;back to chat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;确保你的模型正在后台运行, 接下来就可以开始对话了.&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/2880fe2110c014bc1b6e736d41bb0e71.png&quot; alt=&quot;test chat&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;自动补全设置&lt;/h3&gt;
&lt;p&gt;虽然现在已经可以完成对话, 但还是无法补全.&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/c072cad8863c2a50f9202e05e3442d44.png&quot; alt=&quot;tab 404&quot; /&gt;
因为补全默认使用的是&lt;code&gt;Starcoder2 3b&lt;/code&gt;模型, Starcoder 是一个针对代码场景针对性强化的模型.&lt;/p&gt;
&lt;p&gt;你可以使用以下命令启动该模型.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama run starcoder2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模型启动后就可以触发补全.&lt;/p&gt;
&lt;p&gt;如果你的机器性能不足以同时运行两个模型, 你也可以设置补全也使用&lt;code&gt;llama3&lt;/code&gt;:&lt;br /&gt;
在插件侧边栏的角落, 找到这个小齿轮图标&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/d3b06fcec439af8e2cec4d360f2663c2.png&quot; alt=&quot;config&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击就会进入配置文件(也可以打开~/.continue/config.json), 将&lt;code&gt;tabAutocompleteModel&lt;/code&gt;字段的两个高亮行的值修改为&lt;code&gt;llama3&lt;/code&gt;即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;tabAutocompleteModel&quot;: {
  &quot;title&quot;: &quot;Starcoder2 3b&quot;, // [!code highlight]
  &quot;provider&quot;: &quot;ollama&quot;,
  &quot;model&quot;: &quot;starcoder2:3b&quot; // [!code highlight]
},
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你希望补全 markdown 之类的的文本文件, 那我建议使用 llama3 进行补全&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Cloudflare worker AI&lt;/h2&gt;
&lt;p&gt;有一些轻薄本机型不能在本地运行大模型, 也可以使用 Cloudflare worker 的 AI 模型.&lt;/p&gt;
&lt;p&gt;Cloudflare 提供了免费额度, 不过是以&lt;code&gt;神经元&lt;/code&gt;作为单位, 可以在&lt;a href=&quot;https://ai.cloudflare.com/#pricing-calculator&quot;&gt;这个页面查看估算&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;每日的免费额度大体相当于 500 次调用, 每次 100 个入参字符和 500 个返回字符, 足以应对正常的开发场景.&lt;/p&gt;
&lt;p&gt;首先根据 &lt;a href=&quot;https://developers.cloudflare.com/fundamentals/api/get-started/create-token/&quot;&gt;Cloudflare 的指引, 创建key&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后打开配置文件, 增加一个&lt;code&gt;models&lt;/code&gt;字段值, 并修改&lt;code&gt;tabAutocompleteModel&lt;/code&gt;字段.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;models&quot;: [
    {
      &quot;accountId&quot;: &quot;YOUR CLOUDFLARE ACCOUNT ID&quot;,
      &quot;apiKey&quot;: &quot;YOUR CLOUDFLARE API KEY&quot;,
      &quot;contextLength&quot;: 2400,
      &quot;completionOptions&quot;: {
        &quot;maxTokens&quot;: 500
      },
      &quot;model&quot;: &quot;@cf/meta/llama-3-8b-instruct&quot;,
      &quot;provider&quot;: &quot;cloudflare&quot;,
      &quot;title&quot;: &quot;Llama 3 8B&quot;
    }
  ],
  &quot;tabAutocompleteModel&quot;: {
    &quot;accountId&quot;: &quot;YOUR CLOUDFLARE ACCOUNT ID&quot;,
    &quot;apiKey&quot;: &quot;YOUR CLOUDFLARE API KEY&quot;,
    &quot;model&quot;: &quot;@hf/thebloke/deepseek-coder-6.7b-base-awq&quot;,
    &quot;provider&quot;: &quot;cloudflare&quot;,
    &quot;title&quot;: &quot;DeepSeek 7b&quot;
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 accountId 为你自己的账户id, apiKey 是你刚刚保存的 key 值.&lt;/p&gt;
&lt;p&gt;账户 id 获取比较麻烦. 你必须要有一个 Cloudflare 下的域名或者一个 Worker, 具体可以参照官方文档和这两个页面:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/&quot;&gt;官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.thorn.red/articles/6d89ow4vgfghgnn8&quot;&gt;参考 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://community.cloudflare.com/t/where-can-i-find-my-account-id/164431&quot;&gt;参考 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;model&lt;/code&gt;字段的值可以自行修改, 不同的模型需要的算力不同, 对&lt;code&gt;神经元&lt;/code&gt;的计费方式也会有影响.&lt;br /&gt;
如果你发现免费额度不足以覆盖, 可以修改为低一级的模型.&lt;/p&gt;
&lt;p&gt;点击链接可以看到完整的模型列表:&lt;/p&gt;
&lt;p&gt;https://developers.cloudflare.com/workers-ai/models&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;llama3 有不错的对话效果, 但是速度比较慢, 代码补全也比较笨.&lt;/p&gt;
&lt;p&gt;starcoder2 速度要快得多, 补全效果也更好, 但对文本的处理能力偏弱.&lt;/p&gt;
&lt;p&gt;个人推荐使用 Cloudflare 的 llama3 模型进行对话, 本地运行 starcoder2 进行补全, 是比较完美的解决方案.&lt;/p&gt;
</content:encoded></item><item><title>nginx 配置 https</title><link>https://ray-d-song.com/blog/nginx-config-https</link><guid isPermaLink="true">https://ray-d-song.com/blog/nginx-config-https</guid><pubDate>Tue, 03 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;self-host 系列文章, 讲述如何自己搭建服务.&lt;/p&gt;
&lt;h2&gt;什么是 HTTPS&lt;/h2&gt;
&lt;p&gt;HTTPS 是 HTTP 的安全版本, 通过加密通信内容, 防止&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB&quot;&gt;中间人攻击&lt;/a&gt;, 保证数据传输的安全性.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;中间人攻击可以简单理解为, 有人篡改了服务端发送给你, 或者是你发送给服务端的数据.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;HTTPS 约定了使用 443 端口, 通过 SSL/TLS 协议进行通信.&lt;/p&gt;
&lt;p&gt;最常见的应用部署方法是使用 Nginx 作为反向代理服务器, 监听 443 端口, 将请求转发到服务所在的端口. 并且 Nginx 会负责 SSL/TLS 的加密解密工作.&lt;/p&gt;
&lt;h2&gt;生成证书&lt;/h2&gt;
&lt;p&gt;配置 HTTPS 首先需要生成证书, 证书由 CA 机构颁发, 用来证明你的网站是安全的.&lt;br /&gt;
https://www.cloudflare.com/application-services/products/ssl/&lt;/p&gt;
&lt;p&gt;如果你的域名在 Cloudflare 上, 可以直接在 Cloudflare 上申请免费证书.&lt;/p&gt;
&lt;p&gt;进入 Cloudflare 控制台, 选择左侧的&lt;code&gt;Websites&lt;/code&gt; -&amp;gt; &lt;code&gt;选择你的域名&lt;/code&gt;&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/77608d3883b8b5263435d270e1da4164.png&quot; alt=&quot;cloudflare&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后再选择左侧边栏的&lt;code&gt;SSL/TLS&lt;/code&gt; -&amp;gt; &lt;code&gt;Origin Server&lt;/code&gt; -&amp;gt; &lt;code&gt;Create Certificate&lt;/code&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/2024/09/2ff28057f28f36229fafb74566405b49.png&quot; alt=&quot;cloudflare&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不需要修改任何选项, 直接点击&lt;code&gt;Create&lt;/code&gt;创建即可.&lt;br /&gt;
Cloudflare 会生成证书和私钥, 证书用于证明你的网站是安全的, 私钥用于加密解密通信内容.&lt;br /&gt;
记得保存你的私钥, 离开创建页后就无法再看到了.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你也可以使用 &lt;a href=&quot;https://letsencrypt.org/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt; 获取免费证书.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设你的证书和密钥如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN CERTIFICATE-----
your_certificate
-----END CERTIFICATE-----
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN PRIVATE KEY-----
your_private_key
-----END PRIVATE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将证书保存为 &lt;code&gt;xxx.crt&lt;/code&gt;, 将私钥保存为 &lt;code&gt;xxx.key&lt;/code&gt;.&lt;br /&gt;
记住你的证书和私钥的路径, 用于配置 Nginx.&lt;/p&gt;
&lt;h2&gt;配置 Nginx&lt;/h2&gt;
&lt;p&gt;安装好 Nginx 后, 打开 Nginx 配置文件, 一般在 &lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt; 或者 &lt;code&gt;/usr/local/nginx/conf/nginx.conf&lt;/code&gt;.&lt;br /&gt;
也可以用 &lt;code&gt;nginx -t&lt;/code&gt; 命令查看 Nginx 配置文件的路径.&lt;/p&gt;
&lt;p&gt;新版本的 Nginx 会将配置文件分为多个文件, 一般在 &lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt; 目录下.&lt;br /&gt;
新建一个文件, 命名为 &lt;code&gt;your_domain.com.conf&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;对应的 nginx 配置如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 443 ssl;
    # [!code highlight:5]
    server_name your_domain.com;

    ssl_certificate /path/to/your_certificate.crt;
    ssl_certificate_key /path/to/your_private_key.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://localhost:8080; # [!code highlight]
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中需要关注的高亮行:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;your_domain.com&lt;/code&gt; 替换为你的域名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/path/to/your_certificate.crt&lt;/code&gt; 替换为你的证书路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/path/to/your_private_key.key&lt;/code&gt; 替换为你的私钥路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://localhost:8080&lt;/code&gt; 替换为你的服务地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你希望用户访问路径不是&lt;code&gt;your_domain.com&lt;/code&gt;, 可以将 11 行的&lt;code&gt;/&lt;/code&gt;替换为你想要的路径.&lt;/p&gt;
&lt;p&gt;保存配置文件后, 重启 Nginx 服务.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有使用 systemd 管理 Nginx 服务, 可以使用以下命令重启 Nginx 服务.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nginx -s reload
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 DNS&lt;/h2&gt;
&lt;p&gt;在 Cloudflare 控制台中, 选择左侧的&lt;code&gt;Websites&lt;/code&gt; -&amp;gt; &lt;code&gt;选择你的域名&lt;/code&gt; -&amp;gt; &lt;code&gt;DNS&lt;/code&gt;&lt;br /&gt;
点击&lt;code&gt;Add record&lt;/code&gt;添加一条 A 记录, 将&lt;code&gt;Type&lt;/code&gt;设置为&lt;code&gt;A&lt;/code&gt;, &lt;code&gt;Name&lt;/code&gt;设置为你的域名, &lt;code&gt;IPv4 address&lt;/code&gt;设置为你的服务器 IP.&lt;br /&gt;
点击保存, 等待 DNS 生效. 然后访问你的域名, 大功告成.&lt;/p&gt;
&lt;h2&gt;配置 HTTP 跳转 HTTPS&lt;/h2&gt;
&lt;p&gt;如果你希望用户访问 HTTP 时自动跳转到 HTTPS, 可以在配置文件中添加以下配置.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    server_name your_domain.com; # [!code highlight]
    return 301 https://$host$request_uri;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 Cloudflare 用户, 可以在 Cloudflare 控制台中配置&lt;code&gt;SSL/TLS&lt;/code&gt; -&amp;gt; &lt;code&gt;Edge Certificates&lt;/code&gt; -&amp;gt; &lt;code&gt;Always Use HTTPS&lt;/code&gt;开启强制 HTTPS.&lt;/p&gt;
</content:encoded></item><item><title>使用 Docker 容器进行前端开发</title><link>https://ray-d-song.com/blog/docker-dev-container-old-nodejs</link><guid isPermaLink="true">https://ray-d-song.com/blog/docker-dev-container-old-nodejs</guid><pubDate>Mon, 26 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近要改一个老项目, vue2 和 node.js14.&lt;/p&gt;
&lt;p&gt;我现在使用的 runtime 版本管理器是&lt;a href=&quot;https://asdf-vm.com/&quot;&gt;asdf&lt;/a&gt;, m2 macOS 安装 v14 的 node.js 是从源码构建.&lt;/p&gt;
&lt;p&gt;灾难开始了, 编译 node.js 12 需要 python2.7 或 3.9, macOS 系统自带的 python 版本是 3.12.&lt;/p&gt;
&lt;p&gt;然后我又去用 asdf 编译 2.7.18 的 python. 正当我满心欢喜觉得可以使用的时候, 编译失败了!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.ray-d-song.com/2024/08/c0f4875873e59539cbe39404361ee244.png&quot; alt=&quot;build fail&quot; /&gt;&lt;/p&gt;
&lt;p&gt;好吧我承认我看不懂这个报错.&lt;br /&gt;
当然我也可以自己下载编译好的文件然后手动让 asdf 接管. 但这时候我已经没耐心了.&lt;/p&gt;
&lt;h2&gt;Dev Container&lt;/h2&gt;
&lt;p&gt;Dev Container 就是使用容器作为开发环境, 再使用 vscode、neovim 之类的编辑器将容器作为后端.&lt;br /&gt;
这样做有两个好处:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dev Container 是无状态的, 用完就可以销毁, 不会产生额外占用&lt;/li&gt;
&lt;li&gt;统一开发人员的环境, 不再需要额外花时间去配置, 分发一个 Dockerfile 就行.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然也有问题, 在 Mac 和 windows 上, Docker 运行在虚拟机中, 代码编写和编译会触发跨文件系统读写, 导致 IO 效率降低. 体感上 webpack 项目只有本机开发 50% 左右的编译效率.&lt;/p&gt;
&lt;h2&gt;Dockerfile&lt;/h2&gt;
&lt;p&gt;想使用 Dev Container 就要打包一个包含开发工具的镜像.&lt;/p&gt;
&lt;p&gt;制作镜像的方法有两种, 一种是创建新的系统容器, 进入容器安装必要工具后将容器打包成镜像.&lt;/p&gt;
&lt;p&gt;但这样的缺点是分发时必须以镜像作为单位. debian nodejs14 的镜像体积在压缩前能达到 300M.&lt;/p&gt;
&lt;p&gt;所以这里我选择使用&lt;code&gt;Dockerfile&lt;/code&gt;, Docker 通过读取 Dockerfile 中的指令来自动构建镜像.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# nodejs
# version: v14 -&amp;gt; v20

# Specify the source mirror to use
FROM debian:latest

# Operation command
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
  wget \
  git \
  xz-utils

RUN mkdir -p /usr/local/nodejs

# Define variables and default values
ARG VER=14
RUN if [ &quot;$VER&quot; = &quot;14&quot; ]; then \
      wget -O node.tar.xz https://nodejs.org/dist/v14.21.3/node-v14.21.3-linux-arm64.tar.xz; \
    elif [ &quot;$VER&quot; = &quot;16&quot; ]; then \
      wget -O node.tar.xz https://nodejs.org/dist/v16.20.2/node-v16.20.2-linux-arm64.tar.xz; \
    elif [ &quot;$VER&quot; = &quot;18&quot; ]; then \
      wget -O node.tar.xz https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-arm64.tar.xz; \
    elif [ &quot;$VER&quot; = &quot;20&quot; ]; then \
      wget -O node.tar.xz https://nodejs.org/dist/v20.12.0/node-v20.12.0-linux-arm64.tar.xz; \
    else \
      echo &quot;Unsupported nodejs version: $VER&quot;; exit 1; \
    fi

RUN tar -xJf node.tar.xz -C /usr/local/nodejs --strip-components=1 \
&amp;amp;&amp;amp; rm node.tar.xz

# Define environment variables
ENV PATH=/usr/local/nodejs/bin:$PATH

RUN mkdir -p /usr/src/app

# Designated working folder
WORKDIR /usr/src/app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用到了几个 Dockerfile 的指令, 其含义分别如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FROM: 指定源镜像, 这里使用最新的 debian&lt;/li&gt;
&lt;li&gt;RUN: 执行命令, 可以直接理解为在容器中执行的 shell 命令&lt;/li&gt;
&lt;li&gt;ARG: 定义变量, 可以在构建时传入, 还可以定义默认值&lt;/li&gt;
&lt;li&gt;ENV: 定义环境变量, 这里是将 nodejs 的 bin 目录加入到 PATH 中&lt;/li&gt;
&lt;li&gt;WORKDIR: 指定工作目录, 也就是容器启动时默认进入的目录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里有一个注意点, Dockerfile 有「层」的概念, 每一个 RUN 指令都会产生一个新的层.&lt;br /&gt;
这也是为什么&lt;code&gt;ARG&lt;/code&gt;定义在获取 nodejs 文件命令之前的原因. 因为变量仅在当前层有效.&lt;br /&gt;
如果将&lt;code&gt;ARG&lt;/code&gt;放在最前面, 就会报错.&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;将上面的 Dockerfile 保存为&lt;code&gt;Dockerfile&lt;/code&gt;, 然后在 Dockerfile 同级目录下执行&lt;code&gt;docker build -t nodejs14 --build-arg VER=14 .&lt;/code&gt;&lt;br /&gt;
这样就会构建一个名为&lt;code&gt;nodejs14&lt;/code&gt;的镜像.&lt;/p&gt;
&lt;p&gt;使用以下命令运行容器:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
-v=/Users/ray/workbase:/usr/src/app \
--name=nodejs14 \
-p=9981:9981 \
nodejs14 \
sleep infinity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 -v 参数是将本地目录映射到容器中, 这样就可以在容器中访问本地文件同时不会丢失进度.&lt;br /&gt;
--name 参数是指定容器名, -p 参数是将容器端口映射到本地端口.&lt;br /&gt;
最后的&lt;code&gt;sleep infinity&lt;/code&gt;是为了让容器保持运行状态.&lt;/p&gt;
</content:encoded></item><item><title>Mac 关闭 SIP 并删除自带 ABC 输入法</title><link>https://ray-d-song.com/blog/macos-close-sip-delete-abc</link><guid isPermaLink="true">https://ray-d-song.com/blog/macos-close-sip-delete-abc</guid><pubDate>Wed, 14 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mac 装机自带 ABC 和中文输入, 但可用性不是很强, 例如很多开发者需要在编辑器的中文输入法下使用英文符号, 在 Mac 下只有搜狗输入法还行.&lt;/p&gt;
&lt;p&gt;但 Mac 在开启&lt;code&gt;SIP(系统完整性保护)&lt;/code&gt;时无法将系统自带全部移除, 输入法经常会从搜狗切到 ABC, 很麻烦.&lt;/p&gt;
&lt;h2&gt;关闭 SIP&lt;/h2&gt;
&lt;p&gt;终端输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;csrutil status

# 如果已关闭会显示: System Integrity Protection status: disabled.
# 未关闭显示: System Integrity Protection status: enabled.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续步骤:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关机, 然后按住开机键不松手, 直到看见启动选项界面, 选择&lt;code&gt;选项&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;点击继续&lt;/li&gt;
&lt;li&gt;点击左上角, &lt;code&gt;实用工具&lt;/code&gt; =&amp;gt; &lt;code&gt;终端&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;输入&lt;code&gt;csrutil disable&lt;/code&gt;, 回车&lt;/li&gt;
&lt;li&gt;输入&lt;code&gt;y&lt;/code&gt;, 回车&lt;/li&gt;
&lt;li&gt;输入电脑密码, 回车&lt;/li&gt;
&lt;li&gt;等待结果, 如果是&lt;code&gt;System Integrity Protection is off&lt;/code&gt;, 就是成功关闭了&lt;/li&gt;
&lt;li&gt;重启电脑&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;删除 ABC 输入法&lt;/h2&gt;
&lt;p&gt;删除需要编辑系统的 plist 文件, 常用的编辑器是 xcode 或者&lt;a href=&quot;https://www.fatcatsoftware.com/plisteditpro/&quot;&gt;PlistEdit Pro&lt;/a&gt;. 前者太大, 后者收费, 我推荐一个开源选择&lt;a href=&quot;https://github.com/ic005k/Xplist/releases/download/1.2.47/Xplist_Mac.dmg&quot;&gt;Xplist&lt;/a&gt;, 你可以直接点这个链接下载安装包.&lt;/p&gt;
&lt;p&gt;安装完成后, 先切换输入法到 ABC, 输入命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo open -a Xplist ~/Library/Preferences/com.apple.HIToolbox.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.ray-d-song.com/2024/08/baadb07e1bf2d972ad33d2cbc632132b.png&quot; alt=&quot;Xplist&quot; /&gt;
可以看到&lt;code&gt;AppleEnabledInputSources&lt;/code&gt;选项下几个item, 展开 item 找到&lt;code&gt;KeyboardLayout Name&lt;/code&gt;为&lt;code&gt;ABC&lt;/code&gt;的选项, 右键整个item, 选择&lt;code&gt;remove&lt;/code&gt;.&lt;br /&gt;
按&lt;code&gt;command S&lt;/code&gt;保存后再重启电脑就可以看到输入选项里没有 ABC 了.&lt;/p&gt;
</content:encoded></item><item><title>npm 版本号和版本控制详解</title><link>https://ray-d-song.com/blog/software-versioning</link><guid isPermaLink="true">https://ray-d-song.com/blog/software-versioning</guid><pubDate>Wed, 14 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;语义化版本控制&lt;/h2&gt;
&lt;p&gt;最近在拆分公司的 MonoRepo 仓库, 分割一部分内容成为新的 npm package, 发现公司之前的发布版本控制并不是很规范.&lt;/p&gt;
&lt;p&gt;npm 遵循的是&lt;a href=&quot;https://semver.org/&quot;&gt;语义化版本控制(semantic versioning)&lt;/a&gt;, 其结构为:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;主版本号.次版本号.修订号&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;三种版本号定义分别是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不兼容的更新&lt;/li&gt;
&lt;li&gt;向下兼容的功能性更新&lt;/li&gt;
&lt;li&gt;向下兼容的问题修正&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此以外还可以在修订号后面加上先行版本号(通过&lt;code&gt;-&lt;/code&gt;连接)和版本编译信息(通过&lt;code&gt;+&lt;/code&gt;连接).&lt;br /&gt;
例如 &lt;code&gt;1.0.3-rc.1+exp.sha.5114f85&lt;/code&gt;
表示:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主版本号 1&lt;/li&gt;
&lt;li&gt;次版本号 0&lt;/li&gt;
&lt;li&gt;修订号 3&lt;/li&gt;
&lt;li&gt;先行版本号 rc.1&lt;/li&gt;
&lt;li&gt;版本编译信息 exp.sha.5114f85&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;先行版本号&lt;/h2&gt;
&lt;p&gt;所谓先行版本也就是测试版, 先行版本通常有三种: &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;rc&lt;/code&gt;分别代表&lt;code&gt;alpha(预览版本)&lt;/code&gt;、&lt;code&gt;beta(测试版本)&lt;/code&gt;和&lt;code&gt;release candidate(最终测试版本)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;先行版后可以跟一个数字, 表示先行版本的迭代, 例如上面的&lt;code&gt;-rc.1&lt;/code&gt;表示最终测试版本 1.&lt;/p&gt;
&lt;h2&gt;版本编译信息&lt;/h2&gt;
&lt;p&gt;版本编译信息主要用于跟踪管理不同的构建版本, 属于团队内的约定信息, 并没有什么强制规范.&lt;br /&gt;
但是通常包含以下两种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间戳, 例如&lt;code&gt;1.0.0+20130313144700&lt;/code&gt;表示构建时间为2013 年 3 月 13 日 14:47:00.&lt;/li&gt;
&lt;li&gt;实验性构建哈希, 例如&lt;code&gt;exp.sha.5114f85&lt;/code&gt;由两个部分组成, exp 是 experimental 的缩写, 表示实验性. sha.5114f85 则是某次 git 提交的哈希. 由这两个性能我们便可以定位到编译的源代码.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;npm包的注意点&lt;/h2&gt;
&lt;p&gt;除了以上几点, 还要一些约定事项.&lt;/p&gt;
&lt;h3&gt;版本编译信息不会影响版本&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;1.0.0+exp.sha.5114f85&lt;/code&gt;和&lt;code&gt;1.0.0+exp.sha.7822af2&lt;/code&gt;虽然编译信息不同, 但是会视作同一个版本, 因此版本编译信息不能发布一个特殊版本的方法.&lt;/p&gt;
&lt;p&gt;要为某个客户或者项目提供特供版, 最好的办法是使用先行版本号. 例如&lt;code&gt;1.0.0-customer.1&lt;/code&gt;, 这会被识别为一个新的版本.&lt;/p&gt;
&lt;h3&gt;新项目的初始版本&lt;/h3&gt;
&lt;p&gt;新项目的初始版本不是&lt;code&gt;0.0.1&lt;/code&gt;而是&lt;code&gt;0.1.0&lt;/code&gt;.&lt;br /&gt;
因为从语义的角度讲, 第三位是修订号.&lt;/p&gt;
&lt;h3&gt;package.json 中的 dependencies 版本&lt;/h3&gt;
&lt;p&gt;package.json 可以通过 dependencies 来控制使用的包&lt;code&gt;版本范围&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;补丁发布：1.0 或 1.0.x 或 ~1.0.4&lt;/li&gt;
&lt;li&gt;次要版本： 1 或 1.x 或 ^1.0.4&lt;/li&gt;
&lt;li&gt;主要版本发布：* 或 x&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;dependencies&quot;: {
  &quot;lodash&quot;: &quot;^1.0.0&quot;,
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种配置下, 如果 lodash 发布了 1.1.0, npm 就会拉取这个最新的次要版本.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为部分开源项目没有遵循上面的规范, 会在次要版本进行 breaking change, 推荐JS项目在稳定运行后进行锁版.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>vscode 如何 debug 前端项目</title><link>https://ray-d-song.com/blog/vscode-vite-debug</link><guid isPermaLink="true">https://ray-d-song.com/blog/vscode-vite-debug</guid><pubDate>Mon, 12 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;基础&lt;/h2&gt;
&lt;p&gt;vscode 的 debug 使用项目根目录下的&lt;code&gt;.vscode/launch.json&lt;/code&gt;文件进行配置.&lt;/p&gt;
&lt;p&gt;首次运行 debug, vscode 会创建一个默认的配置文件.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      // 调试器类型
      &quot;type&quot;: &quot;chrome&quot;,
      // 请求类型, launch 代表启动新的调试会话, 也可以选择 attach, 用于附加到已经运行的进程
      &quot;request&quot;: &quot;launch&quot;,
      &quot;name&quot;: &quot;Launch Chrome against localhost&quot;,
      // app 的 url, 端口需要和你的 vite 项目启动端口一致
      &quot;url&quot;: &quot;http://localhost:8080&quot;,
      // 指定项目根目录
      &quot;webRoot&quot;: &quot;${workspaceFolder}&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来只需要点击&lt;code&gt;Run and Debug&lt;/code&gt;按钮, 点击&lt;code&gt;Start Debugging&lt;/code&gt;, 只要端口一致, 程序集就会在你的断点处暂停.&lt;/p&gt;
&lt;h2&gt;source map&lt;/h2&gt;
&lt;p&gt;source map 是源代码和编译产物之间的映射关系文件, 关于 source map, 具体可以看&lt;a href=&quot;https://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html&quot;&gt;阮一峰老师的这篇博客&lt;/a&gt;.&lt;br /&gt;
如果你的项目需要经过编译运行, 在没有 sourcemap 的情况下, vscode 断点会提示&lt;code&gt;Unbound breakpoint(断点未绑定)&lt;/code&gt;, 因为实际运行的代码是编译后的结果, vscode 没法通过你源码中的断点定位到实际运行代码中的位置.&lt;/p&gt;
&lt;p&gt;vite 项目和常见插件都默认生成 map, 如果你的项目是纯 Typescript 编写, 那需要在 tsconfig.json 中增加:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;sourceMap&quot;: true,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;跳过 node_modules&lt;/h2&gt;
&lt;p&gt;在 debug 的时候经常会跳到 node_modules 或者 node_internals 等代码逻辑无关的部分.&lt;br /&gt;
vscode 提供了&lt;code&gt;skiFiles&lt;/code&gt;用于跳过不想进入的逻辑.&lt;br /&gt;
例如跳过 node_modules 和 node_internals 的配置如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      // ...
      &quot;skipFiles&quot;: [
        &quot;${workspaceFolder}/node_modules/**/*.js&quot;,
        &quot;${workspaceFolder}\\node_modules\\**\\*.js&quot;,
        &quot;&amp;lt;node_internals&amp;gt;/**/*.js&quot;,
        &quot;&amp;lt;node_internals&amp;gt;\\**\\*.js&quot;,
      ],
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;以上这种写法是因为 skipFiles 路径配置取决于你的平台(unix-like 还是 windows), 所以一劳永逸的方法就是两种都写上.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>用 Mitosis 同时构建 React 和 Vue 组件</title><link>https://ray-d-song.com/blog/build-reactvue-component-once</link><guid isPermaLink="true">https://ray-d-song.com/blog/build-reactvue-component-once</guid><pubDate>Fri, 09 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上次写了个&lt;a href=&quot;/blog/unocss-icon-viewer&quot;&gt;UnoCSS icon 选择器&lt;/a&gt;, 希望写个库可以同时在 React 和 Vue 中使用.&lt;/p&gt;
&lt;p&gt;首先想到的是 Web Component. 但缺点是在核心逻辑之外需要针对 Vue 和 React 分别编写一套适配代码.&lt;br /&gt;
对于一些比较小的需求, 写适配代码的成本可能比人工适配还要高.&lt;/p&gt;
&lt;p&gt;然后我就发现了&lt;code&gt;Mitosis&lt;/code&gt;这邪门东西.&lt;br /&gt;
Mitosis 的中文是&lt;code&gt;有丝分裂&lt;/code&gt;, 顾名思义, 它可以将组件编译成多个框架的代码, 包括 React, Vue, Qwik, Solid, Angular, Svelte.&lt;/p&gt;
&lt;p&gt;实现原理就是一个编译器, 将 Mitosis 的文件编译成 Vue 和 React 的有限子集. 和 UnoCSS 一样是个前端编译期的探索.&lt;/p&gt;
&lt;h2&gt;创建项目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm create @builder.io/mitosis@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入命令会有一些可配置的选项, 在选择输出目标时, 只有React, Svelte, Qwik. 但支持的框架远不止于此.&lt;br /&gt;
修改&lt;code&gt;library/mitosis.config.cjs&lt;/code&gt;的&lt;code&gt;targets&lt;/code&gt;字段, 增加 Vue 支持:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;targets&quot;: [
  &quot;react&quot;,
  &quot;vue&quot;,
],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目自带&lt;code&gt;todo-app.lite.tsx&lt;/code&gt;和&lt;code&gt;autocomplete.lite.tsx&lt;/code&gt;两个例子, 试运行一下.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd library
npm run build

# Mitosis: react: generated 2 components, 1 regular files.
# Mitosis: vue: generated 2 components, 1 regular files.
# Mitosis: generation completed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;/library/package&lt;/code&gt;会生成 React 和 Vue 的组件.&lt;/p&gt;
&lt;h2&gt;编写组件&lt;/h2&gt;
&lt;p&gt;Mitosis 的组件后缀是&lt;code&gt;.lite.tsx&lt;/code&gt;, 和 React 近似的 jsx 语法, 但有一套自己的 API 和限制.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useState } from &quot;@builder.io/mitosis&quot;;

export default function MyComponent(props) {
  const [name, setName] = useState(&quot;Steve&quot;);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input
        css={{
          color: &quot;red&quot;,
        }}
        value={name}
        onChange={(event) =&amp;gt; setName(event.target.value)}
      /&amp;gt;
      Hello! I can run natively in React, Vue, Svelte, Qwik, and many more frameworks!
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如由于编译器限制, 组件只支持上面这种 &lt;code&gt;export default function&lt;/code&gt; 的写法, 不能用 const 定义箭头函数再导出.&lt;/p&gt;
&lt;h3&gt;状态管理&lt;/h3&gt;
&lt;p&gt;现代前端框架最重要的就是&lt;code&gt;state&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;useStore&lt;/h4&gt;
&lt;p&gt;mitosis组件状态维护通过&lt;code&gt;useStore&lt;/code&gt;钩子, 返回值接收必须叫&lt;code&gt;state&lt;/code&gt;(也是编译器限制).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function MyComponent() {
  // ...
  const state = useStore({
    iconList: [],
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React 组件中, useStore 会被转换成一个简单的&lt;code&gt;useState&lt;/code&gt;定义.&lt;/p&gt;
&lt;p&gt;store 的参数塞啥都可以, 但如果你想要的值需要经过表达式计算得到, 就得写个&lt;code&gt;getter&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const state = useStore({
  iconList: [],
  get outlineIcon() {
    return this.iconList.filter(icon =&amp;gt; icon.endsWith(&apos;-outline&apos;))
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getter&lt;/code&gt;会转换成一个独立的方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [iconList, setIconList] = useState(() =&amp;gt; []);

function outlineIcon() {
  return iconList.filter((icon) =&amp;gt; icon.endsWith(&quot;-outline&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;useState&lt;/h4&gt;
&lt;p&gt;mitosis 也支持useState api, 使用方式和 React 一样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [count, setCount] = useState(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Vue 中, useState 和 useStore 都会被编译成 options api 中的&lt;code&gt;data&lt;/code&gt;选项.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;顺带一提, 现在 mitosis 还不支持 Vue 3 的&lt;code&gt;setup&lt;/code&gt;写法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;export default defineComponent({
  data() {
    return { count: 0 };
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;样式&lt;/h3&gt;
&lt;p&gt;Mitosis 通过标签上的&lt;code&gt;css&lt;/code&gt;属性来编写&lt;code&gt;camelCase&lt;/code&gt;的样式, 你甚至可以像这样写 css query.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div
  css={{
    marginTop: &apos;10px&apos;,
    &apos;@media (max-width: 500px)&apos;: {
      marginTop: &apos;0px&apos;,
    },
  }}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且编译后的结果还做了样式隔离. (React 本体到现在也没支持😂)&lt;/p&gt;
&lt;p&gt;当然你也可以选择使用&lt;code&gt;class&lt;/code&gt;属性(不需要 React 中的 className), 不过这样就没法做样式隔离了.&lt;/p&gt;
&lt;h2&gt;流程控制&lt;/h2&gt;
&lt;p&gt;为了屏蔽不同框架的差异, Mitosis 通过一标签实现了自己的流程控制, 以下是对照表, 详细可以看&lt;a href=&quot;https://mitosis.builder.io/docs/components/&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mitosis&lt;/th&gt;
&lt;th&gt;React&lt;/th&gt;
&lt;th&gt;Vue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;Show&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v-if&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;For&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v-for&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;集成其他库&lt;/h2&gt;
&lt;p&gt;因为 Mitosis 并不是一个前端框架, 而是一个编译器, 所以它没有 webpack 或者 vite 集成.&lt;br /&gt;
比如, 想要使用 UnoCSS, 只能使用&lt;code&gt;uno cli&lt;/code&gt;, 监听文件变化然后编译出 css 文件, 再进行引入.&lt;/p&gt;
&lt;p&gt;当然还有个更好的办法, 本质上 Mitosis 只是一个转化管道, 那我们只要设置&lt;code&gt;产物的环境&lt;/code&gt;就行了, Mitosis 完成转换后，由其他工具接管, 进行打包.&lt;br /&gt;
最终我们编写组件的流程就变成了:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编写 Mitosis 组件&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;npm run build&lt;/code&gt;编译, 输出到 vite 环境中&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;vite&lt;/code&gt;之类的的打包器去打包最终的产物, npm 发布&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你可以以这个项目为示例&lt;a href=&quot;https://github.com/Ray-D-Song/unocss-icon-viewer&quot;&gt;unocss-icon-viewer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;有一些关键点你可能需要注意, 比如mitosis的 tsconfig 将 jsxImportSource 设置为&lt;code&gt;@builder.io/mitosis&lt;/code&gt;.&lt;br /&gt;
这会导致你 build react 或者 solid 之类的库时, jsx 类型报错.&lt;br /&gt;
解决方案是在 Vite 中配置 esbuild 和 ets 插件, 动态修改 compilerOptions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineConfig({
  plugins: [
    // ...
    dts({
      outDir: &apos;dist&apos;,
      include: &apos;packages/*/src/**&apos;,
      compilerOptions: {
        jsxImportSource: &apos;react&apos;,
      }
    }),
  ],
  // ...
  esbuild: {
    tsconfigRaw: {
      compilerOptions: {
        jsxImportSource: &apos;react&apos;,
      }
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;缺陷&lt;/h2&gt;
&lt;p&gt;Mitosis 算是不错的解决方案, 但也有缺陷, 比如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法控制产物质量, 生成的代码并不是最优解&lt;/li&gt;
&lt;li&gt;奇奇怪怪的规定, 比如 props 不可以解构, 只能用&lt;code&gt;props.xxx&lt;/code&gt;, 换言之就是编译器太弱了&lt;/li&gt;
&lt;li&gt;编写源码 =&amp;gt; 编译到打包器环境 =&amp;gt; 运行测试环境, 这里面至少涉及 3 个 watch 和环境, 太过复杂&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>如何创建一个 UnoCSS icon 选择器</title><link>https://ray-d-song.com/blog/unocss-icon-viewer</link><guid isPermaLink="true">https://ray-d-song.com/blog/unocss-icon-viewer</guid><pubDate>Thu, 08 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import UnoCSSIconViewer from &apos;@/components/UnoCSSIconViewer.vue&apos;
export const iconName = &apos;iconName&apos;&lt;/p&gt;
&lt;p&gt;效果展示:  &amp;lt;UnoCSSIconViewer /&amp;gt;&lt;/p&gt;
&lt;p&gt;UnoCSS Icon 的使用方法是在标签上添加&lt;code&gt;i-&lt;/code&gt;开头的 icon 名, 例如&lt;br /&gt;
&lt;code&gt;&amp;lt;div class=&quot;i-mdi-access-point&quot; /&amp;gt;&lt;/code&gt;&lt;br /&gt;
就会渲染
&amp;lt;div class=&quot;w-full flex justify-center&quot;&amp;gt;
&amp;lt;div class=&quot;i-mdi-access-point w-25px h-25px&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;不需要在&lt;code&gt;public&lt;/code&gt;或者&lt;code&gt;assets&lt;/code&gt;目录引入额外的 icon 文件或者 js 文件, 还能用 class 来自由控制大小和颜色, 非常方便.&lt;/p&gt;
&lt;p&gt;前端一个很常见的场景是让用户自己选择 icon, 由于UnoCSS 是一个作用于编译时的引擎, 它会根据这个 icon 名生成&lt;code&gt;纯css icon&lt;/code&gt;, 不会将合集中所有的icon打包进去.&lt;/p&gt;
&lt;p&gt;事实上&lt;code&gt;&amp;lt;div class={&lt;/code&gt;i-mdi-${iconName}&lt;code&gt;}&amp;gt;&lt;/code&gt;这种写法也是无效的, 因为编译时无法确认&lt;code&gt;iconName&lt;/code&gt;的值引擎就不会生成对应的样式.&lt;/p&gt;
&lt;p&gt;要自己实现这个功能, 只需要以下几步:&lt;/p&gt;
&lt;h2&gt;获取合集中的所有 icon&lt;/h2&gt;
&lt;p&gt;以&lt;a href=&quot;https://www.npmjs.com/package/@iconify-json/fe&quot;&gt;Feather Icon&lt;/a&gt;为例, &lt;code&gt;npm i -D @iconify-json/fe&lt;/code&gt; 安装iconify集合.&lt;/p&gt;
&lt;p&gt;安装后可以通过&lt;code&gt;import { icons } from &apos;@iconify-json/fe/index.js&apos;&lt;/code&gt;获取所有的 icon 数据, 结构如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  // icon 前缀
  prefix: &apos;fe&apos;,
  // icon 集合
  icons: {
    // icon 名称
    icon_name: {
      // 数据
      body: //...
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意, 导入的 icons 是包含 prefix 字段的对象, 而不是 icon 集合对象, 要访问 icon 集合对象需要&lt;code&gt;icons.icons&lt;/code&gt;
接下来我们只需要根据&lt;code&gt;icons.icons&lt;/code&gt;的key遍历渲染即可, 以Vue为例.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { icons } from &apos;@iconify-json/fe/index.js&apos;
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;dark:bg-[#181818] bg-[#ffffff] w-full h-40 rounded-lg overflow-y-scroll&quot;&amp;gt;
    &amp;lt;div class=&quot;grid grid-cols-6 gap-4&quot;&amp;gt;
      &amp;lt;template v-for=&quot;icon in Object.keys(icons.icons)&quot;&amp;gt;
        &amp;lt;div class=&quot;flex flex-col items-center justify-center cursor-pointer p-4px dark:hover:bg-[#2A2A2A] hover:bg-[#EBEAEA]&quot;&amp;gt;
          &amp;lt;div :class=&quot;`i-${icons.prefix}-${icon}`&quot; class=&quot;mb-2px&quot; /&amp;gt;
          &amp;lt;div class=&quot;text-xs text-center&quot;&amp;gt;{{ icon }}&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/template&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这个时候 icon 是不可见的, 因为采用了&lt;code&gt;i-${icons.prefix}-${icon}&lt;/code&gt;这样的动态写法.&lt;/p&gt;
&lt;h2&gt;添加白名单&lt;/h2&gt;
&lt;p&gt;uno.config.ts 中有&lt;a href=&quot;https://unocss.dev/guide/extracting#safelist&quot;&gt;&lt;code&gt;safelist&lt;/code&gt;&lt;/a&gt;字段, 名单内的 class 无论是否使用都会被编译.&lt;br /&gt;
再一次遍历&lt;code&gt;icons.icons&lt;/code&gt;的key, 将所有 icon 名添加到&lt;code&gt;safelist&lt;/code&gt;中.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { icons } from &apos;@iconify-json/fe/index.js&apos;

export default defineConfig({
  safelist: Object.keys(icons.icons).map(icon =&amp;gt; `i-fe-${icon}`),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样界面上就能看到所有 icon 了.&lt;/p&gt;
&lt;h2&gt;过滤&lt;/h2&gt;
&lt;p&gt;这里还有一个问题, 比如&lt;code&gt;material-design-icons&lt;/code&gt;这个集合中有七千多个 icon, 但是我们只需要其中的一部分.&lt;br /&gt;
最简单的做法是加个&lt;code&gt;filter&lt;/code&gt;, 例如只保留&lt;code&gt;outline&lt;/code&gt;类型的 icon.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { icons } from &apos;@iconify-json/fe/index.js&apos;

export default defineConfig({
  safelist: Object
    .keys(icons.icons)
    .filter(icon =&amp;gt; icon.includes(&apos;outline&apos;))
    .map(icon =&amp;gt; `i-fe-${icon}`),
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Vue 宏编译: 以 defineProps 为例</title><link>https://ray-d-song.com/blog/vue-compiler-macro-defineprops</link><guid isPermaLink="true">https://ray-d-song.com/blog/vue-compiler-macro-defineprops</guid><pubDate>Thu, 29 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Vue macro(宏) 随着&lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt;写法一同引入 Vue 生态, 进一步丰富了 Vue 在编译期的想象力.&lt;br /&gt;
本文主要分析 Vue defineProps 的 type-only 写法是如何根据类型信息生成运行时代码.&lt;/p&gt;
&lt;p&gt;我们先拉取 Vue 的源代码, 并切换到3.0.3版本, 这是最早引入 script setup 和 defineProps macro 的版本.&lt;/p&gt;
&lt;p&gt;Vue 的 script setup 编译器源码位于&lt;code&gt;packages/compiler-sfc/src/compileScript.ts&lt;/code&gt;, 接下来所有的代码都出自这个文件&lt;/p&gt;
&lt;p&gt;为了方便查看运行结果和打印, 在根目录下的&lt;code&gt;package.json&lt;/code&gt;新增&lt;code&gt;test-compiler&lt;/code&gt;命令, 这条命令表示只运行&lt;code&gt;packages/compiler-sfc&lt;/code&gt;文件夹下的&lt;code&gt;with TypeScript&lt;/code&gt;测试.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  &quot;test-compiler&quot;: &quot;jest packages/compiler-sfc --testPathPattern=&apos;packages/compiler-sfc&apos; --testNamePattern=&apos;with TypeScript&apos; &quot;,
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看 defineProps&amp;lt;{}&amp;gt;() 会被编译成什么&lt;br /&gt;
在&lt;code&gt;packages/compiler-sfc/__tests__/compileScript.spec.ts&lt;/code&gt;文件的451 行打个&lt;code&gt;console.log(content)&lt;/code&gt;, 查看编译完成的内容&lt;br /&gt;
输入:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { defineProps } from &apos;vue&apos;


defineProps&amp;lt;{
  string: string
  number: number
  boolean: boolean
  object: object
  // ...
}&amp;gt;()
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineComponent as _defineComponent } from &apos;vue&apos;


export default _defineComponent({
  expose: [],
  props: {
    string: { type: String, required: true },
    number: { type: Number, required: true },
    boolean: { type: Boolean, required: true },
    object: { type: Object, required: true },
    // ...
  } as unknown as undefined,
  setup(__props: {
        string: string
        number: number
        boolean: boolean
        object: object
        // ...
      }) {

return {  }
}

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 defineProps 被编译为&lt;code&gt;defineComponent&lt;/code&gt;方法中的 options props 写法, 同时还定义了 defineComponent 方法中的 setup 函数选项并保留了类型的定义.&lt;/p&gt;
&lt;h2&gt;定位&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;packages/compiler-sfc/src/compileScript.ts&lt;/code&gt;文件中, 我们一眼能找到一个看起来是处理 props 的方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function processDefineProps(node: Node): boolean {
  if (isCallOf(node, DEFINE_PROPS)) {
    hasDefinePropsCall = true
    // context call has type parameters - infer runtime types from it
    if (node.typeParameters) {
      const typeArg = node.typeParameters.params[0]
      if (typeArg.type === &apos;TSTypeLiteral&apos;) {
        propsTypeDecl = typeArg
      }
    }
    return true
  }
  return false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数接受参数&lt;code&gt;node&lt;/code&gt;, 并将propsTypeDecl赋值为 node.typeParameters.params[0]&lt;br /&gt;
接下来我们按照看源码的惯例: 向前找入参 node, 向后找 propsTypeDecl 的作用.&lt;/p&gt;
&lt;h2&gt;向前看: node 是什么, 从哪来&lt;/h2&gt;
&lt;p&gt;我们在 processDefineProps 方法中打印一下 node:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Node {
  type: &apos;CallExpression&apos;,
  start: 101,
  end: 732,
  loc: SourceLocation {
    start: Position { line: 7, column: 6 },
    end: Position { line: 30, column: 10 },
    filename: undefined,
    identifierName: undefined
  },
  range: undefined,
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  callee: Node {
    type: &apos;Identifier&apos;,
    start: 101,
    end: 112,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: &apos;defineProps&apos;
    },
    range: undefined,
    leadingComments: undefined,
    trailingComments: undefined,
    innerComments: undefined,
    extra: undefined,
    name: &apos;defineProps&apos;
  },
  arguments: [],
  typeParameters: Node {
    type: &apos;TSTypeParameterInstantiation&apos;,
    start: 112,
    end: 730,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    range: undefined,
    leadingComments: undefined,
    trailingComments: undefined,
    innerComments: undefined,
    extra: undefined,
    params: [ [Node] ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;熟悉 babel 的哥们应该一眼能看出这是 babel 的 AST(抽象语法树).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抽象语法树可以简单理解为分析源代码产生的相关信息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们继续找, 看看是哪里提供了这个 node.&lt;br /&gt;
在 552 行, 我们可以找到 node 是 scriptSetupAst 的遍历子节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// line 552
for (const node of scriptSetupAst)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 &lt;code&gt;scriptSetupAst&lt;/code&gt; 是调用了 parse 函数, 传入了&lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt;标签内的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// parse &amp;lt;script setup&amp;gt; and  walk over top level statements
const scriptSetupAst = parse(
  scriptSetup.content,
  {
    plugins: [
      ...plugins,
    ],
    sourceType: &apos;module&apos;
  },
  startOffset
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续寻找 parse 函数的定义, 在 209 行.&lt;br /&gt;
parse 函数调用了_parse 函数, 而 _parse 函数是&lt;code&gt;@babel/parser&lt;/code&gt;包中 parse 函数的别名, 该函数返回的就是 Babel AST 格式的 AST, 证实了之前的猜想&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function parse(
  input: string,
  options: ParserOptions,
  offset: number
): Statement[] {
  try {
    return _parse(input, options).program.body
  } catch (e) {
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里, 「向前看」的工作已经完成, 大体流程为
&lt;img src=&quot;https://r2.ray-d-song.com/2024/02/2101e0b9f3936ba65773c8f9a77e3db2.png&quot; alt=&quot;half&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;向后看: propsTypeDecl 有什么用, 怎么处理&lt;/h2&gt;
&lt;p&gt;processDefineProps 的作用是对 propsTypeDecl 赋值, 那么赋值后对 propsTypeDecl 进行了哪些操作就是生成运行时 props 定义的关键.&lt;br /&gt;
按照惯例, 首先打印 propsTypeDecl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;propsTypeDecl:  Node {
  type: &apos;TSTypeLiteral&apos;,
  start: 113,
  end: 729,
  loc: SourceLocation {
    start: Position { line: 7, column: 18 },
    end: Position { line: 30, column: 7 },
    filename: undefined,
    identifierName: undefined
  },
  range: undefined,
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  members: [
    Node {
      type: &apos;TSPropertySignature&apos;,
      start: 123,
      end: 137,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: undefined,
      key: [Node],
      computed: false,
      typeAnnotation: [Node]
    },
    Node {
      type: &apos;TSPropertySignature&apos;,
      start: 146,
      end: 160,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: undefined,
      key: [Node],
      computed: false,
      typeAnnotation: [Node]
    },
    Node {
      type: &apos;TSPropertySignature&apos;,
      start: 169,
      end: 185,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: undefined,
      key: [Node],
      computed: false,
      typeAnnotation: [Node]
    },
    ...
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到, propsTypeDecl 就是 defineProps 类型声明的 AST. 主要内容是&lt;code&gt;members&lt;/code&gt;字段, 每一个Node对应着一个 props 元素声明&lt;/p&gt;
&lt;p&gt;接下来我们找哪里使用了这个&lt;code&gt;propsTypeDecl&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;propsTypeDecl 有两处引用&lt;/p&gt;
&lt;h3&gt;生成运行时props(重点)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 4. extract runtime props/emits code from setup context type
if (propsTypeDecl) {
  extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数除了 propsTypeDecl 之外, 还有&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;typeDeclaredProps: 类型定义为&lt;code&gt;Record&amp;lt;string, string[]&amp;gt;&lt;/code&gt;, 默认为{}的变量&lt;/li&gt;
&lt;li&gt;declaredTypes: 类型&lt;code&gt;Record&amp;lt;string, string[]&amp;gt;&lt;/code&gt;, 默认{}的变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来看看这个函数都做了啥操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function extractRuntimeProps(
  node: TSTypeLiteral,
  props: Record&amp;lt;string, PropTypeData&amp;gt;,
  declaredTypes: Record&amp;lt;string, string[]&amp;gt;
) {
  // members 即 literal type 的AST数组
  for (const m of node.members) {
    // 判断是否为 literal type
    if (m.type === &apos;TSPropertySignature&apos; &amp;amp;&amp;amp; m.key.type === &apos;Identifier&apos;) {
      // 为 typeDeclaredProps 添加字段
      props[m.key.name] = {
        key: m.key.name,
        required: !m.optional,
        type:
          // dev 下生成 type 字段, 生产环境不需要类型信息, 直接赋值 null
          __DEV__ &amp;amp;&amp;amp; m.typeAnnotation
            ? inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes)
            : [`null`]
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数的作用就是根据 AST 的信息生成运行时的 props 声明, 并赋值给第二个参数&lt;code&gt;typeDeclaredProps&lt;/code&gt;, 这个参数最终就是编译完成的 Props.&lt;br /&gt;
这里还调用了&lt;code&gt;inferRuntimeType&lt;/code&gt;方法, 方法主体就是 switch 语句, 根据不同的&lt;code&gt;node.type&lt;/code&gt;字段返回不同的运行时类型声明&lt;br /&gt;
截取其中一小段:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch (node.type) {
  case &apos;TSStringKeyword&apos;:
    return [&apos;String&apos;]
  case &apos;TSNumberKeyword&apos;:
    return [&apos;Number&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生成 __props 字段&lt;/h3&gt;
&lt;p&gt;propsTypeDecl 另一处引用是用来生成&lt;code&gt;defineProps&lt;/code&gt;方法中的__props字段:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 9. finalize setup() argument signature
let args = `__props`
if (propsTypeDecl) {
  args += `: ${scriptSetup.content.slice(
    propsTypeDecl.start!,
    propsTypeDecl.end!
  )}`
}
// inject user assignment of props
// we use a default __props so that template expressions referencing props
// can use it directly
if (propsIdentifier) {
  s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
}
if (emitIdentifier) {
  args +=
    emitIdentifier === `emit` ? `, { emit }` : `, { emit: ${emitIdentifier} }`
  if (emitTypeDecl) {
    args += `: {
      emit: (${scriptSetup.content.slice(
        emitTypeDecl.start!,
        emitTypeDecl.end!
      )}),
      slots: any,
      attrs: any
    }`
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;到这里我们分析完了整个的流程, 如下:
&lt;img src=&quot;https://r2.ray-d-song.com/2024/02/4feb8aaf8c3cda71a6383100a72cb3e2.png&quot; alt=&quot;full flow&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Vue 使用 Vitest 进行单元测试</title><link>https://ray-d-song.com/blog/vitest-unit-test</link><guid isPermaLink="true">https://ray-d-song.com/blog/vitest-unit-test</guid><pubDate>Mon, 18 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;单元测试的定义&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文源码: https://github.com/Ray-D-Song/vitest-tutorial-sourcecode&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;单元测试（英语：Unit Testing）又称为模块测试 [来源请求] ，是针对程序模块（软件设计的最小单位）来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中，一个单元就是单个程序、函数、过程等；对于面向对象编程，最小单元就是方法，包括基类（超类）、抽象类、或者派生类（子类）中的方法。     ———wiki&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在一个典型的 Vue 项目中, 我们经常需要进行单元测试的模块包括以下几种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hooks&lt;/li&gt;
&lt;li&gt;工具函数 (utils)&lt;/li&gt;
&lt;li&gt;校验规则 (reg)&lt;/li&gt;
&lt;li&gt;脚手架 (bin/scaffold)&lt;/li&gt;
&lt;li&gt;自定义 vue 指令&lt;/li&gt;
&lt;li&gt;全局状态 (store)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;初始化 Vitest&lt;/h1&gt;
&lt;p&gt;安装 Vitest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm add -D vitest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vitest 默认和 Vite 配置集成, 不需要额外的配置文件.&lt;br /&gt;
修改&lt;code&gt;package.json&lt;/code&gt;, 在&lt;code&gt;script&lt;/code&gt;下新增&lt;code&gt;test:unit&lt;/code&gt;命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;scripts&quot;: {
    ...
    &quot;test:unit&quot;: &quot;vitest&quot;,
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;编写测试用例&lt;/h1&gt;
&lt;p&gt;测试一个手机号校验函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /reg/phone.ts */
// 校验是否国内手机号
export function regChinesePhoneNumber(phoneNumber: string) {
  const regex = /^1[3456789]\d{9}$/
  return regex.test(phoneNumber)
}

/** /reg/phone.test.ts */
import { describe, expect, test } from &apos;vitest&apos;
import { regChinesePhoneNumber } from &apos;../phone&apos;

describe(&apos;phone&apos;, () =&amp;gt; {
  test(&apos;phone number belongs to Chinese&apos;, () =&amp;gt; {
    expect(regChinesePhoneNumber(&apos;17725663831&apos;)).toBe(true)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入命令&lt;code&gt;npm run test:unit&lt;/code&gt;, 不出意外你会看到以下内容, 表明测试通过:
&lt;img src=&quot;https://r2.ray-d-song.com/202312071947299.png&quot; alt=&quot;test result&quot; /&gt;&lt;/p&gt;
&lt;p&gt;编写测试的代码文件通常放置在&lt;code&gt;__test__&lt;/code&gt;文件夹中, 命名为&lt;code&gt;xxx.test.ts&lt;/code&gt;或&lt;code&gt;xxx.spec.ts&lt;/code&gt;, Vitest 会自动运行所有此类命名的文件.&lt;br /&gt;
编写测试的基础是&lt;code&gt;describe、test、expect&lt;/code&gt;关键字.&lt;br /&gt;
&lt;code&gt;describe&lt;/code&gt; 用于定义一个测试&lt;code&gt;套件&lt;/code&gt;, 套件类似于 js 的模块, 用于将多个 test 封装到一起, 搭配&lt;code&gt;skip、only&lt;/code&gt;等 api 可以实现选择性测试等功能.&lt;br /&gt;
&lt;code&gt;test&lt;/code&gt; 用于声明一个测试, 接收测试名称和保存测试期望的函数.&lt;br /&gt;
&lt;code&gt;expect&lt;/code&gt; 用于设置断言, Vitest 默认提供基于&lt;code&gt;chai&lt;/code&gt;的断言, 你不知道什么是 chai 没关系, 断言是单元测试的关键助手, 后面会详细介绍&lt;/p&gt;
&lt;h1&gt;实践&lt;/h1&gt;
&lt;p&gt;接下来, 我们对几种常见的场景进行单测&lt;/p&gt;
&lt;h2&gt;hooks&lt;/h2&gt;
&lt;p&gt;hooks 是逻辑的封装, 是最需要单测的场景.&lt;br /&gt;
编辑器和表单有一个常见需求是记录上一次更改时间, 我们可以通过几行代码封装一个 hooks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/hooks/useLastChange.ts */
import { watch, ref, type WatchSource } from &apos;vue&apos;
import moment from &apos;moment/moment&apos;

export function useLastChange(source: WatchSource) {
  const lastChange = ref(&apos;&apos;)

  watch(source, () =&amp;gt; {
    lastChange.value = moment().format(&apos;YYYY-MM-DD HH:mm:ss&apos;)
  })

  return lastChange
}

/** /src/hooks/__test__/useLastChange.test.ts */
import { expect, it } from &apos;vitest&apos;
import { nextTick, ref } from &apos;vue&apos;
import { useLastChange } from &apos;../useLastChange&apos;

it(&apos;update lastChange when source changes&apos;, async () =&amp;gt; {
  const source = ref(&apos;&apos;)
  const lastChange = useLastChange(source)
  const snapShot01 = lastChange.value
  source.value = &apos;Ray&apos;
  await nextTick()
  const snapShot02 = lastChange.value

  expect(snapShot01).not.toBe(snapShot02)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;hooks 的测试比较简单, 就是执行后比对结果. utils 和 reg 也是类似, 此处略过.&lt;/p&gt;
&lt;h2&gt;scaffold&lt;/h2&gt;
&lt;p&gt;脚手架一般是框架自身提供的命令行工具, 用于生成模板代码, 放置于 bin 目录.&lt;br /&gt;
Vue 和 React 自身只提供了创建项目工具, 没有提供类似 Rails Command Line 的项目搭建工具. 因此前端spa项目的脚手架都是自己针对常见业务场景的封装.&lt;br /&gt;
脚手架一般通过 npm 命令执行, 在 package.json 中添加一条命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;create&quot;: &quot;node ./bin/create.js --type=$npm_config_type --path=$npm_config_path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;npm run create --type=table --path=&apos;/Users/ray-d-song/temp/SourceCode/vitest-tutorial-sourcecode/src/views/TestTable.vue&apos; &lt;/code&gt;, 就会执行 /bin/index 脚本中的内容并将 type 和 path 值作为参数传递.&lt;br /&gt;
编写 create 脚本:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/bin/create.js */
import minimist from &apos;minimist&apos;
import { readFileSync, writeFileSync } from &apos;fs&apos;

function main() {
  // 使用 minimist 处理参数
  const args = minimist(process.argv.slice(2))
  const { type, path } = args
  let temp = &apos;&apos;
  if(type === &apos;table&apos;) {
    temp = readFileSync(&apos;./src/bin/dist/table&apos;, &apos;utf-8&apos;) 
  }
  writeFileSync(path, temp)
  console.log(&apos;\n创建完成&apos;)
}

main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚手架其实只进行 3 个操作, 获取参数, 读取对应的模板, 创建目标文件.&lt;br /&gt;
vitest 和 jest 并没有比较好的执行 npm 命令的方法, 因此我们要对上面的方法稍加改造:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/bin/create.js */
import { readFileSync, writeFileSync } from &apos;fs&apos;

function create(args) {
  const { type, path } = args
  let temp = &apos;&apos;
  if(type === &apos;table&apos;) {
    temp = readFileSync(&apos;./src/bin/dist/table&apos;, &apos;utf-8&apos;) 
  } else {
    throw &apos;当前仅支持生成 table 类型&apos;
  }
  writeFileSync(path, temp)
  console.log(&apos;\n创建完成&apos;)
}

export default create

/** /src/bin/index.js */
import create from &apos;./create&apos;

// 使用 minimist 处理参数
const args = minimist(process.argv.slice(2))
try {
  create(args)
} catch(e) {
  console.log(e)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将脚本入口和执行方法分离是为了防止 unit test 过程中自动执行 main 方法报错.&lt;br /&gt;
接下来我们就可以在 test 中导入 create 方法进行测试. 断言可以通过读取生成的模板内容和 table 文件中内容进行比较.&lt;br /&gt;
以下是测试用例, 从参数、生成内容进行测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/bin/__test__/create.test.ts */
import create from &apos;../create&apos;
import { describe, expect, it } from &apos;vitest&apos;
import { readFileSync } from &apos;fs&apos;

describe(&apos;bin scaffold module&apos;, () =&amp;gt; {

  describe(&apos;create table error args&apos;, () =&amp;gt; {
    it(&apos;error temp type&apos;, () =&amp;gt; {
      try {
        create({
          type: &apos;dialog&apos;,
          path: &apos;/Users/ray-d-song/temp/SourceCode/vitest-tutorial-sourcecode/src/bin/__test__/dialog.vue&apos;
        })
      } catch(e) {
        expect(e).toBe(&apos;当前仅支持生成 table 类型&apos;)
        const product = readFileSync(&apos;/Users/ray-d-song/temp/SourceCode/vitest-tutorial-sourcecode/src/bin/__test__/dialog.vue&apos;, &apos;utf-8&apos;)
        expect(product).toBeUndefined
      }
    })
  })

  it(&apos;create table&apos;, () =&amp;gt; {
    create({
      type: &apos;table&apos;,
      path: &apos;/Users/ray-d-song/temp/SourceCode/vitest-tutorial-sourcecode/src/bin/__test__/table.vue&apos;
    })
    const temp = readFileSync(&apos;/Users/ray-d-song/temp/SourceCode/vitest-tutorial-sourcecode/src/bin/dist/table&apos;, &apos;utf-8&apos;)
    const product = readFileSync(&apos;/Users/ray-d-song/temp/SourceCode/vitest-tutorial-sourcecode/src/bin/__test__/table.vue&apos;, &apos;utf-8&apos;)
    expect(temp).toBe(product)
  })

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://r2.ray-d-song.com/202312121120257.png&quot; alt=&quot;result&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;自定义 Vue 指令&lt;/h2&gt;
&lt;p&gt;自定义一个 v-debounce, 用于给按钮增加防抖效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import _ from &apos;lodash&apos;

const vDebounce = {
  beforeMount(el: HTMLElement, binding: {value: () =&amp;gt; void}) {
    el.addEventListener(&apos;click&apos;, _.debounce(binding.value, 600))
  }
}

app.directive(&apos;debounce&apos;, vDebounce)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了测试这个指令, 我们需要模拟&lt;code&gt;组件挂载-&amp;gt;用户点击-&amp;gt;方法调用的过程&lt;/code&gt;, 也就是我们需要调用部分组件测试的能力. &lt;code&gt;@vue/test-utils&lt;/code&gt; 是常用的 Vue 组件测试库.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install @vue/test-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vue-test-utils 兼容 jest 和 Vitest, 安装后可以直接使用. 下面是自定义指令的功能测试:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/utils/__test__/directives.test.ts */
import { describe, expect, it } from &apos;vitest&apos;
import { defineComponent, nextTick, ref } from &apos;vue&apos;
import { mount } from &apos;@vue/test-utils&apos;
import { vDebounce } from &apos;../directives&apos;

// 使用 defineComponent 定义测试组件
// 该 api 除了 template 作为字符串传递外
// 还可以使用 tsx 获得更好的代码提示
const tc = defineComponent({
  setup() {
    const num = ref(0)
    function add() {
      num.value++
    }
    return {
      num,
      add  
    }
  },
  template: &apos;&amp;lt;button v-debounce=&quot;add&quot;&amp;gt;num: {{num}}&amp;lt;/button&amp;gt;&apos;
})

describe(&apos;custom directives&apos;, async () =&amp;gt; {
  // vue-test-utils 的 mount api, 将组件实例化
  it(&apos;debounce submit&apos;, async () =&amp;gt; {
    const wrapper = mount(tc, {
      global: {
        directives: {
          debounce: vDebounce
        }
      }
    })

    // 每过 200 毫秒点击一次 button
    const interval = setInterval(async () =&amp;gt; {
      // find api 查找目标元素
      await wrapper.find(&apos;button&apos;).trigger(&apos;click&apos;)
    }, 200)
    // 阻塞 1000 秒, 让点击事件在 1 秒内执行 4 次
    await new Promise(resolve =&amp;gt; setTimeout(resolve, 1000))

    clearInterval(interval)

    // 阻塞 700 秒, 等待函数执行
    await new Promise(resolve =&amp;gt; setTimeout(resolve, 700))
    // 确保组件重新渲染
    await nextTick()
    // 获取目标元素
    const btn = wrapper.get(&apos;button&apos;)
    // 对目标元素的文本进行断言
    expect(btn.text()).toBe(&apos;num: 1&apos;)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;store&lt;/h2&gt;
&lt;p&gt;大型的前端项目基本都会用到全局的状态管理库, Vue3 最流行的是 Pinia.&lt;br /&gt;
Pinia 提供了可用于测试的 api.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createPinia, setActivePinia } from &apos;pinia&apos;
import { beforeEach, describe, expect, it } from &apos;vitest&apos;
import { useCounterStore } from &apos;../counter&apos;

describe(&apos;Counter Store&apos;, () =&amp;gt; {
  beforeEach(() =&amp;gt; {
    setActivePinia(createPinia())
  })

  it(&apos;increments&apos;, () =&amp;gt; {
    const counter = useCounterStore()
    expect(counter.count).toBe(0)
    counter.increment()
    expect(counter.count).toBe(1)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里引入了一个新的 api: &lt;code&gt;beforeEach&lt;/code&gt;, 这是一种&lt;code&gt;测试生命周期钩子&lt;/code&gt;, 传入的方法在每个测试用例之前执行.&lt;br /&gt;
我们传入了 pinia 的 &lt;code&gt;setActivePinia&lt;/code&gt; api, 这是个 SSR 经常使用的 api, 用户设置一个激活的 Pinia 实例.&lt;/p&gt;
&lt;h1&gt;断言&lt;/h1&gt;
&lt;p&gt;断言是单元测试的核心, 本质一堆用来判断结果的函数库.&lt;br /&gt;
在上面的例子, 我们使用了&lt;code&gt;toBe&lt;/code&gt;断言, 这是最常用的断言, 该断言用于判断值相等或者相同引用, 类似于&lt;code&gt;Object.is&lt;/code&gt;.&lt;br /&gt;
以下为常用的几种断言:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;断言&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;not&lt;/td&gt;
&lt;td&gt;取反&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toBe&lt;/td&gt;
&lt;td&gt;判断值或对象引用是否相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toEqual&lt;/td&gt;
&lt;td&gt;判断对象值是否相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toBeGreaterThan&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toBeGreaterThanOrEqual&lt;/td&gt;
&lt;td&gt;大于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toBeLessThan&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toBeLessThanOrEqual&lt;/td&gt;
&lt;td&gt;小于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;soft&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expect.soft&lt;/code&gt; 遇到某条用例失败也不会停止测试, 直到所有用例执行完毕展示所有错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toBeTypeOf&lt;/td&gt;
&lt;td&gt;断言类型是否相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toBeInstanceOf&lt;/td&gt;
&lt;td&gt;判断是否是目标类的实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toStrictEqual&lt;/td&gt;
&lt;td&gt;严格相等, 会检查具有 undefined 属性的 key、数组稀疏性、对象类型是否相等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toContain&lt;/td&gt;
&lt;td&gt;检查数组元素和子串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toThrowError&lt;/td&gt;
&lt;td&gt;检查函数调用过程中是否报错&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resolves&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expect(func()).resolves.toEqual&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rejects&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expect(func()).rejects.toThrow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;mock&lt;/h1&gt;
&lt;p&gt;为了应对业务耦合, 做到仅测试功能代码, 我们可以使用 mock 工具进行数据模拟.&lt;br /&gt;
最常见的场景是模拟请求, 一般情况下你可以自己用一个 Promise 来模拟请求结果, 但有些情况也许你需要更真实的场景.&lt;br /&gt;
常用的 mock 工具, 例如 APIfox, 是通过接口定义来「启动一个真实的服务器返回假数据」. 而 Vitest 并不会启动真正的 node 服务器, 而是通过&lt;code&gt;mswjs&lt;/code&gt;来拦截对应地址的请求.&lt;br /&gt;
也就是说你可以很方便的去模拟「异常请求」的场景.&lt;br /&gt;
首先安装 msw: &lt;code&gt;pnpm install msw&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;假设我们需要请求&lt;code&gt;https://thorn.mock/test&lt;/code&gt;这个接口, 获取&lt;code&gt;{msg: &apos;hey&apos;}&lt;/code&gt;的 json 数据, 以下是我们的测试用例.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/api/__test__/mock.test.ts */
import { describe, expect, it } from &apos;vitest&apos;

describe(&apos;mock&apos;, async () =&amp;gt; {
  it(&apos;mock api&apos;, async () =&amp;gt; {
    const response = await fetch(&apos;https://thorn.mock/test&apos;) 
    // 使用 toEqual 来比较对象值
    expect(await response.json()).toEqual({
      msg: &apos;hey&apos;
    })
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了 mock 这个接口, 我们需要按照 msw 的语法编写一个 server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/mocks/server.ts */
import { setupServer } from &apos;msw/node&apos;
import { HttpHandler, HttpResponse, http } from &apos;msw&apos;

export const handlers: Array&amp;lt;HttpHandler&amp;gt; = [
  http.get(&apos;https://thorn.mock/test&apos;, () =&amp;gt; {
    return HttpResponse.json({
      msg: &apos;hey&apos;
    })
  })
]

export const server = setupServer(...handlers)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理想状况下, 我们希望每一次测试开始都启动 mock 服务器, 测试结束将服务关掉, Vitest 提供了四个 api 来实现这个过程.&lt;br /&gt;
在 Vitest.config.ts 中, 添加 setupFiles 选项, 该选项接受&lt;code&gt;string|string[]&lt;/code&gt;作为文件路径, 写入的文件每次 Vitest 启动都会自动执行.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defineConfig({
  test: {
    // ...
    setupFiles: &apos;./src/setup.ts&apos;
    // ...
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 setup.ts 文件中, 我们调用 Vitest 钩子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { server } from &apos;./mocks/server&apos;
import { beforeAll, afterAll, afterEach } from &apos;vitest&apos;

// 在每一次测试开始前开启服务器
beforeAll(() =&amp;gt; server.listen({ onUnhandledRequest: &apos;error&apos; }))
// 在每一次测试结束后关闭服务器
afterAll(() =&amp;gt; server.close())
// 在每一个用例结束后重置 handlers
afterEach(() =&amp;gt; server.resetHandlers())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启测试, 会显示测试已通过.&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/202312141529162.png&quot; alt=&quot;unit test pass&quot; /&gt;
请注意, 因为 msw 的作用是拦截请求而不是开启模拟服务器, 因此直接本地调用并不起作用.&lt;/p&gt;
&lt;h1&gt;异步测试&lt;/h1&gt;
&lt;h1&gt;优化测试过程&lt;/h1&gt;
&lt;h2&gt;内联测试&lt;/h2&gt;
&lt;p&gt;就和内联样式表一样, 所谓内联测试就是将测试代码和源码写在一起.&lt;br /&gt;
改造一下正则表达式的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/reg/phone.ts */
export function regChinesePhoneNumber(phoneNumber: string) {
  const regex = /^1[3456789]\d{9}$/
  return regex.test(phoneNumber)
}

// 内联测试应该放在源码的底部
/**
 * 为了解决 ts 报错
 * 需要在 tsconfig.json 中添加
 * {&quot;compilerOptions&quot;: {&quot;types&quot;: [&quot;vitest/importMeta&quot;]}}
 */
if(import.meta.vitest) {
  const { it, expect } = import.meta.vitest
  it(&apos;phone number belongs to Chinese&apos;, () =&amp;gt; {
    expect(regChinesePhoneNumber(&apos;41772566381&apos;)).toBe(false)
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后更新一下&lt;code&gt;vitest.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineConfig({
  test: {
    includeSource: [&apos;src/**/*.{js,ts}&apos;], 
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到新运行的用例增加了一个非&lt;code&gt;test.ts&lt;/code&gt;结尾的文件.&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/202312141609782.png&quot; alt=&quot;in source test&quot; /&gt;
这样做的缺点是会造成打包后的代码体积膨胀, 但我们可以做一些处理让代码块作为不会被执行的区块被 tree-shake 掉.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** vite.config.ts */
export default defineConfig({
  define: {
    &apos;import.meta.vitest&apos;: &apos;undefined&apos;, 
  }, 
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类型测试 (Experimental)&lt;/h2&gt;
&lt;p&gt;ts 的类型在编译后会被擦除, 也就是说无论类型代码写的有多完备, 对真正运行的 js 代码并不会有影响. 但通过类型测试可以促使类型代码更sound, 以此提高 ts 项目的强健性.&lt;br /&gt;
不过总的来说类型测试并不常用, 此处仅介绍一下一般流程.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/types/MPick.d.ts */
// 实现一个自己的 Pick 方法
export type MPick&amp;lt;T, K extends keyof T&amp;gt; = {
  [P in K]: T[P]
}

/** /src/types/__test__/MPick.test-d.ts */

// vitest 会自动扫描 .test-d.ts 命令的文件
import { expectTypeOf, test } from &apos;vitest&apos;
import type { MPick } from &apos;../MPick&apos;

test(&apos;test mock Pick&apos;, () =&amp;gt; {
  interface Foo {
    a: string
    b: number
  }
  type Bar = MPick&amp;lt;Foo, &apos;a&apos;&amp;gt;
  interface Exp {
    a: string
  }
  expectTypeOf&amp;lt;Bar&amp;gt;().toEqualTypeOf&amp;lt;Exp&amp;gt;()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了启动类型测试, 需要在启动时指定 --typecheck, 添加一个新的 npm 命令 &lt;code&gt;&quot;test:type&quot;: &quot;vitest --typecheck&quot;&lt;/code&gt;&lt;br /&gt;
运行&lt;code&gt;pnpm run test:type&lt;/code&gt;运行测试&lt;/p&gt;
&lt;h2&gt;可视化&lt;/h2&gt;
&lt;p&gt;Vitest 提供了 UI 界面进行交互, 运行&lt;code&gt;pnpm install @vitest/ui&lt;/code&gt;安装, &lt;code&gt;pnpm run test:unit --ui&lt;/code&gt; 启动
&lt;img src=&quot;https://r2.ray-d-song.com/202312141744611.png&quot; alt=&quot;ui&quot; /&gt;
里面涵盖了几乎所有的操作, 还可以查看 console、源码和依赖图.
&lt;img src=&quot;https://r2.ray-d-song.com/202312141745142.png&quot; alt=&quot;Graph&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;代码覆盖率&lt;/h2&gt;
&lt;p&gt;Vitest 通过 V8 提供代码覆盖率检查, 运行&lt;code&gt;pnpm install @vitest/coverage-v8&lt;/code&gt;安装&lt;br /&gt;
在 vitest.config 中配置覆盖率选项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defineConfig({
  test: {
    coverage: {
      enabled: true,
      reporter: [&apos;html&apos;]
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以在 ui 界面上看到代码覆盖率结果了&lt;br /&gt;
&lt;img src=&quot;https://r2.ray-d-song.com/202312141752020.png&quot; alt=&quot;coverage ui&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>cloudflare worker/D1 初探</title><link>https://ray-d-song.com/blog/cloudflare-d1</link><guid isPermaLink="true">https://ray-d-song.com/blog/cloudflare-d1</guid><pubDate>Sat, 18 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;创建项目&lt;/h1&gt;
&lt;p&gt;如果是第一次使用 worker 服务, 需要登录一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx wrangler login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过命令行交互创建一个名为 event-tracking 的项目&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm create cloudflare@latest event-tracking
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目创建完成后有两个关键的文件, &lt;code&gt;src/index.ts&lt;/code&gt; 是 worker 的入口, &lt;code&gt;wrangler.toml&lt;/code&gt; 是总的配置文件.&lt;br /&gt;
进入 dev01 目录, 执行命令创建数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx wrangler d1 create event-tracking-db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的命令会生成类似如下内容, 粘贴到&lt;code&gt;wrangler.toml&lt;/code&gt;中即可配置项目的数据库连接&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[d1_databases]]
binding = &quot;DB&quot;
database_name = &quot;event-tracking-db&quot;
database_id = &quot;xxxxxxxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxxx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在项目的根目录下创建 sql/schema.sql 文件用于初始化数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE IF EXISTS view_tracking;

CREATE TABLE IF NOT EXISTS view_tracking
(article_id CHAR(13) PRIMARY KEY, view_count INT);

INSERT INTO view_tracking (article_id, view_count) values (&apos;1692667782462&apos;, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行以下命令在本地创建数据库进行测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx wrangler d1 execute event-tracking-db --local --file=./sql/schema.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所谓本地数据库其实是&lt;code&gt;.wrangler/state/d1/miniflare-D1DatabaseObject&lt;/code&gt;文件夹下创建了一个 sqlite.&lt;br /&gt;
所以你可以用自己的数据库软件比如 datagrip 验证操作是否成功.&lt;/p&gt;
&lt;h1&gt;worker 操作数据库&lt;/h1&gt;
&lt;p&gt;我的需求是每次用户访问文章, 前端将文章拼接在路径参数中请求后端, 后端为文章的阅读量 + 1 后返回阅读量给前端.&lt;br /&gt;
修改我们的 worker 文件, 先完成获取 articleId 并将其原样返回的步骤.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise&amp;lt;Response&amp;gt; {
    const articleId = request.url.split(&apos;/&apos;).pop()

		return new Response(articleId)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;单测&lt;/h2&gt;
&lt;p&gt;接下来我们对这个简单的 worker 运行测试.&lt;br /&gt;
我这里选择使用 Vitest, 它和 TS 的集成度更高且性能更好. 你可以通过我这篇文章来了解它&lt;a href=&quot;https://ray-d-song.com/post/1701870426245&quot;&gt;在 Vue 项目中使用 Vitest&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { unstable_dev, type UnstableDevWorker } from &apos;wrangler&apos;
import { describe, beforeAll, afterAll, it, expect } from &apos;vitest&apos;

describe(&apos;Event Tracking&apos;, () =&amp;gt; {
  let worker: UnstableDevWorker

  beforeAll(async () =&amp;gt; {
    worker = await unstable_dev(&apos;src/index.ts&apos;, {
      experimental: { disableExperimentalWarning: true }
    })
  })

  afterAll(async () =&amp;gt; {
    await worker?.stop()
  })

  it(&apos;should return path param&apos;, async () =&amp;gt; {
    const response = await worker.fetch(&apos;https://thorn.com/post/1701870426245&apos;)
    const str = await response.text()
    expect(str).toBe(&apos;1701870426245&apos;)
  })

})
/**
 *  Test Files  1 passed (1)
 *  Tests  1 passed (1)
 *  Start at  10:59:31
 *  Duration  524ms
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询&lt;/h2&gt;
&lt;p&gt;在创建数据库时, 我们插入了初试数据, 利用这个数据进行查询测试:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** index.ts */
export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise&amp;lt;Response&amp;gt; {
    const articleId = request.url.split(&apos;/&apos;).pop()

    const result = await env.DB.prepare(
      &apos;SELECT view_count FROM view_tracking WHERE article_id = ?&apos;
    )
      .bind(articleId)
      .first()
    
		return new Response(String(result===null?0:result[&apos;view_count&apos;]))
	}
}

/** index.test.ts */

// 将第一个用例标记为 skit, 跳过该测试
it.skip(&apos;should return path param&apos;, async () =&amp;gt; {
  const response = await worker.fetch(&apos;https://thorn.com/post/1701870426245&apos;)
  const str = await response.text()
  expect(str).toBe(&apos;1701870426245&apos;)
})

it(&quot;should return &apos;1&apos;&quot;, async () =&amp;gt; {
  const response = await worker.fetch(&apos;https://thorn.com/post/1692667782462&apos;)
  const str = await response.text()
  expect(str).toBe(&apos;1&apos;)
})

/**
 *  ✓ src/index.test.ts (2)
 *  ✓ Event Tracking (2)
 *    ↓ should return path param [skipped]
 *    ✓ should return &apos;1&apos;
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更新&lt;/h2&gt;
&lt;p&gt;我们继续在此基础上修改, 在查询数据前, 我们需要为该数据的阅读数 + 1.&lt;br /&gt;
该连续操作并不需要强一致, 因此只需要在之前的操作前再加一条更新操作即可. D1 提供了自己的批处理语法.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** index.ts */
export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise&amp;lt;Response&amp;gt; {
    const articleId = request.url.split(&apos;/&apos;).pop()

    const db = env.DB
    const batchRes = await db.batch([
      db.prepare(&apos;UPDATE view_tracking SET view_count=view_count+1 WHERE article_id = ?&apos;).bind(articleId),
      db.prepare(&apos;SELECT view_count FROM view_tracking WHERE article_id = ?&apos;).bind(articleId)
    ])

    const viewCountVal = batchRes[1]===null ? 0 : (batchRes[1].results[0] as ViewCountObj)[&apos;view_count&apos;]
		return new Response(String(viewCountVal))
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了测试每次请求阅读数是否真的+1, 本来应该提供一个新的只读接口, 但为了偷懒我们可以打印每次的结果, 自己观察一下即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** index.test.ts */
it(&quot;should return new view_count&quot;, async () =&amp;gt; {
  const response = await worker.fetch(&apos;https://thorn.com/post/1692667782462&apos;)
  const str = await response.text()
  console.log(str)
  expect(str).toBeTypeOf(&apos;string&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里该接口的编写基本完成.&lt;/p&gt;
&lt;h2&gt;写入&lt;/h2&gt;
&lt;p&gt;为了每次发布新的文章都自动插入数据, 我们还需要另一个插入数据接口.&lt;br /&gt;
博客每次执行发布的 actions, 就调用该接口将新的 article_id 写入数据库.&lt;br /&gt;
每个 worker 仅能完成一个工作, 需要的接口就需要创建新的 worker. 为了偷懒我们对之前的 worker 进行改造, 根据倒数第二个参数判断操作类型, 最后一个路径参数作为 article_id.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/index.ts */
import createHandler from &apos;./handlers/create&apos;
import readHandler from &apos;./handlers/read&apos;

export interface Env {
  DB: D1Database
}

export default {
  async fetch(request: Request, env: Env): Promise&amp;lt;Response&amp;gt; {
    try {
      const url = request.url.split(&apos;/&apos;)
      const articleId = url.pop()
      const handleType = url.pop()
      if(articleId?.length !== 13) {
        throw { code: 400, msg: &apos;err param&apos; }
      }
      switch (handleType){
        case &apos;read&apos;:
          return readHandler(articleId, env)
        case &apos;create&apos;:
          return createHandler(articleId, env)
        default:
          throw { code: 400, msg: &apos;err handle type&apos; }
      }
    } catch(e: any) {
      return new Response(e.msg, {status: e.code})
    }
  }
}

/** /src/handlers/create.ts */
import { Env } from &apos;..&apos;;

export default async function createHandler(articleId: string, env: Env) {
  const db = env.DB
  const execRes = await db.prepare(&apos;INSERT INTO view_tracking (article_id, view_count) values (?, 1)&apos;)
    .bind(articleId)
    .run()
  if(execRes.error) {
    throw { code: 500, msg: &apos;err exec&apos;}
  }
  return new Response(null, {status: 200})
}

/** /src/handlers/read.ts */
import { Env } from &apos;..&apos;

interface ViewCountObj {
  view_count: number
}

export default async function readHandler(articleId: string, env: Env): Promise&amp;lt;Response&amp;gt; {
  const db = env.DB
  const batchRes = await db.batch([
    db.prepare(&apos;UPDATE view_tracking SET view_count=view_count+1 WHERE article_id = ?&apos;).bind(articleId),
    db.prepare(&apos;SELECT view_count FROM view_tracking WHERE article_id = ?&apos;).bind(articleId)
  ])

  const viewCountVal = batchRes[1]===null ? 0 : (batchRes[1].results[0] as ViewCountObj)[&apos;view_count&apos;]
  return new Response(String(viewCountVal))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的单测文件如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** /src/__test__/index.test.ts */
import { unstable_dev, type UnstableDevWorker } from &apos;wrangler&apos;
import { describe, beforeAll, afterAll, it, expect } from &apos;vitest&apos;

describe(&apos;Event Tracking&apos;, () =&amp;gt; {
  let worker: UnstableDevWorker

  beforeAll(async () =&amp;gt; {
    worker = await unstable_dev(&apos;src/index.ts&apos;, {
      experimental: { disableExperimentalWarning: true }
    })
  })

  afterAll(async () =&amp;gt; {
    await worker?.stop()
  })

  // ...

  it(&apos;should return 200&apos;, async () =&amp;gt; {
    const response = await worker.fetch(&apos;https://thorn.com/create/1701870426247&apos;)
    const code = response.status
    console.log(await response.text())
    expect(code).toBe(200)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;部署&lt;/h1&gt;
&lt;p&gt;首先将数据库创建 sql文件发布&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx wrangler d1 execute event-tracking-db --file=./sql/schema.sql 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证是否成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx wrangler d1 execute event-tracking-db --command=&quot;SELECT * FROM view_tracking&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不出意外会出现这玩意
&lt;img src=&quot;https://r2.ray-d-song.com/202312151552033.png&quot; alt=&quot;result image&quot; /&gt;
最后发布你的 worker&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx wrangler deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功提示
&lt;img src=&quot;https://r2.ray-d-song.com/202312151618252.png&quot; alt=&quot;success&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本博客为服务端渲染, 客户端不会直接调用 worker 接口. 如果你的 app 是 spa, 请记得添加一些防刷操作和入参判断拦截.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>TailwindCSS 的价值</title><link>https://ray-d-song.com/blog/value-of-tailwind</link><guid isPermaLink="true">https://ray-d-song.com/blog/value-of-tailwind</guid><pubDate>Wed, 11 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;tailwind 一直饱受非议, 其中说的最多的大概就是「我为什么不直接写内联样式?」.
其实 tailwind 不仅是简写了样式名这么简单, 它是一系列便利的封装.&lt;/p&gt;
&lt;h1&gt;动画&lt;/h1&gt;
&lt;p&gt;举个简单的例子, &lt;code&gt;animate-spin&lt;/code&gt; 属性用于添加旋转动画, 常用在 loading 中. 如果你用传统的 css/sass 编写, 那你至少需要以下代码.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;animation: spin 1s linear infinite;

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;交互&lt;/h1&gt;
&lt;p&gt;如果你希望一个 button 在鼠标炫富时颜色变浅, 在 tailwind 中只需要简单的&lt;code&gt;hover:opacity-90&lt;/code&gt;, 传统的 css 则需要:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.btn:hover {
  background-color: rga(xxx,xxx,xxx, 0.9)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;响应式设计&lt;/h1&gt;
&lt;p&gt;同时现代很多网页需要同时满足大屏设备和移动小屏设备的适配, 一般我们会用 css3 的 @media 来根据窗口尺寸编写不同的 css.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (max-width: 768px) {
  body {
    width: 80%
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 tailwind 上, 适配 768px 宽度的设备, 你只要在样式前加入 &lt;code&gt;md&lt;/code&gt; 前缀即可.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body class=&quot;md:w-4/5&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;利好无设计开发&lt;/h1&gt;
&lt;p&gt;个人或者小团队开发产品, 没有专业的设计师出图, 写组件相当痛苦.
例如 box-shadow 属性, button、card、modal, 每个都需要不一样的 shadow 样式. 在自己编写的时候经常要重复修改多次来达到一个比较好的效果, twc 提供了 7 个不同规格的选项, 都是泛用性非常高的选择.
现在你要写一个现代化外观的按钮只需要&lt;code&gt;class=&quot;shadow-md rounded-lg&quot;&lt;/code&gt;就行了.&lt;/p&gt;
&lt;h1&gt;新的组件模式&lt;/h1&gt;
&lt;p&gt;tailwind 还有个衍生品, Headless UI. 将组件的功能和样式彻底分离. 很多时候组件的功能是相同的 , 但是需要不同的样式, 有时候样子一样的组件却需要完全不同的 api. Headless 就是为了解决这个问题.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NuxtLab UI 就是一个无头tailwind组件库, 闲得蛋疼可以看看, 代码很干净.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;class 也太特么长了&lt;/h1&gt;
&lt;p&gt;这是一个由 tailwind 编写的 checkbox, 做了动效和深色适配.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;checkbox&quot; class=&quot;w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到这个长度是不是蚌埠住了. 没关系 tailwind 有&lt;code&gt;@layer&lt;/code&gt;和&lt;code&gt;@apply&lt;/code&gt;, 你可以像组织传统 css 一样去组织 tailwind, 封装自己的样式组件.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@layer components {
  .checkbox {
    @apply
    w-4
    h-4
    text-blue-600
    bg-gray-100
    border-gray-300
    rounded
    focus:ring-blue-500
    dark:focus:ring-blue-600
    dark:ring-offset-gray-800
    focus:ring-2
    dark:bg-gray-700
    dark:border-gray-600
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;过长的样式导致的问题是无法避免的, 即便你直接写内联也是一样, 最终一定会回到封装样式组件的路上. 但之前所有的好处(抽象、减少代码量、响应式...)都还在.
这也是一种「不过早抽象」的价值. 一个小的 idea, 你可以一把梭抢先实现原型, 后期再抽离维护, tailwind 降低了这一操作的难度.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然, 这种种好处的前提是, 要么你写风格多样化的 C 端界面, 要么需要创建属于自己团队的组件库. 然而很明显, 大多数公司还停留在&lt;code&gt;element antd&lt;/code&gt;一把梭的阶段. 但这当然不是 tailwind 的问题.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上海外 &lt;code&gt;3T架构(Next tRPC Tailwind)&lt;/code&gt; 已经火了有一阵了, 当然你也可以说这是前端瞎折腾, 但这确实是写出「现代、好看、交互强的界面」的捷径.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>协程(Coroutine)和纤程(Fiber)</title><link>https://ray-d-song.com/blog/coroutine-and-fiber</link><guid isPermaLink="true">https://ray-d-song.com/blog/coroutine-and-fiber</guid><pubDate>Wed, 13 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在看 C++ 引入 Fiber 的&lt;a href=&quot;https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf&quot;&gt;N4024文档: 区分纤程和协程&lt;/a&gt;, 文章给纤程和协程十分明确的区分. 但过去我看过的很多资料会将其混为一谈或者模糊二者的边界, 所以写了这篇博客来总结一下.&lt;/p&gt;
&lt;h1&gt;纤程&lt;/h1&gt;
&lt;p&gt;线程是一种轻量级的线程, 本质是对线程时间进行切片处理, 调度也由用户进行. 最早是 Microsoft 为了解决 Unix 平台的引用程序移植到 Windows 上时出现的问题而发起的提案.
因此, 纤程是一种偏底层的概念, 通常由操作系统或runtime自动管理, 并不需要程序员手动干预. 例如 Windows 上的纤程就是在其内核上实现.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;值得注意的是, php8 引入的 Fiber 虽然叫做纤程, 其本质是协程.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个线程可以包含多个纤程, 纤程的好处是可以有效防止长时间的线程挂起. 例如在 IO 操作时, 实例化一个纤程进行 IO 操作, 在该纤程阻塞的过程中, 因为仅占用了一部分的时间切片, 程序依旧可以继续执行.&lt;/p&gt;
&lt;h1&gt;协程&lt;/h1&gt;
&lt;p&gt;协程的核心就是协作, 通过明确的挂起和恢复来控制执行的流程，使得不同任务之间可以更好地协同工作，共享信息，避免竞态条件，提高性能.
协程在许多语言中都有实现, 例如 &lt;code&gt;Lua、Go、Ruby、Java(Virtual Threads)&lt;/code&gt;, &lt;code&gt;Python 和 JavaScript(基于生成器)&lt;/code&gt;.
如果要用一句话来总结协程: &lt;code&gt;控制流的主动让出和恢复&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;有栈协程和无栈协程&lt;/h2&gt;
&lt;p&gt;协程有很多种, 按照多个协程之间是否存在调用栈, 可以分为&lt;code&gt;有栈协程(如goroutine、lua协程)&lt;/code&gt;和&lt;code&gt;无栈协程(如JavaScript、Dart、Python)&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;调用栈的作用是保存协程挂起时的状态.
有栈协程在恢复时会将其上下文从栈内存中捞出恢复到系统栈中.
无栈协程的挂起和恢复则依赖闭包和状态机实现.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;对称协程和非对称协程&lt;/h2&gt;
&lt;p&gt;非对称协程提供&lt;code&gt;两种控制操作&lt;/code&gt;, &lt;code&gt;调用协程&lt;/code&gt;和&lt;code&gt;挂起协程&lt;/code&gt;. 非对称协程可以看做是其调用者的从属, 跟特定调用者绑定, 在让出控制权时, 只能回到原调用者.
对称协程提供&lt;code&gt;单个控制操作&lt;/code&gt;, 可以通过该操作在协程之间显示的传递控制权. 对称协程在启动后和原调用者就没什么关系了, 也因此, 对称协程需要一个调度器去选择转移控制权操作的目标协程.&lt;/p&gt;
&lt;h3&gt;对称&lt;/h3&gt;
&lt;p&gt;对称协程的例子非常少, 以下是 C++ &lt;code&gt;Boost.Coroutine&lt;/code&gt; 的示例.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;coroutine&amp;gt;

struct CountingCoroutine {
    struct promise_type {
        int current_value = 0;

        CountingCoroutine get_return_object() {
            return CountingCoroutine(std::coroutine_handle&amp;lt;promise_type&amp;gt;::from_promise(*this));
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
    };

    std::coroutine_handle&amp;lt;promise_type&amp;gt; coroutine;

    CountingCoroutine(std::coroutine_handle&amp;lt;promise_type&amp;gt; handle) : coroutine(handle) {}

    ~CountingCoroutine() {
        if (coroutine)
            coroutine.destroy();
    }

    int getValue() const {
        return coroutine.promise().current_value;
    }

    void resume() {
        coroutine.resume();
    }
};

CountingCoroutine generateNumbers() {
    for (int i = 0; i &amp;lt; 10; ++i) {
        //移交控制权
        co_yield i;
    }
}

int main() {
    CountingCoroutine counter = generateNumbers();

    while (counter.coroutine) {
        std::cout &amp;lt;&amp;lt; &quot;Value: &quot; &amp;lt;&amp;lt; counter.getValue() &amp;lt;&amp;lt; std::endl;
        //回到 co_yield 继续执行
        counter.resume();
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Go 语言中, 我们可以通过 chan 在没有特定从属关系的情况下完成协程间控制权的转移, 也算是对称协程的实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    var wg sync.WaitGroup

    wg.Add(1)
    go func() { // &amp;lt;--- 1
        defer wg.Done()
        for val := range ch1 {
            fmt.Println(val)
        }
    }()

    wg.Add(1)
    go func() { // &amp;lt;--- 2
        defer wg.Done()
        for val := range ch2 {
            ch1 &amp;lt;- val
        }
        close(ch1)
    }()

    wg.Add(1)
    go func() { // &amp;lt;--- 3
        defer wg.Done()
        for i := 1; i &amp;lt;= 5; i++ {
            ch2 &amp;lt;- i
        }
        close(ch2)
    }()

    wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果按照代码的顺序执行, 那正确的排列顺序是 3 2 1, 但在此处由于channel的阻塞特性, 第一个goroutine会等待第二个goroutine将数据从 ch2 传输到 ch1, 而第二个goroutine会等待第三个goroutine将数据发送到 ch2, 这就保证了它们的执行顺序. 最后，当所有goroutine都完成并且WaitGroup计数为0时, wg.Wait() 返回, 程序退出.&lt;/p&gt;
&lt;h3&gt;非对称&lt;/h3&gt;
&lt;p&gt;JavaScript 的 async、await、promise 则是创建非对称协程的工具.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// async 标记的函数会创建协程
const useFetch = async () =&amp;gt; { 
  // 执行到 await 处会将该协程挂起, 直到 fetch 返回
  const res = await fetch(&apos;https://www.v2ex.com/api/topics/hot.json&apos;)
  return res.json()
}

const main = () =&amp;gt; {
  console.log(&apos;main start&apos;)
  // 执行到此处, 控制权转移到协程
  useFetch().then(res =&amp;gt; { 
    // 执行到函数内部 await 处, 暂时挂起, 控制权返回到 main
    console.log(`fetch res: ${res}`)
  })
  console.log(&apos;main end&apos;)
}

main()
/**
 * main start
 * main end
 * fetch res: ...
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到, 在 JS 中, 协程转移的控制权一定会返回到协程的调用者上.&lt;/p&gt;
&lt;h1&gt;异同&lt;/h1&gt;
&lt;p&gt;看到这里, 会发现纤程和协程极为相似.
没错, 大框架上两者的基本概念是一样的. 仅有的区别是纤程的状态保存由操作系统API提供, 而协程保存和恢复的方式由语言或库提供, 背后的实现更是则多种多样.
所以我们可以认为: 纤程实际上就是由操作系统提供的协程.
参考&lt;a href=&quot;https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf&quot;&gt;Distinguishing coroutines and fibers&lt;/a&gt;, 以下是一些细节的差别:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fiber 在发起后不再依赖于发起它的程序存在, 可以拥有独立的生命周期.
Coroutine 作为发起者的「子程序」, 不存在独立的生命周期.&lt;/li&gt;
&lt;li&gt;Fiber 和 Thread 行为模式基本一致. 存在一个调度器, 某个 Fiber 被阻塞, 会将控制权移交至调度器, 由调度器去唤起其他准备运行的 Fiber.
对称协程的行为方式和 Fiber 基本一致. 非对称协程不存在调度器, 控制权会回到发起者.&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>单线程并发</title><link>https://ray-d-song.com/blog/single-thread-concurrency</link><guid isPermaLink="true">https://ray-d-song.com/blog/single-thread-concurrency</guid><pubDate>Thu, 17 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文封面和思路来源于 Ruby China 2021 By 东仙队长的分享: &lt;a href=&quot;https://www.bilibili.com/video/BV1h3411v7kq/?spm_id_from=333.999.0.0&amp;amp;vd_source=371668a779fa4a755fcbf62901a22d71&quot;&gt;Ruby 高并发编程指北&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;并发&lt;/h1&gt;
&lt;p&gt;假设有一个处理器, 因为只有一个核心, 所以同时他只能运行一个进程, 又因为一个进程上同一时间只能运行一个线程(task). 所以你可以认为, 同一时间, 这颗处理器只能处理一个任务.&lt;/p&gt;
&lt;p&gt;操作系统会将&lt;code&gt;cpu 使用时间&lt;/code&gt;这一资源进行切分, 这就是&lt;code&gt;时间分片&lt;/code&gt;. 划分好的资源会按照调度算法分配给各个线程去使用. 进程内线程切换大概就是下面这种感觉:
&lt;img src=&quot;https://r2.ray-d-song.com/2024/02/1674c2a68a1f5732789d854fce7ce515.png&quot; alt=&quot;线程调度&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为现代计算机性能足够强劲, 所以使用者体感上是 task_1、2、3 在同时运行, 这就是并发.&lt;/p&gt;
&lt;p&gt;我并不想用&lt;code&gt;「多个任务同时发生」&lt;/code&gt;这种模糊的表达, 在我看来&lt;code&gt;「并发」&lt;/code&gt;之所以被称为&lt;code&gt;「并发」&lt;/code&gt;单纯是翻译后中文表达的问题.&lt;br /&gt;
&lt;code&gt;无论如何, 同时确实只有一个任务在进行&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;并行&lt;/h1&gt;
&lt;p&gt;并行就要好理解的多, 你有一个多核心处理器, 例如拥有两个核心的老古董E6600. 因为有两个核心, 同时能运行两个进程, 每个进程可以运行一个线程. 因此, 你的电脑就能真正的在同一时间运行两个任务.&lt;br /&gt;
大概就是下面这种感觉:
&lt;img src=&quot;https://r2.ray-d-song.com/2024/02/dca335668b68d1e7e0c5f0071e9ccc23.png&quot; alt=&quot;并行&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;程序设计&lt;/h1&gt;
&lt;p&gt;之前刷知乎时, 看到这样一种说法:&lt;code&gt;「你的程序首先必须支持并发, 才能支持并行」&lt;/code&gt;&lt;br /&gt;
因为并发不仅是对程序执行状态的一种描述, 也是一种程序设计的方法论:&lt;br /&gt;
设计并发程序的条件就是&lt;code&gt;「程序的执行不依赖先后顺序、不依赖精确时序」&lt;/code&gt;.&lt;br /&gt;
而一段不依赖先后顺序、不依赖精确时序的程序, 自然可以拆分成多段, 在多个线程上运行. 这多个线程又可以运行在不同的进程之上, 进而实现&lt;code&gt;并行&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;首先我们来看看最常见的 web 应用场景, 从接收到用户请求到返回, 需要这些步骤:
&lt;img src=&quot;https://r2.ray-d-song.com/202309092334861.png&quot; alt=&quot;web app&quot; /&gt;
其中&lt;code&gt;DB operation&lt;/code&gt;占据了最大的空间, 因为数据库操作确实是最耗时的操作.&lt;/p&gt;
&lt;h1&gt;多线程并发&lt;/h1&gt;
&lt;p&gt;我们来假设一种极端的情况, 如果一直只有一个线程在运行, 那么运行过程就会变成这样:
&lt;img src=&quot;https://r2.ray-d-song.com/202309092339287.png&quot; alt=&quot;单线程&quot; /&gt;
如果是这样的话, 我想我们这台单核的机器没几个用户就 burn out 了.
应对这种情况, 最简单的思路就是多开几个线程, 虽然我们只有一个进程, 但当第一个线程卡在数据库操作时, 可以将控制权转移到其他线程进行操作.
&lt;img src=&quot;https://r2.ray-d-song.com/202309092351360.png&quot; alt=&quot;多线程&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;协程&lt;/h1&gt;
&lt;p&gt;多线程并发, 一切看起来都很美好, 但有两个问题, 一个是计算机能启动的线程数量是有限的, 还有就是线程的切换有着客观的成本.
那么如果我们可以自己在程序中&lt;code&gt;模拟线程&lt;/code&gt;, 切换的不再是线程, 而是执行的程序方法. 岂不是可以用极低的成本去并发?
这个模拟线程, 或者说&lt;code&gt;轻量线程/用户线程&lt;/code&gt;, 就是协程.
有了协程, 我们可以更细粒度的控制程序的执行. 以下是 node.js 协程(async异步)的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function App(request, response) {
  const id = request.query.id // &amp;lt;- read req and parsing
  try {
    const result = await DB.find({ id }) // &amp;lt;- DB operation
    res.json({ // generate json and response
      code: 200,
      data: result
    })
  } catch(e) {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当程序执行到&lt;code&gt;await&lt;/code&gt;标识的数据库操作时, 就会在当前方法内形成阻塞, 等待数据库的返回结果. 但对于调用 App() 的方法来说, 程序则会继续向下执行(处理其他的请求).&lt;/p&gt;
&lt;p&gt;整个过程的简略图大概如下:
&lt;img src=&quot;https://r2.ray-d-song.com/202309101809693.png&quot; alt=&quot;async&quot; /&gt;&lt;/p&gt;
&lt;p&gt;关于协程更具体的介绍, 可以看我的这篇文章&lt;a href=&quot;https://ray-d-song.com/post/1692667782462&quot;&gt;协程(Coroutine)和纤程(Fiber)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;实际上这种&lt;code&gt;单线程+协程&lt;/code&gt;并发模型, 就是 Node.js 的并发模型.
这几年协程的火爆, jdk21、php8 都支持了协程方案. 足以提现这种模式的优势.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说到这里, 有没有什么比单线程异步并发更强的方案呢?
那就是&lt;code&gt;多线程+协程&lt;/code&gt;, Go 就是这样的方案. Go 实现了一种更复杂的 Goroutine 到实际线程资源的映射, 使其可以以极低的配置达到恐怖的并发性能&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item></channel></rss>