Skip to main content

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;

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

# 重定向根路径到 /danny-website
location = / {
return 301 /danny-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.

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>

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

流数据渲染优化

· 3 min read
marvin-season
Maintainer of Docusaurus

切入点

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

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

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

AI Chat

· 4 min read
marvin-season
Maintainer of Docusaurus

概要

核心数据结构: 消息列表 (messages)

核心操作:调用模型接口,生成消息

次要操作:复制、引用、重新生成、编辑、参考附件选择

UI:聚合上操作以及数据

class CoreUI {
useMessage(){
// connect to message
return this
}
useHandle(){
// connect to handle
return this
}
useChat(){
// connect to AI
return this
}
render(){
// render ui
return <></>
}
}
new CoreUI().useMessage().useHandle().useChat().render();

Concept

S-S-E

Server-Send-Event: data is flowing from server to client format: tag:string for example:

data: "{ name: 'marvin', age: 20 }"

在SSE中 数据以固定的格式传输到客户端,在使用之前客户端或许需要先进行解析。

React Think

· 2 min read
marvin-season
Maintainer of Docusaurus

Web开发的本质就是将 数据 呈现到页面,平滑的交互带来好的用户体验(User Perceived Performance),性能优化也此为主。

原生JS

如果不使用 React,我们可以使用原生JS来实现页面的交互,比如下面的例子:

const addbtn = document.getElementById("add");
const counter = document.getElementById("counter");

addbtn.onclick = function() {
// some business logic
num.innerText = parseInt(counter.innerText) + 1;
};

可以看到 数据UI有很强的粘性,业务代码不够清晰,维护性差。且频繁的操作DOM会导致性能问题。

如何优化?

不直接操作DOM,而是在DOM和数据之间加一层,当我们需要更新数据时去通知这一层,至于更新优化的逻辑,全部集中在这一层。

React就扮演了这样的角色。

React如何工作

https://github.com/acdlite/react-fiber-architecture

https://jser.pro/ddir/rie?reactVersion=18.3.1&snippetKey=hq8jm2ylzb9u8eh468

以数据为核心,驱动视图更新 React架构如下: Scheduler -> Reconciler -> Renderer

const Counter = () => {
const [counter, setCounter] = useState(0);
return <>
<span>{counter}</span>
<button onClick={() => setCounter(prev => prev + 1)}>add</button>
</>
}

调用setCounter触发更新任务,Scheduler依据任务的优先级选择任务,将任务交给 Reconciler, Reconciler 负责找出变化的部份,将变化的部份交给Render,Render负责将变化的部份渲染到页面上。 Reconciler的工作是最复杂的,初次渲染时,Reconciler会生成一颗Fiber树,当更新时Reconciler 利用双缓存机制克隆原来的树,将更新后的节点更新到新树上,最后将新树替换原来的树。