DeepSeek Harness 源码深度分析:像 VS Code 一样插件化的 Agent 框架

DeepSeek Harness 源码深度分析:像 VS Code 一样插件化的 Agent 框架

2026年8月13日,DeepSeek 以 MIT 协议开源了自研 Agent 框架 deepseek-harnessdsh)。它的架构哲学和 VS Code 一模一样------一切皆插件。但 dsh 做得更狠:别说工具和 UI 了,连 Agent 循环、模型适配器、沙箱策略、会话存储,全都能换。本文从源码层面拆解它的可扩展性设计、核心数据结构和工程实践,掰开揉碎了讲清楚它到底牛在哪、你能从中学到什么。


一、先给个画面:dsh 到底是个啥

如果你把 Agent 比作一个司机------

  • 模型是大脑,负责判断"下一步怎么走"
  • Harness 就是整辆车:方向盘、油门、刹车、导航仪、后视镜

模型只负责预测下一个 token,但 Harness 决定了模型能看到什么、能调用哪些工具、上下文怎么组织、出错怎么重试、什么时候判断任务完成。同一个模型放进不同的 Harness,表现能差出一个量级。

dsh 的思路是:这辆车的每个零件,从发动机到雨刮器,都能换。 而且你不需要拆车------写个配置文件就行。

bash 复制代码
# 一行命令,车就发动了
npx @deepseek-ai/dsh web

仓库在 github.com/deepseek-ai/deepseek-harness,MIT 协议,97% 是 TypeScript,代码质量极高。


二、和 VS Code 对着看:同样是插件化,谁更彻底?

用过 VS Code 的话,理解 dsh 只需要 30 秒。两者都是微内核架构,但有一个关键区别:

对比维度 VS Code DeepSeek Harness
微内核 Electron + Extension Host Cordis 插件框架
扩展点 contributes(commands、languages、views...) ctx.on() 类型化事件 + ctx.effect() 服务注册
依赖声明 extensionDependencies inject 字段,依赖就绪才激活
配置方式 package.json + contributes.configuration cordis.yml,支持 !!js 环境变量注入
可替换范围 编辑器功能(主题、语言、调试器、视图) Agent 的全部组件(模型、工具、循环、沙箱、持久化、UI)

VS Code 的硬边界:你能换主题、换语言服务器、换调试器,但你换不掉编辑器的核心循环------文本缓冲区怎么管理、光标怎么渲染、选区怎么追踪,这些是写死在 VS Code 内核里的。

dsh 没有这个边界 。Agent Loop 本身就是一个插件,叫 agent-loop。扩展插件依赖的是抽象的 Agent 接口(ctx.agents),永远不直接依赖具体的 agent-loop 实现。这意味着------你可以写一个新的 agent-loop 实现,把整个 Agent 循环逻辑换掉,其他插件完全不受影响。

这就像 VS Code 允许你换掉整个编辑器引擎,而不仅仅是换主题。这就是 Cordis 微内核的核心理念:没有特权核心,一切皆插件,只通过服务和事件通信。

2.1 眼见为实:连编辑器界面都能整个换掉

说"UI 也是插件"你可能觉得抽象,直接看效果------同一个 dsh,换个插件,编辑器就完全变了一张脸:

这就好比你的 VS Code 突然变成了 Vim 的界面,但插件、快捷键、语言服务器全部照常工作。因为编辑器本身只是一个 UI 插件,和模型适配器、工具注册表没有本质区别------都是 ctx 上挂载的一个服务。 想换?改一行 cordis.yml 就行。


三、可扩展性:你到底能换什么?

dsh 的扩展性不是"功能",是它的存在方式本身。我把它拆成三个维度:

3.1 维度一:12 个可替换的核心能力

就像你换电脑配件------显卡、内存、硬盘都有标准接口,dsh 的每个能力也有标准接口:

能力 接口 Key 类比 替换一次影响
模型适配器 ctx.llm 换发动机 所有 Agent 的模型调用
文件系统 ctx.fs 换硬盘 全部文件读写
Shell 执行 ctx.shell 换变速箱 所有命令执行
子进程 ctx.subprocess 换电路系统 Bash、LSP、子 Agent 的进程管理
沙箱 ctx.sandbox 换安全气囊 进程安全隔离
Web 访问 ctx.web 换导航仪 搜索和网页抓取
子 Agent ctx.subagents 换车队管理系统 多 Agent 编排
会话持久化 ctx.sessionPersistence 换行车记录仪 所有会话的存储
上下文压缩 ctx.compaction 换内存管理 长对话的上下文
代码执行 ctx.codeRuntime 换车载电脑 Code Mode 的代码执行环境
凭证管理 ctx.credentials 换钥匙系统 所有鉴权
用户设置 ctx.settings 换中控面板 全部配置

3.2 维度二:8 个生命周期钩子

dsh 把 Agent 的一呼一吸都暴露为事件,你可以在任何时候插一脚:

钩子 时机 类比
agent/pre-step 模型即将看到输入之前 红绿灯------允许通行/禁止进入/改道
agent/request 模型请求发出前 安检------检查/修改请求内容
agent/request-error 模型请求失败后 拖车服务------重试或换路线
agent/turn-stopping 一个 Turn 结束前 终点检查站------决定是否继续跑下一圈
tools/pre-execute 工具执行前 门禁------允许/拒绝/需审批
tools/execute 包裹工具执行 计时器------超时/重试/打点
tools/post-execute 工具执行后 质检------结果改写/阻止/添加上下文
tools/result 最终结果确定后 监控摄像头------只看不改

这些钩子都是瀑布流(Waterfall) ,和 Koa/Express 中间件一模一样:每个监听器收到 (args, next),调用 next() 放行,不调用就短路。比如工具执行前,三个监听器可以依次检查 Hook 规则、权限策略、沙箱策略,任何一个说"不"就整个链条终止。

关键点 :Code Mode 中模型生成的代码调用工具时,同样必须经过这条链 。比如模型写 await tools.bash("rm -rf /"),pre-execute 链会先检查权限,拒绝的话代码直接抛异常。权限模型无法被代码绕过。

3.3 维度三:多 Agent 的"万能遥控器"

dsh 的子 Agent 系统只有一个接口 ctx.subagents,但背后可以挂完全不同的实现:

  • 进程内 Spawn:创建一个全新 Agent,独立上下文
  • 进程内 Fork:从现有会话分支创建子 Agent,继承历史
  • ACP 协议:通过 Agent Client Protocol 对接远程 Agent
  • Claude Code:把 Claude Code 当作子 Agent 调用
  • Codex:把 Codex 当作子 Agent 调用

这就像一个万能遥控器,能控制电视、空调、音响------上层不关心底层是什么牌子,只按同一个接口发指令。换一个 Provider,整个多 Agent 系统的工作方式就变了,但编排代码一行不改。


四、三种扩展方式,直接上代码

4.1 注册一个工具(最常用,5 分钟搞定)

js 复制代码
import { defineTool } from '@deepseek-ai/dsh-tools'
​
export const name = 'my-tool'
export const inject = ['tools']  // 声明依赖,tools 就绪后才加载我
​
export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'read_file',
    description: 'Read a file from disk.',   // 模型看到的功能描述
    parameters: {
      path: { type: 'string', required: true },
      limit: { type: 'number' },
    },
    output: {
      schema: { type: 'string' },            // 返回值类型
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      // args 是类型安全的,exec.signal 用于取消
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
  // 插件卸载时,工具自动注销------就像 useEffect 的 cleanup
}

三个你不需要操心的事:参数校验自动完成、Code Mode 自动可用、卸载时自动清理。你只写业务逻辑。

4.2 接入一个新模型(核心是翻译工作)

js 复制代码
class MyAdapter extends LlmAdapter {
  async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
    // 唯一的职责:把 Provider 的原始响应翻译成 StreamChunk 序列
    // 规矩:usage 在 finish 之前,finish 之后不产生任何输出
    // 错误两种路径:throw LlmError(传输失败)或 finish { kind: 'error' }(业务失败)
  }
}
​
export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
  ctx.llm.registerAdapter(['my-provider'], new MyAdapter(config))
}

模型适配器本质上是一个翻译器 ------把 OpenAI 格式、Anthropic 格式、自研格式的流式响应,统一翻译成 dsh 内部的 StreamChunk 序列。Tool-call 的 arguments 必须保持原始 JSON 字符串,不能解析------这是为了保持日志的 "lossless JSON" 特性。

4.3 写一个 Hook 插件(权限门禁,10 行代码)

js 复制代码
export const name = 'permission-gate'
​
export function apply(ctx: Context) {
  ctx.on('tools/pre-execute', async (exec, next) => {
    if (!(await isAllowed(exec))) {
      return { kind: 'deny', reason: '这个操作被公司策略禁止了' }
    }
    return next()  // 放行给下一个监听器
  })
}

放在 tools/pre-execute 而非 tools/execute 是关键:pre-execute 的监听器可以按优先级排序,高优先级的先判断;execute 的监听器则用来包裹实际执行(比如加超时、加重试、打指标)。


五、会话日志:dsh 最精妙的设计,没有之一

5.1 把 Agent 的一生写成 Git 提交记录

dsh 最核心的数据结构是一个仅追加的会话事件日志。把它想象成 Git------Git 把每一次代码变更存成 commit,dsh 把 Agent 的每一次"呼吸"存成 event:

bash 复制代码
Session 日志(就像 git log):
  [seq 0]  turn/start     → 新一轮对话开始
  [seq 1]  step/start     → 开始一次模型调用
  [seq 2]  user/message   → 用户说了什么
  [seq 3]  assistant/chunk → 模型吐了一个 token "我"
  [seq 4]  assistant/chunk → 模型又吐了一个 token "来"
  [seq 5]  assistant/message → 模型完整回复 + token 消耗
  [seq 6]  tool/call      → 模型决定调用 bash 工具
  [seq 7]  tool/result    → bash 返回了结果
  [seq 8]  step/end       → 这次模型调用结束
  [seq 9]  turn/end       → 这轮对话结束

核心原则Model-visible ⟺ Logged。模型能看到的一切,都必须能从日志中完整重建。这不是文档里的约定------代码里硬编码了断言,违反就崩溃。

5.2 为什么这个设计是神来之笔

第一,调试体验质变。 Agent 任务失败了,你不需要猜"模型当时看到了什么"------日志就是完整的行车记录仪,每一帧都有。Trajectory 视图可以按来源查看每一次上下文注入、每一个工具调用结果。

第二,"时光机"能力。 因为模型历史是从日志"派生"出来的(deriveMessages()),而不是另外存储的,你可以:

  • 回放:从任意时间点重放日志,看不同插件组合在相同轨迹上的表现------这等于给 Agent 做 A/B 测试
  • 分叉 :从任意事件点分叉出新的会话,复用之前的历史,追加新操作------相当于 git checkout -b,不需要复制任何数据
  • 恢复:崩溃后从日志重建整个会话状态,精确到最后一个 token

第三,插件之间零耦合的共享数据层。 UI 渲染、Hook 桥接、遥测导出、审计------都是 ctx.on('session/event', ...) 的监听器,各自消费同一份日志,互不干扰。这就像 Linux 的 /dev 目录------"一切皆文件"让所有工具能协同工作。

第四,日志自带版本控制。 插件可以通过 TypeScript 声明合并向 SessionEventMap 添加新事件类型。比如 Compaction 插件添加 compaction/startcompaction/summarycompaction/endSESSION_FORMAT_VERSION 机制确保:旧版本遇到不认识的事件类型会拒绝加载(除非事件标记了 ignorable: true),防止静默数据损坏。

5.3 一个具体例子:日志的"Surface"机制

日志中的消息事件(user/messageassistant/messagetool/result)携带 SurfaceOp 元数据,声明它如何进入模型的"可见消息列表":

  • 'append':正常追加到末尾------这是 99% 的情况
  • { op: 'replace', start, end }:替换一段消息------用于 Compaction 把旧消息替换成摘要

想象一下:当对话长了需要压缩,Compaction 插件不是修改原始日志,而是追加一个带 replace 标记的新事件。模型看到的是压缩后的历史,但原始数据完整保留。这就像 Photoshop 的图层------你不破坏原始图片,只是在上面叠加了新的图层。


六、工具执行管道:三层关卡,插翅难逃

把工具调用想象成过海关,dsh 设了三道关卡:

sql 复制代码
[模型输出 tool-call:"bash rm -rf /"]
  │
  ├─ 第一关:tools/pre-execute (waterfall)
  │   安检员 A(Hook 规则):"检查你的 hook 配置..."
  │   安检员 B(权限策略):"你没有 root 权限,拒绝!"
  │   └─ 单调守卫 (ctx.tools.guard):"最终裁决,不可上诉"
  │
  ├─ 第二关:tools/execute (waterfall)
  │   计时员:"给你 30 秒,超时就终止"
  │   重试员:"失败了?再试一次"
  │   └─ 实际 execute() 函数体
  │
  ├─ 第三关:tools/post-execute (waterfall)
  │   质检员:"结果太长,截断到 10000 字符"
  │   上下文注入:"顺便告诉模型,这个文件已经改了"
  │
  └─ 监控摄像头:tools/result (同步通知)
      "记录到审计日志,不可篡改"

选关卡的规则

你要做什么 进哪道门 为什么
权限检查、审批、沙箱拦截 tools/pre-execute 可以在执行前阻止,监听器可排序
不可被覆盖的最终否决 ctx.tools.guard() 后面的监听器无法撤销
超时/重试/指标 tools/execute 包裹执行,能访问 exec.signal
改写结果或添加上下文 tools/post-execute 此时结果已产生但未最终确定
审计/日志(只观察不改写) tools/result 此时结果是不可变的

Code Mode 也不例外 :模型生成的 TypeScript 代码调用 await tools.bash(cmd) 时,每个子调用都会记录 tool/code-dispatch 事件,然后完整走一遍三道关卡。Denial 会变成代码中的异常------模型写的代码无法绕过权限模型。


七、Capability Seam:为什么换一个 Provider 能改变整个系统

7.1 不只是"策略模式"

dsh 把每个可替换的能力抽象为 Capability Seam,包含三个角色:

css 复制代码
Service Definition  → 声明接口("一个文件系统应该能读、写、删、列目录")
       │
       ├── Service Provider A → 本地文件系统实现
       ├── Service Provider B → E2B 远程沙箱实现
       └── Service Provider C → 内存文件系统实现(测试用)
       │
       └── Consumer → 模型可调用的 tool-fs,只依赖接口

这看起来像策略模式,但有一个策略模式没有的关键特性:传递性

7.2 传递性:一次替换,全家迁移

文件系统(ctx.fs)和子进程(ctx.subprocess)共享同一个执行世界。所以当你把两者都指向远程沙箱时:

scss 复制代码
ctx.fs → fs-e2b (远程)
ctx.subprocess → subprocess-e2b (远程)

Bash、PTY、LSP------所有这些依赖 ctx.subprocess 的组件------全部自动迁移到远程执行,不需要逐个修改。这就像你换了一个房子的地基,所有建在上面的房间自动跟着迁移。

这个设计在 dsh 源码中体现为:每个 Provider 都声明自己属于哪个"执行世界",Consumer 不关心 Provider 是谁,只关心它提供的接口。一次 Provider 替换,整个执行环境跟着变。

7.3 多 Agent 的 Seam 是最佳案例

ctx.subagents 是最能体现 Seam 价值的例子。这个接口定义了一个子 Agent 应该能做什么(接收任务、返回结果),但背后的 Provider 可以是:

  • 进程内全新 Agent(dsh-subagent-spawn-in-process
  • 从当前会话 Fork(dsh-subagent-fork-in-process
  • 通过 ACP 协议对接的远程 Agent(dsh-subagent-acp
  • Claude Code 实例(dsh-subagent-claude-code
  • Codex 实例(dsh-subagent-codex

上层编排逻辑(比如 workflow 工具)只依赖 ctx.subagents 接口,完全不感知底层是谁在执行。你今天用 Claude Code 做子 Agent,明天换成 Codex,编排代码一行不改。 这就是 Seam 的威力。


八、Profile 与 Bundle:Agent 的 Docker Compose

8.1 配置即组装

dsh 的插件组合不是写死在代码里的,而是通过 YAML 配置文件声明。这非常像 Docker Compose------你声明需要哪些服务,框架负责启动和管理:

yaml 复制代码
# cordis.yml ------ 就像 docker-compose.yml
plugins:
  dsh-llm-deepseek:              # 模型服务
    config:
      apiKey: !!js process.env.DEEPSEEK_API_KEY
  dsh-tool-bash:                 # Shell 工具
  dsh-tool-fs:                   # 文件工具
  dsh-tool-web:                  # Web 搜索工具
  dsh-session-persistence-sqlite: # 持久化

8.2 分层覆盖:像 CSS 一样的层叠

配置不是扁平的一层,而是像 CSS 一样有层叠优先级:

css 复制代码
第 1 层:bundle 默认配置(官方预置的插件组合)
第 2 层:Profile 的 cordis.patch.yml(你的自定义覆盖)
第 3 层:Home 级别的 patch(全局偏好)
第 4 层:--patch 命令行参数(临时覆盖,优先级最高)

这意味着你可以在不修改源码的情况下,替换任何插件。 比如官方默认用 SQLite 做持久化,你一个 patch 就能换成 JSONL:

yaml 复制代码
# cordis.patch.yml
plugins:
  dsh-session-persistence-sqlite:
    disabled: true
  dsh-session-persistence-jsonl:
    config:
      path: ~/my-sessions/

运行 dsh --profile web --dump-config 可以查看当前机器实际启动的完整插件树,调试配置变得极其简单。


九、Creator 模式:Agent 能给自己装插件

四种运行模式中,Creator 模式是最有想象力的一个。它做的事情很简单:让 Agent 能检查、试验和修改自己的插件树。

markdown 复制代码
传统工具:你能用的功能 = 开发者写死的
Creator 模式:Agent 可以------
  1. ctx.cordisInspect → 查看当前运行时有哪些插件
  2. ctx.dynamicCordisRunner → 动态挂载/卸载插件
  3. 试验不同插件组合的效果
  4. 把验证过的组合导出为新的 Preset

这就像你给一个修车工配了一整套工具,然后跟他说:"你不仅可以用这些工具,你还可以自己改装工具、发明新工具、甚至重建整个车间。"

Harness 的配置本身变成了 Agent 可以操作的对象。 这离"Agent 自己改造自己的 Harness Runtime"还有距离,但方向已经非常明确。技术上,这是通过 self-modification 包实现的------它把 ctx.cordisInspectctx.dynamicCordisRunner 作为模型可调用的工具暴露出来,Agent 可以像操作文件一样操作自己的插件系统。


十、工程实践:TypeScript 能写到什么程度

dsh 的源码本身就是 TypeScript 工程化的教科书。几个你可能会"哇"的点:

声明合并:不碰核心代码,扩展核心类型

事件类型和 Context 接口通过 TypeScript 的 declaration merging 扩展,编译时就有类型检查,不是运行时字符串拼接:

typescript 复制代码
// 核心定义(dsh-session 包)
interface SessionEventMap {
  'turn/start': { turn: number }
  'turn/end': { turn: number; reason: TurnEndReason }
  // ...
}

// 插件扩展(dsh-compaction 包,不修改核心代码)
declare module '@deepseek-ai/dsh-session' {
  interface SessionEventMap {
    'compaction/start': { turn: number }
    'compaction/summary': { summary: string }
    'compaction/end': { turn: number }
  }
}

这比运行时注册安全得多------如果你拼错了事件名,编译器直接报错,不会到生产环境才发现。

Branded Types:string 不安全,但 SessionId 安全

所有跨边界的 ID 都用 branded type 而非裸 string

typescript 复制代码
type SessionId = string & { readonly __brand: 'SessionId' }
type CallId = string & { readonly __brand: 'CallId' }
type MessageId = string & { readonly __brand: 'MessageId' }

这意味着你不可能把 CallId 当成 SessionId 传给函数------编译器会直接拒绝。这是一种零成本的类型安全增强,运行时没有任何开销。

所有注册都是可逆的------就像 useEffect 的 cleanup

javascript 复制代码
// dsh 的注册模式
const dispose = ctx.effect(() => {
  // 注册副作用
  return () => { /* 清理副作用 */ }
})
// 插件卸载时,dispose() 自动调用

这和 React 的 useEffect(() => { ... return cleanup }, []) 一模一样。天然支持 HMR(热模块替换) ------插件卸载时所有副作用自动清理,新版本加载后重新注册,整个过程不需要重启进程。

运行时不变量:代码里硬编码的断言

dsh 的代码里有一些"要么正确,要么崩溃"的断言:

  • 模型可见内容必须能从日志重建------否则 throw
  • 瀑布流监听器必须调用 next() 除非有意短路------否则 throw
  • 未知事件类型必须标记 ignorable: true 否则拒绝加载会话------否则 throw
  • 配置缺失了必须字段------加载时 throw,不是静默跳过

这些不是文档里的约定,是运行时会崩溃的代码。 这种"fail loud"的设计哲学,让系统在问题发生的第一时间就暴露,而不是悄悄积累错误状态。


十一、你能从中学到什么

要点 具体做法 类比 推荐
微内核 + 声明式扩展点 核心只做调度,能力全是插件。扩展点用类型化事件而非字符串 key VS Code 的架构哲学 ★★★★★
Event Sourcing 做日志 所有交互存为追加事件,模型历史从日志派生。天然支持回放、分叉、A/B 测试 Git 的 commit log ★★★★★
Capability Seam 三件套 每个可替换能力都有 Definition + Provider + Consumer,一次替换全家迁移 电脑配件的标准接口 ★★★★★
Waterfall 中间件链 (args, next) 模式做拦截,每个监听器可以 allow/deny/rewrite Koa/Express 中间件 ★★★★☆
声明合并扩类型 TypeScript 项目中用 declaration merging 扩接口,编译时检查 插件不碰核心代码就能扩展类型 ★★★★☆
三层工具管道 pre-execute(权限)→ execute(超时/重试)→ post-execute(改写) 海关三道关卡 ★★★★☆
Profile/Bundle 配置分层 插件组合用 YAML 声明,支持多层 patch 覆盖,--dump-config 调试 Docker Compose + CSS 层叠 ★★★★☆
Branded Types 跨边界 ID 用 branded type 而非裸 string 给 ID 贴上"防伪标签" ★★★☆☆
注册即副作用 所有注册返回 disposer,卸载时自动清理,天然 HMR React useEffect ★★★☆☆

十二、局限

  • v0.1 阶段,API 不稳定:官方明确声明会有破坏兼容性变更,现在接入需要承担迁移成本。适合学习架构,不适合生产使用。
  • Cordis 学习曲线:declaration merging、waterfall 语义、scope 管理------TypeScript 不够熟的话,刚上手会比较吃力。
  • "什么都能换"不等于更好的效果:插件系统提供的是差异化空间,但真正决定体验的还是默认插件的质量。就像 Linux 的"一切皆文件"很优雅,但没人用裸 Linux 内核。
  • 多 Agent 编排范式没有突破:Spawn/Fork/Pipeline/Ralph 都是已有模式,创新在架构层面(统一接口可替换 Provider),而非编排算法。

十三、总结

DeepSeek Harness 不是又一个 Coding Agent 客户端,而是一份 "如何用微内核架构做 Agent 运行时"的参考实现。它把 VS Code 的插件化哲学推到了极限------连 Agent 的核心循环都是可替换的插件。

如果你正在做 Agent 系统,从 dsh 里最值得拿走的三样东西:

  1. Event Sourcing 会话日志------Git 般的 Agent 交互记录,让回放、分叉、A/B 测试、崩溃恢复全部变成"免费"的能力
  2. Capability Seam 三件套------Definition + Provider + Consumer 的标准化接口模式,一次替换全家迁移,不像策略模式那样只管换算法
  3. Waterfall 中间件链做扩展点------用 Koa 中间件模式做 Agent 生命周期的拦截,比事件总线灵活,比 AOP 类型安全

如果只记住一句话 :dsh 证明了一件事------Agent 框架的架构,完全可以像 VS Code 的插件系统一样优雅。 区别在于,它把"可替换"的边界从"工具"推到了"一切"。


参考来源

相关推荐
无糖可可果1 小时前
从零读懂一个 Next.js 全栈笔记应用
前端
喜欢睡觉1 小时前
从零看懂一个 Next.js 笔记应用
前端
cindershade1 小时前
从零实现画布「双击创建节点」:一个 VueFlow 项目的交互全记录
前端
YHL1 小时前
🚀 SSE 服务器发送事件与 BFF 层实战
前端·后端
用户852495071841 小时前
Next.js App Router:一篇笔记的服务端奇幻漂流
架构
今日无bug1 小时前
HTML5 Canvas:从画图到游戏开发
前端·canvas
cindershade1 小时前
用 Web Workers 优化前端重计算任务:避免主线程卡顿的实战方案
前端
爱丶不疚1 小时前
搞不清 CommonJS 与 ESM,你是否也有这些疑问🤔
前端·node.js
YIAN1 小时前
http无状态?State来展示!从基础路由到鉴权守卫,吃透 SPA 前端路由核心
前端·react.js