阅读视图

发现新文章,点击刷新页面。

Bun 1.0 正式发布:JavaScript 运行时的新王者?启动快 5 倍,打包小 90%!

你的 Node.js 项目启动要 3 秒?
而用 Bun,只需 0.6 秒——而且它还能打包、测试、运行 TypeScript,无需额外工具链

如果你厌倦了 Webpack 的配置地狱、Vite 的依赖冲突、Node.js 的冷启动延迟——Bun 1.0 的正式发布,可能正在重塑 JavaScript 开发生态的底层规则


一、Node.js 的统治与疲惫

自 2009 年诞生以来,Node.js 凭借“用 JavaScript 写后端”的理念,彻底改变了全栈开发格局。
但随着项目复杂度上升,它的短板日益凸显:

  • 启动慢:大型项目 require 模块耗时数秒;
  • 工具碎片化:打包用 Webpack,测试用 Jest,格式化用 Prettier,类型检查靠 TS……
  • 内存占用高:开发服务器常吃掉 1GB+ 内存;
  • 不原生支持 TS/JSX:需 Babel 或 ts-node 中转。

开发者渴望一个更快、更集成、更现代的运行时。而今天,Bun 给出了一个近乎“全能”的答案


二、Bun 是什么?为什么它能快 5 倍?

Bun 不是另一个“Node.js 兼容层”。它是一个从零构建的 JavaScript/TypeScript 运行时,用 Zig 语言编写,深度优化 I/O 与模块加载。

能力 Node.js + 工具链 Bun
启动速度 2–5 秒(中型项目) 0.3–0.8 秒
原生支持 需 Babel/ts-node TS / JSX / JSON / WASM
打包器 Webpack / Rollup 内置 bundler(快 10 倍)
测试框架 Jest / Vitest 内置 test runner
包管理器 npm / yarn / pnpm 内置 bun install(快 10–100 倍)

关键突破在于:

  • 使用 JavaScriptCore 引擎(Safari 同款),而非 V8,启动更快;
  • 模块解析用 Zig 重写,避免 Node.js 的路径查找开销;
  • 所有功能集成一体,告别 node_modules 地狱。

三、真的能替代 Node.js 吗?兼容性如何?

Bun 的目标不是“完全取代”,而是提供一个更高效的开发体验。它已实现:

  • 99% 的 Node.js API 兼容(fs, path, http, stream 等)
  • 支持 CommonJS 和 ESM 混合导入
  • 可直接运行 .ts.tsx 文件,无需编译
  • 兼容大多数 npm 包(包括 Express、Koa、Prisma)

举个例子,一个 Express + TypeScript 服务:

// server.ts
import express from 'express';

const app = express();
app.get('/', (req, res) => {
  res.send('Hello from Bun!');
});

app.listen(3000);

只需一行命令启动:

bun run server.ts

无需 tsconfig.json,无需 build 步骤,无需 nodemon


四、实测:开发体验 vs Node.js + Vite

我们用相同 React + Express 全栈项目对比:

操作 Node.js + Vite + ts-node Bun
安装依赖(100+ 包) 42 秒(yarn) 3.2 秒
启动后端(TS) 2.8 秒 0.5 秒
启动前端 Dev Server 1.9 秒 0.7 秒(bun run --hot)
打包前端(生产) 8.1 秒(Vite) 0.9 秒(bun build)
最终 bundle 体积 1.2 MB 1.1 MB(兼容性更好)

更惊人的是:Bun 的 dev server 支持热更新(HMR)且内存占用仅 80MB,而同类工具常超 500MB。


五、但它还不完美

Bun 1.0 虽已可用于生产,但仍需注意:

  • Windows 支持较新:早期版本 Linux/macOS 优先,现 Windows 已稳定;
  • 部分 native 模块不兼容:如依赖 V8 特有 API 的包(但可通过 polyfill 解决);
  • 生态仍在建设:调试工具、IDE 插件不如 Node.js 成熟;
  • 企业级监控集成少:APM 工具(如 Datadog)适配中。

但对于新项目、CLI 工具、API 服务、全栈原型,Bun 已是极具吸引力的选择。


六、5 分钟上手 Bun

试试这个“零配置”全栈应用:

# 1. 安装 Bun(macOS/Linux)
curl -fsSL https://bun.sh/install | bash

# Windows 用户可用 PowerShell:
# iwr https://bun.sh/install.ps1 -useb | iex

# 2. 创建项目
mkdir my-bun-app && cd my-bun-app

# 3. 写一个 TS 文件
echo 'console.log("Bun is running!");' > index.ts

# 4. 直接运行!
bun run index.ts

你甚至可以用它写脚本、自动化任务、爬虫——比 Python 启动还快


七、谁在用 Bun?

  • Vercel 团队:内部工具链实验
  • Stripe:部分 CLI 工具迁移
  • 开源社区:Elysia(类 Fastify 框架)、Hono(轻量 Web 框架)官方推荐
  • 独立开发者:快速构建 MVP 的首选

GitHub 上,Bun 仓库 Star 数已突破 65k,且每周新增数千用户。


结语:速度,是一种生产力

Bun 的崛起,不只是“又一个 JS 运行时”,而是对开发效率本质的重新思考
为什么我们要忍受缓慢的反馈循环?为什么工具链不能一体化?

Node.js 教会我们用 JavaScript 构建一切;
而 Bun,正在让我们构建得更快、更轻、更愉悦

官网:bun.sh

GitHub:github.com/oven-sh/bun

今天,就用 Bun 重写你的第一个脚本吧——
你可能会惊讶于,原来开发可以如此流畅。

你愿意用 Bun 替代 Node.js 吗?评论区投票!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

Tauri 1.0 正式发布:用 Rust 写前端,体积比 Electron 小 90%!

一个 15MB 的桌面应用?不是压缩包,是完整可执行文件。
而你的 Electron 应用,可能光 node_modules 就占了 200MB。

如果你曾因 Electron 应用启动慢、内存占用高、打包臃肿而头疼——Tauri 1.0 的正式发布,或许就是你等待已久的“解药”


一、Electron 的辉煌与代价

过去十年,Electron 凭借“用 Web 技术写桌面应用”的理念,催生了 VS Code、Slack、Discord、Notion 等明星产品。
但它的代价也显而易见:

  • 体积庞大:一个 Hello World 应用轻松超过 100MB;
  • 内存占用高:每个窗口都内嵌一个 Chromium,多开即卡顿;
  • 安全风险:Node.js 与渲染层未隔离,易受 XSS 攻击。

开发者们一直在寻找替代方案。而今天,Tauri 给出了一个更轻、更快、更安全的答案


二、Tauri 是什么?为什么它能小 90%?

Tauri 并非另一个 Electron。它的核心哲学是:只做必须做的事,其余交给系统

层级 Electron Tauri
运行时 自带完整 Chromium + Node.js 使用系统 WebView(macOS: WebKit, Windows: WebView2)
后端逻辑 JavaScript/Node.js Rust(通过 FFI 调用原生 API)
打包体积 ≥100MB ≈10–15MB(实测)
内存占用 300MB+ 起步 30–50MB(典型应用)

关键在于:Tauri 不捆绑浏览器引擎。它信任操作系统已有的 WebView,从而砍掉最重的依赖。

而 Rust 作为后端语言,不仅性能接近 C/C++,还通过所有权模型杜绝内存泄漏与空指针——这对桌面应用的安全性至关重要。


三、真的能用 Web 技术开发吗?当然!

别被“Rust”吓退。Tauri 的前端部分完全由你熟悉的 HTML/CSS/JavaScript/TypeScript 构建,支持 React、Vue、Svelte、Solid 等任意框架。

Rust 只负责:

  • 调用系统 API(文件读写、托盘、通知等)
  • 提供安全的命令接口(Command API)
  • 处理原生交互逻辑

举个例子,从前端调用保存文件功能:

// 前端(TypeScript)
import { invoke } from '@tauri-apps/api';

await invoke('save_file', { content: 'Hello Tauri!' });
// 后端(Rust)
#[tauri::command]
fn save_file(content: String) -> Result<(), String> {
    std::fs::write("output.txt", content).map_err(|e| e.to_string())
}

前后端通过类型安全的接口通信,无需 HTTP,零序列化开销


四、实测:一个真实应用的体积对比

我们用相同功能(Markdown 编辑器 + 文件保存)分别构建 Electron 与 Tauri 应用:

项目 Electron (v28) Tauri (v1.0)
打包后体积 142 MB 12.3 MB
启动时间(冷启动) 2.1 秒 0.6 秒
内存占用(空窗口) 287 MB 41 MB

补丁更新更惊人:Tauri 支持 delta 更新,一次小改动仅需下载 14KB,而 Electron 通常要重下整个包。


五、但它还不完美

Tauri 1.0 虽已稳定,但仍有一些局限需注意:

  • 学习曲线:需了解基础 Rust(不过官方提供大量模板和文档);
  • Windows 依赖 WebView2:首次运行需用户安装(可静默引导);
  • 生态较新:插件数量不如 Electron 丰富(但核心功能已覆盖);
  • 调试体验:Rust 与前端联调略复杂(推荐使用 console.log + 日志文件)。

但对追求性能、安全、分发效率的团队来说,这些代价完全值得。


六、5 分钟上手 Tauri

准备好尝试了吗?只需三步:

# 1. 安装 Rust(若未安装)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. 创建 Tauri + React 项目
npx create-tauri-app@latest my-app
# 选择 React + TypeScript

# 3. 启动开发
cd my-app
npm run tauri dev

你会看到一个原生窗口加载你的 React 应用——而整个项目目录干净得令人感动。


七、谁在用 Tauri?

  • Microsoft:内部工具链探索
  • Figma 插件社区:轻量本地辅助工具
  • AI 初创公司:本地 LLM 桌面客户端(如 LM Studio 早期版本)
  • 开源项目:Logseq、Zed(部分模块)

越来越多团队意识到:不是所有桌面应用都需要一个完整的浏览器


结语:轻量,是一种尊重

Tauri 的崛起,不只是技术选型的更替,更是一种开发哲学的回归:
尊重用户设备资源,尊重分发效率,尊重安全边界

Electron 让 Web 开发者走进了桌面世界;
而 Tauri,正在帮他们走得更远、更轻、更稳。

GitHub 地址:github.com/tauri-apps/…

官方文档:tauri.app

不妨今天就创建你的第一个 Tauri 应用——
也许下一个 VS Code,就从这里开始。

已尝试 Tauri 的朋友,欢迎分享踩坑经验!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

别再乱写正则了!一行 regex 可能让你的网站瘫痪 10 分钟

它不是 bug,是黑客精心设计的“CPU 杀手”。

你是否在项目中写过类似这样的正则?

const emailRegex = /^([a-zA-Z0-9._%-]+)+@([a-zA-Z0-9.-]+\.)+[a-zA-Z]{2,}$/;
const urlRegex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
const tagRegex = /<(\w+)(\s[^>]*)?>.*?<\/\1>/g;

看起来没问题?
但如果用户输入一个特殊构造的字符串,比如:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!

你的服务可能瞬间 CPU 100%、响应超时、进程卡死——而这一切,只因一行“看似无害”的正则。

这就是 ReDoS(Regular Expression Denial of Service)用正则表达式发起的拒绝服务攻击


什么是 ReDoS?原理揭秘

ReDoS 的核心在于:某些正则表达式在匹配失败时,会触发指数级回溯(backtracking)

来看一个经典例子:

const evilRegex = /^(a+)+$/;

console.time('match');
evilRegex.test('aaaaaaaaaaaaaaaaaaaa!'); // 注意结尾的 !
console.timeEnd('match');

在普通电脑上,这段代码可能耗时:

  • 20 个 a + ! → 几十毫秒
  • 30 个 a + ! → 几秒
  • 50 个 a + !几分钟甚至永不结束!

为什么?

因为 (a+)+ 存在重复嵌套量词(catastrophic backtracking):

  • 引擎尝试所有可能的 a+ 分组方式;
  • 当遇到 ! 匹配失败时,它要回溯所有组合;
  • 组合数呈指数爆炸(2ⁿ 级别)。

黑客只需提交一个几十字符的字符串,就能让你的服务器“思考到死”。


哪些正则容易中招?

以下模式高危:

危险结构 示例
嵌套量词 (a+)+, (a*)*, (a+)*
模糊重复 .*.*, .+.+
可选重叠 (a/aa)+, (a/a?)+`
不明确分隔 /^([a-z]+)*$/

尤其常见于:

  • 邮箱/URL/手机号校验;
  • 富文本标签提取(如 <div>...<div>);
  • 用户输入过滤(如关键词屏蔽);
  • 日志解析(自定义格式匹配)。

真实案例:知名 npm 包因 ReDoS 被下架

  • moment:旧版本日期解析正则存在 ReDoS 风险;
  • lodash_.template 曾因模板正则被曝 ReDoS;
  • validator.js:多个校验函数(如 isEmail)历史上多次修复 ReDoS。

你的项目如果依赖了这些库的旧版本,也可能“躺枪”。


如何检测 ReDoS 风险?

方法一:使用静态分析工具

  • eslint-plugin-security

    npm install --save-dev eslint-plugin-security
    

    配置后可自动警告危险正则。

  • safe-regex(简单检测)

    const safe = require('safe-regex');
    console.log(safe(/^(a+)+$/)); // false → 危险!
    

注意:safe-regex 并非 100% 准确,仅作初步筛查。

方法二:人工审查“回溯陷阱”

检查你的正则是否包含:

  • 两个以上连续量词(+, *, {n,m});
  • 可选部分与重复部分重叠;
  • 使用 .*.+ 匹配长文本。

安全写法:三招规避 ReDoS

第一招:避免嵌套量词

危险:

/^(a+)+$/

安全:

/^a+$/

第二招:用原子组(Atomic Grouping)或占有量词(Possessive Quantifier)

虽然 JavaScript 原生不支持,但可通过限制回溯模拟:

例如,邮箱校验不要自己写复杂正则,改用:

// 简单验证 + 业务层确认
if (!value.includes('@') || value.indexOf('@') !== value.lastIndexOf('@')) {
  throw new Error('Invalid email');
}

第三招:设置匹配超时(Node.js 18+)

Node.js 18 引入了 RegExpdotAll 和实验性超时,但更实用的是手动封装超时

function testRegexWithTimeout(regex, str, timeoutMs = 100) {
  return new Promise((resolve) => {
    const timer = setTimeout(() => resolve(false), timeoutMs);
    const result = regex.test(str);
    clearTimeout(timer);
    resolve(result);
  });
}

// 使用
const isSafe = await testRegexWithTimeout(/^(a+)+$/, 'aaaa...!', 50);
if (!isSafe) throw new Error('Possible ReDoS attack');

终极建议:能不用正则,就不用

对于复杂格式(如邮箱、URL、HTML),优先考虑:

  • 使用专用库(如 validator.js 的最新版);
  • 用解析器代替正则(如 DOMParser 解析 HTML);
  • 先做长度限制(如 if (input.length > 255) return false);
  • 在沙箱或 Worker 中执行高风险正则。

结语

正则表达式是强大的工具,
但不当使用,它就是埋在你代码里的“逻辑炸弹”。

记住:

用户输入 + 复杂正则 = 潜在 DoS 攻击面。

下次写 /.../ 之前,请先问自己:
“这个正则,会被恶意字符串卡死吗?”

安全无小事,一行 regex 也能毁掉整个系统。

转发给你团队里那个“正则高手”吧!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

从 Grunt 到 Vite:前端构建工具十几年的演化

如果你是做前端开发的,大概率接触过这些名字:

  • Grunt
  • Gulp
  • Webpack
  • Rollup
  • Parcel
  • Vite

很多人会有一个疑问:

为什么前端工具一直在换?
这些工具到底解决了什么问题?

如果把时间线拉长,你会发现这些工具并不是互相替代,而是在解决 不同阶段的工程问题

本文就从时间线和宏观视角,系统梳理前端构建工具的发展。


一、最早的问题:前端没有工程体系

在 2010 年之前,前端开发几乎没有“构建”概念。

一个典型项目结构是这样的:


index.html
style.css
app.js
jquery.js

开发流程通常是:

  1. 写 JS
  2. 手动合并文件
  3. 手动压缩
  4. 上传服务器

当项目变大之后,就出现了一些明显问题:

  • 文件越来越多
  • 代码无法自动压缩
  • 构建流程完全依赖人工
  • 项目缺乏自动化

于是第一代前端工具出现了。


二、Grunt:任务驱动的自动化工具

Grunt 是最早流行的前端自动化工具之一。

它的核心思想是:

用配置驱动任务自动执行

开发者可以配置各种任务,例如:

  • 压缩 JS
  • 编译 Sass
  • 合并文件
  • 监听文件变化

示例:

grunt.initConfig({
  uglify: {
    build: {
      src: "src/app.js",
      dest: "dist/app.min.js"
    }
  }
})

运行:

grunt build

就会自动完成任务。

Grunt 的特点

优点:

  • 提供自动化构建流程
  • 插件生态丰富
  • 可扩展

缺点:

  • 配置复杂
  • 执行速度慢
  • 每个任务都是独立进程

随着项目规模扩大,Grunt 的效率开始成为问题。

于是出现了新的工具。


三、Gulp:基于流的构建系统

Gulp 的设计目标是解决 Grunt 的性能问题。

核心思想:

使用 Node Stream(流)处理文件

文件不会被反复读写磁盘,而是通过内存流处理。

示例:

gulp.task("scripts", function () {
  return gulp.src("src/*.js")
    .pipe(uglify())
    .pipe(gulp.dest("dist"))
})

Gulp 的特点

优点:

  • 基于流处理,速度更快
  • API 更简洁
  • 代码式配置更灵活

缺点:

  • 仍然是任务工具
  • 不是模块打包工具

随着前端模块化的发展,一个新的问题出现了。


四、模块化时代:Webpack 的出现

随着以下技术的流行:

  • CommonJS
  • ES Modules
  • npm

前端代码不再是简单的脚本文件,而是 模块系统

例如:

import utils from "./utils"

浏览器当时无法直接处理这种依赖关系。

于是 Webpack 出现了。

Webpack 的核心思想是:

一切资源都是模块。

它不仅能处理 JS,还能处理:

  • CSS
  • 图片
  • 字体
  • JSON

示例:

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js"
  }
}

Webpack 会:

  1. 分析依赖
  2. 构建依赖图
  3. 打包成一个 bundle

Webpack 的特点

优点:

  • 强大的模块系统
  • 插件生态极其丰富
  • 支持复杂应用

缺点:

  • 配置复杂
  • 构建速度慢
  • 学习成本高

随着应用规模继续增长,Webpack 的开发体验开始成为瓶颈。


五、Rollup:为库而生的打包工具

Rollup 的目标非常明确:

打包 JavaScript 库。

它最大的特点是:

Tree Shaking

即删除未使用代码。

示例:

import { add } from "./math"

如果 math 里还有其他函数,Rollup 会自动移除。

Rollup 的特点

优点:

  • 打包结果更干净
  • 非常适合库开发
  • Tree Shaking 优秀

缺点:

  • 不适合大型应用
  • 插件生态不如 Webpack

六、零配置工具:Parcel

随着前端复杂度增加,很多开发者开始抱怨:

Webpack 配置太复杂。

于是 Parcel 出现了。

核心理念:

Zero Configuration

开发者只需要运行:

parcel index.html

Parcel 会自动:

  • 识别依赖
  • 编译代码
  • 打包资源

Parcel 的特点

优点:

  • 零配置
  • 开箱即用
  • 开发体验好

缺点:

  • 灵活性不如 Webpack
  • 社区生态较小

七、现代构建工具:Vite

随着浏览器原生支持 ES Module,新的构建模式出现了。

Vite 的核心思想是:

开发阶段不打包。

开发时:

浏览器直接加载 ES Modules

例如:

import App from "./App.js"

浏览器会直接请求这个模块。

只有在生产环境才会打包。

Vite 的特点

优点:

  • 启动极快
  • HMR 非常快
  • 配置简单

背后的关键技术是:

  • ES Modules
  • 按需编译
  • esbuild

八、构建工具的演化逻辑

如果从时间线总结,可以看到一条非常清晰的路径。

第一阶段

解决 自动化问题

工具:

  • Grunt
  • Gulp

目标:

自动化构建流程。


第二阶段

解决 模块化问题

工具:

  • Webpack
  • Rollup

目标:

管理前端模块依赖。


第三阶段

解决 开发体验问题

工具:

  • Parcel
  • Vite

目标:

提升开发速度。


九、从宏观角度看,这些工具改变了什么?

如果站在更高层看,前端构建工具的发展,其实改变了三件事。


1 前端从“脚本”变成“工程”

早期前端只是:

HTML + CSS + JS

现在前端是完整工程:

TypeScript
Sass
React
测试
构建
CI/CD

构建工具是这套工程体系的核心。


2 浏览器不再是唯一运行环境

现代前端需要支持:

  • SSR
  • SSG
  • Node
  • Edge Runtime

构建工具负责把代码转换成适合不同环境的版本。


3 开发效率革命

现代工具链带来的提升非常巨大:

  • 热更新
  • 自动打包
  • 按需加载
  • 代码拆分

大型项目开发效率因此大幅提升。


十、今天的前端构建生态

目前主流的工具格局大致是:

应用开发:

  • Vite
  • Webpack(仍然大量存在)

库开发:

  • Rollup
  • tsup
  • esbuild

新一代工具:

  • Turbopack
  • Rspack

这些工具的目标都很一致:

更快、更简单、更高效。


总结

前端构建工具的发展,大致经历了四个阶段:

1 自动化阶段(Grunt / Gulp) 2 模块化阶段(Webpack / Rollup) 3 工程化阶段(Webpack 生态) 4 极速开发阶段(Vite)

如果说一句总结的话:

前端工具的发展,本质上是在不断降低开发复杂度,同时提升工程能力。

而随着浏览器能力的增强,未来的构建工具可能会继续向 更快、更轻、更少配置 的方向发展。

OpenCode 进阶使用指南(第二章:Skills 系统)

本文档是《OpenCode 进阶使用指南》的第二章专注于 Skills 技能系统的深入讲解,预计阅读时间:30-40 分钟


目录

  1. 什么是 Skills 系统
  2. Skills 的核心概念
  3. 创建你的第一个 Skill
  4. SKILL.md 文件详解
  5. 高级技能编写技巧
  6. 技能调试与优化
  7. 团队协作与技能共享
  8. 实战案例集
  9. 底层原理剖析
  10. 常见问题与解决方案

什么是 Skills 系统

2.1.1 从重复劳动说起

想象一个场景:你刚入职一家新公司,发现团队的代码审查流程非常规范——每次提交都要检查代码风格、潜在 bug、性能问题、安全漏洞。这些检查点有十几条,每次审查都要一条条过。

刚开始你觉得这个流程很好,能保证代码质量。但几周后,你开始觉得烦了。每次审查都要重复同样的检查项,写同样的评论格式,提同样的修改建议。

你会想:"如果能把这个流程自动化就好了。"

Skills 系统就是来解决这个问题的。

2.1.2 Skills 的本质

Skills 是一套可复用的指令模板。

当你发现自己在重复做某类任务时,你可以:

  1. 把这类任务的步骤、标准、注意事项整理出来
  2. 写成 SKILL.md 文件
  3. 放到特定目录
  4. 以后让 AI 自动按照这个模板执行

简单来说,Skills 就是把"你的经验"变成"AI 的能力"。

2.1.3 Skills vs Agent 模式的区别

很多人问:"Skills 和 Agent 模式有什么区别?"

维度 Agent 模式 Skills 系统
定位 执行能力 知识封装
作用 让 AI 能动手 让 AI 懂规则
使用方式 临时指令 预定义模板
复用性 每次都要说 一次定义多次使用
适用场景 具体任务 标准化流程

举例说明

Agent 模式就像你让一个程序员"去重构代码"。他会自己思考怎么做。

Skills 系统就像你给程序员一本《代码重构手册》,告诉他:"以后重构都按这个手册来"。

两者可以结合使用:用 Skills 定义流程,用 Agent 模式执行流程。

2.1.4 Skills 能做什么

代码质量类

  • 代码审查检查清单
  • 重构标准流程
  • 性能优化指南
  • 安全审计规范

开发流程类

  • 新功能开发流程
  • Bug 修复标准流程
  • 测试用例生成规范
  • 文档编写模板

团队协作类

  • 代码提交规范
  • PR 审查标准
  • 发布流程检查
  • 新人入职指南

领域专业类

  • 前端组件开发规范
  • API 设计最佳实践
  • 数据库操作规范
  • 微服务拆分原则

Skills 的核心概念

2.2.1 技能发现机制

OpenCode 是如何找到 Skills 的?这涉及到一个"发现机制"。

搜索路径

当你启动 OpenCode 时,它会从当前目录开始,向上查找所有包含 .opencode/skills/.claude/skills/ 的目录。

搜索顺序:

当前工作目录/.opencode/skills/
当前工作目录/.claude/skills/
父目录/.opencode/skills/
父目录/.claude/skills/
...
直到 git 仓库根目录
~
~/.config/opencode/skills/    (全局)
~/.claude/skills/             (全局兼容)

为什么要向上查找?

考虑一个 monorepo 项目结构:

my-project/                    ← git 根目录
├── .opencode/skills/          ← 项目级 Skills
│   ├── code-review/
│   └── api-design/
├── packages/
│   ├── frontend/
│   │   └── .opencode/skills/  ← 前端专属 Skills
│   │       └── react-component/
│   └── backend/
│       └── .opencode/skills/  ← 后端专属 Skills
│           └── express-route/

如果你在 packages/frontend/ 目录下工作,OpenCode 会加载:

  1. packages/frontend/.opencode/skills/ - 前端专属
  2. my-project/.opencode/skills/ - 项目级
  3. ~/.config/opencode/skills/ - 全局

这样设计的好处:子项目可以覆盖或扩展父项目的 Skills。

2.2.2 技能加载过程

当一个 Skill 被发现后,OpenCode 会:

  1. 读取 SKILL.md:解析 YAML frontmatter 和 Markdown 内容
  2. 验证格式:检查必填字段、格式规范
  3. 解析依赖:如果有依赖其他 Skills,先加载依赖
  4. 注入上下文:将 Skill 信息注入到 AI 的上下文中
  5. 注册工具:如果 Skill 定义了自定义工具,注册到工具集

加载时机

  • 启动时:加载所有发现的 Skills
  • 运行时:可以动态加载新 Skills(通过 /skill reload
  • 按需加载:某些 Skills 可以设置 lazy loading

2.2.3 技能匹配机制

OpenCode 怎么知道什么时候该用哪个 Skill?

关键词匹配

Skill 的 description 字段包含关键词,AI 会根据用户输入匹配最合适的 Skill。

例如:

  • 用户说"帮我审查代码" → 匹配 code-review Skill
  • 用户说"生成 API 文档" → 匹配 api-doc Skill

显式调用

用户可以直接指定使用某个 Skill:

> /skill use code-review
> 开始审查 src/components/Button.tsx

自动推荐

OpenCode 会分析当前上下文,主动推荐可能相关的 Skills:

看起来你正在写 React 组件。
可用的 Skills:
1. react-component - React 组件开发规范
2. code-review - 代码审查检查清单
3. testing - 测试用例生成

要使用哪个?(输入编号或名称)

2.2.4 技能生命周期

一个 Skill 从创建到使用,经历以下阶段:

1. 开发阶段

  • 编写 SKILL.md
  • 本地测试
  • 迭代优化

2. 发布阶段

  • 提交到 Git
  • 版本标记
  • 文档更新

3. 分发阶段

  • 团队成员拉取
  • 全局安装(可选)
  • 内部 npm 包(可选)

4. 使用阶段

  • 自动匹配
  • 显式调用
  • 结果反馈

5. 维护阶段

  • 收集反馈
  • 更新优化
  • 版本迭代

创建你的第一个 Skill

2.3.1 准备工作

环境要求

  • OpenCode 0.1.40+
  • 一个 Git 仓库(推荐)
  • 基本的 Markdown 知识

选择技能类型

第一次创建 Skill,建议从简单的开始。比如:

  • 代码风格检查
  • 提交信息规范
  • 简单的代码生成

2.3.2 实战:创建一个代码提交规范 Skill

场景:团队要求所有提交信息都要遵循 Conventional Commits 规范,但新人总是记不住格式。

目标:创建一个 Skill,帮助生成符合规范的提交信息。

Step 1:创建目录结构

mkdir -p .opencode/skills/conventional-commit
cd .opencode/skills/conventional-commit

Step 2:编写 SKILL.md

创建文件 SKILL.md

---
name: conventional-commit
description: 帮助生成符合 Conventional Commits 规范的提交信息
version: 1.0.0
author: your-name
tags:
  - git
  - workflow
  - automation
---

# Conventional Commit 助手

## 什么时候用

当你需要写 Git 提交信息时,使用这个 Skill 来生成符合团队规范的提交信息。

## 规范说明

我们团队使用 Conventional Commits 规范,格式如下:

### Type 类型

- **feat**: 新功能
- **fix**: 修复 bug
- **docs**: 文档修改
- **style**: 代码格式修改(不影响功能)
- **refactor**: 重构
- **perf**: 性能优化
- **test**: 测试相关
- **chore**: 构建过程或辅助工具变动

### Scope 范围

根据修改的内容选择:

- **api**: API 相关
- **ui**: 界面相关
- **db**: 数据库相关
- **config**: 配置相关
- **deps**: 依赖相关

## 使用步骤

1. 询问用户本次修改的主要内容
2. 分析修改的文件,判断 type  scope
3. 生成多个提交信息选项
4. 让用户选择或修改
5. 输出最终的提交命令

## 示例

**示例 1:新功能**

用户:添加了用户登录功能

生成:

git commit -m "feat(auth): 添加用户登录功能

- 实现 JWT token 验证
- 添加登录页面 UI
- 集成后端登录接口

Closes #123"

**示例 2:Bug 修复**

用户:修复了登录按钮点击无响应的问题

生成:

git commit -m "fix(ui): 修复登录按钮点击无响应

问题原因:事件监听器未正确绑定
解决方案:使用 useEffect 确保 DOM 加载完成后再绑定

Fixes #456"

**示例 3:文档更新**

用户:更新了 API 文档

生成:

git commit -m "docs(api): 更新用户认证接口文档

- 添加 JWT 认证说明
- 补充错误码列表
- 添加请求示例"

## 注意事项

1. subject 首字母不要大写
2. subject 末尾不要加句号
3. body 每行不超过 72 个字符
4. 如果有关联的 issue,在 footer 中引用

Step 3:测试 Skill

在 OpenCode 中测试:

> /skill list
可用的 Skills:
- conventional-commit: 帮助生成符合 Conventional Commits 规范的提交信息

> 我要提交代码,帮我写提交信息
[AI 会使用 conventional-commit Skill]

AI:请描述一下本次修改的主要内容:

> 修复了用户列表页面分页不生效的问题

AI:分析修改内容...
      ↓
AI:为你生成了以下提交信息选项:

选项 1:
git commit -m "fix(ui): 修复用户列表分页不生效

- 修正分页参数传递错误
- 添加分页边界检查

Fixes #789"

选项 2:
git commit -m "fix(api): 修复用户列表接口分页参数处理

问题原因:page 参数未正确解析
解决方案:使用 parseInt 转换并添加默认值

Fixes #789"

请选择一个(输入 1 或 2),或告诉我修改建议:

> 1

AI:好的,使用选项 1。请复制以下命令执行:

git commit -m "fix(ui): 修复用户列表分页不生效

- 修正分页参数传递错误
- 添加分页边界检查

Fixes #789"

2.3.3 进阶:添加智能分析

上面的 Skill 还需要用户手动描述修改内容。我们可以让它更智能——自动分析修改的文件。

更新 SKILL.md

---
name: conventional-commit
description: 智能生成符合 Conventional Commits 规范的提交信息
version: 1.1.0
author: your-name
tags:
  - git
  - workflow
  - automation
  - smart-analysis
---

# Conventional Commit 助手(智能版)

## 新增功能

### 自动分析修改

使用以下命令自动分析工作区的修改:

git diff --name-only          # 查看修改了哪些文件
git diff                      # 查看具体修改内容
git status                    # 查看文件状态

### 文件类型映射

根据修改的文件自动推断 type  scope:

**文件路径  Type 推断**:

- `src/**/*.test.ts`  test
- `docs/**/*.md`  docs
- `package.json`, `package-lock.json`  chore
- 其他  需要人工判断(feat  fix)

**文件路径  Scope 推断**:

- `src/components/**/*.tsx`  ui
- `src/api/**/*.ts`  api
- `src/db/**/*.ts`  db
- `src/utils/**/*.ts`  utils
- `config/**/*.js`  config

## 使用步骤(智能版)

1. 执行 `git status`  `git diff --name-only`,了解修改了哪些文件
2. 执行 `git diff`,了解具体的修改内容
3. 根据文件路径和修改内容,自动推断 type  scope
4. 生成提交信息草稿
5. 询问用户确认或修改
6. 输出最终的提交命令

## 示例流程

用户:帮我生成提交信息

AI:[执行 git status]
    修改的文件:
    - src/components/UserList.tsx
    - src/api/users.ts
    - src/components/UserList.test.tsx

AI:[执行 git diff]
    UserList.tsx: 添加了分页功能
    users.ts: 添加了分页参数支持
    UserList.test.tsx: 添加了分页测试

AI:分析结果:
    - Type: feat (新功能)
    - Scope: ui (主要是 UI 组件)
    - 涉及 API 修改,需要在 body 中说明

生成提交信息:

git commit -m "feat(ui): 添加用户列表分页功能

- 实现前端分页组件和交互
- 后端 API 添加分页参数支持
- 添加单元测试覆盖

涉及文件:
- src/components/UserList.tsx
- src/api/users.ts
- src/components/UserList.test.tsx

Closes #234"

请确认或修改:

2.3.4 提交到团队仓库

创建好 Skill 后,记得提交到 Git:

git add .opencode/skills/conventional-commit/
git commit -m "feat(devtools): 添加 Conventional Commit 助手 Skill

- 自动生成符合规范的提交信息
- 支持智能分析修改内容
- 提供多种选项供选择"

git push

团队成员拉取代码后就能使用这个 Skill 了。


SKILL.md 文件详解

2.4.1 YAML Frontmatter 完整字段

SKILL.md 文件以 YAML frontmatter 开头,放在 --- 之间。

必需字段

字段 类型 说明 示例
name string 技能名称,必须唯一,只能用小写字母、数字和连字符 code-review
description string 技能描述,用于匹配和展示 自动进行代码审查

可选字段

字段 类型 说明 示例
version string 版本号,遵循 semver 1.2.3
author string 作者信息 张三 <zhangsan@example.com>
license string 许可证 MIT
tags array 标签,用于分类和搜索 ["testing", "automation"]
compatibility string 兼容性标识 opencode>=0.1.40
dependencies array 依赖的其他 Skills ["base-coding", "testing-utils"]
metadata object 自定义元数据 { "team": "frontend", "priority": "high" }

完整示例

---
name: react-component-review
description: 针对 React 组件的代码审查,检查 hooks 使用、性能优化、可访问性等
version: 2.1.0
author: 前端团队 <frontend@company.com>
license: MIT
tags:
  - react
  - review
  - performance
  - a11y
compatibility: opencode>=0.1.40
dependencies:
  - base-code-review
metadata:
  team: frontend
  priority: high
  review_level: strict
---

2.4.2 Markdown 内容结构

YAML frontmatter 之后是 Markdown 内容,用来详细说明技能的使用方法。

推荐结构

# 技能标题

## 什么时候用

描述使用场景和触发条件。

## 前置条件

列出使用这个技能前需要满足的条件。

## 输入参数

如果技能需要参数,说明参数格式和示例。

## 使用步骤

详细说明执行步骤,可以包含:

1. 第一步做什么
2. 第二步做什么
3. ...

## 检查清单

列出需要检查的项目,用复选框表示。

## 示例

提供具体的使用示例。

### 示例 1:场景 A

详细说明...

### 示例 2:场景 B

详细说明...

## 输出格式

说明执行结果的格式。

## 注意事项

列出常见问题和注意事项。

## 相关资源

- 链接 1
- 链接 2

2.4.3 特殊语法

变量替换

可以在 Skill 中使用变量,这些变量会在执行时被替换:

## 使用步骤

1. 读取文件 {{file_path}}
2. 检查 {{check_item}}
3. 输出结果到 {{output_path}}

使用时:

> /skill use code-review file_path=src/App.tsx check_item=performance

条件逻辑

可以使用条件注释:

<!-- if: framework == "react" -->

## React 特定检查

- 检查 hooks 规则
- 检查 JSX 语法
<!-- endif -->

<!-- if: framework == "vue" -->

## Vue 特定检查

- 检查模板语法
- 检查生命周期使用
<!-- endif -->

代码块

使用代码块展示示例代码:

// 示例代码
function example() {
  return "Hello";
}

表格

使用表格展示对照信息:

| 情况 | 处理方式   | 示例        |
| ---- | ---------- | ----------- |
| 正常 | 直接通过   | -           |
| 警告 | 提示但允许 | console.log |
| 错误 | 必须修复   | eval()      |

高级技能编写技巧

2.5.1 条件技能

根据不同的条件执行不同的逻辑。

示例:根据项目类型使用不同的检查规则

---
name: project-lint
description: 根据项目类型自动选择合适的代码检查规则
---

# 项目类型检测

## 检测项目类型

1. 检查 package.json 中的依赖:
   - 如果有 react  前端项目
   - 如果有 express  后端项目
   - 如果有 pytest  Python 项目

2. 根据类型加载对应的检查规则:

<!-- if: project_type == "frontend" -->
## 前端项目检查规则

### JavaScript/TypeScript
- ESLint 规则
- Prettier 格式
- TypeScript 类型检查

### React 特定
- Hooks 规则检查
- JSX 语法检查
- 组件命名规范
<!-- endif -->

<!-- if: project_type == "backend" -->
## 后端项目检查规则

### Node.js
- 异步处理检查
- 错误处理检查
- 安全问题检查

### API 设计
- RESTful 规范
- 参数校验
- 错误码规范
<!-- endif -->

2.5.2 组合技能

一个 Skill 可以调用其他 Skills。

示例:完整的代码提交流程

---
name: complete-code-submit
description: 完整的代码提交流程:检查  测试  生成提交信息  提交
dependencies:
  - code-review
  - conventional-commit
  - pre-commit-check
---

# 完整代码提交流程

## 流程步骤

1. **代码检查**(调用 code-review Skill)
   - 运行 linter
   - 类型检查
   - 安全检查

2. **测试验证**(调用 pre-commit-check Skill)
   - 运行单元测试
   - 运行集成测试
   - 检查测试覆盖率

3. **生成提交信息**(调用 conventional-commit Skill)
   - 分析修改内容
   - 生成符合规范的提交信息

4. **执行提交**
   - git add
   - git commit
   - (可选)git push

## 失败处理

如果某一步失败:
- 停止后续步骤
- 报告失败原因
- 提供修复建议

2.5.3 交互式技能

让 Skill 支持交互式输入。

示例:交互式项目初始化

---
name: interactive-init
description: 交互式项目初始化,根据用户选择生成项目结构
---

# 交互式项目初始化

## 步骤 1:选择项目类型

询问用户:
"你要创建什么类型的项目?"
选项:
1. React 前端应用
2. Node.js 后端 API
3. Python 脚本
4. 其他

根据选择进入不同分支。

## 步骤 2:收集项目信息

<!-- if: project_type == "react" -->
询问:
- 项目名称?
- 使用 TypeScript 吗?
- 使用哪个 UI 库?(Ant Design / Material-UI / Tailwind)
- 需要路由吗?
- 需要状态管理吗?
<!-- endif -->

## 步骤 3:确认配置

展示将要生成的配置:

项目:my-app 类型:React + TypeScript UI 库:Ant Design 路由:是状态管理:Redux Toolkit

确认生成吗?(y/n)

## 步骤 4:生成项目

根据确认的配置生成项目文件。

## 步骤 5:安装依赖并启动

cd {{project_name}}
npm install
npm run dev

2.5.4 模板技能

使用模板引擎生成代码。

示例:组件生成器

---
name: component-generator
description: 根据模板生成 React 组件
templates:
  - name: functional-component
    path: ./templates/FunctionalComponent.tsx
  - name: class-component
    path: ./templates/ClassComponent.tsx
---

# React 组件生成器

## 使用方法

> /skill use component-generator name=Button type=functional props="onClick: () => void; children: ReactNode"

## 模板变量

模板中可以使用以下变量:

- `{{component_name}}` - 组件名(大驼峰)
- `{{component_name_lower}}` - 组件名(小写)
- `{{props}}` - 属性定义
- `{{imports}}` - 导入语句
- `{{today}}` - 当前日期

## 生成的文件

- `src/components/{{component_name}}/index.tsx` - 组件主体
- `src/components/{{component_name}}/index.test.tsx` - 测试文件
- `src/components/{{component_name}}/index.stories.tsx` - Storybook 文档
- `src/components/{{component_name}}/styles.module.css` - 样式文件

模板文件示例templates/FunctionalComponent.tsx):

import React from 'react';
import styles from './styles.module.css';

export interface {{component_name}}Props {
  {{props}}
}

/**
 * {{component_name}} 组件
 *
 * @example
 * ```tsx
 * <{{component_name}} {{example_props}} />
 * ```
 */
export const {{component_name}}: React.FC<{{component_name}}Props> = (props) => {
  const { {{prop_names}} } = props;

  return (
    <div className={styles.container}>
      {/* 组件内容 */}
    </div>
  );
};

{{component_name}}.displayName = '{{component_name}}';

export default {{component_name}};

技能调试与优化

2.6.1 调试技巧

本地测试

在提交 Skill 前,先在本地测试:

# 创建测试目录
mkdir /tmp/skill-test
cd /tmp/skill-test

# 复制 Skill
cp -r /path/to/your/skill ./.opencode/skills/

# 启动 OpenCode 测试
opencode --agent

# 测试 Skill
> /skill list
> /skill use your-skill-name

日志输出

在 Skill 中添加调试信息:

## 调试模式

如果设置环境变量 `DEBUG_SKILL=true`,输出详细的执行日志。

DEBUG_SKILL=true opencode --agent

逐步执行

使用 step_by_step 模式查看每一步的执行:

> /agent config mode step_by_step
> /skill use your-skill

2.6.2 性能优化

减少上下文大小

  • 只加载必要的文件
  • 使用摘要而非完整内容
  • 压缩历史记录
---
name: optimized-skill
description: 优化过的技能,只加载必要信息
---

# 优化执行

## 文件读取优化

不要读取整个文件:
 cat src/large-file.ts

只读取相关部分:
 head -n 50 src/large-file.ts
 grep -n "function target" src/large-file.ts

## 上下文管理

- 使用 `max_lines_per_file: 100` 限制每文件读取行数
- 使用 `compression: true` 启用上下文压缩

缓存机制

对于重复使用的数据,可以缓存:

## 缓存配置

缓存以下数据以提高性能:

- 项目结构(5 分钟)
- 依赖列表(1 小时)
- 检查规则(24 小时)

2.6.3 错误处理

优雅降级

## 错误处理

如果某一步失败:

1. 尝试备用方案
2. 如果备用方案也失败,报告错误
3. 提供手动操作指南

### 示例:文件读取失败

错误:无法读取 src/config.ts

可能原因:

  1. 文件不存在
  2. 权限不足
  3. 文件被占用

解决方案:

  1. 检查文件是否存在:ls src/
  2. 检查权限:ls -la src/config.ts
  3. 手动创建文件(提供模板)

重试机制

## 网络操作重试

对于网络请求(如 API 调用):

- 失败时自动重试 3 次
- 每次重试间隔 1 秒
- 如果都失败,报错并退出

2.6.4 版本管理

语义化版本

遵循 semver 规范:

  • MAJOR(主版本):不兼容的修改
  • MINOR(次版本):新增功能,向后兼容
  • PATCH(修订):Bug 修复,向后兼容

示例

1.0.0 - 初始版本
1.1.0 - 添加自动分析功能
1.1.1 - 修复 scope 检测 bug
2.0.0 - 重构 API,不兼容 1.x

变更日志

维护 CHANGELOG.md:

# Changelog

## [2.0.0] - 2026-01-15

### 重大变更

- 重构 API,使用新的配置格式
- 不再支持 OpenCode < 0.1.40

### 新功能

- 支持 Vue 项目
- 添加自定义规则

### 修复

- 修复了 Windows 路径问题

团队协作与技能共享

2.7.1 团队 Skills 仓库

集中管理

创建一个专门的仓库管理团队 Skills:

company-opencode-skills/
├── README.md
├── skills/
│   ├── code-review/
│   │   └── SKILL.md
│   ├── commit-message/
│   │   └── SKILL.md
│   └── pr-template/
│       └── SKILL.md
├── templates/
│   └── component/
├── scripts/
│   └── install.sh
└── package.json

安装脚本scripts/install.sh):

#!/bin/bash
# 安装团队 Skills 到全局

SKILLS_DIR="$HOME/.config/opencode/skills"
REPO_URL="https://github.com/yourcompany/opencode-skills.git"

# 克隆或更新
if [ -d "$SKILLS_DIR/.git" ]; then
  cd "$SKILLS_DIR" && git pull
else
  git clone "$REPO_URL" "$SKILLS_DIR"
fi

echo "✓ 团队 Skills 已安装到 $SKILLS_DIR"
echo "可用的 Skills:"
ls "$SKILLS_DIR/skills/"

2.7.2 项目级 Skills

与项目代码一起管理

将项目特定的 Skills 放在项目仓库中:

my-project/
├── .opencode/
│   └── skills/
│       ├── domain-specific/     # 领域特定
│       ├── framework-specific/  # 框架特定
│       └── team-conventions/    # 团队约定
├── src/
├── package.json
└── README.md

Git 子模块(可选):

如果多个项目共享同一套 Skills,可以使用 Git 子模块:

# 添加 Skills 子模块
git submodule add https://github.com/yourcompany/opencode-skills.git .opencode/skills

# 克隆项目时递归克隆子模块
git clone --recursive https://github.com/yourcompany/my-project.git

2.7.3 Skills 市场(未来规划)

虽然目前 OpenCode 还没有官方的 Skills 市场,但可以预见未来的发展方向:

社区共享

  • 类似 npm 的 Skills 仓库
  • 版本管理和依赖解析
  • 评分和评论系统

企业内部市场

  • 私有 Skills 仓库
  • 权限管理
  • 审计日志

实战案例集

2.8.1 案例一:前端代码审查 Skill

---
name: frontend-code-review
description: 针对前端项目的全面代码审查,包括 React/Vue、性能、安全、可访问性
tags:
  - frontend
  - review
  - react
  - vue
---

# 前端代码审查

## 审查范围

### 1. 代码规范
- ESLint 规则合规性
- Prettier 格式
- 命名规范
- 文件组织

### 2. React 特定检查
- Hooks 使用规范
  - 不要在循环、条件中调用 Hooks
  - 依赖数组完整性
  - useEffect 清理函数
- 组件设计
  - 单一职责原则
  - Props 类型定义
  - 默认 Props
- 性能优化
  - useMemo/useCallback 使用
  - 不必要的重渲染
  - 代码分割

### 3. Vue 特定检查
- Composition API 使用
- Props/Emits 类型定义
- 生命周期使用
- 响应式数据

### 4. 样式检查
- CSS-in-JS 规范
- 样式隔离
- 响应式设计
- 浏览器兼容性

### 5. 可访问性 (a11y)
- 语义化 HTML
- ARIA 属性
- 键盘导航
- 屏幕阅读器支持

### 6. 安全检查
- XSS 防护
- 不安全的 innerHTML
- 敏感信息泄露

## 输出格式

```markdown
## 代码审查报告

### 概览
- 审查文件:{{file_count}} 
- 问题总数:{{issue_count}} 
  - 严重:{{critical_count}} 
  - 警告:{{warning_count}} 
  - 建议:{{suggestion_count}} 

### 详细问题

#### 1. [严重] src/components/UserForm.tsx:45
**问题**:useEffect 缺少依赖项
**代码**:

useEffect(() => {
  fetchUser(userId);
}, []); //  userId 未在依赖数组中

**修复**:

useEffect(() => {
  fetchUser(userId);
}, [userId]);

#### 2. [警告] src/utils/api.ts:12

**问题**:缺少错误处理 ...

### 建议改进

1. ...
2. ...

## 使用示例

> /skill use frontend-code-review path=src/components/

2.8.2 案例二:API 设计审查 Skill

---
name: api-design-review
description: RESTful API 设计审查,包括规范、安全性、性能
tags:
  - backend
  - api
  - rest
---

# API 设计审查

## 审查清单

### 1. RESTful 规范
- [ ] URL 设计是否符合资源导向
- [ ] HTTP 方法使用是否正确
- [ ] 状态码返回是否恰当
- [ ] 版本控制策略

### 2. 请求/响应设计
- [ ] 请求参数校验
- [ ] 响应格式统一
- [ ] 错误信息规范
- [ ] 分页设计

### 3. 安全性
- [ ] 认证机制
- [ ] 权限控制
- [ ] 输入验证
- [ ] 防注入
- [ ] 敏感数据保护

### 4. 性能
- [ ] 响应时间
- [ ] 缓存策略
- [ ] 并发处理
- [ ] 数据库查询优化

### 5. 文档
- [ ] OpenAPI/Swagger 文档
- [ ] 示例请求/响应
- [ ] 错误码说明

## 示例审查

**待审查 API**:

GET /api/getUserData?id=123

**问题**:
1. URL 不符合 RESTful 规范(动词 getUserData)
2. 使用 query 参数传递 ID(应该是路径参数)
3. 缺少版本号

**建议**:

GET /api/v1/users/123

2.8.3 案例三:数据库迁移 Skill

---
name: database-migration
description: 安全的数据库迁移流程,包括备份、迁移、验证、回滚
tags:
  - database
  - migration
  - devops
---

# 数据库迁移

## 流程

### 1. 迁移前检查
- [ ] 确认迁移脚本已测试
- [ ] 检查数据库连接
- [ ] 验证磁盘空间
- [ ] 通知相关人员

### 2. 备份

# 创建备份
mysqldump -u root -p database_name > backup_$(date +%Y%m%d_%H%M%S).sql

# 验证备份
mysql -u root -p -e "source backup_xxx.sql" test_db

### 3. 执行迁移


# 使用迁移工具
npx prisma migrate deploy
# 或
npm run migration:up

### 4. 验证

- [ ] 检查表结构
- [ ] 验证数据完整性
- [ ] 运行关键查询
- [ ] 检查应用日志

### 5. 回滚准备

如果失败,执行:

# 方式1:使用迁移工具回滚
npx prisma migrate rollback

# 方式2:从备份恢复
mysql -u root -p database_name < backup_xxx.sql

## 迁移脚本模板

-- 迁移:添加用户表
-- 作者:张三
-- 日期:2026-01-15

-- 开始事务
BEGIN;

-- 检查表是否存在
DROP TABLE IF EXISTS users;

-- 创建表
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 创建索引
CREATE INDEX idx_users_email ON users(email);

-- 验证
SELECT COUNT(*) FROM users;

-- 提交事务
COMMIT;

底层原理剖析

2.9.1 Skills 加载机制

发现阶段

// 伪代码
function discoverSkills(startPath) {
  const skills = [];
  let currentPath = startPath;

  while (currentPath !== root) {
    // 检查项目级 Skills
    if (exists(join(currentPath, '.opencode/skills'))) {
      skills.push(...loadSkillsFromDir(join(currentPath, '.opencode/skills')));
    }

    // 检查兼容的 Claude Skills
    if (exists(join(currentPath, '.claude/skills'))) {
      skills.push(...loadSkillsFromDir(join(currentPath, '.claude/skills')));
    }

    currentPath = parent(currentPath);
  }

  // 加载全局 Skills
  skills.push(...loadSkillsFromDir('~/.config/opencode/skills'));

  return skills;
}

**解析阶段**:

```javascript
function parseSkill(skillPath) {
  const content = readFile(skillPath + '/SKILL.md');

  // 解析 YAML frontmatter
  const { frontmatter, body } = parseFrontmatter(content);

  // 验证必填字段
  validateRequiredFields(frontmatter, ['name', 'description']);

  // 解析 Markdown
  const ast = parseMarkdown(body);

  return {
    meta: frontmatter,
    content: body,
    ast: ast,
    path: skillPath,
  };
}

注入阶段

function injectSkillToContext(skill, context) {
  // 将 Skill 信息添加到 AI 上下文
  context.availableSkills.push({
    name: skill.meta.name,
    description: skill.meta.description,
  });

  // 如果 Skill 定义了工具,注册工具
  if (skill.tools) {
    for (const tool of skill.tools) {
      context.registerTool(tool);
    }
  }
}

2.9.2 技能匹配算法

关键词匹配

function matchSkill(userInput, skills) {
  const scores = skills.map((skill) => {
    let score = 0;

    // 名称匹配
    if (userInput.includes(skill.meta.name)) {
      score += 10;
    }

    // 描述匹配
    const descriptionWords = skill.meta.description.toLowerCase().split(' ');
    const inputWords = userInput.toLowerCase().split(' ');
    const commonWords = intersection(descriptionWords, inputWords);
    score += commonWords.length * 2;

    // 标签匹配
    if (skill.meta.tags) {
      for (const tag of skill.meta.tags) {
        if (userInput.includes(tag)) {
          score += 5;
        }
      }
    }

    return { skill, score };
  });

  // 按分数排序
  scores.sort((a, b) => b.score - a.score);

  // 返回分数最高的
  return scores[0]?.score > threshold ? scores[0].skill : null;
}

上下文感知

function contextualMatch(userInput, skills, context) {
  // 根据当前上下文调整匹配权重
  const currentFile = context.currentFile;
  const projectType = detectProjectType(context.projectRoot);

  return skills.map((skill) => {
    let score = baseMatchScore(userInput, skill);

    // 文件类型匹配
    if (skill.fileTypes && skill.fileTypes.includes(currentFile.extension)) {
      score += 3;
    }

    // 项目类型匹配
    if (skill.projectTypes && skill.projectTypes.includes(projectType)) {
      score += 5;
    }

    return { skill, score };
  });
}

2.9.3 技能执行流程

[用户输入][意图识别] ← 使用 LLM 分析用户意图
    ↓
[技能匹配] ← 根据意图匹配 Skills
    ↓
[技能加载] ← 读取 SKILL.md 内容
    ↓
[参数提取] ← 从输入中提取参数
    ↓
[提示词构建] ← 构建执行提示词
    ↓
[LLM 执行] ← AI 执行 Skill 指令
    ↓
[结果输出] ← 返回执行结果

2.9.4 提示词工程

系统提示词片段

你是一名专业的软件开发助手。你可以使用以下 Skills 来帮助用户:

<available_skills>
{{#each skills}}
<skill>
  <name>{{this.name}}</name>
  <description>{{this.description}}</description>
</skill>
{{/each}}
</available_skills>

当用户需要执行特定任务时,你可以选择最合适的 Skill 来帮助他们。

使用 Skill 的方式:
1. 分析用户需求
2. 匹配最合适的 Skill
3. 调用 skill({ name: "skill-name" }) 加载 Skill
4. 根据 Skill 的指引执行任务
5. 报告执行结果

约束:
- 只在用户需要时使用 Skill
- 不要主动推荐不相关的 Skill
- 执行过程中可以询问用户确认

任务提示词构建

function buildSkillPrompt(skill, userInput, context) {
  return `
# 当前任务

用户输入:${userInput}

当前上下文:
- 项目类型:${context.projectType}
- 当前文件:${context.currentFile}
- 相关文件:${context.relatedFiles.join(', ')}

# 使用的 Skill

名称:${skill.meta.name}
描述:${skill.meta.description}

## Skill 内容

${skill.content}

# 执行要求

请根据上述 Skill 的指引,帮助用户完成任务。
如果需要更多信息,可以向用户提问。
`;
}

常见问题与解决方案

2.10.1 Skill 不生效

问题:创建了 Skill,但 OpenCode 没发现。

检查清单

  1. 路径是否正确

    • 必须在 .opencode/skills/<name>/.claude/skills/<name>/
    • 文件名必须是 SKILL.md(全大写)
  2. YAML 格式是否正确

    • 必须用 --- 包裹
    • namedescription 必须存在
    • 缩进必须用空格,不能用 Tab
  3. 名字是否符合规范

    • 只能用小写字母、数字和连字符
    • 必须和文件夹名完全一致
  4. 是否重新加载

    • 重启 OpenCode
    • 或执行 /skill reload

2.10.2 Skill 匹配错误

问题:OpenCode 总是匹配到错误的 Skill。

解决方案

  1. 优化 description

    • 使用更具体、独特的描述
    • 包含关键词
  2. 使用显式调用

    • /skill use skill-name
  3. 调整标签

    • 添加更多标签帮助匹配
  4. 上下文感知

    • 在 Skill 中指定 fileTypesprojectTypes

2.10.3 Skill 执行失败

问题:Skill 匹配成功,但执行时报错。

排查步骤

  1. 检查 YAML 语法
  2. 检查 Markdown 格式
  3. 查看 OpenCode 日志
  4. 简化 Skill 内容,逐步测试
  5. 检查依赖的 Skills 是否存在

2.10.4 性能问题

问题:使用 Skill 后,响应变慢。

优化建议

  1. 减少上下文大小

    • 限制加载的文件数量
    • 使用文件摘要而非完整内容
  2. 使用缓存

    • 缓存不变的数据
    • 设置合理的缓存时间
  3. 延迟加载

    • 只在需要时加载大型资源
    • 使用 lazy: true 标记
  4. 优化提示词

    • 减少不必要的说明
    • 使用更简洁的语言

总结与下一步

本章要点

  1. Skills 是什么:可复用的指令模板,把经验变成工具
  2. 如何创建:编写 SKILL.md 文件,放到指定目录
  3. 高级技巧:条件技能、组合技能、交互式技能、模板技能
  4. 调试优化:本地测试、性能优化、错误处理
  5. 团队协作:共享 Skills、版本管理
  6. 底层原理:发现机制、匹配算法、执行流程

学习路径

初级

  • 理解 Skills 概念
  • 创建简单的 Skill
  • 在团队内共享

中级

  • 掌握高级技巧
  • 优化 Skill 性能
  • 处理复杂场景

高级

  • 开发 Skill 框架
  • 贡献开源社区
  • 设计 Skill 生态

下一步

学完 Skills 系统后,建议继续学习:

  • 第三章:MCP 集成 - 让 AI 能使用外部工具
  • 第五章:最佳实践 - 掌握使用技巧

文档信息

  • 字数:约 8,000 字
  • 适用版本:OpenCode 0.1.40+

相关资源

(第二章完)

浏览器到底在偷偷帮你做什么?——HTTP缓存与刷新机制

你的浏览器可能比你想象中更"勤俭持家"。这篇文章聊聊它是怎么帮你省流量、省时间的,以及什么时候该阻止它。


你有没有注意过这样一个现象?

第一次打开一个网页,要等好几秒。第二次打开同一个网页,基本秒开。

你是不是以为浏览器記住了这个页面?它到底是怎么做到的?

今天我们来聊聊浏览器缓存——这个你天天在用,却可能从来没认真了解过的机制。


原文地址

墨渊书肆/浏览器到底在偷偷帮你做什么?——HTTP缓存与刷新机制


缓存到底是个什么东西?

想象一下这个场景。

你每天早上都去楼下的早餐店买豆浆。第一次去的时候,老板娘要问你「要甜的還是甜的」「要不要加鸡蛋」「打包还是在这吃」——问半天,你回答半天。

但如果你连着去了一个星期,每天都点「原味豆浆,不加糖,带走」。

第七天的时候,你刚走到门口,老板娘已经把豆浆打包好了递给你,连话都不用说一句。

这就是缓存。

浏览器做的就是这件事。第一次访问一个网页,它要千里迢迢去服务器「要」资源。第二次访问,它就聪明了——直接从自己的「小仓库」里拿出来给你用,省时又省力。

但问题来了:浏览器怎么知道哪些东西可以直接用?哪些东西已经过期了?

这就涉及到HTTP缓存的两个核心概念:强缓存协商缓存


强缓存:先用了再说

强缓存就像是你跟浏览器达成的一个小协议。

你告诉它:「这个资源在未来一年内都不会变,你就放心大胆地用,别来问我。」

浏览器听了这话,就直接把资源扔给你,连服务器的大门都不带敲的。

那这个"一年"的承诺是怎么实现的呢?

Expires:看时间

最早的方式是用Expires这个响应头:

Expires: Wed, 21 Oct 2026 07:28:00 GMT

这就像在商品上贴了个过期日期:「2026年10月21日之前都有效。」

但这个方式有个bug:如果你的电脑时间和服务器时间不一致,比如你把电脑调快了一个月,那缓存就直接失效了——浏览器会以为已经过期了,实际上还能用。

Cache-Control:更聪明的做法

后来HTTP/1.1推出了Cache-Control,就像给缓存装了个更智能的计时器:

Cache-Control: max-age=3600, public

max-age=3600的意思是:缓存有效期是3600秒,也就是一个小时。

这个比Expires靠谱多了——它是相对时间,不受系统时间影响。

而且Cache-Control还支持一堆指令,让你能更精细地控制缓存行为:

指令 意思 什么时候用
public 缓存可以放在CDN、代理服务器上 静态资源
private 只准浏览器自己缓存 个性化内容
no-cache 每次用之前先问问我 经常变化的资源
no-store 别存了,每次都重新拿 敏感数据
immutable 这东西永远不会变 版本化后的静态资源

这里有个重点:no-cache不是"不缓存",而是"用之前先问一下"。


协商缓存:问一下再决定

强缓存虽然快,但有个问题:如果服务器上的资源已经变了,浏览器可不知道。

你跟浏览器说「这个文件缓存一年」,结果三个月后你更新了网站,浏览器还在用旧的——用户就看不到新内容了。

怎么办?

协商缓存就是这个问题的答案。

它的逻辑是这样的:浏览器还是要去问一下服务器:「我这里有个缓存,还能用吗?」

服务器说「能用」(资源没变),返回304 Not Modified,浏览器继续用缓存。

服务器说「不能用」(资源变了),返回200 OK + 新资源,浏览器更新缓存。

Last-Modified:看修改时间

最早的方式是用Last-Modified

# 服务器返回
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

# 浏览器下次请求
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

就像你问老板:「这个文件最后改过是什么时候?如果还是那天下午,我就继续用我的版本。」

但这个方式有个缺点:精度只到秒。如果一秒内文件被修改了两次,浏览器可看不出来。

ETag:看"指纹"

后来就有了ETag,它给每个文件生成一个"指纹":

# 服务器返回
ETag: "abc123"

# 浏览器下次请求
If-None-Match: "abc123"

这个指纹通常是文件的MD5哈希值,或者其他能唯一标识内容的东西。

服务器收到请求后,会比较一下ETag:如果指纹一样,说明内容没变, 返回304;如果不一样,返回200 + 新内容。

划重点:ETag的优先级高于Last-Modified。如果服务器同时给了这两个,浏览器会先用ETag。


缓存到底是怎么工作的?

让我给你串起来整个流程:

你访问一个页面
    ↓
浏览器先看强缓存(Cache-Control / Expires)
    ↓
[命中了] → 直接从本地拿 → 页面秒开 ✅
    ↓ [没命中]
浏览器带着缓存标识去问服务器
    ↓
服务器比较ETag / Last-Modified
    ↓
[资源没变] → 返回304 → 浏览器用缓存 → 依然很快 ⚡
    ↓ [资源变了]
服务器返回200 + 新资源 + 新标识
    ↓
浏览器更新缓存,展示新内容

这个流程你理解之后,很多奇怪的现象就能解释了:

  • 为什么更新了CSS但页面没变?——可能被强缓存了
  • 为什么F5刷新后还是旧内容?——正常,优先用强缓存
  • 为什么Ctrl+F5就变了?——强制刷新,绕过了所有缓存

刷新这件事,讲究这么多?

说到刷新,我发现很多人(包括以前的我)对浏览器的刷新按钮有误解。

你以为的刷新:「重新加载这个页面」

实际上的刷新:「emmm,让我先看看缓存有没有过期」

正常刷新(F5 / 点击链接)

这是最常用的方式。

浏览器会先看强缓存,过期了吗?没有就直接用。过期了就去协商,服务器说能用就用。

结果:可能快(304),也可能慢(200),取决于缓存状态。

强制刷新(Ctrl+Shift+R / Ctrl+F5)

这个才是真正的"重新加载"。

浏览器会跟服务器说:「少废话,把所有资源都给我拿新的,别跟我提缓存!」

请求头里会带上Cache-Control: no-cache,或者直接忽略所有的缓存标识。

结果:总是200,慢但保证最新。

开发者工具里的选项

Chrome DevTools里其实有更精细的控制:

  • 禁用缓存:只有DevTools打开时才生效,模拟no-cache
  • 硬性重新加载:等于强制刷新
  • 空缓存并硬性重新加载:先清空所有缓存,再强制刷新——相当于把浏览器的小仓库一把火烧了

说完了缓存,来聊聊安全

缓存虽好,但有时候也会带来安全问题。

比如你登录了一个网站,浏览器缓存了你的个人信息。然后别人用了你的电脑,打开同一个网站——诶,怎么直接就登录了?

这就是缓存的"副作用"。所以有些东西是不能缓存的。

no-store:敏感数据别存

Cache-Control: no-store

这个指令告诉浏览器:「看归看,但别存到硬盘上。」内存里用完就扔,下次重启就没了。

登录后的用户信息、token、支付相关的数据——这些都应该加上no-store

private:只存浏览器

Cache-Control: private

这个的意思是:「可以缓存,但只能存在用户自己的浏览器里,别给CDN、代理那些中间商。」

用户信息存private,静态资源存public——这是基本的安全常识。


聊完缓存,再聊聊HTTPS

既然说到安全,顺便提一下HTTPS。

你可能知道HTTPS是"加密的HTTP",但它跟缓存有什么关系呢?

其实没有直接关系。但HTTPS有一个相关的安全头:

Strict-Transport-Security: max-age=31536000; includeSubDomains

这叫HSTS,简单说就是告诉浏览器:「以后别用HTTP跟我玩了,直接给我用HTTPS。」

防止一种叫"SSL剥离"的攻击——黑客在中间把HTTPS降级成HTTP,偷看你的数据。

还有哪些安全头值得关注?

作用
X-Frame-Options: DENY 防止别人把你的页面嵌在iframe里(点击劫持)
X-Content-Type-Options: nosniff 告诉浏览器别乱猜内容类型
Content-Security-Policy 限制资源加载来源,防止XSS
Referrer-Policy 控制 Referrer 信息泄露

这些安全头配置起来不麻烦,但能挡住很多常见攻击。建议都配置上。


到底该怎么用?

说了一堆原理,最后来点实用的。

不同资源,不同策略

资源类型 推荐策略
JS / CSS / 图片 max-age=31536000, immutable(一年,不用改)
HTML页面 no-cache,每次都协商
API接口 no-cacheprivate
用户敏感数据 no-store

核心原则

  • 静态资源:狠命缓存,文件名带hash,变了就改文件名
  • HTML:别缓存,随时要最新
  • API:看场景,频繁变化的要协商,不变的可以强缓存
  • 敏感数据:有多远滚多远,别缓存

总结一下

这篇文章聊了浏览器缓存和刷新机制的核心概念:

  • 强缓存Cache-Control / Expires):直接用,不过问服务器
  • 协商缓存ETag / Last-Modified):问一下服务器能不能用
  • 刷新:F5看缓存,Ctrl+F5硬刷新
  • 安全no-store防敏感数据泄露,private防中间商缓存
  • HTTPS相关:HSTS、安全响应头

理解这些机制之后,你就能解释很多奇怪的现象,也能更好地优化你的Web应用性能。

用 AI,0 基础复刻网页顶级特效!😀

先看效果

螺旋效果.gif在线访问地址包含了移动端版本和 PC 版本。

这个效果其实是我模仿 Awwwards 顶级网站的 3D 特效复刻出来的。核心框架使用的是 Three.js。

更夸张的是:

我对于 Three.js 的知识储备几乎是 0。好吧,实际上花了 4 个小时了解基本概念和学习 Shader(点击文字,会跳转到学习的视频(B站),请关注一下,是我的账号,因为有 100 粉丝后 B 站才能解锁发合集的功能, 我会将更好的酷炫动效视频教程分享给你 )。

而实现这个效果的核心工具只有一个:AI + 正确的 Vibe Coding 方法

过去是:学习 -> 写代码 -> 调试 -> 再学习

现在是:理解效果 -> 拆解核心算法 -> 提示 AI -> 快速迭代

核心能力不再是手写代码。而是:如何和 AI 协作。接下来我用一个最关键的例子说明。

为什么很多人用 AI 写网页特效总是失败?

假设我们要实现这样一个效果:图片围绕形成一个 3D 圆柱画廊(如上面展示的动画效果一样,)

大多数人给 AI 的提示词是这样的:

使用 Three.js 做一个 3D 图片画廊。

要求:

1. 页面中展示多张图片卡片
2. 图片在 3D 空间中排列,看起来有层次感
3. 用户滚动页面时,图片会产生移动动画
4. 整体效果类似一个环绕的圆柱型的画廊
5. 动画需要流畅自然

请给出完整代码。

如果你这样问 AI。我可以很负责地说:90% 情况下生成不出来。即使你用的是:Claude, GPT 最强的模型。

原因很简单:提示词太模糊。,而模糊的本质是确实对这类效果知之甚少。

AI 编程的关键:给 AI “思考路径”

后来我换了一种提示方式。效果完全不一样。提示词如下:

# 角色(Role)

你是一名 Three.js 工程师。
实现一个 Cylindrical Image Gallery。

# 要求(Requirement)

1. 使用 Three.js
2. 图片使用 PlaneGeometry 提供的免费图片
3. 将图片均匀排列在一个圆柱表面

位置计算公式:

const angle = (index / total) * 2π
const x = cos(angle) * radius
const z = sin(angle) * radius

# 技术栈
html + css + javascript + threejs

# 输出
完整 HTML

你会发现一个关键区别:

我给了 AI 一个核心算法。

也就是这个:

const angle = (index / total) * 2π
const x = cos(angle) * radius
const z = sin(angle) * radius

只要 AI 有这个信息。生成效果成功率会暴涨。

真正的 Vibe Coding:不是“不会写代码”

很多人误解了 Vibe Coding。他们以为是:什么都不懂,让 AI 写代码,其实这是错的,起码目前为止做简单的效果没问题,复杂的效果还是远远不够的。

所以目前最佳的 Vibe Coding 实现酷炫的网页效果这一领域而言,需要提供一定的。核心算法素材

比如我自己平时就会收集一些常见特效算法,例如:

  • 圆柱螺旋布局算法
  • 无限滚动算法
  • 图片弯曲 Shader(如上面展示的案例,图片是弯曲环绕的)

例如这段代码j就是 圆柱螺旋布局算法

class RingLayoutEngine {
  calculatePosition(index, scrollProgress, config) {
    const angle = (index / config.perLevel) * Math.PI * 2 + scrollProgress * 0.03;

    const x = Math.cos(angle) * config.radius;
    const z = Math.sin(angle) * config.radius;

    let y = (baseY - scrollProgress * 0.8) % fullHeight;

    return { x, y, z, angle };
  }
}

注:文章末尾附录,会有整个网页的 ai 代码素材的链接。

复刻酷炫网页的公式

当我看到一个酷炫网站时。我只需要:

  1. 观察效果
  2. 找相似算法
  3. 给 AI 当案例
  4. 生成代码

这就是 AI 时代,当前 AI 发展阶段的酷炫网页开发流程。

对于我实现的网页特效,

如何获取更多素材

目前来说还是需要懂一定代码的人去提取,当然有兴趣的同学可以加入我的酷炫网页成长群(免费),一起沟通分享,我也会偶尔分享一些类似的代码片段。

当然也有付费的群,很便宜,目前是 199 终身(后续人多了一定会涨很多),然后至少每个月 4 个 awwwards(聚集了世界顶级网页特效的网站) 级别效果的网站源码。还有很多动效方面的学习资料。我的能力覆盖 ai 全栈开发,还有高级前端开发等等内容,完全可以作为福利分享到咋们的酷炫网页成长群。

如果你是完全编程基础很少,或者 0 基础,我也很愿意分享你很多教程,补一些基础的编程基础会更好的帮助你跟 ai 协作。

未来更多分享内容

未来我会持续分享 AI + 前端开发的内容,例如:

如果你对这些内容感兴趣,可以关注我的公众号,包括在小红书、知乎、x 等平台的账号,都叫:ai超级个人。

我是一个不断进步的人,并且会一直进步下去,还有一些隐藏技能还在提升中,例如英语的听说,坚持已经两年了,我有自己的一套方法论,如果能成,也会单独拿来分享。

最后,我希望做的事情很简单:在 AI 时代,帮助更多人掌握真正的技术杠杆。

我们一起成长。一起做出更酷的东西!加油!

附录

拒绝盲目 Git:VS Code 神级插件 GitLens 的 9 个进效杀手锏

前言:在团队开发中,你是否常发出“灵魂质问”:这段代码谁写的?为什么这么写?两周前它长什么样?

如果你的 VS Code 只装了 GitLens 却只看行末那行半透明的 blame,那真是暴殄天物。今天分享 9 个让开发效率翻倍的 GitLens 进阶技巧,带你开启“上帝视角”。


1. 瞬时定位:Current Line Blame(责权显示)

痛点: 看到一段诡异代码,想找人对齐逻辑却不知道找谁。

技巧: 默认开启。只需光标停留,行末即刻浮现作者、时间和提交信息。

  • 进阶操作: 点击行末提示,会直接弹出该 Commit 的详细变更看板。
  • 避坑配置: 觉得太吵?设置 gitlens.currentLine.enabled: false,改为悬停触发。

2. 深度溯源:File History(文件时光机)

痛点: 一个文件被改烂了,想看它在两周前的逻辑演变。

技巧: 在 GitLens 侧边栏找到 File History 视图。

这里按时间线列出了所有修改记录。点击任一记录,VS Code 会进入 Diff 模式,左侧是历史,右侧是当前,改动点一目了然。


3. 视觉破局:Commit Graph(可视化图谱)

痛点: 分支错综复杂,git log --graph 看着眼晕。

技巧: 点击状态栏或侧边栏的 Graph 图标。

它会生成一张极其漂亮的彩色分支树状图。不仅能看,你还可以直接在图上右键进行 Merge、Rebase、甚至 Cherry-pick,将复杂的命令行操作转化为直观的点选。


4. 侦探模式:Search Commits(全局搜索)

痛点: 记得写过某段逻辑,但记不清在哪个 Commit 里的,也不知道文件叫啥。

技巧: 使用 Search & Compare 面板。

支持按 Message(提交信息)、Author(作者)、甚至是 Changed Files(修改的文件名) 进行模糊搜索。找代码历史,不再靠翻页。


5. 极速对撞:Compare Commits(版本比对)

痛点: 准备发版前,想确认 main 分支比 dev 多了哪些内容。

技巧: 在 GitLens 侧边栏选中两个分支或两个 Commit,右键选择 Compare with...

它会把所有的差异文件打包列出。这是进行 Pre-code Review 的最佳手段。


6. 优雅停车:Visual Stashes(暂存管理)

痛点: git stash 存了太多东西,早忘了 stash@{2} 里写的是啥。

技巧: 展开 GitLens 的 Stashes 面板。

这里可以预览每一个 Stash 里的代码改动,支持一键 Apply(应用)或 Pop(弹出)。再也不用盲猜暂存区里的内容。


7. 链路打通:GitHub/GitLab 深度集成

痛点: 看到 Commit 里的 PR 编号,还要手动去浏览器搜?

技巧: 配置远程仓库权限后,GitLens 会在悬停框中直接展示 PR 标题、状态和链接

点击编号,直接在浏览器跳到 PR 页面。打通了代码与协作工具的“最后一公里”。


8. 避雷雷达:File Heatmap(代码热力图)

痛点: 新接手项目,不知道哪些文件逻辑最复杂、最不稳定。

技巧: 开启侧边栏的 Heatmap 模式。

编辑器左侧行号处会根据修改频率显示不同深浅的色块。颜色越红/深,代表该处改动越频繁。 相信我,这种地方通常就是 Bug 的老巢。


9. 局部回滚:Open Changes with Working File

痛点: 发现当前代码改错了,只想把其中一小段还原回 3 天前的样子。

技巧: 在文件历史中找到 3 天前那个版本,右键选择 Open Changes with Working File

在 Diff 窗口中,你可以精准地把左侧(旧代码)的某几行点击小箭头同步到右侧(当前代码),实现“手术刀式”的局部回滚。


💡 总结与建议

GitLens 是典型的“瑞士军刀”,功能多但没必要全开。

  • 极致性能党: 建议关闭所有“自动浮现”功能,仅通过侧边栏面板进行交互。
  • 配置建议: 搜索 gitlens.plus.enabled。GitLens 很多高级功能(如 Graph)现在属于 Pro 版,但基础版功能对个人开发者已完全够用。

每日一题-找出所有稳定的二进制数组 II🔴

给你 3 个正整数 zero ,one 和 limit 。

一个 二进制数组 arr 如果满足以下条件,那么我们称它是 稳定的

  • 0 在 arr 中出现次数 恰好 为 zero 。
  • 1 在 arr 中出现次数 恰好 为 one 。
  • arr 中每个长度超过 limit 的 子数组同时 包含 0 和 1 。

请你返回 稳定 二进制数组的 数目。

由于答案可能很大,将它对 109 + 7 取余 后返回。

 

示例 1:

输入:zero = 1, one = 1, limit = 2

输出:2

解释:

两个稳定的二进制数组为 [1,0] 和 [0,1] ,两个数组都有一个 0 和一个 1 ,且没有子数组长度大于 2 。

示例 2:

输入:zero = 1, one = 2, limit = 1

输出:1

解释:

唯一稳定的二进制数组是 [1,0,1] 。

二进制数组 [1,1,0] 和 [0,1,1] 都有长度为 2 且元素全都相同的子数组,所以它们不稳定。

示例 3:

输入:zero = 3, one = 3, limit = 2

输出:14

解释:

所有稳定的二进制数组包括 [0,0,1,0,1,1] ,[0,0,1,1,0,1] ,[0,1,0,0,1,1] ,[0,1,0,1,0,1] ,[0,1,0,1,1,0] ,[0,1,1,0,0,1] ,[0,1,1,0,1,0] ,[1,0,0,1,0,1] ,[1,0,0,1,1,0] ,[1,0,1,0,0,1] ,[1,0,1,0,1,0] ,[1,0,1,1,0,0] ,[1,1,0,0,1,0] 和 [1,1,0,1,0,0] 。

 

提示:

  • 1 <= zero, one, limit <= 1000

找出所有稳定的二进制数组 II

方法一:记忆化搜索

思路

根据稳定数组的前两个条件,可知稳定数组的长度为 $\textit{zero} + \textit{one}$。第三个条件可知,稳定数组不存在长度为 $\textit{limit} + 1$ 的全 $0$ 或全 $1$ 子数组。

接下来我们分解问题,包含 $\textit{zero}$ 个 $0$ 和 $\textit{one}$ 个 $1$ 的稳定数组,末位元素可能为 $1$,也可能为 $0$。

  • 如果末位元素为 $1$,我们需要知道有多少个包含 $\textit{zero}$ 个 $0$ 和 $\textit{one}-1$ 个 $1$ 的稳定数组,再去掉“由于添加了一个 $1$ 而使得原来的稳定数组变得不稳定”的情况。那么有哪些情况会使得原来稳定的数组变得不稳定呢?即原来的稳定数组的末尾连续 $1$ 的个数正好为 $\textit{limit}$ 个。在这种情况下,添加一个 $1$ 会使得原来稳定的数组变得不稳定。这种情况出现的次数,即为包含 $\textit{zero}$ 个 $0$ 和 $\textit{one}-1-\textit{limit}$ 个 $1$,且末位元素为 $0$ 的稳定数组的个数。
  • 如果末位元素为 $0$,我们需要知道有多少个包含 $\textit{zero}-1$ 个 $0$ 和 $\textit{one}$ 个 $1$ 的稳定数组,再去掉“由于添加了一个 $0$ 而使得原来的稳定数组变得不稳定”的情况。

这样一来,我们就将问题分解为子问题了,可以用动态规划求解。用函数 $\textit{dp}(\textit{zero},\textit{one},\textit{lastBit})$,来求解包含 $\textit{zero}$ 个 $0$ 和 $\textit{one}$ 个 $1$,并且末位元素为 $\textit{lastBit}$ 的稳定数组的个数,其中 $\textit{lastBit}$ 为 $0$ 或 $1$。根据上面的讨论,可以得到递推公式:

  • $\textit{dp}(\textit{zero},\textit{one},0)$ = $\textit{dp}(\textit{zero}-1,\textit{one},0) + \textit{dp}(\textit{zero}-1,\textit{one},1) - \textit{dp}(\textit{zero}-1-\textit{limit},\textit{one},1)$
  • $\textit{dp}(\textit{zero},\textit{one},1)$ = $\textit{dp}(\textit{zero},\textit{one}-1,0) + \textit{dp}(\textit{zero},\textit{one}-1,1) - \textit{dp}(\textit{zero},\textit{one}-1-\textit{limit},0)$。

另外考虑边界情况。如果 $\textit{zero}$ 为 $0$,那么当 $\textit{lastBit}$ 为 $1$ 或者 $\textit{one}$ 大于 $\textit{limit}$ 时,不存在这样的稳定数组,返回 $0$,否则返回 $1$。如果 $\textit{zero}$ 为 $1$,也有对应的结论。

我们用记忆化搜索的方式来计算结果,记录所有的中间状态,最终返回 $\textit{dp}(\textit{zero},\textit{one},0)$ + $\textit{dp}(\textit{zero},\textit{one},1)$ 取模后的结果。

代码

###Python

class Solution:
    def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
        mod = 10 ** 9 + 7

        @cache
        def dp(zero, one, lastBit):
            if zero == 0:
                if lastBit == 0 or one > limit:
                    return 0
                else:
                    return 1
            elif one == 0:
                if lastBit == 1 or zero > limit:
                    return 0
                else:
                    return 1
            if lastBit == 0:
                res = dp(zero - 1, one, 0) + dp(zero - 1, one, 1)
                if zero > limit:
                    res -= dp(zero - limit - 1, one, 1)
            else:
                res = dp(zero, one - 1, 0) + dp(zero, one - 1, 1)
                if one > limit:
                    res -= dp(zero, one - limit - 1, 0)
            return res % mod
            
        res = (dp(zero, one, 0) + dp(zero, one, 1)) % mod
        dp.cache_clear()
        return res

###C++

class Solution {
public:
    int numberOfStableArrays(int zero, int one, int limit) {
        int mod = 1e9 + 7;
        vector<vector<vector<int>>> memo(zero + 1, vector<vector<int>>(one + 1, vector<int>(2, -1)));

        function<int(int, int, int)> dp = [&](int zero, int one, int lastBit) -> int {
            if (zero == 0) {
                return (lastBit == 0 || one > limit) ? 0 : 1;
            } else if (one == 0) {
                return (lastBit == 1 || zero > limit) ? 0 : 1;
            }

            if (memo[zero][one][lastBit] == -1) {
                int res = 0;
                if (lastBit == 0) {
                    res = (dp(zero - 1, one, 0) + dp(zero - 1, one, 1)) % mod;
                    if (zero > limit) {
                        res = (res - dp(zero - limit - 1, one, 1) + mod) % mod;
                    }
                } else {
                    res = (dp(zero, one - 1, 0) + dp(zero, one - 1, 1)) % mod;
                    if (one > limit) {
                        res = (res - dp(zero, one - limit - 1, 0) + mod) % mod;
                    }
                }
                memo[zero][one][lastBit] = res % mod;
            }
            return memo[zero][one][lastBit];
        };

        return (dp(zero, one, 0) + dp(zero, one, 1)) % mod;
    }
};

###Java

class Solution {
    static final int MOD = 1000000007;
    int[][][] memo;
    int limit;

    public int numberOfStableArrays(int zero, int one, int limit) {
        this.memo = new int[zero + 1][one + 1][2];
        for (int i = 0; i <= zero; i++) {
            for (int j = 0; j <= one; j++) {
                Arrays.fill(memo[i][j], -1);
            }
        }
        this.limit = limit;
        return (dp(zero, one, 0) + dp(zero, one, 1)) % MOD;
    }

    public int dp(int zero, int one, int lastBit) {
        if (zero == 0) {
            return (lastBit == 0 || one > limit) ? 0 : 1;
        } else if (one == 0) {
            return (lastBit == 1 || zero > limit) ? 0 : 1;
        }

        if (memo[zero][one][lastBit] == -1) {
            int res = 0;
            if (lastBit == 0) {
                res = (dp(zero - 1, one, 0) + dp(zero - 1, one, 1))% MOD;
                if (zero > limit) {
                    res = (res - dp(zero - limit - 1, one, 1) + MOD) % MOD;
                }
            } else {
                res = (dp(zero, one - 1, 0) + dp(zero, one - 1, 1)) % MOD;
                if (one > limit) {
                    res = (res - dp(zero, one - limit - 1, 0) + MOD) % MOD;
                }
            }
            memo[zero][one][lastBit] = res % MOD;
        }
        return memo[zero][one][lastBit];
    }
}

###C#

public class Solution {
    const int MOD = 1000000007;
    int[][][] memo;
    int limit;

    public int NumberOfStableArrays(int zero, int one, int limit) {
        this.memo = new int[zero + 1][][];
        for (int i = 0; i <= zero; i++) {
            memo[i] = new int[one + 1][];
            for (int j = 0; j <= one; j++) {
                memo[i][j] = new int[2];
                Array.Fill(memo[i][j], -1);
            }
        }
        this.limit = limit;
        return (DP(zero, one, 0) + DP(zero, one, 1)) % MOD;
    }

    public int DP(int zero, int one, int lastBit) {
        if (zero == 0) {
            return (lastBit == 0 || one > limit) ? 0 : 1;
        } else if (one == 0) {
            return (lastBit == 1 || zero > limit) ? 0 : 1;
        }

        if (memo[zero][one][lastBit] == -1) {
            int res = 0;
            if (lastBit == 0) {
                res = (DP(zero - 1, one, 0) + DP(zero - 1, one, 1))% MOD;
                if (zero > limit) {
                    res = (res - DP(zero - limit - 1, one, 1) + MOD) % MOD;
                }
            } else {
                res = (DP(zero, one - 1, 0) + DP(zero, one - 1, 1)) % MOD;
                if (one > limit) {
                    res = (res - DP(zero, one - limit - 1, 0) + MOD) % MOD;
                }
            }
            memo[zero][one][lastBit] = res % MOD;
        }
        return memo[zero][one][lastBit];
    }
}

###C

#define MOD 1000000007

int ***createMemo(int zero, int one) {
    int ***memo = malloc((zero + 1) * sizeof(int **));
    for (int i = 0; i <= zero; ++i) {
        memo[i] = malloc((one + 1) * sizeof(int *));
        for (int j = 0; j <= one; ++j) {
            memo[i][j] = malloc(2 * sizeof(int));
            memo[i][j][0] = -1;
            memo[i][j][1] = -1;
        }
    }
    return memo;
}

void freeMemo(int zero, int one, int ***memo) {
    for (int i = 0; i <= zero; ++i) {
        for (int j = 0; j <= one; ++j) {
            free(memo[i][j]);
        }
        free(memo[i]);
    }
    free(memo);
}

int dp(int zero, int one, int lastBit, int limit, int ***memo) {
    if (zero == 0) {
        return (lastBit == 0 || one > limit) ? 0 : 1;
    } else if (one == 0) {
        return (lastBit == 1 || zero > limit) ? 0 : 1;
    }
    if (memo[zero][one][lastBit] == -1) {
        int res = 0;
        if (lastBit == 0) {
            res = (dp(zero - 1, one, 0, limit, memo) + dp(zero - 1, one, 1, limit, memo)) % MOD;
            if (zero > limit) {
                res = (res - dp(zero - limit - 1, one, 1, limit, memo) + MOD) % MOD;
            }
        } else {
            res = (dp(zero, one - 1, 0, limit, memo) + dp(zero, one - 1, 1, limit, memo)) % MOD;
            if (one > limit) {
                res = (res - dp(zero, one - limit - 1, 0, limit, memo) + MOD) % MOD;
            }
        }
        memo[zero][one][lastBit] = res % MOD;
    }
    return memo[zero][one][lastBit];
}

int numberOfStableArrays(int zero, int one, int limit) {
    int ***memo = createMemo(zero, one);
    int result = (dp(zero, one, 0, limit, memo) + dp(zero, one, 1, limit, memo)) % MOD;
    freeMemo(zero, one, memo);
    return result;
}

###Go

const MOD = 1000000007

func numberOfStableArrays(zero int, one int, limit int) int {
    memo := make([][][]int, zero + 1)
for i := range memo {
memo[i] = make([][]int, one + 1)
for j := range memo[i] {
memo[i][j] = []int{-1, -1}
}
}

var dp func(int, int, int) int
dp = func(zero, one, lastBit int) int {
if zero == 0 {
if lastBit == 0 || one > limit {
return 0
} else {
return 1
}
} else if one == 0 {
if lastBit == 1 || zero > limit {
return 0
} else {
return 1
}
}

if memo[zero][one][lastBit] == -1 {
res := 0
if lastBit == 0 {
res = (dp(zero-1, one, 0) + dp(zero - 1, one, 1)) % MOD
if zero > limit {
res = (res - dp(zero - limit - 1, one, 1) + MOD) % MOD
}
} else {
res = (dp(zero, one - 1, 0) + dp(zero, one - 1, 1)) % MOD
if one > limit {
res = (res - dp(zero, one - limit - 1, 0) + MOD) % MOD
}
}
memo[zero][one][lastBit] = res % MOD
}
return memo[zero][one][lastBit]
}

return (dp(zero, one, 0) + dp(zero, one, 1)) % MOD
}

###JavaScript

const MOD = 1000000007;

var numberOfStableArrays = function(zero, one, limit) {
    const memo = Array.from({ length: zero + 1 }, () =>
        Array.from({ length: one + 1 }, () => [-1, -1])
    );

    function dp(zero, one, lastBit) {
        if (zero === 0) {
            return lastBit === 0 || one > limit ? 0 : 1;
        } else if (one === 0) {
            return lastBit === 1 || zero > limit ? 0 : 1;
        }

        if (memo[zero][one][lastBit] === -1) {
            let res = 0;
            if (lastBit === 0) {
                res = (dp(zero - 1, one, 0) + dp(zero - 1, one, 1)) % MOD;
                if (zero > limit) {
                    res = (res - dp(zero - limit - 1, one, 1) + MOD) % MOD;
                }
            } else {
                res = (dp(zero, one - 1, 0) + dp(zero, one - 1, 1)) % MOD;
                if (one > limit) {
                    res = (res - dp(zero, one - limit - 1, 0) + MOD) % MOD;
                }
            }
            memo[zero][one][lastBit] = res % MOD;
        }
        return memo[zero][one][lastBit];
    }

    return (dp(zero, one, 0) + dp(zero, one, 1)) % MOD;
};

###TypeScript

const MOD = 1000000007;

function numberOfStableArrays(zero: number, one: number, limit: number): number {
    const memo: number[][][] = Array.from({ length: zero + 1 }, () =>
        Array.from({ length: one + 1 }, () => [-1, -1])
    );

    function dp(zero: number, one: number, lastBit: number): number {
        if (zero === 0) {
            return lastBit === 0 || one > limit ? 0 : 1;
        } else if (one === 0) {
            return lastBit === 1 || zero > limit ? 0 : 1;
        }

        if (memo[zero][one][lastBit] === -1) {
            let res = 0;
            if (lastBit === 0) {
                res = (dp(zero - 1, one, 0) + dp(zero - 1, one, 1)) % MOD;
                if (zero > limit) {
                    res = (res - dp(zero - limit - 1, one, 1) + MOD) % MOD;
                }
            } else {
                res = (dp(zero, one - 1, 0) + dp(zero, one - 1, 1)) % MOD;
                if (one > limit) {
                    res = (res - dp(zero, one - limit - 1, 0) + MOD) % MOD;
                }
            }
            memo[zero][one][lastBit] = res % MOD;
        }
        return memo[zero][one][lastBit];
    }

    return (dp(zero, one, 0) + dp(zero, one, 1)) % MOD;
};

###Rust

const MOD: i32 = 1000000007;

impl Solution {
    pub fn number_of_stable_arrays(zero: i32, one: i32, limit: i32) -> i32 {
        let mut memo = vec![vec![vec![-1; 2]; (one + 1) as usize]; (zero + 1) as usize];

        fn dp(zero: usize, one: usize, last_bit: usize, limit: usize, memo: &mut Vec<Vec<Vec<i32>>>) -> i32 {
            if zero == 0 {
                return if last_bit == 0 || one > limit { 0 } else { 1 };
            } else if one == 0 {
                return if last_bit == 1 || zero > limit { 0 } else { 1 };
            }

            if memo[zero][one][last_bit] == -1 {
                let mut res = 0;
                if last_bit == 0 {
                    res = (dp(zero - 1, one, 0, limit, memo) + dp(zero - 1, one, 1, limit, memo)) % MOD;
                    if zero > limit {
                        res = (res - dp(zero - limit - 1, one, 1, limit, memo) + MOD) % MOD;
                    }
                } else {
                    res = (dp(zero, one - 1, 0, limit, memo) + dp(zero, one - 1, 1, limit, memo)) % MOD;
                    if one > limit {
                        res = (res - dp(zero, one - limit - 1, 0, limit, memo) + MOD) % MOD;
                    }
                }
                memo[zero][one][last_bit] = res % MOD;
            }
            memo[zero][one][last_bit]
        }

        let zero = zero as usize;
        let one = one as usize;
        let limit = limit as usize;
        (dp(zero, one, 0, limit, &mut memo) + dp(zero, one, 1, limit, &mut memo)) % MOD
    }
}

复杂度分析

  • 时间复杂度:$O(\textit{zero}\times\textit{one})$,动态规划的状态一共有 $O(\textit{zero}\times\textit{one})$ 种,每个状态消耗 $O(1)$ 时间消耗。

  • 空间复杂度:$O(\textit{zero}\times\textit{one})$。

方法二:动态规划

思路

方法一用的是记忆化搜索,状态的求解是自顶向下的。方法二中我们使用动态规划,从而自底向上来求出所有状态,并用数组保存结果。状态方程的关系和方法一一致。

代码

###Python

class Solution:
    def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
        mod = 10 ** 9 + 7

        dp = [[[0, 0] for _ in range(one + 1)] for _ in range(zero + 1)]
        for i in range(zero+1):
            for j in range(one+1):
                for lastBit in range(2):
                    if i == 0:
                        if lastBit == 0 or j > limit:
                            dp[i][j][lastBit] = 0
                        else:
                            dp[i][j][lastBit] = 1
                    elif j == 0:
                        if lastBit == 1 or i > limit:
                            dp[i][j][lastBit] = 0
                        else:
                            dp[i][j][lastBit] = 1
                    elif lastBit == 0:
                        dp[i][j][lastBit] = dp[i-1][j][0] + dp[i-1][j][1]
                        if i > limit:
                            dp[i][j][lastBit] -= dp[i-limit-1][j][1]
                    else:
                        dp[i][j][lastBit] = dp[i][j-1][0] + dp[i][j-1][1]
                        if j > limit:
                            dp[i][j][lastBit] -= dp[i][j-limit-1][0]
                    dp[i][j][lastBit] %= mod
        return (dp[-1][-1][0] + dp[-1][-1][1]) % mod

###C++

class Solution {
public:
    constexpr static int MOD = 1000000007;
    int numberOfStableArrays(int zero, int one, int limit) {
        vector<vector<vector<int>>> dp(zero + 1, vector<vector<int>>(one + 1, vector<int>(2)));
        for (int i = 0; i <= zero; i++) {
            for (int j = 0; j <= one; j++) {
                for (int lastBit = 0; lastBit <= 1; lastBit++) {
                    if (i == 0) {
                        if (lastBit == 0 || j > limit) {
                            dp[i][j][lastBit] = 0;
                        } else {
                            dp[i][j][lastBit] = 1;
                        }
                    } else if (j == 0) {
                        if (lastBit == 1 || i > limit) {
                            dp[i][j][lastBit] = 0;
                        } else {
                            dp[i][j][lastBit] = 1;
                        }
                    } else if (lastBit == 0) {
                        dp[i][j][lastBit] = dp[i - 1][j][0] + dp[i - 1][j][1];
                        if (i > limit) {
                            dp[i][j][lastBit] -= dp[i - limit - 1][j][1];
                        }
                    } else {
                        dp[i][j][lastBit] = dp[i][j - 1][0] + dp[i][j -1 ][1];
                        if (j > limit) {
                            dp[i][j][lastBit] -= dp[i][j - limit - 1][0];
                        }
                    }
                    dp[i][j][lastBit] %= MOD;
                    if (dp[i][j][lastBit] < 0) {
                        dp[i][j][lastBit] += MOD;
                    }
                }
            }
        }

        return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
    }
};

###Java

class Solution {
    public int numberOfStableArrays(int zero, int one, int limit) {
        final int MOD = 1000000007;
        int[][][] dp = new int[zero + 1][one + 1][2];
        for (int i = 0; i <= zero; i++) {
            for (int j = 0; j <= one; j++) {
                for (int lastBit = 0; lastBit <= 1; lastBit++) {
                    if (i == 0) {
                        if (lastBit == 0 || j > limit) {
                            dp[i][j][lastBit] = 0;
                        } else {
                            dp[i][j][lastBit] = 1;
                        }
                    } else if (j == 0) {
                        if (lastBit == 1 || i > limit) {
                            dp[i][j][lastBit] = 0;
                        } else {
                            dp[i][j][lastBit] = 1;
                        }
                    } else if (lastBit == 0) {
                        dp[i][j][lastBit] = dp[i - 1][j][0] + dp[i - 1][j][1];
                        if (i > limit) {
                            dp[i][j][lastBit] -= dp[i - limit - 1][j][1];
                        }
                    } else {
                        dp[i][j][lastBit] = dp[i][j - 1][0] + dp[i][j -1 ][1];
                        if (j > limit) {
                            dp[i][j][lastBit] -= dp[i][j - limit - 1][0];
                        }
                    }
                    dp[i][j][lastBit] %= MOD;
                    if (dp[i][j][lastBit] < 0) {
                        dp[i][j][lastBit] += MOD;
                    }
                }
            }
        }
        return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
    }
}

###C#

public class Solution {
    public int NumberOfStableArrays(int zero, int one, int limit) {
        const int MOD = 1000000007;
        int[][][] dp = new int[zero + 1][][];
        for (int i = 0; i <= zero; i++) {
            dp[i] = new int[one + 1][];
            for (int j = 0; j <= one; j++) {
                dp[i][j] = new int[2];
                for (int lastBit = 0; lastBit <= 1; lastBit++) {
                    if (i == 0) {
                        if (lastBit == 0 || j > limit) {
                            dp[i][j][lastBit] = 0;
                        } else {
                            dp[i][j][lastBit] = 1;
                        }
                    } else if (j == 0) {
                        if (lastBit == 1 || i > limit) {
                            dp[i][j][lastBit] = 0;
                        } else {
                            dp[i][j][lastBit] = 1;
                        }
                    } else if (lastBit == 0) {
                        dp[i][j][lastBit] = dp[i - 1][j][0] + dp[i - 1][j][1];
                        if (i > limit) {
                            dp[i][j][lastBit] -= dp[i - limit - 1][j][1];
                        }
                    } else {
                        dp[i][j][lastBit] = dp[i][j - 1][0] + dp[i][j -1 ][1];
                        if (j > limit) {
                            dp[i][j][lastBit] -= dp[i][j - limit - 1][0];
                        }
                    }
                    dp[i][j][lastBit] %= MOD;
                    if (dp[i][j][lastBit] < 0) {
                        dp[i][j][lastBit] += MOD;
                    }
                }
            }
        }
        return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
    }
}

###Go

const MOD = 1000000007

func numberOfStableArrays(zero int, one int, limit int) int {
    dp := make([][][]int, zero + 1)
    for i := range dp {
        dp[i] = make([][]int, one + 1)
        for j := range dp[i] {
            dp[i][j] = make([]int, 2)
        }
    }

    for i := 0; i <= zero; i++ {
        for j := 0; j <= one; j++ {
            for lastBit := 0; lastBit <= 1; lastBit++ {
                if i == 0 {
                    if lastBit == 0 || j > limit {
                        dp[i][j][lastBit] = 0
                    } else {
                        dp[i][j][lastBit] = 1
                    }
                } else if j == 0 {
                    if lastBit == 1 || i > limit {
                        dp[i][j][lastBit] = 0
                    } else {
                        dp[i][j][lastBit] = 1
                    }
                } else if lastBit == 0 {
                    dp[i][j][lastBit] = dp[i - 1][j][0] + dp[i - 1][j][1]
                    if i > limit {
                        dp[i][j][lastBit] -= dp[i - limit - 1][j][1]
                    }
                } else {
                    dp[i][j][lastBit] = dp[i][j - 1][0] + dp[i][j - 1][1]
                    if j > limit {
                        dp[i][j][lastBit] -= dp[i][j - limit - 1][0]
                    }
                }
                dp[i][j][lastBit] %= MOD
                if dp[i][j][lastBit] < 0 {
                    dp[i][j][lastBit] += MOD
                }
            }
        }
    }

    return (dp[zero][one][0] + dp[zero][one][1]) % MOD
}

###C

#define MOD 1000000007

int numberOfStableArrays(int zero, int one, int limit) {
    int dp[zero + 1][one + 1][2];
    memset(dp, 0, sizeof(dp));
    for (int i = 0; i <= zero; i++) {
        for (int j = 0; j <= one; j++) {
            for (int lastBit = 0; lastBit <= 1; lastBit++) {
                if (i == 0) {
                    if (lastBit == 0 || j > limit) {
                        dp[i][j][lastBit] = 0;
                    } else {
                        dp[i][j][lastBit] = 1;
                    }
                } else if (j == 0) {
                    if (lastBit == 1 || i > limit) {
                        dp[i][j][lastBit] = 0;
                    } else {
                        dp[i][j][lastBit] = 1;
                    }
                } else if (lastBit == 0) {
                    dp[i][j][lastBit] = dp[i - 1][j][0] + dp[i - 1][j][1];
                    if (i > limit) {
                        dp[i][j][lastBit] -= dp[i - limit - 1][j][1];
                    }
                } else {
                    dp[i][j][lastBit] = dp[i][j - 1][0] + dp[i][j - 1][1];
                    if (j > limit) {
                        dp[i][j][lastBit] -= dp[i][j - limit - 1][0];
                    }
                }
                dp[i][j][lastBit] %= MOD;
                if (dp[i][j][lastBit] < 0) {
                    dp[i][j][lastBit] += MOD;
                }
            }
        }
    }

    return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
}

###JavaScript

const MOD = 1000000007;

var numberOfStableArrays = function(zero, one, limit) {
    let dp = Array.from({ length: zero + 1 }, () =>
        Array.from({ length: one + 1 }, () => [0, 0])
    );

    for (let i = 0; i <= zero; i++) {
        for (let j = 0; j <= one; j++) {
            for (let lastBit = 0; lastBit <= 1; lastBit++) {
                if (i === 0) {
                    if (lastBit === 0 || j > limit) {
                        dp[i][j][lastBit] = 0;
                    } else {
                        dp[i][j][lastBit] = 1;
                    }
                } else if (j === 0) {
                    if (lastBit === 1 || i > limit) {
                        dp[i][j][lastBit] = 0;
                    } else {
                        dp[i][j][lastBit] = 1;
                    }
                } else if (lastBit === 0) {
                    dp[i][j][lastBit] = dp[i - 1][j][0] + dp[i - 1][j][1];
                    if (i > limit) {
                        dp[i][j][lastBit] -= dp[i - limit - 1][j][1];
                    }
                } else {
                    dp[i][j][lastBit] = dp[i][j - 1][0] + dp[i][j - 1][1];
                    if (j > limit) {
                        dp[i][j][lastBit] -= dp[i][j - limit - 1][0];
                    }
                }
                dp[i][j][lastBit] %= MOD;
                if (dp[i][j][lastBit] < 0) {
                    dp[i][j][lastBit] += MOD;
                }
            }
        }
    }

    return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
};

###TypeScript

const MOD = 1000000007;

function numberOfStableArrays(zero: number, one: number, limit: number): number {
    let dp: number[][][] = Array.from({ length: zero + 1 }, () =>
        Array.from({ length: one + 1 }, () => [0, 0])
    );

    for (let i = 0; i <= zero; i++) {
        for (let j = 0; j <= one; j++) {
            for (let lastBit = 0; lastBit <= 1; lastBit++) {
                if (i === 0) {
                    if (lastBit === 0 || j > limit) {
                        dp[i][j][lastBit] = 0;
                    } else {
                        dp[i][j][lastBit] = 1;
                    }
                } else if (j === 0) {
                    if (lastBit === 1 || i > limit) {
                        dp[i][j][lastBit] = 0;
                    } else {
                        dp[i][j][lastBit] = 1;
                    }
                } else if (lastBit === 0) {
                    dp[i][j][lastBit] = dp[i - 1][j][0] + dp[i - 1][j][1];
                    if (i > limit) {
                        dp[i][j][lastBit] -= dp[i - limit - 1][j][1];
                    }
                } else {
                    dp[i][j][lastBit] = dp[i][j - 1][0] + dp[i][j - 1][1];
                    if (j > limit) {
                        dp[i][j][lastBit] -= dp[i][j - limit - 1][0];
                    }
                }
                dp[i][j][lastBit] %= MOD;
                if (dp[i][j][lastBit] < 0) {
                    dp[i][j][lastBit] += MOD;
                }
            }
        }
    }

    return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
};

###Rust

const MOD: i32 = 1000000007;

impl Solution {
    pub fn number_of_stable_arrays(zero: i32, one: i32, limit: i32) -> i32 {
        let mut dp = vec![vec![vec![0; 2]; one as usize + 1]; zero as usize + 1];

        for i in 0..=zero as usize {
            for j in 0..=one as usize {
                for last_bit in 0..=1 {
                    if i == 0 {
                        if last_bit == 0 || j > limit as usize {
                            dp[i][j][last_bit] = 0;
                        } else {
                            dp[i][j][last_bit] = 1;
                        }
                    } else if j == 0 {
                        if last_bit == 1 || i > limit as usize {
                            dp[i][j][last_bit] = 0;
                        } else {
                            dp[i][j][last_bit] = 1;
                        }
                    } else if last_bit == 0 {
                        dp[i][j][last_bit] = dp[i - 1][j][0] + dp[i - 1][j][1];
                        if i > limit as usize {
                            dp[i][j][last_bit] -= dp[i - (limit as usize) - 1][j][1];
                        }
                    } else {
                        dp[i][j][last_bit] = dp[i][j - 1][0] + dp[i][j - 1][1];
                        if j > limit as usize {
                            dp[i][j][last_bit] -= dp[i][j - (limit as usize) - 1][0];
                        }
                    }
                    dp[i][j][last_bit] %= MOD;
                    if dp[i][j][last_bit] < 0 {
                        dp[i][j][last_bit] += MOD;
                    }
                }
            }
        }

        return (dp[zero as usize][one as usize][0] + dp[zero as usize][one as usize][1]) % MOD;
    }
}

复杂度分析

  • 时间复杂度:$O(\textit{zero}\times\textit{one})$,动态规划的状态一共有 $O(\textit{zero}\times\textit{one})$ 种,每个状态消耗 $O(1)$ 时间消耗。

  • 空间复杂度:$O(\textit{zero}\times\textit{one})$。

从 DP 到组合数学(Python/Java/C++/Go)

方法一:记忆化搜索

前置知识动态规划入门:从记忆化搜索到递推,其中包含如何把记忆化搜索 1:1 翻译成递推的技巧。

先解释 $\textit{limit}$,意思是数组中至多有连续 $\textit{limit}$ 个 $0$,且至多有连续 $\textit{limit}$ 个 $1$。

看示例 3,$\textit{zero}=3,\ \textit{one}=3,\ \textit{limit}=2$。

考虑稳定数组的最后一个位置填 $0$ 还是 $1$:

  • 填 $0$,问题变成剩下 $2$ 个 $0$ 和 $3$ 个 $1$ 怎么填。
  • 填 $1$,问题变成剩下 $3$ 个 $0$ 和 $2$ 个 $1$ 怎么填。
  • 这两个都是和原问题相似的子问题。

看上去,定义 $\textit{dfs}(i,j)$ 表示用 $i$ 个 $0$ 和 $j$ 个 $1$ 构造稳定数组的方案数?但这样定义不方便计算 $\textit{limit}$ 带来的影响。

比如 $\textit{limit}=3$,如果在以 $1000$ 结尾的稳定数组的后面,再添加一个 $0$,得到以 $10000$ 结尾的数组,这是不合法的。我们需要把这种不合法的情况减掉。

为了减去不合法的情况,我们需要知道稳定数组的最后一个数是 $0$ 还是 $1$。

改成定义 $\textit{dfs}(i,j,k)$ 表示用 $i$ 个 $0$ 和 $j$ 个 $1$ 构造长为 $i+j$ 的稳定数组的方案数,其中最后一个位置(第 $i+j$ 个位置)已经填入了 $k$,其中 $k$ 为 $0$ 或 $1$。

以 $k=0$ 为例,考虑 $\textit{dfs}(i,j,0)$ 怎么算。现在最后一个位置(第 $i+j$ 个位置)已经填入了 $0$,消耗了一个 $0$,还剩下 $i-1$ 个 $0$。问题变成用 $i-1$ 个 $0$ 和 $j$ 个 $1$ 构造长为 $i+j-1$ 的稳定数组的方案数。对于这个子问题,枚举最后一个位置(第 $i+j-1$ 个位置)填入 $k=0$ 还是 $k=1$,对应的方案数为 $\textit{dfs}(i-1,j,0)$ 和 $\textit{dfs}(i-1,j,1)$。

看上去,把这两种情况加起来,我们就得到了

$$
\textit{dfs}(i,j,0) = \textit{dfs}(i-1,j,0) + \textit{dfs}(i-1,j,1)
$$

但是,这会产生不合法的情况。

以 $\textit{limit}=3$ 为例说明。$\textit{dfs}(i-1,j,0)$ 是一些以 $0$ 结尾的稳定数组(合法数组),讨论末尾 $0$ 的个数:

  • 末尾恰好有连续 $1$ 个 $0$,即 $10$。
  • 末尾恰好有连续 $2$ 个 $0$,即 $100$。
  • 末尾恰好有连续 $3$ 个 $0$,即 $1000$。由于末尾不能超过连续 $3$ 个 $0$,末尾是 $000$ 的稳定数组,倒数第 $4$ 个数一定是 $1$,也就是在 $\textit{dfs}(i-1,j,0)$ 中有 $\textit{dfs}(i-4,j,1)$ 个末尾是 $1000$ 的稳定数组。

若要通过 $\textit{dfs}(i-1,j,0)$ 计算 $\textit{dfs}(i,j,0)$,相当于往这 $\textit{dfs}(i-1,j,0)$ 个稳定数组的末尾再加一个 $0$:

  • 末尾恰好有连续 $2$ 个 $0$,即 $100$,这是合法的。
  • 末尾恰好有连续 $3$ 个 $0$,即 $1000$,这是合法的。
  • 末尾恰好有连续 $4$ 个 $0$,即 $10000$,这是不合法的,要全部去掉!根据上面的分析,要减去 $\textit{dfs}(i-4,j,1)$。

一般地,在 $\textit{dfs}(i-1,j,0)$ 个稳定数组的末尾添加一个 $0$,会得到 $\textit{dfs}(i-\textit{limit}-1,j,1)$ 个不合法的数组。从上文的状态转移方程中减掉 $\textit{dfs}(i-\textit{limit}-1,j,1)$,得

$$
\textit{dfs}(i,j,0) = \textit{dfs}(i-1,j,0) + \textit{dfs}(i-1,j,1) - \textit{dfs}(i-\textit{limit}-1,j,1)
$$

同理得

$$
\textit{dfs}(i,j,1) = \textit{dfs}(i,j-1,0) + \textit{dfs}(i,j-1,1) - \textit{dfs}(i,j-\textit{limit}-1,0)
$$

递归边界

  • 如果 $i<0$ 或者 $j<0$,返回 $0$。也可以在递归 $\textit{dfs}(i-\textit{limit}-1,j,1)$ 前判断 $i>\textit{limit}$,在递归 $\textit{dfs}(i,j-\textit{limit}-1,0)$ 前判断 $j>\textit{limit}$。下面代码在递归前判断。
  • 如果 $i=0$,那么当 $k=1$ 且 $j\le \textit{limit}$ 的情况下返回 $1$,否则返回 $0$;如果 $j=0$,那么当 $k=0$ 且 $i\le \textit{limit}$ 的情况下返回 $1$,否则返回 $0$。

递归入口:$\textit{dfs}(\textit{zero},\textit{one},0)+\textit{dfs}(\textit{zero},\textit{one},1)$,即答案。

请看 视频讲解 第四题,欢迎点赞关注~

###py

class Solution:
    def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
        MOD = 1_000_000_007
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
        def dfs(i: int, j: int, k: int) -> int:
            if i == 0:
                return 1 if k == 1 and j <= limit else 0
            if j == 0:
                return 1 if k == 0 and i <= limit else 0
            if k == 0:
                return (dfs(i - 1, j, 0) + dfs(i - 1, j, 1) - (dfs(i - limit - 1, j, 1) if i > limit else 0)) % MOD
            else:  # else 可以去掉,这里仅仅是为了代码对齐
                return (dfs(i, j - 1, 0) + dfs(i, j - 1, 1) - (dfs(i, j - limit - 1, 0) if j > limit else 0)) % MOD
        ans = (dfs(zero, one, 0) + dfs(zero, one, 1)) % MOD
        dfs.cache_clear()  # 防止爆内存
        return ans

###java

class Solution {
    private static final int MOD = 1_000_000_007;

    public int numberOfStableArrays(int zero, int one, int limit) {
        int[][][] memo = new int[zero + 1][one + 1][2];
        for (int[][] m : memo) {
            for (int[] m2 : m) {
                Arrays.fill(m2, -1); // -1 表示没有计算过
            }
        }
        return (dfs(zero, one, 0, limit, memo) + dfs(zero, one, 1, limit, memo)) % MOD;
    }

    private int dfs(int i, int j, int k, int limit, int[][][] memo) {
        if (i == 0) { // 递归边界
            return k == 1 && j <= limit ? 1 : 0;
        }
        if (j == 0) { // 递归边界
            return k == 0 && i <= limit ? 1 : 0;
        }
        if (memo[i][j][k] != -1) { // 之前计算过
            return memo[i][j][k];
        }
        if (k == 0) {
            // + MOD 保证答案非负
            memo[i][j][k] = (int) (((long) dfs(i - 1, j, 0, limit, memo) + dfs(i - 1, j, 1, limit, memo) +
                    (i > limit ? MOD - dfs(i - limit - 1, j, 1, limit, memo) : 0)) % MOD);
        } else {
            memo[i][j][k] = (int) (((long) dfs(i, j - 1, 0, limit, memo) + dfs(i, j - 1, 1, limit, memo) +
                    (j > limit ? MOD - dfs(i, j - limit - 1, 0, limit, memo) : 0)) % MOD);
        }
        return memo[i][j][k];
    }
}

###cpp

class Solution {
public:
    int numberOfStableArrays(int zero, int one, int limit) {
        const int MOD = 1'000'000'007;
        vector memo(zero + 1, vector<array<int, 2>>(one + 1, {-1, -1})); // -1 表示没有计算过

        auto dfs = [&](this auto&& dfs, int i, int j, int k) -> int {
            if (i == 0) { // 递归边界
                return k == 1 && j <= limit;
            }
            if (j == 0) { // 递归边界
                return k == 0 && i <= limit;
            }
            int& res = memo[i][j][k]; // 注意这里是引用
            if (res != -1) { // 之前计算过
                return res;
            }
            if (k == 0) {
                // + MOD 保证答案非负
                res = ((long long) dfs(i - 1, j, 0) + dfs(i - 1, j, 1) +
                       (i > limit ? MOD - dfs(i - limit - 1, j, 1) : 0)) % MOD;
            } else {
                res = ((long long) dfs(i, j - 1, 0) + dfs(i, j - 1, 1) +
                       (j > limit ? MOD - dfs(i, j - limit - 1, 0) : 0)) % MOD;
            }
            return res;
        };

        return (dfs(zero, one, 0) + dfs(zero, one, 1)) % MOD;
    }
};

###go

func numberOfStableArrays(zero, one, limit int) int {
const mod = 1_000_000_007
memo := make([][][2]int, zero+1)
for i := range memo {
memo[i] = make([][2]int, one+1)
for j := range memo[i] {
memo[i][j] = [2]int{-1, -1}
}
}
var dfs func(int, int, int) int
dfs = func(i, j, k int) (res int) {
if i == 0 { // 递归边界
if k == 1 && j <= limit {
return 1
}
return
}
if j == 0 { // 递归边界
if k == 0 && i <= limit {
return 1
}
return
}
p := &memo[i][j][k]
if *p != -1 { // 之前计算过
return *p
}
if k == 0 {
// +mod 保证答案非负
res = (dfs(i-1, j, 0) + dfs(i-1, j, 1)) % mod
if i > limit {
res = (res - dfs(i-limit-1, j, 1) + mod) % mod
}
} else {
res = (dfs(i, j-1, 0) + dfs(i, j-1, 1)) % mod
if j > limit {
res = (res - dfs(i, j-limit-1, 0) + mod) % mod
}
}
*p = res // 记忆化
return
}
return (dfs(zero, one, 0) + dfs(zero, one, 1)) % mod
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\textit{zero}\cdot \textit{one})$。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(\textit{zero}\cdot \textit{one})$,单个状态的计算时间为 $\mathcal{O}(1)$,所以动态规划的时间复杂度为 $\mathcal{O}(\textit{zero}\cdot \textit{one})$。
  • 空间复杂度:$\mathcal{O}(\textit{zero}\cdot \textit{one})$。有多少个状态,$\textit{memo}$ 数组的大小就是多少。

方法二:递推

和 $\textit{dfs}(i,j,k)$ 一样,定义 $f[i][j][k]$ 表示用 $i$ 个 $0$ 和 $j$ 个 $1$ 构造稳定数组的方案数,其中第 $i+j$ 个位置要填 $k$,其中 $k$ 为 $0$ 或 $1$。

状态转移方程:

$$
\begin{aligned}
f[i][j][0] &= f[i-1][j][0] + f[i-1][j][1] - f[i-\textit{limit}-1][j][1] \
f[i][j][1] &= f[i][j-1][0] + f[i][j-1][1] - f[i][j-\textit{limit}-1][0] \
\end{aligned}
$$

如果 $i\le \textit{limit}$ 则 $f[i-\textit{limit}-1][j][1]$ 视作 $0$,如果 $j\le \textit{limit}$ 则 $f[i][j-\textit{limit}-1][0]$ 视作 $0$。

初始值:$f[i][0][0] = f[0][j][1] = 1$,其中 $1\le i \le \min(\textit{limit}, \textit{zero}),\ 1\le j \le \min(\textit{limit}, \textit{one})$。翻译自递归边界。

答案:$f[\textit{zero}][\textit{one}][0] + f[\textit{zero}][\textit{one}][1]$。翻译自递归入口。

###py

class Solution:
    def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
        MOD = 1_000_000_007
        f = [[[0, 0] for _ in range(one + 1)] for _ in range(zero + 1)]
        for i in range(1, min(limit, zero) + 1):
            f[i][0][0] = 1
        for j in range(1, min(limit, one) + 1):
            f[0][j][1] = 1
        for i in range(1, zero + 1):
            for j in range(1, one + 1):
                f[i][j][0] = (f[i - 1][j][0] + f[i - 1][j][1] - (f[i - limit - 1][j][1] if i > limit else 0)) % MOD
                f[i][j][1] = (f[i][j - 1][0] + f[i][j - 1][1] - (f[i][j - limit - 1][0] if j > limit else 0)) % MOD
        return sum(f[-1][-1]) % MOD

###java

class Solution {
    public int numberOfStableArrays(int zero, int one, int limit) {
        final int MOD = 1_000_000_007;
        int[][][] f = new int[zero + 1][one + 1][2];
        for (int i = 1; i <= Math.min(limit, zero); i++) {
            f[i][0][0] = 1;
        }
        for (int j = 1; j <= Math.min(limit, one); j++) {
            f[0][j][1] = 1;
        }
        for (int i = 1; i <= zero; i++) {
            for (int j = 1; j <= one; j++) {
                // + MOD 保证答案非负
                f[i][j][0] = (int) (((long) f[i - 1][j][0] + f[i - 1][j][1] + (i > limit ? MOD - f[i - limit - 1][j][1] : 0)) % MOD);
                f[i][j][1] = (int) (((long) f[i][j - 1][0] + f[i][j - 1][1] + (j > limit ? MOD - f[i][j - limit - 1][0] : 0)) % MOD);
            }
        }
        return (f[zero][one][0] + f[zero][one][1]) % MOD;
    }
}

###cpp

class Solution {
public:
    int numberOfStableArrays(int zero, int one, int limit) {
        const int MOD = 1'000'000'007;
        vector<vector<array<int, 2>>> f(zero + 1, vector<array<int, 2>>(one + 1));
        for (int i = 1; i <= min(limit, zero); i++) {
            f[i][0][0] = 1;
        }
        for (int j = 1; j <= min(limit, one); j++) {
            f[0][j][1] = 1;
        }
        for (int i = 1; i <= zero; i++) {
            for (int j = 1; j <= one; j++) {
                // + MOD 保证答案非负
                f[i][j][0] = ((long long) f[i - 1][j][0] + f[i - 1][j][1] + (i > limit ? MOD - f[i - limit - 1][j][1] : 0)) % MOD;
                f[i][j][1] = ((long long) f[i][j - 1][0] + f[i][j - 1][1] + (j > limit ? MOD - f[i][j - limit - 1][0] : 0)) % MOD;
            }
        }
        return (f[zero][one][0] + f[zero][one][1]) % MOD;
    }
};

###go

func numberOfStableArrays(zero, one, limit int) (ans int) {
const mod = 1_000_000_007
f := make([][][2]int, zero+1)
for i := range f {
f[i] = make([][2]int, one+1)
}
for i := 1; i <= min(limit, zero); i++ {
f[i][0][0] = 1
}
for j := 1; j <= min(limit, one); j++ {
f[0][j][1] = 1
}
for i := 1; i <= zero; i++ {
for j := 1; j <= one; j++ {
f[i][j][0] = (f[i-1][j][0] + f[i-1][j][1]) % mod
if i > limit {
// + mod 保证答案非负
f[i][j][0] = (f[i][j][0] - f[i-limit-1][j][1] + mod) % mod
}
f[i][j][1] = (f[i][j-1][0] + f[i][j-1][1]) % mod
if j > limit {
f[i][j][1] = (f[i][j][1] - f[i][j-limit-1][0] + mod) % mod
}
}
}
return (f[zero][one][0] + f[zero][one][1]) % mod
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\textit{zero}\cdot \textit{one})$。
  • 空间复杂度:$\mathcal{O}(\textit{zero}\cdot \textit{one})$。

方法三:容斥原理+乘法原理

回顾一个经典的组合数学问题:

  • 把 $n$ 个无区别的小球,放入 $m$ 个有区别的盒子,不允许空盒,有多少种方案?

这可以用隔板法解决,$n$ 个小球之间有 $n-1$ 个空隙,从中选择 $m-1$ 个空隙,插入 $m-1$ 个隔板,这样就把小球分成了 $m$ 组,并且每一组都是非空的,方案数就是 $n-1$ 选 $m-1$ 的组合数 $\dbinom {n-1} {m-1}$。

  • 只考虑 $0$,把 $0$ 分成 $i$ 组,方案数就是 $f_0[i] = \dbinom {\textit{zero}-1} {i-1}$;
  • 只考虑 $1$,把 $1$ 分成 $i$ 组,方案数就是 $f_1[i] = \dbinom {\textit{one}-1} {i-1}$。

如何综合考虑 $0$ 和 $1$?要如何计算方案数?

例如 $10110001$,相当于把 $0$ 分成了 $2$ 组,把 $1$ 分成了 $3$ 组。

一般地,设 $1$ 分成了 $i$ 组,那么 $0$ 会分成多少组?有哪些情况?

有如下四种情况:

  • $0$ 分成 $i-1$ 组,例如 $10110001$。注意第一个数和最后一个数一定是 $1$。
  • $0$ 分成 $i$ 组,且第一个数是 $0$,例如 $01010011$。注意最后一个数一定是 $1$。
  • $0$ 分成 $i$ 组,且第一个数是 $1$,例如 $10100110$。注意最后一个数一定是 $0$。
  • $0$ 分成 $i+1$ 组,例如 $01010110$。注意第一个数和最后一个数一定是 $0$。

注意 $0$ 和 $1$ 内部的分组方案是互相独立的,例如

$$
\begin{aligned}
&11010001\
&10110001\
&10100011
\end{aligned}
$$

这些例子的 $0$ 的组数不变,$1$ 的组数不变,$0$ 的分组方式也不变(都是一个 $0$ 和三个 $0$),只有 $1$ 的分组方式在变。

根据乘法原理,综合考虑 $0$ 和 $1$,把 $1$ 分成 $i$ 组总的方案数,等于上面说的四种情况(只考虑 $0$,把 $0$ 分成 $i-1,i,i+1$ 组)的方案数之和,乘以只考虑 $1$,把 $1$ 分成 $i$ 组的方案数,即

$$
(f_0[i-1] + 2\cdot f_0[i] + f_0[i+1])\cdot f_1[i]
$$

接下来,考虑 $\textit{limit}$ 带来的影响。推荐先看 2929. 给小朋友们分糖果 II 以及 我的题解

根据容斥原理,对于 $f_0[i] = \dbinom {\textit{zero}-1} {i-1}$,我们需要减去「至少 $1$ 组有超过 $\textit{limit}$ 个 $0$」的方案数,再加上「至少 $2$ 组有超过 $\textit{limit}$ 个 $0$」的方案数,再减去「至少 $3$ 组有超过 $\textit{limit}$ 个 $0$」的方案数,……,直到「至少 $j$ 组有超过 $\textit{limit}$ 个 $0$」的方案数,$j$ 的值见下文。

  • 至少 $j$ 组有超过 $\textit{limit}$ 个 $0$,相当于先从 $i$ 组中选 $j$ 组,每组先放入 $\textit{limit}$ 个 $0$,然后再把剩下的 $\textit{zero} - j\cdot \textit{limit}$ 分成 $i$ 组(需要满足 $\textit{zero} - j\cdot \textit{limit} \ge i$),方案数为

$$
\dbinom {i} {j} \dbinom {\textit{zero} - j\cdot \textit{limit}-1} {i-1}
$$

所以

$$
f_0[i] = \dbinom {\textit{zero}-1} {i-1} + \sum_{j} (-1)^j \dbinom {i} {j} \dbinom {\textit{zero} - j\cdot \textit{limit}-1} {i-1}
$$

其中 $j\ge 1$ 且需要满足 $\textit{zero} - j\cdot \textit{limit} \ge i$,即

$$
1\le j\le \left\lfloor\dfrac{zero - i}{\textit{limit}}\right\rfloor
$$

同理有

$$
f_1[i] = \dbinom {\textit{one}-1} {i-1} + \sum_{j} (-1)^j \dbinom {i} {j} \dbinom {\textit{one} - j\cdot \textit{limit}-1} {i-1}
$$

其中

$$
1\le j\le \left\lfloor\dfrac{one - i}{\textit{limit}}\right\rfloor
$$

最终答案为

$$
\sum_{i} (f_0[i-1] + 2\cdot f_0[i] + f_0[i+1])\cdot f_1[i]
$$

其中:

  1. $i\le \textit{one}$。因为 $1$ 最多分成 $\textit{one}$ 组。
  2. $i-1\le \textit{zero}$。因为 $0$ 最多分成 $\textit{zero}$ 组,当 $i-1 > \textit{zero}$ 时,上式中的 $f_0[i-1] = f_0[i] = f_0[i+1] = 0$,无需累加。
  3. $i\cdot \textit{limit}\ge \textit{one}$,即 $i\ge\left\lceil\dfrac{\textit{one}}{\textit{limit}}\right\rceil$。因为每组至多 $\textit{limit}$ 个 $1$,分成 $i$ 组,至多 $i\cdot \textit{limit}$ 个 $1$,这个数必须 $\ge \textit{one}$,不然剩下的 $1$ 放到哪一组都会导致组内 $1$ 的个数超过 $\textit{limit}$。

整理得

$$
\left\lceil\dfrac{\textit{one}}{\textit{limit}}\right\rceil \le i\le \min(\textit{one},\textit{zero}+1)
$$

代码实现时:

  1. 上取整 $\left\lceil\dfrac{a}{b}\right\rceil$ 转换成下取整 $\left\lfloor\dfrac{a-1}{b}\right\rfloor+1$。
  2. $(-1)^j$ 可以用 $1-j\bmod 2\cdot 2$ 表示,因为当 $j$ 是偶数时,该式为 $1$;当 $j$ 是奇数时,该式为 $-1$,符合 $(-1)^j$。
  3. 预处理阶乘及其逆元,利用公式 $\dbinom {n} {m} = \dfrac{n!}{m!(n-m)!}$ 计算组合数。

关于取模的知识点,见 模运算的世界:当加减乘除遇上取模

###py

MOD = 1_000_000_007
MX = 1001

fac = [0] * MX  # f[i] = i!
fac[0] = 1
for i in range(1, MX):
    fac[i] = fac[i - 1] * i % MOD

inv_f = [0] * MX  # inv_f[i] = i!^-1
inv_f[-1] = pow(fac[-1], -1, MOD)
for i in range(MX - 1, 0, -1):
    inv_f[i - 1] = inv_f[i] * i % MOD

def comb(n: int, m: int) -> int:
    return fac[n] * inv_f[m] * inv_f[n - m] % MOD

class Solution:
    def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
        if zero > one:
            zero, one = one, zero  # 保证空间复杂度为 O(min(zero, one))
        f0 = [0] * (zero + 3)
        for i in range((zero - 1) // limit + 1, zero + 1):
            f0[i] = comb(zero - 1, i - 1)
            for j in range(1, (zero - i) // limit + 1):
                f0[i] = (f0[i] + (-1 if j % 2 else 1) * comb(i, j) * comb(zero - j * limit - 1, i - 1)) % MOD

        ans = 0
        for i in range((one - 1) // limit + 1, min(one, zero + 1) + 1):
            f1 = comb(one - 1, i - 1)
            for j in range(1, (one - i) // limit + 1):
                f1 = (f1 + (-1 if j % 2 else 1) * comb(i, j) * comb(one - j * limit - 1, i - 1)) % MOD
            ans = (ans + (f0[i - 1] + f0[i] * 2 + f0[i + 1]) * f1) % MOD
        return ans

###java

class Solution {
    private static final int MOD = 1_000_000_007;
    private static final int MX = 1001;

    private static final long[] F = new long[MX]; // f[i] = i!
    private static final long[] INV_F = new long[MX]; // inv_f[i] = i!^-1

    static {
        F[0] = 1;
        for (int i = 1; i < MX; i++) {
            F[i] = F[i - 1] * i % MOD;
        }

        INV_F[MX - 1] = pow(F[MX - 1], MOD - 2);
        for (int i = MX - 1; i > 0; i--) {
            INV_F[i - 1] = INV_F[i] * i % MOD;
        }
    }

    public int numberOfStableArrays(int zero, int one, int limit) {
        if (zero > one) {
            // swap,保证空间复杂度为 O(min(zero, one))
            int t = zero;
            zero = one;
            one = t;
        }
        long[] f0 = new long[zero + 3];
        for (int i = (zero - 1) / limit + 1; i <= zero; i++) {
            f0[i] = comb(zero - 1, i - 1);
            for (int j = 1; j <= (zero - i) / limit; j++) {
                f0[i] = (f0[i] + (1 - j % 2 * 2) * comb(i, j) * comb(zero - j * limit - 1, i - 1)) % MOD;
            }
        }

        long ans = 0;
        for (int i = (one - 1) / limit + 1; i <= Math.min(one, zero + 1); i++) {
            long f1 = comb(one - 1, i - 1);
            for (int j = 1; j <= (one - i) / limit; j++) {
                f1 = (f1 + (1 - j % 2 * 2) * comb(i, j) * comb(one - j * limit - 1, i - 1)) % MOD;
            }
            ans = (ans + (f0[i - 1] + f0[i] * 2 + f0[i + 1]) * f1) % MOD;
        }
        return (int) ((ans + MOD) % MOD); // 保证结果非负
    }

    private long comb(int n, int m) {
        return F[n] * INV_F[m] % MOD * INV_F[n - m] % MOD;
    }

    private static long pow(long x, int n) {
        long res = 1;
        for (; n > 0; n /= 2) {
            if (n % 2 > 0) {
                res = res * x % MOD;
            }
            x = x * x % MOD;
        }
        return res;
    }
}

###cpp

const int MOD = 1'000'000'007;
const int MX = 1001;

long long F[MX]; // F[i] = i!
long long INV_F[MX]; // INV_F[i] = i!^-1

long long pow(long long x, int n) {
    long long res = 1;
    for (; n; n /= 2) {
        if (n % 2) {
            res = res * x % MOD;
        }
        x = x * x % MOD;
    }
    return res;
}

auto init = [] {
    F[0] = 1;
    for (int i = 1; i < MX; i++) {
        F[i] = F[i - 1] * i % MOD;
    }

    INV_F[MX - 1] = pow(F[MX - 1], MOD - 2);
    for (int i = MX - 1; i; i--) {
        INV_F[i - 1] = INV_F[i] * i % MOD;
    }
    return 0;
}();

long long comb(int n, int m) {
    return F[n] * INV_F[m] % MOD * INV_F[n - m] % MOD;
}

class Solution {
public:
    int numberOfStableArrays(int zero, int one, int limit) {
        if (zero > one) {
            swap(zero, one); // 保证空间复杂度为 O(min(zero, one))
        }
        vector<long long> f0(zero + 3);
        for (int i = (zero - 1) / limit + 1; i <= zero; i++) {
            f0[i] = comb(zero - 1, i - 1);
            for (int j = 1; j <= (zero - i) / limit; j++) {
                f0[i] = (f0[i] + (1 - j % 2 * 2) * comb(i, j) * comb(zero - j * limit - 1, i - 1)) % MOD;
            }
        }

        long long ans = 0;
        for (int i = (one - 1) / limit + 1; i <= min(one, zero + 1); i++) {
            long long f1 = comb(one - 1, i - 1);
            for (int j = 1; j <= (one - i) / limit; j++) {
                f1 = (f1 + (1 - j % 2 * 2) * comb(i, j) * comb(one - j * limit - 1, i - 1)) % MOD;
            }
            ans = (ans + (f0[i - 1] + f0[i] * 2 + f0[i + 1]) * f1) % MOD;
        }
        return (ans + MOD) % MOD; // 保证结果非负
    }
};

###go

const mod = 1_000_000_007
const mx = 1001

var f [mx]int    // f[i] = i!
var invF [mx]int // invF[i] = i!^-1

func init() {
f[0] = 1
for i := 1; i < mx; i++ {
f[i] = f[i-1] * i % mod
}

invF[mx-1] = pow(f[mx-1], mod-2)
for i := mx - 1; i > 0; i-- {
invF[i-1] = invF[i] * i % mod
}
}

func comb(n, m int) int {
return f[n] * invF[m] % mod * invF[n-m] % mod
}

func numberOfStableArrays(zero, one, limit int) (ans int) {
if zero > one {
zero, one = one, zero // 保证空间复杂度为 O(min(zero, one))
}
f0 := make([]int, zero+3)
for i := (zero-1)/limit + 1; i <= zero; i++ {
f0[i] = comb(zero-1, i-1)
for j := 1; j <= (zero-i)/limit; j++ {
f0[i] = (f0[i] + (1-j%2*2)*comb(i, j)*comb(zero-j*limit-1, i-1)) % mod
}
}

for i := (one-1)/limit + 1; i <= min(one, zero+1); i++ {
f1 := comb(one-1, i-1)
for j := 1; j <= (one-i)/limit; j++ {
f1 = (f1 + (1-j%2*2)*comb(i, j)*comb(one-j*limit-1, i-1)) % mod
}
ans = (ans + (f0[i-1]+f0[i]*2+f0[i+1])*f1) % mod
}
return (ans + mod) % mod // 保证结果非负
}

func pow(x, n int) int {
res := 1
for ; n > 0; n /= 2 {
if n%2 > 0 {
res = res * x % mod
}
x = x * x % mod
}
return res
}

复杂度分析

  • 时间复杂度:$\mathcal{O}\left(\dfrac{\textit{zero}\cdot\textit{one}}{\textit{limit}}\right)$。忽略预处理的时间和空间。
  • 空间复杂度:$\mathcal{O}(\min(\textit{zero},\textit{one}))$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Taro 支付宝小程序可观测最佳实践

为什么要做可观测?—— 解决“不可见”带来的业务风险

在 Taro 开发的支付宝小程序中,可观测性(Observability)的本质是通过数据化、实时化的监控手段,让小程序的运行状态、用户行为及系统交互“可见、可分析、可追溯”。其必要性源于小程序在实际运行中面临的三大核心挑战:

1、“黑盒化”运行环境:问题难以感知,故障发现滞后

小程序部署在支付宝客户端及云端环境中,开发者无法直接接触用户设备的真实运行状态。当出现以下问题时,若无可观测能力,往往只能依赖用户反馈或事后投诉。

典型案例:某电商小程序上线后,用户投诉“提交订单按钮点击无效”,但开发团队通过日志仅能看到“按钮渲染成功”,因缺乏用户操作路径和接口调用的关联数据,耗时 3 天才定位到是“异步数据未加载完成时按钮未禁用”的逻辑缺陷。若提前部署可观测能力,可通过用户行为流+接口状态实时发现该问题。

2、业务依赖复杂:跨端、跨服务的故障定位成本高

Taro 小程序通常依赖多方服务:前端通过 Taro 框架调用支付宝小程序 API(如支付、登录)、与后端服务交互(如订单查询、用户数据同步),甚至涉及第三方服务(如地图 SDK、营销活动平台)。任何一个环节异常都可能导致整体功能失效,但问题根源可能隐藏在任意一层,若无统一的观测数据(如接口调用链、用户操作路径、错误上下文),排查效率极低,可能导致用户流失或业务损失。

3、用户体验敏感:微小问题可能引发大规模负面反馈

支付宝小程序的用户对体验的容忍度极低——页面加载慢 1 秒可能导致 5% 的用户流失,支付流程卡顿可能直接引发投诉。但用户通常不会详细描述问题(如“首屏渲染慢是因为图片未压缩”),而是给出模糊反馈(“用不了”“太卡了”)。若无可观测能力,开发者只能通过“猜测+灰度测试”被动优化,无法精准识别影响用户体验的关键节点。

这些问题的解决依赖于对用户行为、性能指标的实时监控,而非事后统计。

把观测云作为 Taro 支付宝小程序可观测最佳实践的核心工具--落地路径与关键动作

  • 将观测云定位为小程序的“统一可观测平台”,以统一标签(如 service、env、version)贯穿指标、日志、链路、用户访问四类数据,形成端到端的观测视图。
  • 在 Taro 多端(含支付宝小程序)场景中,前端接入 RUM(用户访问监测),后端与基础设施接入 Logging/Tracing/Metrics,实现跨端、跨服务的统一观测与关联。
  • 通过“Dashboard + Explorer”构建业务与技术双视角的可观测体系,既看得到用户体验,也定位得到根。

前端 RUM 接入(Taro 支付宝小程序)

本最佳实践实验版本为 Taro v4.1.7

查看编译环境
➜  ~ taro --version 
? Taro v4.1.7
4.1.7
➜  ~ 

1、观测云后台->用户访问监测->新建应用,选择应用类型为小程序,输入应用名称、应用ID,系统会自动根据配置信息生成引入 SDK 的代码,可选择 NPM 接入或 CDN 文件接入(这里我们选择 CDN 文件方式接入)。

  • 应用名称:用于识别当前用户访问监测的应用名称。
  • 应用 ID :应用在当前工作空间唯一标识,对应字段:app_id 。

2、根据链接下载 CDN 文件,打开项目代码,在小程序项目中的 app.ts 入口文件引入观测云后台生成的配置代码。

注意项目中引入代码的位置,必须要在 App() 初始化之前,否则会出现数据无法正常采集上报,或者只能上报一小部分数据的情况。

3、本地运行项目或者打包发布测试后,通过观察接口调用发现 /v1/write/rum 接口成功调用,说明数据成功上报,可在对应应用的查看器查看上报数据。

4、js 会自动生成一个 trace_id,如果在 allowedTracingOrigins 配置了正则(本最佳实践中配置的是 *,意味着所有接口都会带 ),会在对应的请求头中加入一些参数,ddtrace 的是这几个参数。

实现效果

1、小程序部署在支付宝客户端及云端环境中,开发者直接接触用户设备的真实运行状态
进入「用户访问监测」页面—> 选择创建的对应微信小程序—> 分析看板,可以查看小程序运行中的部分重点信息。

进入「用户访问监测」页面—> 选择创建的对应微信小程序—> 查看器,可以查看小程序应用中用户访问的动作,请求资源以及项目中的报错信息等。

2、多方服务全链路打通

3、后端服务透明化,可以观测到更详细的代码级方法,开发可以快速定位

4、用户体验可视化,全局可观测

观测云为 Taro 支付宝小程序创建一份“数字神经系统”

在 Taro 开发的小程序中,可观测性不是“可选能力”,而是保障业务健康增长的必备基础设施。它通过将“不可见的运行状态”转化为“可见的数据洞察”,帮助开发者:

  • 从被动救火到主动预防(快速发现问题,降低故障损失);
  • 从经验驱动到数据驱动(精准优化体验,提升转化效率);
  • 从局部优化到全局可控(支撑规模化增长,保障业务稳定性);
  • 从单点协作到团队协同(提升研发效率,加速产品迭代)。

每日一题-找出所有稳定的二进制数组 I🟡

给你 3 个正整数 zero ,one 和 limit 。

一个 二进制数组 arr 如果满足以下条件,那么我们称它是 稳定的

  • 0 在 arr 中出现次数 恰好 为 zero 。
  • 1 在 arr 中出现次数 恰好 为 one 。
  • arr 中每个长度超过 limit 的 子数组同时 包含 0 和 1 。

请你返回 稳定 二进制数组的 数目。

由于答案可能很大,将它对 109 + 7 取余 后返回。

 

示例 1:

输入:zero = 1, one = 1, limit = 2

输出:2

解释:

两个稳定的二进制数组为 [1,0] 和 [0,1] ,两个数组都有一个 0 和一个 1 ,且没有子数组长度大于 2 。

示例 2:

输入:zero = 1, one = 2, limit = 1

输出:1

解释:

唯一稳定的二进制数组是 [1,0,1] 。

二进制数组 [1,1,0] 和 [0,1,1] 都有长度为 2 且元素全都相同的子数组,所以它们不稳定。

示例 3:

输入:zero = 3, one = 3, limit = 2

输出:14

解释:

所有稳定的二进制数组包括 [0,0,1,0,1,1] ,[0,0,1,1,0,1] ,[0,1,0,0,1,1] ,[0,1,0,1,0,1] ,[0,1,0,1,1,0] ,[0,1,1,0,0,1] ,[0,1,1,0,1,0] ,[1,0,0,1,0,1] ,[1,0,0,1,1,0] ,[1,0,1,0,0,1] ,[1,0,1,0,1,0] ,[1,0,1,1,0,0] ,[1,1,0,0,1,0] 和 [1,1,0,1,0,0] 。

 

提示:

  • 1 <= zero, one, limit <= 200

找出所有稳定的二进制数组 I

方法一:动态规划

题目要求二进制数组 $\textit{arr}$ 中每个长度超过 $\textit{limit}$ 的子数组同时包含 $0$ 和 $1$,这个条件等价于二进制数组 $\textit{arr}$ 中每个长度等于 $\textit{limit} + 1$ 的子数组都同时包含 $0$ 和 $1$(读者可以思考一下证明过程)。

按照题目要求,我们需要将 $\textit{zero}$ 个 $0$ 和 $\textit{one}$ 个 $1$ 依次填入二进制数组 $\textit{arr}$,使用 $\textit{dp}_0[i][j]$ 表示已经填入 $i$ 个 $0$ 和 $\textit{j}$ 个 $1$,并且最后填入的数字为 $0$ 的可行方案数目,$\textit{dp}_1[i][j]$ 表示已经填入 $i$ 个 $0$ 和 $\textit{j}$ 个 $1$,并且最后填入的数字为 $1$ 的可行方案数目。对于 $\textit{dp}_0[i][j]$,我们分析一下它的转换方程:

  • 当 $j = 0$ 且 $i \in [0, \min(\textit{zero}, \textit{limit})]$ 时:我们可以不断地填入 $0$,所以 $\textit{dp}_0[i][j] = 1$。

  • 当 $i = 0$,或者 $j = 0$ 且 $i \notin [0, \min(\textit{zero}, \textit{limit})]$ 时:我们没法构造可行的方案,所以 $\textit{dp}_0[i][j] = 0$。

  • 当 $i > 0$ 且 $j > 0$ 时:$\textit{dp}_0[i][j]$ 可以分别由 $\textit{dp}_0[i - 1][j]$ 和 $\textit{dp}_1[i - 1][j]$ 转移而来,分别考虑两种情况:

    • 对于 $\textit{dp}_1[i - 1][j]$:显然可以通过在 $\textit{dp}_1[i - 1][j]$ 对应的所有填入方案后再填入一个 $0$ 得到对应的可行方案。

    • 对于 $\textit{dp}_0[i - 1][j]$:当 $i \le \textit{limit}$ 时,显然可以通过在 $\textit{dp}_1[i - 1][j]$ 对应的所有填入方案后再填入一个 $0$ 得到对应的可行方案;当 $i \gt \textit{limit}$ 时,我们需要去除一些不可行的方案数。因为 $\textit{dp}_0[i - 1][j]$ 对应的所有填入方案都是可行的,而只有一种情况会在额外填入一个 $0$ 时,变成不可行,即先前已经连续填入 $\textit{limit}$ 个 $0$,对应的方案数为 $\textit{dp}_1[i - \textit{limit} - 1][j]$。

根据以上分析,我们有 $\textit{dp}_0[i][j]$ 的转移方程:

$$
\textit{dp}_0[i][j] = \begin{cases}
1, & i \in [0, \min(\textit{zero}, \textit{limit})], j = 0 \
\textit{dp}_1[i - 1][j] + \textit{dp}_0[i - 1][j] - \textit{dp}_1[i - \textit{limit} - 1][j], & i > limit, j > 0 \
\textit{dp}_1[i - 1][j] + \textit{dp}_0[i - 1][j], & i \in [0, limit], j > 0 \
0, & otherwise
\end{cases}
$$

同理,我们也可以获得 $\textit{dp}_1[i][j]$ 的转移方程:

$$
\textit{dp}_1[i][j] = \begin{cases}
1, & i = 0, j \in [0, \min(\textit{one}, \textit{limit})] \
\textit{dp}_0[i][j - 1] + \textit{dp}_1[i][j - 1] - \textit{dp}_0[i][j - \textit{limit} - 1], & i > 0, j > limit \
\textit{dp}_0[i][j - 1] + \textit{dp}_1[i][j - 1], & i > 0, j \in [0, limit] \
0, & otherwise
\end{cases}
$$

最后,稳定二进制数组的数目等于 $\textit{dp}_0[\textit{zero}][\textit{one}] + \textit{dp}_1[\textit{zero}][\textit{one}]$。

###C++

class Solution {
public:
    int numberOfStableArrays(int zero, int one, int limit) {
        vector<vector<vector<long long>>> dp(zero + 1, vector<vector<long long>>(one + 1, vector<long long>(2)));
        long long mod = 1e9 + 7;
        for (int i = 0; i <= min(zero, limit); i++) {
            dp[i][0][0] = 1;
        }
        for (int j = 0; j <= min(one, limit); j++) {
            dp[0][j][1] = 1;
        }
        for (int i = 1; i <= zero; i++) {
            for (int j = 1; j <= one; j++) {
                if (i > limit) {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1];
                } else {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1];
                }
                dp[i][j][0] = (dp[i][j][0] % mod + mod) % mod;
                if (j > limit) {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0];
                } else {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0];
                }
                dp[i][j][1] = (dp[i][j][1] % mod + mod) % mod;
            }
        }
        return (dp[zero][one][0] + dp[zero][one][1]) % mod;
    }
};

###Java

class Solution {
    public int numberOfStableArrays(int zero, int one, int limit) {
        final long MOD = 1000000007;
        long[][][] dp = new long[zero + 1][one + 1][2];
        for (int i = 0; i <= Math.min(zero, limit); i++) {
            dp[i][0][0] = 1;
        }
        for (int j = 0; j <= Math.min(one, limit); j++) {
            dp[0][j][1] = 1;
        }
        for (int i = 1; i <= zero; i++) {
            for (int j = 1; j <= one; j++) {
                if (i > limit) {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1];
                } else {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1];
                }
                dp[i][j][0] = (dp[i][j][0] % MOD + MOD) % MOD;
                if (j > limit) {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0];
                } else {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0];
                }
                dp[i][j][1] = (dp[i][j][1] % MOD + MOD) % MOD;
            }
        }
        return (int) ((dp[zero][one][0] + dp[zero][one][1]) % MOD);
    }
}

###C#

public class Solution {
    public int NumberOfStableArrays(int zero, int one, int limit) {
        const long MOD = 1000000007;
        long[][][] dp = new long[zero + 1][][];
        for (int i = 0; i <= zero; i++) {
            dp[i] = new long[one + 1][];
            for (int j = 0; j <= one; j++) {
                dp[i][j] = new long[2];
            }
        }
        for (int i = 0; i <= Math.Min(zero, limit); i++) {
            dp[i][0][0] = 1;
        }
        for (int j = 0; j <= Math.Min(one, limit); j++) {
            dp[0][j][1] = 1;
        }
        for (int i = 1; i <= zero; i++) {
            for (int j = 1; j <= one; j++) {
                if (i > limit) {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1];
                } else {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1];
                }
                dp[i][j][0] = (dp[i][j][0] % MOD + MOD) % MOD;
                if (j > limit) {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0];
                } else {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0];
                }
                dp[i][j][1] = (dp[i][j][1] % MOD + MOD) % MOD;
            }
        }
        return (int) ((dp[zero][one][0] + dp[zero][one][1]) % MOD);
    }
}

###Go

func numberOfStableArrays(zero int, one int, limit int) int {
    dp := make([][][2]int, zero + 1)
    mod := int(1e9 + 7)
    for i := 0; i <= zero; i++ {
        dp[i] = make([][2]int, one + 1)
    }
    for i := 0; i <= min(zero, limit); i++ {
        dp[i][0][0] = 1
    }
    for j := 0; j <= min(one, limit); j++ {
        dp[0][j][1] = 1
    }
    for i := 1; i <= zero; i++ {
        for j := 1; j <= one; j++ {
            if i > limit {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1]
            } else {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1]
            }
            dp[i][j][0] = (dp[i][j][0] % mod + mod) % mod
            if j > limit {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0]
            } else {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0]
            }
            dp[i][j][1] = (dp[i][j][1] % mod + mod) % mod
        }
    }
    return (dp[zero][one][0] + dp[zero][one][1]) % mod
}

###Python

class Solution:
    def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
        dp = [[[0, 0] for _ in range(one + 1)] for _ in range(zero + 1)]
        mod = int(1e9 + 7)
        for i in range(min(zero, limit) + 1):
            dp[i][0][0] = 1
        for j in range(min(one, limit) + 1):
            dp[0][j][1] = 1
        for i in range(1, zero + 1):
            for j in range(1, one + 1):
                if i > limit:
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1]
                else:
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1]
                dp[i][j][0] = (dp[i][j][0] % mod + mod) % mod
                if j > limit:
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0]
                else:
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0]
                dp[i][j][1] = (dp[i][j][1] % mod + mod) % mod
        return (dp[zero][one][0] + dp[zero][one][1]) % mod

###C

#define MOD 1000000007

int numberOfStableArrays(int zero, int one, int limit) {
    long long dp[zero + 1][one + 1][2];
    memset(dp, 0, sizeof(dp));
    for (int i = 0; i <= (zero < limit ? zero : limit); ++i) {
        dp[i][0][0] = 1;
    }
    for (int j = 0; j <= (one < limit ? one : limit); ++j) {
        dp[0][j][1] = 1;
    }
    for (int i = 1; i <= zero; ++i) {
        for (int j = 1; j <= one; ++j) {
            if (i > limit) {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1];
            } else {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1];
            }
            dp[i][j][0] = (dp[i][j][0] % MOD + MOD) % MOD;
            if (j > limit) {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0];
            } else {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0];
            }
            dp[i][j][1] = (dp[i][j][1] % MOD + MOD) % MOD;
        }
    }
    int result = (dp[zero][one][0] + dp[zero][one][1]) % MOD;
    return result;
}

###JavaScript

const MOD = 1000000007;

var numberOfStableArrays = function(zero, one, limit) {
    const dp = Array.from({ length: zero + 1 }, () =>
        Array.from({ length: one + 1 }, () => [0, 0])
    );

    for (let i = 0; i <= Math.min(zero, limit); i++) {
        dp[i][0][0] = 1;
    }
    for (let j = 0; j <= Math.min(one, limit); j++) {
        dp[0][j][1] = 1;
    }

    for (let i = 1; i <= zero; i++) {
        for (let j = 1; j <= one; j++) {
            if (i > limit) {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1];
            } else {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1];
            }
            dp[i][j][0] = (dp[i][j][0] % MOD + MOD) % MOD;
            if (j > limit) {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0];
            } else {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0];
            }
            dp[i][j][1] = (dp[i][j][1] % MOD + MOD) % MOD;
        }
    }
    return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
};

###TypeScript

const MOD = 1000000007;

function numberOfStableArrays(zero: number, one: number, limit: number): number {
    const dp: number[][][] = Array.from({ length: zero + 1 }, () =>
        Array.from({ length: one + 1 }, () => [0, 0])
    );

    for (let i = 0; i <= Math.min(zero, limit); i++) {
        dp[i][0][0] = 1;
    }
    for (let j = 0; j <= Math.min(one, limit); j++) {
        dp[0][j][1] = 1;
    }

    for (let i = 1; i <= zero; i++) {
        for (let j = 1; j <= one; j++) {
            if (i > limit) {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit - 1][j][1];
            } else {
                dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1];
            }
            dp[i][j][0] = (dp[i][j][0] % MOD + MOD) % MOD;
            if (j > limit) {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit - 1][0];
            } else {
                dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0];
            }
            dp[i][j][1] = (dp[i][j][1] % MOD + MOD) % MOD;
        }
    }
    return (dp[zero][one][0] + dp[zero][one][1]) % MOD;
};

###Rust

const MOD: i32 = 1000000007;

impl Solution {
    pub fn number_of_stable_arrays(zero: i32, one: i32, limit: i32) -> i32 {
        let mut dp = vec![vec![vec![0; 2]; one as usize + 1]; zero as usize + 1];

        for i in 0..=zero.min(limit) as usize {
            dp[i][0][0] = 1;
        }
        for j in 0..=one.min(limit) as usize {
            dp[0][j][1] = 1;
        }

        for i in 1..=zero as usize {
            for j in 1..=one as usize {
                if i > limit as usize {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1] - dp[i - limit as usize - 1][j][1];
                } else {
                    dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1];
                }
                dp[i][j][0] = (dp[i][j][0] % MOD + MOD) % MOD;
                if j > limit as usize {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0] - dp[i][j - limit as usize - 1][0];
                } else {
                    dp[i][j][1] = dp[i][j - 1][1] + dp[i][j - 1][0];
                }
                dp[i][j][1] = (dp[i][j][1] % MOD + MOD) % MOD;
            }
        }
        (dp[zero as usize][one as usize][0] + dp[zero as usize][one as usize][1]) % MOD
    }
}

复杂度分析

  • 时间复杂度:$O(\textit{zero} \times \textit{one})$,其中 $\textit{zero}$ 和 $\textit{one}$ 分别为 $0$ 和 $1$ 的出现次数。

  • 空间复杂度:$O(\textit{zero} \times \textit{one})$。

逐步优化 DP 状态

前记:读了读别人的题解,感觉 newhar 老师的思路更清晰易懂一点,推荐大家看 newhar 的题解,这里就记一下自己的解法。

解法:DP

以下题解中把 limit 简记为 $l$。

转化题目条件

首先,题目要求序列中恰好出现 zero 个 $0$ 以及 one 个 $1$,显然序列的长度必须为 zero + one。记序列的长度为 $n$。

只要一个子数组包含 $0$ 和 $1$,那么所有完全包含它的子数组也会包含 $0$ 和 $1$。因此,题目中“arr 中每个长度超过 $l$ 的子数组都同时包含 $0$ 和 $1$”这一条件可以重写为:

arr 中每个长度恰为 $(l + 1)$ 的子数组都同时包含 $0$ 和 $1$。

设计 DP 状态

考虑任一下标 $i$,假设 $i$ 左边最近的 $0$ 到 $i$ 的距离是 $p_i$($p_i = 0$ 说明 arr[i] == 0),$i$ 左边最近的 $1$ 到 $i$ 的距离是 $q_i$($q_i = 0$ 说明 arr[i] == 1)。不妨假设下标 $i$ 是某个长度为 $(l + 1)$ 的子数组的右端点,因为这个子数组里既有 $0$ 又有 $1$,那么我们同时有 $p_i \le l$ 以及 $q_i \le l$。

由此可以设计出朴素的 DP 状态。设 $f(i, j, p, q)$ 表示考虑序列的前 $i$ 个元素中有 $j$ 个 $1$,且 $i$ 到最近的 $0$ 距离为 $p$,到最近的 $1$ 距离为 $q$ 的方案数。但这样状态总数为 $\mathcal{O}(n^2l^2)$,无论是 C 题还是 D 题都无法通过。

优化 DP 状态

注意到元素要么是 $0$ 要么是 $1$,也就是说对于任意的 $i$,$p = 0$ 和 $q = 0$ 恰有一个成立。因此可以将 DP 状态压缩为 $f(i, j, t = 0/1, d)$ 表示考虑序列的前 $i$ 个元素中有 $j$ 个 $1$,且第 $i$ 个元素填的是 $t$,$i$ 到最近的另一种元素(即 $1 - t$)距离为 $d$ 的方案数。这样状态总数为 $\mathcal{O}(n^2l)$。

转移方程分为两部分。

f(i, j, 0, d) += f(i - 1, j, 0, d - 1)
f(i, j, 1, d) += f(i - 1, j - 1, 1, d - 1)

这一部分的含义是,让第 $i$ 个元素和第 $(i - 1)$ 个元素相同,那么到另一种元素的距离自然增加 $1$。

f(i, j, 0, 1) += f(i - 1, j, 1, *)
f(i, j, 1, 1) += f(i - 1, j - 1, 0, *)

这一部分的含义是,让第 $i$ 个元素和第 $(i - 1)$ 个元素不同,那么到另一种元素(也就是上一个元素)的距离就是 $1$。这里的星号通过维护前缀和即可 $\mathcal{O}(1)$ 计算。

初值就是枚举第一个元素填 $0$ 还是 $1$。对于第一个元素来说,另一种元素已经 $1$ 个位置没出现了,所以 $d = 1$,即 $f(1, 0, 0, 1) = f(1, 1, 1, 1) = 1$。

整体复杂度 $\mathcal{O}(n^2l)$,可以通过 C 题。

继续优化 DP 状态

我们发现,DP 主要的运算复杂度来自转移方程的第一部分。第一部分的转移方程看起来很简单,好像只是把上一个位置所有 $(d - 1)$ 的 DP 值都照搬过来而已。由于 $d \le l$ 的限制,只是丢弃了 $f(i - 1, j, *, l)$ 的 DP 值。而 DP 方程的第二部分没有丢弃任何 DP 值,通过前缀和直接把所有情况照单全收。

这里其实在暗示我们:只要上个位置距离另一种元素的距离不到 $l$,那么当前位置无论填什么都是安全的。只有上个位置距离另一种元素的距离恰为 $l$ 时,当前位置必须填与上个位置不同的元素。也就是说:

(当前位置填元素 t 的方案数) = (上个位置的所有方案数) - (元素 1 - t 最近一次在 i - limit - 1 出现,从 i - limit 到 i 全部都是元素 t 的方案数)

设 $f(i, j, t = 0/1)$ 表示考虑序列的前 $i$ 个元素中有 $j$ 个 $1$,且第 $i$ 个元素填的是 $t$ 的方案数。“元素 $(1 - t)$ 最近一次在 $(i - l - 1)$ 出现,从 $(i - l)$ 到 $i$ 全部都是元素 $t$ 的方案数”怎么算呢?当然就是 $f(i - l - 1, j', 1 - t)$。这里 $j'$ 要看如果 $t = 0$ 那么 $j' = j$,否则如果 $t = 1$ 那么由于从 $(i - l)$ 到 $i$ 全部都是 $1$,则 $j' = j - l - 1$。

由此得到转移方程.

f(i, j, 0) = f(i - 1, j, 0) + f(i - 1, j, 1) - f(i - l - 1, j, 1)
f(i, j, 1) = f(i - 1, j - 1, 0) + f(i - 1, j - 1, 1) - f(i - l - 1, j - l - 1, 0)

整体复杂度 $\mathcal{O}(n^2)$,可以通过 D 题。

但是,实现过程中有一个初值的细节问题。当 $i = l + 1$ 时,我们要求 “元素 $(1 - t)$ 最近一次在下标 $0$ 出现的方案数”。空序列一定是合法的,所以有 $f(0, 0, 0) = f(0, 0, 1) = 1$。然而当 $i = 1$ 的时候,这样的初值设置会导致 $f(i - 1, j, 0) + f(i - 1, j, 1)$ 这部分的转移方程出现重复计算,所以我们直接继续设置初值 $f(1, 0, 0) = f(1, 1, 1) = 1$。

参考代码(c++)

###c++

class Solution {
public:
    int numberOfStableArrays(int zero, int one, int limit) {
        const int MOD = 1e9 + 7;
        int n = zero + one;

        long long g[n + 1][one + 1][2];
        memset(g, 0, sizeof(g));
        g[0][0][0] = g[0][0][1] = g[1][0][0] = g[1][1][1] = 1;

        auto update = [&](long long &x, long long y) {
            x = (x + y) % MOD;
        };

        for (int i = 2; i <= n; i++) {
            for (int j = 0; j <= one; j++) {
                update(g[i][j][0], g[i - 1][j][1] + g[i - 1][j][0]);
                if (i > limit) update(g[i][j][0], MOD - g[i - 1 - limit][j][1]);

                if (j > 0) update(g[i][j][1], g[i - 1][j - 1][0] + g[i - 1][j - 1][1]);
                if (i > limit && j > limit) update(g[i][j][1], MOD - g[i - 1 - limit][j - 1 - limit][0]);
            }
        }

        return (g[n][one][0] + g[n][one][1]) % MOD;
    }
};

uni-app x 蒸汽模式 性能测试基准报告 Benchmark

背景

uni-app x 蒸汽模式,是DCloud于2026年推出的跨平台开发框架新版本。

该产品的特点是:比原生更快

本基准测试的目标,即为了真实呈现主要性能指标,并确保开发者可自行重现本基准测试,并得出相近结论。

先简要介绍 uni-app x 及 蒸汽模式

  • uni-app x 使用vue语法,并在蒸汽模式中去除了虚拟DOM
  • 蒸汽模式中,模板和样式编译为c或c++代码(在Android也会编译出部分kotlin代码),script仍然为uts语言
  • uni-app x 基于原生渲染管线,可融合原生组件生态,并占用更小的内存
  • 蒸汽模式提供了大量自研高性能组件,如view、text、image、list、rich-text、swiper、slider、picker等

测试指标

UI系统的核心性能指标是:渲染速度和帧率

追求渲染速度更快、掉帧更少。

人工体感可以录像,但测试指标必须可精准度量,需要准确的度量方案。

环境声明

目前 uni-app x 蒸汽模式,仅在鸿蒙平台公开发布,本测试报告仅包含鸿蒙设备的测试。

本Benchmark使用了2台鸿蒙系统在售的最低端机型 nove 12,具体信息如下:

  • 设备型号:nova 12 (不是pro) 运行内存8G
  • OS版本:6.0.0.130(截止测试时间的最新版OS,patch02)
  • 全部使用release方式运行
  • 电量90%左右,未开启节能模式。该设备仅支持普通模式和节能模式
  • 屏幕的刷新率设置为高,即120hz
  • 测试前所有设备重启,并静置2分钟。除关于本机的界面外,杀掉所有其他App的进程

view和text渲染速度测试

view和text是渲染引擎的核心基础,大量组件基于这2个基础组件构建。这2个基础组件的渲染速度是一套渲染引擎最核心的性能指标。

验证一个view和text创建速度是否足够快,可靠的方式是在同一个屏幕内创建大量view和text组件,计算耗时。

测试方法

点击按钮后,在屏幕上创建2000个view,每个view有一个背景色,每个view中再套入一个text组件。

2000个view需在同一屏幕区显示,view不设宽高,text字体较小。view们被分为50行,每行40个view,同时每行外层再套一个view。

即,一共4050的元素,其中2050个view和2000个text。

对比使用 uni-app x 蒸汽模式arkUI原生,进行创建速度的测试。

首先看录屏对比。 左边为arkUI原生,右边为uni-app x 蒸汽模式

IMG_3827.GIF

高清视频,可到 B 站观看:www.bilibili.com/video/BV1Rp…

界面中弹出的toast显示了耗时,单位为ms。arkUI为804ms、uni-app x蒸汽模式为267ms。计时说明:

  • 开始时间为按钮的click事件触发时间
  • 结束时间为主线程渲染指令已全部送达OS渲染进程时间。此时主线程已经完成本次渲染所需的工作,处于空闲状态。

该结束时间并非肉眼所见的屏幕显示时间,实际上渲染进程和GPU仍需一定时间工作才能让屏幕显示图像,但后续时间段无法通过编程打点计时。

经过录屏和计时的粗略对比,发现arkUIuni-app x 蒸汽模式在渲染进程和GPU的耗时接近,都在1帧左右,故在后续精准比较中忽略这段时间,保留目前的结束时间定义。

该实验重复5次。每次均杀掉应用进程重新进入,精准计算的耗时如下:

arkUI uni-app x蒸汽模式
791 243
805 244
811 244
771 243
810 242

平均值:

arkUI uni-app x蒸汽模式
797.6 243.2

以上数据单位均为ms。

测试结论

结论:在4050 view和text同屏渲染测试中,uni-app x 蒸汽模式的渲染速度是 arkUI 3.3倍

复现工程源码和体验方式

上述2个示例,源码如下:

arkUI版本,需要自行编译原始工程。

uni-app x 蒸汽模式,可以在HBuilderX 5.0以上版本编译运行(注意选用release方式运行,或者发行为正式包安装)。

hello uni-app x示例应用已经在鸿蒙应用商店上架,可以搜索“DCloud开发者中心系统”,或使用鸿蒙手机扫描下方二维码: 

图片

安装 hello uni-app x 后,点击右下角模板 -> 顶部有 view和text性能测试。

uni-app x 作为通用引擎,未对该示例做任何定制优化,没有诸如预加载、预测量等影响实验结果的行为。

长列表掉帧测试

list组件的地位,在渲染引擎中仅次于view和text。

现代渲染引擎,都采用复用技术实现长列表,确保持续滑动长列表后,内存没有持续增长。

使用复用技术的长列表进入速度都很快,因为只加载了一部分数据,但在滚动过程中持续加载数据并复用已存在视图时,如果列表复杂,会发生滚动掉帧。

测试方法

设计一个非常复杂的“死亡长列表”:

  • 加载4000行数据,7.4M的JSON
  • 每行超过40+元素,包括文字、图片、视频、自定义vue组件
  • 每行嵌套10+层
  • 渲染2万个元素,占据普通手机1333屏左右
  • 列表中还有大量的阴影、圆角、边框等复杂渲染样式

在人工体验中,用户可以体验加载速度、快速滑动时的流畅度,但在严谨的Benchmark中,需要精准的对比数据。

首先需要制作一个fps组件,监听系统的帧回调,在120Hz高刷屏上,每8.33ms会触发一次帧回调。如果2个帧回调的代码响应时长超过了8.33ms,就意味着掉帧。

该fps组件需要使用同样的逻辑分别实现arkUI版本和uni-app x版本。源码见后续 复现工程 章节。

同时死亡长列表的代码,也需要在arkUI和uni-app x中使用相同逻辑实现。

arkUI中使用lazy foreach,uni-app x中使用list-view。

使用arkUI版本和uni-app x版本分别进入长列表,滚动到底部,加载完4000行数据,然后点击鸿蒙手机的顶部状态栏,此时会滚动回到列表顶部。

两个版本回滚速度一样,均为1秒,在这个回滚到顶部的过程中,计算帧率,验证掉帧情况。同时从录像视觉上进行直观感受。

首先看录屏对比。 左边为arkUI原生,右边为uni-app x蒸汽模式

deadlist.GIF

高清视频,可移步B站观看,uni-app x vs arkUI 原生 长列表掉帧对比视频

视觉体验中可看出,arkUI的fps组件数字在1秒的动画期间更低,在回滚过程中很多视频呈现黑块。

该实验重复5次,每次均杀掉应用重新进入,重新滚动到顶部。

5次的测试数据如下:

  • arkUI:
    • 回滚过程中的平均FPS: 19.67, 最高FPS: 49, 最低FPS: 13
    • 回滚过程中的平均FPS: 24.14, 最高FPS: 61, 最低FPS: 15
    • 回滚过程中的平均FPS: 19.50, 最高FPS: 43, 最低FPS: 13
    • 回滚过程中的平均FPS: 20.67, 最高FPS: 53, 最低FPS: 13
    • 回滚过程中的平均FPS: 21.67, 最高FPS: 56, 最低FPS: 13
  • uni-app-x 蒸汽模式:
    • 回滚过程中的平均FPS: 78.29, 最高FPS: 100, 最低FPS: 52
    • 回滚过程中的平均FPS: 112.30, 最高FPS: 120, 最低FPS: 90
    • 回滚过程中的平均FPS: 106.33, 最高FPS: 120, 最低FPS: 45
    • 回滚过程中的平均FPS: 88.43, 最高FPS: 120, 最低FPS: 29
    • 回滚过程中的平均FPS: 104.50, 最高FPS: 120, 最低FPS: 55

arkUI的5次平均fps为 21.13,最高fps为61,最低fps为13。

uni-app x蒸汽模式的5次平均fps为 97.97,最高fps为120,最低为29。

测试结论

结论:在长列表帧率测试中,uni-app x蒸汽模式的平均帧率是 arkUI 4.6倍

实测发现arkUI版本的长列表中的video,无法记忆video的播放进度,即播放A视频到5s时,滚动到其他地方,然后再滚回来显示A视频,A视频会重头播放。

uni-app x的版本记忆了播放进度。除了功能的不同外,此差异也需要考虑到帧率对比中,记忆播放进度本身也耗费时间,也就是如果uni-app x取消记忆播放进度,帧率还能再提升。

复现工程源码和体验方式

上述2个示例,源码如下:

arkUI版本,需要自行编译原始工程。

uni-app x蒸汽模式,可以在HBuilderX 5.0以上版本编译运行(注意选用release方式运行,或者发行为正式包安装)。

hello uni-app x示例应用已经在鸿蒙应用商店上架,可以搜索“DCloud开发者中心系统”,或使用鸿蒙手机扫描下方二维码: 

图片

安装hello uni-app x后,点击右下角模板 -> 顶部有 死亡长列表。

uni-app x作为通用引擎,未对该示例做任何定制优化,没有诸如预加载、预测量等影响实验结果的行为。

此示例中7M多的4000行数据并非静态数据存在本地,而是由代码生成的数据,生成数据的代码是预执行的,在arkUI版和uni-app x版均如此。

其他组件

一套渲染引擎,除了view、text、list外,还需要更多高性能的组件。

uni-app x中对各种组件都做了极限性能测试,但受限于精力,未对arkUI组件全面做性能测试。

开发者可以在 hello uni-app x 中体验各种组件的性能测试,几乎每个组件的示例中,都单独提供了 组件性能测试。

  • rich-text组件:App平台的rich-text过去一直没有太好的解决方案。鸿蒙自身的richText组件也是基于webview渲染的,存在加载慢、内存占用高、快滑白屏等问题。uni-app x 蒸汽模式 提供更快的rich-text组件。以下测试,加载5万字长文、59张插图。可以看到无等待进入页面。上下快滑不掉帧、除了联网图片加载外滑不出白屏。高清演示视频,可移步 B 站观看:uni-app x rich-text 演示

     注:鸿蒙手机录屏时帧率只能为60Hz,实际使用时是完整的120Hz。下同。

  • swiper组件:加载100个item。无等待进入页面。在上述5万字长文中点击图片,可以看到swiper中无等待呈现59张图片,左右切换图片无延迟

  • picker组件:加载省市区4000条数据,无等待弹出组件。高清视频,可移步 B 站观看:uni-app x picker 组件演示

  • slide组件:拖动100个slider,丝滑流畅。高清演示视频,可移步 B 站观看:uni-app x slide组件演示

  • loading组件:屏幕上同时旋转100个loading不掉帧(录屏后从120掉帧到60)。高清视频,可移步 B 站观看:uni-app x loading组件演示

  • canvas组件:屏幕上同时移动数百个小球不掉帧。高清演示视频,可移步 B 站观看:uni-app x canvas组件演示

  • 众多表单组件均有100或200个创建速度测试监控。hello uni-app x 模板中还提供了日历、竖滑视频、侧滑删除长列表、ai chat的流式打字机等性能考验示例。高清演示视频,可移步 B 站观看:uni-app x 侧滑删除长列表演示uni-ai x 流式打字演示

在ai时代,很多App都需要内嵌一个开源的AI对话聊天库,能流式解析markdown,解析过程不掉帧。为此DCloud推出开源的uni-ai x,详见ext.dcloud.net.cn/plugin?id=2…

没有用户喜欢等待、没有用户喜欢卡顿掉帧。

从2007年iPhone发布后,手机用户每天都要为每次页面转场等待300ms。uni-app x 将支撑这个时间大幅缩短。hello uni-app x的蒸汽模式中已默认改为150ms,这150ms更多是留给网络。

如果开发者使用h3等新兴网络技术,优化好服务器速度,还可以把等待时间缩的更短。

FAQ

uni-app x 的App平台到底是自渲染还是原生渲染?

是原生渲染。准确的讲,是在原生渲染管线上自己做几乎所有组件。

如果使用xComponent自渲染,会因为2条渲染管线并存额外消耗硬件资源。

并且鸿蒙有很多原生组件,比如权限按钮、map地图以及三方生态中大量arkUI原生组件,自渲染方案在与原生生态融合时问题较多。两条渲染管线的滚动同步、资源消耗均导致这一路线不是最佳方案。

站在宏观视角,在原生渲染管线中优化,提供更快的核心组件,兼容所有原生组件,比自立一套组件生态对产业更有意义。

为什么都是原生渲染,uni-app x的蒸汽模式比原生渲染更快?

这里面涉及数千项工程优化,举例一些:

  1. Android的compose ui也是基于原生渲染管线的,但没有使用Android自带的view、textview,而是实现了自己的组件系统。

    这条路可行,只不过compose ui没有成为一个好标杆,它实际渲染速度比view体系更慢。(在上述4050示例对比中,有原生view和compose ui的测试例,详见

    uni-app x 蒸汽模式,也几乎没有使用系统自带的组件,不管是textView、recycleView、viewPage...,或者是鸿蒙的arkUI相关组件,基本都没用。全新研发的组件做到了性能更高。

  2. vue里template和style里的代码,被直接编译为优化度非常高的C代码。它的运行速度远快于arkts、kotlin及k/n。

    当然它的副作用就是编译速度很慢,开发C的工程师应该知道编译大型C工程是一件耗时工作。

    后续DCloud也会提供开发期间的热刷新方案。

在uni-app x的示例中发现了拍平。如果不拍平的话,uni-app x蒸汽模式中渲染速度还会比原生快吗?

如果不拍平的话,同屏创建4050个view和text的示例的平均耗时为467ms。仍远快于arkUI的797.6ms。

k/n驱动c层渲染,是否也快过arkUI或uni-app x蒸汽模式?

截止到目前(2026年2月初),基于k/n的开源跨平台框架,在上述基准测试中的表现均比arkUI差很多。更无法与uni-app x蒸汽模式相比。

uni-app x蒸汽模式在Android和iOS是否也快过原生?

uni-app x蒸汽模式的iOS版和Android版的渲染引擎已开发完毕,但产品化还有一些工作要做。预计分别在2026年Q1和Q2发布。

不管在iOS还是Android,均比原生快2~3倍,均基于原生渲染管线。

已公开如下预览版对比测试例:

上述示例在华为mate30 Android版上,对比数据如下:

5次冷启动平均耗时(单位:ms)
原生view 436
原生compose ui 673.2
原生compose ui aot 544.2
uni-app x 蒸汽模式 224

即跨平台,又比原生性能更高,曾经被认为是天方夜谭。

在 uni-app x蒸汽模式 发布前,DCloud曾给行业内小范围演示,也被认为不可能。特将本文赠予哪些不相信这件事的人。

在中国,因为小程序和鸿蒙等多平台现状,一个优秀的跨平台框架对于产业有巨大的意义。

在中国,被封锁高端芯片技术的鸿蒙系统,更需要高性能的软件框架来支撑用户体验。

继续前行!

🚀 两年小程序开发,我把踩过的坑做成了开源 Skills

还记得第一次用 miniprogram-automator 写自动化测试时的绝望吗?

  • querySelector() 死活选不到自定义组件里的元素
  • waitFor() 不知道该等什么,测试时好时坏
  • wx.request Mock 不生效,每次测试都打真实接口
  • 截图对比总是因为时间戳、动画而误报

还有 CI 发布时的噩梦:

  • 上传到微信后台经常超时,CI 流水线红成一片
  • pnpm 项目的 npm 包打不进去,shamefully-hoist 是什么鬼?
  • GitHub Actions 里 secrets 配置错了,排查半天才发现是 IP 白名单问题

我决定不再让其他人重复踩这些坑。


📦 wechat-miniprogram-skills

这是我开源的微信小程序 AI 编程技能包,基于 Skills 规范,支持 40+ AI 编程工具(Claude Code、Cursor、Windsurf、Continue、Copilot 等)。

包含两个核心 Skill:

1️⃣ miniprogram-automation - 自动化测试不再是玄学

核心能力:

  • ✅ 生成可复用的 Node.js 自动化脚本(不强制 Jest)
  • ✅ 处理自定义组件边界问题(.shadow()>>> 选择器)
  • ✅ 智能 waitFor 策略(等待元素、网络、页面栈)
  • ✅ wx.request Mock 方案(mockWxMethod / restoreWxMethod)
  • ✅ 截图 + 回归验证(console / exception 监听)
  • ✅ 基于官方 miniprogram-demo 实际验证

常见触发词: "小程序自动化"、"automator"、"E2E 测试"、"选不到元素"、"mock wx.request"

实际案例:

// 自动生成的脚本会处理这些细节:
const input = await page.$('.custom-input >>> input')
await input.input('测试内容')
await page.waitFor(500) // 智能等待策略

2️⃣ miniprogram-ci - 让 CI/CD 稳如老狗

核心能力:

  • ✅ 生成 pack-npm / preview / upload 独立脚本
  • ✅ 内置超时重试机制(3次重试,每次等待 5 秒)
  • ✅ 完整 GitHub Actions 模板(npm / pnpm 双版本)
  • ✅ pnpm 兼容性配置(shamefully-hoist / public-hoist-pattern)
  • ✅ secrets 管理 + IP 白名单检查
  • ✅ 自动创建 GitHub Release

常见触发词: "上传小程序"、"CI 部署"、"miniprogram-ci"、"预览二维码"、"自动化发布"

实际案例:

# 自动生成的 GitHub Actions 会帮你:
- name: Upload to WeChat
  run: node scripts/upload.js
  env:
    PRIVATE_KEY: ${{ secrets.WX_PRIVATE_KEY }}

🎯 为什么要做成 Skills?

1. AI 编程时代,让 AI 直接生成正确的代码

  • 不用再翻文档、查 Stack Overflow
  • AI 知道如何处理自定义组件、如何配置 CI

2. 知识可复用、可迭代

  • 把经验固化成 Skill,团队共享
  • 遇到新坑就更新 Skill,让 AI 帮后来者避坑

3. 支持 40+ AI 工具

  • 不绑定特定 IDE 或 AI 助手
  • Claude、Cursor、Copilot 都能用

📥 如何使用?

# 安装整个仓库
npx skills add whinc/wechat-miniprogram-skills

# 只安装自动化测试 Skill
npx skills add whinc/wechat-miniprogram-skills --skill miniprogram-automation

# 只安装 CI 发布 Skill
npx skills add whinc/wechat-miniprogram-skills --skill miniprogram-ci

安装后,直接在 AI 编程工具中说:

  • "帮我生成小程序自动化测试脚本"
  • "配置小程序 CI 自动上传"

AI 会基于这些 Skills 生成经过实战验证的代码


🤝 欢迎贡献

如果你也在小程序开发中踩过坑、总结过经验,欢迎提交 PR 补充新的 Skill!

比如:

  • 小程序性能监控
  • 分包加载优化
  • 云开发自动化部署
  • ...

让我们一起让小程序开发不再痛苦 💪


🔗 链接

❌