Skip to main content

开发工具配置

· One min read

VSCODE 配置

{
"editor.fontSize": 15,
"editor.fontFamily": "JetBrains Mono, monospace",
"window.commandCenter": true,
"explorer.confirmDelete": false,
"window.openFilesInNewWindow": "default",
"window.openFoldersInNewWindow": "on",
"editor.gotoLocation.multipleDefinitions": "goto",
"typescript.experimental.useTsgo": true,
"typescript.validate.enable": true,
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.referencesCodeLens.enabled": true,
"typescript.implementationsCodeLens.enabled": true,
"workbench.colorTheme": "One Dark Pro Flat",
"workbench.iconTheme": "material-icon-theme",
"redhat.telemetry.enabled": false,
"xstate.nestTypegenFiles": false,
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.js",
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts",
"*.jsx": "${capture}.js",
"*.tsx": "${capture}.ts",
"tsconfig.json": "tsconfig.*.json",
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock"
},
"prettier.requireConfig": true,

"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.removeUnused.ts": "explicit",
"source.addMissingImports.ts": "explicit"
},

"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"reactSnippets.settings.prettierEnabled": true
}

Network

· One min read

无法科学上网

github

解决思路:修改Host

使用 https://www.ipaddress.com/ 查询 github.com 和 raw.githubusercontent.com 的IP地址

最后,修改服务器的/etc/hosts,添加如下两行:

140.82.112.4 github.com
185.199.108.133 raw.githubusercontent.com

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 留给调用方” 的 组合式组件开发方式

深度思考扩展

· 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

· 5 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 "部署完成!"

升级Https

申请使用通配符证书(覆盖所有二级域名)

使用 letsencrypt 申请证书

  1. 安装 Certbot(在宿主机上即可,不用在容器里):
sudo apt update
sudo apt install certbot -y
  1. 申请通配符证书(DNS 验证):
sudo certbot -d "*.fuelstack.icu" -d "fuelstack.icu" --manual --preferred-challenges dns certonly
  1. Certbot 会提示你在 DNS 添加 TXT 记录:(控制台会输出信息)
_acme-challenge.fuelstack.icu  value_from_certbot
  1. 证书默认位置:
/etc/letsencrypt/live/fuelstack.icu/fullchain.pem
/etc/letsencrypt/live/fuelstack.icu/privkey.pem

将证书挂载到 Docker 中的 Nginx

- /etc/letsencrypt/live/fuelstack.icu-0001/fullchain.pem:/etc/ssl/certs/fullchain.pem:ro
- /etc/letsencrypt/live/fuelstack.icu-0001/privkey.pem:/etc/ssl/private/privkey.pem:ro
services:
nginx-service:
image: nginx:latest
container_name: nginx_service
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./build:/usr/share/nginx/html
- /etc/letsencrypt/live/fuelstack.icu-0001/fullchain.pem:/etc/ssl/certs/fullchain.pem:ro
- /etc/letsencrypt/live/fuelstack.icu-0001/privkey.pem:/etc/ssl/private/privkey.pem:ro
restart: always
networks:
- common_network
networks:
common_network:
external: true
http {
server {
listen 443 ssl;
server_name fuelstack.icu www.fuelstack.icu;

ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;

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

Github OAuth

创建配置 Github OAuth 创建App, 生产clientid等信息, 配合NextAuth

配置url,生产环境需要https

FAP

Auth 504

  • 查看日志, 认证授权需要一些基础的表, 需要先创建 npx prisma db push

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>