Skip to main content

Think In Headless

· 3 min read
marvin-season
Maintainer of Docusaurus

What

一种快速组装UI的模式

Comparing In Dev A Closable TodoCard

传统流程

  • 定义组件 TodoCard
  • 实现UI
  • 注册事件
  • 维护状态
function TodoCard() {
return (
<div>
<div>header</div>
<button onClose={() => {}}>X</button>
<div>
<div>content1</div>
<div>content2</div>
</div>
</div>
);
}

无头组件开发流程

实际上初次看到这种代码是抵触的,比如 headlessui, shadcn, Radix等等

可以看到从写法上Headless的开发代码似乎很臃肿, 为了实现一个简单的组件,往往需要搭很多积木

但是牛就牛在他的设计哲学,传统的方式预定义样式,然后勾入功能业务逻辑,进而实现完整的组件 但是headless相反, 预定义功能逻辑,然后在使用的时候注入样式 后者天然支持主题定制,这是对SoC的践行(最少知道,高内聚,低耦合那一套)

"use client";

import { createElement } from "react";

export function TodoHeader({
children,
className,
as = "div",
}: {
children: React.ReactNode;
className?: string;
as?: React.ElementType;
}) {
return createElement(as, { className }, children);
}

export function TodoCardContainer({
children,
className,
as = "div",
}: {
children: React.ReactNode;
className?: string;
as?: React.ElementType;
}) {
return createElement(as, { className }, children);
}

export function TodoCardContent({
children,
className,
as = "div",
}: {
children: React.ReactNode;
className?: string;
as?: React.ElementType;
}) {
return createElement(as, { className }, children);
}

/**
* 二进制位码说明:
* 000 不需要权限也不需要确认
* 001 需要确认是否删除
* 010 需要权限
*/
const CODE = {
NOTHING: 0b000,
NEED_CONFIRM: 0b001,
NEED_AUTH: 0b010,
} as const;

type CodeType = (typeof CODE)[keyof typeof CODE];

interface TodoCardCloseButtonProps {
children: React.ReactNode;
className?: string;
as?: React.ElementType;
codeNumber?: CodeType;
onClick?: () => void;
}

export function TodoCardCloseButton({
children,
className,
as = "button",
onClick,
codeNumber = CODE.NEED_CONFIRM,
}: TodoCardCloseButtonProps) {
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (codeNumber & CODE.NEED_AUTH) {
alert("需要权限");
return;
}
if (codeNumber & CODE.NEED_CONFIRM) {
if (!confirm("需要确认是否删除")) return;
}
onClick?.();
};

return createElement(as, { className, onClick: handleClick }, children);
}

上述是一个headless的组件,在使用的时候,自己去组合拼装即可

<div className="flex gap-4">
{todos.map((todo) => (
<TodoCardContainer
className="border border-green-500 rounded-lg px-2"
as="div"
key={todo.id}
>
<TodoHeader className="flex justify-between">
<div>{todo.title}</div>
<TodoCardCloseButton
className="text-red-500"
onClick={() =>
setTodos(todos.filter((t) => t.id !== todo.id))
}
>
close
</TodoCardCloseButton>
</TodoHeader>
<TodoCardContent>
<div>{todo.description}</div>
<div>{todo.dueDate}</div>
<div>{todo.priority}</div>
</TodoCardContent>
</TodoCardContainer>
))}
</div>

Conclude

可以把 Headless 组件看作是一种 “逻辑控制抽象 + UI 留给调用方” 的 组合式组件开发方式

useSyncExternalStore

· One min read

useSyncExternalStore 是 React 18 引入的一个 Hook,专门用于订阅外部可变状态(external store),并确保 一致性和并发安全,特别适用于状态库或响应式框架的桥接。


🧠 为什么需要 useSyncExternalStore?

React 的并发渲染模式(Concurrent Mode)可能导致不一致的订阅行为。传统的 useEffect + useState 容易出现数据“撕裂”(tearing)现象。

useSyncExternalStore 提供了一个官方推荐的订阅外部状态方案,具备以下特性:

  • 保证在渲染期间获取最新快照
  • 在服务端渲染(SSR)中也能安全使用

深度思考扩展

· 3 min read
marvin-season
Maintainer of Docusaurus

S

DeepSeek 的爆火带来的深度思考模式,需要在已有流式对话中实现【前端】

T

在原有的对话基础上加入深度思考的交互,并支持不同的思考风格

A

解析深度思考内容,开发新的深度思考组件,尽可能不改变原有组件实现深度思考的功能扩展,=> HOC正是这一思想

R

扩展原有组件,实现深度思考功能

Concrete

原有组件

function Content({content}) {
return <div>{content}</div>
}

目标组件

错误的方式:直接修改原组件

function ContentWithThink({content, think}) {
return <div>
<div>{think}</div>
<div>{content}</div>
</div>
}

推荐的方式:HOC


const withThink = <P extends object>(
Component: ComponentType<P>,
ThinkComponent: FunctionComponent<ThinkContentStyleProps>) =>
{
return (props: P & { content: string }) => {
const {
content,
think_content,
closedMatch,
openMatch
} = parseThinkContent(props.content)

return (
<>
<Think
closedMatch={!!closedMatch}
openMatch={!!openMatch}
think_content={think_content}
ThinkComponent={ThinkComponent} />
<Component {...props} content={content} />
</>
)
}
}

export const ContentWithThink = memo(withThink(Content, ThinkContentStyle), (prev, next) => {
return prev.content === next.content
})

withThink是一个HOC组件,用于扩展传入的Content组件,其中ThinkContentStyle为配置思考组件的风格提供了入口, HOC中的Think组件则定一个思考组件的逻辑以及布局

const Think = ({ closedMatch, openMatch, think_content, ThinkComponent }: ThinkProps) => {
const [status, setStatus] = useState<ThinkStatus>(ThinkStatus.completed)

const match = useMemo(() => {
return openMatch || closedMatch
}, [openMatch, closedMatch])

useEffect(() => {
if (openMatch) {
setStatus(ThinkStatus.thinking)
}
}, [openMatch])

useEffect(() => {
if (closedMatch) {
setStatus(ThinkStatus.completed)
}
}, [closedMatch])

useEffect(() => {
EE.on(ThinkEvent, ({ thinkStatus }: { thinkStatus: ThinkStatus; }) => {
// 完整匹配到了
if (closedMatch) {
setStatus(ThinkStatus.completed)
return
}
setStatus(thinkStatus)
})
return () => {
EE.off(ThinkEvent)
}
}, [status])

return <>{match && <ThinkComponent think_content={think_content} status={status} />}</>
}

How To Use

直接替换原来的组件为高阶组件

() => <ContentWithThink content={content} className={className}/>

Recap

  • 使用高阶组件扩展了业务功能,尽可能的没有操作原有的代码
  • 将新功能全部聚合在HOC中

Website Deploy

· 3 min read

Static Website

借助Nginx来完成部署静态站点。

  • 编写docker-compose
  • 配置 nginx
  • docker-compose up -d

docker-compose.yml

services:
nginx:
container_name: nginx_resume
image: nginx:latest
ports:
- "9999:80" # 暴露此ng容器的端口为:8888
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf # 挂载 Nginx 配置文件
- ./dist/:/usr/share/nginx/html
networks:
- common_network

networks:
common_network:
external: true

nginx.conf

server {
listen 80;
server_name fuelstack.icu;
include mime.types;
types {
application/javascript js mjs; # make sure .mjs file's header convert to be application/javascript
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
index index.html;
}
}

Main WebSite

配置一个主站点。分发或导航到其他子站点, 例如:fuelstack.icu, sub.fuelstack.icu

nginx.conf

events {
worker_connections 1024;
}

http {
# 主站点配置
server {
listen 80;
server_name fuelstack.icu;
include mime.types;

# 将 /blog-website 路径映射到网站根目录
location /blog-website {
# With alias, your files should be directly in /usr/share/nginx/html/
# With root, your files should be in /usr/share/nginx/html/blog-website/
alias /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /blog-website/index.html;
add_header Cache-Control "public, max-age=3600";
}

# 重定向根路径到 /blog-website
location = / {
return 301 /blog-website/;
}
# 添加 404 错误页面映射
error_page 404 /404.html;
# 404 page
location = /404.html {
root /usr/share/nginx/html;
internal;
}
}
# 处理 resume.fuelstack.icu 子域名
server {
listen 80;
server_name resume.fuelstack.icu;

location / {
proxy_pass http://nginx_resume:80/;
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;
}
# 添加 404 错误页面映射
error_page 404 /404.html;
# 404 page
location = /404.html {
root /usr/share/nginx/html;
internal;
}
}
}

Deploy script

本地构建产物,并将产物推送到服务器,然后在服务器上解压并重启容器。

deploy.sh

#!/bin/bash
# pnpm build

# 服务器信息
SERVER="root@fuelstack.icu"
TARGET_DIR="/root/projects/nginx"

# 检查远程目录是否存在,如果不存在则创建
ssh $SERVER "mkdir -p $TARGET_DIR"

# 生成带时间戳的 zip 文件名
TIMESTAMP=$(date +%Y%m%d%H%M%S)
ZIP_FILE="build_${TIMESTAMP}.zip"

# 压缩本地的 build 目录
echo "压缩本地的 build 目录为 $ZIP_FILE..."
# zip -r $ZIP_FILE ./build
# 使用 -x 选项排除不必要的文件:
zip -r $ZIP_FILE ./build -x "*/__MACOSX*" "*/.DS_Store"

# 上传 zip 文件到服务器
echo "上传 $ZIP_FILE 到服务器..."
scp $ZIP_FILE $SERVER:$TARGET_DIR/
scp docker-compose.yml $SERVER:$TARGET_DIR/
scp nginx.conf $SERVER:$TARGET_DIR/

# 在服务器上解压并替换 build 目录
echo "在服务器上解压并替换 build 目录..."
ssh $SERVER "
cd $TARGET_DIR && \
rm -rf build && \
unzip -o $ZIP_FILE -d $TARGET_DIR && \
docker-compose up -d --force-recreate --build
"

# 删除本地的 zip 文件
echo "清理本地的 $ZIP_FILE..."
rm $ZIP_FILE

echo "部署完成!"

How Browsers Work

· 2 min read

Reference

https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/How_browsers_work

Conclusion

Here is a brief summary of the key points:

  1. User Input: The process starts when a user enters a URL in the browser's address bar.
  2. DNS Lookup: The browser performs a DNS lookup to translate the domain name into an IP address.
  3. TCP Connection: A TCP connection is established between the browser and the server.
  4. HTTP Request: The browser sends an HTTP request to the server for the desired resource.
  5. Server Response: The server responds with the requested resources, such as HTML, CSS, JavaScript, images, etc.
  6. Rendering: The browser renders the page by parsing the HTML and building the DOM tree, parsing CSS to create the CSSOM tree, combining them into the render tree, and then performing layout and paint operations to display the content on the screen.

Here is a more detailed breakdown of the rendering process:

  • HTML Parsing: The browser parses the HTML to create the DOM (Document Object Model) tree.
  • CSS Parsing: The browser parses the CSS to create the CSSOM (CSS Object Model) tree.
  • Style Calculation: The browser combines the DOM and CSSOM trees to create the render tree.
  • Layout: The browser calculates the layout of each element in the render tree.
  • Paint: The browser paints the pixels to the screen based on the layout.
  • Composite: The browser composites the layers to create the final visual representation.

Comprehension of JavaScript

· 6 min read
marvin-season
Maintainer of Docusaurus

As we all know, JavaScript is a single-threaded language. This means that only one task can be executed at a time. So, if you have a long-running task, it will block the execution of other tasks.

Actually, JavaScript is a single-threaded language, but it has a non-blocking I/O model. This means that JavaScript can perform multiple tasks at the same time. How does JavaScript achieve this? The answer is Event Loop.

Tiptap Practise

· 2 min read
marvin-season
Maintainer of Docusaurus

Core Concept

  • Editor: The main editor component
    • Node: A piece of content in the editor
    • Mark: A piece of text formatting
    • Extension: A piece of functionality
  • Schema: The structure of the document
  • Commands: Functions to manipulate the editor
  • Plugins: Extend the editor with custom functionality
  • State: The current state of the editor

LazyLoad

· One min read
marvin-season
Maintainer of Docusaurus

参考资料

https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Lazy_loading

Intersection Observer API

  • row: 10000
  • startIndex: 7
  • end: 17
  • acceleration: 4
  • buffer: 1
  • rows: [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15},{"id":16},{"id":17}]
6
7
8
9
10
11
12
13
14
15
16
17

How

借助 IntersectionObserver API, 监听将要进入视口内的dom,当该 dom 出现在视口中时,加载更多!

Pseudo

const observer = new IntersectionObserver((entries, observer) => {
if('discover'){
// load more
}
})

observer.observe('dom' as HTMLElement)

Images and iframes

示例图片

以图片为例,打开控制台,筛选网络中的图片,然后滚动上述视口,当图片靠近视口区域时,可以看到图片资源加载

loading="lazy"

<img loading="lazy" src="image.jpg" alt="..." />
<iframe loading="lazy" src="video-player.html" title="..."></iframe>

流数据渲染优化

· 3 min read
marvin-season
Maintainer of Docusaurus

切入点

如何对流失数据优化,提高渲染性能。从以下两点切入:

懒加载渲染: 可视区外部的信息不用渲染,当滚动到可视区时再渲染。 注意,此时需要一个buff来缓冲渲染到页面的信息,防止给用户造成流暂停的假象

增量式渲染: 已经渲染到页面上的数据不用重复渲染,只渲染新增的数据。