普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月26日掘金 前端

100s 带你了解 Bun 为什么这么火

作者 冴羽
2026年2月26日 17:14

1995 年, JavaScript 诞生,主要用于广告弹窗。

2009 年,Node.js 诞生,JS 可以写后端了。

然而这是罪恶的开始,之后 JS 发展出了世界上最复杂的工具链。

于是写一个 Web 项目,你需要 Node.js 作为运行环境,Npm 作为包管理器,Webpack 作为打包工具,Jest 作为测试,还要用 Babel 转译,还要写一大堆没人看懂的配置文件。

这样的痛苦想必你已经体会到了。

2021 年,Bun 说:“为什么不能在运行时就完成所有得事情呢?”

于是它火了。

Bun 是什么?

Bun 本质上是一个 JavaScript 运行时,类似于 Node.js,但极其注重性能。

为了实现高性能,Bun 的核心策略是将:

  1. Node.js 的 C++ 替换成 Zig

  2. Node.js 的 V8 引擎替换成 Safari 使用的 JavaScript Core

这确实让 Bun 取得了不错的性能测试成绩。

image.png

但 Bun 真正革命性的地方在于它不仅仅是一个运行时。

它取代了你的打包工具,于是你可以直接写 TypeScript 或 JavaScript,而不用做任何配置。

它取代了你的测试框架和包管理器,甚至内置数据库驱动程序,同时又保持了与 Node.js 生态的兼容性。

从此以后,你只用一个工具就可以完成所有任务。

当然直接说还是有些抽象,我们直接看代码吧。

Bun 的使用

安装 Bun:

curl -fsSL https://bun.sh/install | bash

创建新项目:

bun init

现在你已经可以编写 TypeScript 代码了。

现在我们搭建一个 Web 服务器,不需要 express,只需要:

const server = Bun.serve({
  port: 3000,
  routes: {
    "/": () => new Response("Bun!"),
  },
});

console.log(`Listening on ${server.url}`);

运行 bun run index.ts 你就可以直接看到效果。

如果你想操作数据库,直接写:

import { Database } from "bun:sqlite";
const db = new Database("./app.sqlite");

如果你想使用 Redis,直接写:

import { redis } from "bun";

// 设置 Key
await redis.set("greeting", "Hello from Bun!");

// 读取数据
const cachedDate = await redis.exists("greeting");

如果你需要安装包,直接运行:

# 安装速度比 npm 快 25 倍
bun install

如果你想写测试,直接写:

// 内置测试工具
import { test, expect } from "bun:test";

test("2 + 2 = 4", () => {
  expect(2 + 2).toBe(4);
});

为什么要关注 Bun?

Bun 本身其实已经很火了。

2025 年底,Anthropic 收购 Bun,更是为 Bun 的发展添了一把柴。

Bun 现在已经普遍被用于 Claude Code 等工具、云平台上的 Serverless Functions 等,这预示着它正在成为 JavaScript 生态系统中的重要力量。

所以如果你正在学 JavaScript,或者想尝试新工具,Bun 值得一看。

即使现在不用,了解这个“未来趋势”也会让你对前端生态有更深的理解。

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

2026 春晚魔术大揭秘:作为程序员,分分钟复刻一个 | 掘金一周 2.26

作者 掘金一周
2026年2月26日 17:07

本文字数1300+ ,阅读时间大约需要 4分钟。

【掘金一周】本期亮点:

「上榜规则」:文章发布时间在本期「掘金一周」发布时间的前一周内;且符合各个栏目的内容定位和要求。 如发现文章有抄袭、洗稿等违反社区规则的行为,将取消当期及后续上榜资格。

一周“金”选

掘金一周 文章头图 1303x734.jpg

内容评审们会在过去的一周内对社区深度技术好文进行挖掘和筛选,优质的技术文章有机会出现在下方榜单中,排名不分先后。

前端

2026 春晚魔术大揭秘:作为程序员,分分钟复刻一个(附源码) @程序员Sunday

在这个 App 进入“魔术模式”后,键盘事件已经被 e.preventDefault() 拦截了。无论你按哪个数字键,屏幕上只会依次显示程序预设好的那个 差值字符串

我写了个 code-review 的 Agent Skill, 没想到火了 @神三元

四份 checklist 的内容加起来好几千字,如果全塞进 SKILL.md,一上来就会吃掉大量上下文窗口。所以我把它们放在 references/ 里,SKILL.md 里只在需要的步骤写 Load references/xxx.md

构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的 @sunny_

robuild 就是为解决这些问题而设计的。它基于 Rolldown(Rust 实现的 Rollup 替代品)和 Oxc(Rust 实现的 JavaScript 工具链),专注于库构建场景。

后端

Spring 源码分析 事务管理的实现原理(下) @暮色妖娆丶

这里我以 SpringBoot 源码入口为起点,画了一个相关的流程图,包含了 SpringBoot、Spring 事务、Spring AOP、Spring 事件、BeanFactoryPostProcessor、BeanPostProcessor 等所有 Spring 知识,以及相关模块之间的交互联系

一站式了解Agent Skills @想用offer打牌

因为skills只会暴露name和description,所以agent会自己判断什么场景使用这个skill,就像我们玩游戏一样,脑子已经潜移默化这种场景使用这种skills或者按这样的顺序将多种skills结合使用。

Android

丰田正在使用 Flutter 开发游戏引擎 Fluorite @恋猫de小郭

对于 Fluorite ,目前主要的集成方式就是用 FluoriteView 在 Flutter App 中添加多个 3D 视图,所以可以直接用 Flutter 生态,而 C++ 核心确保在低端硬件(如车载屏)实现主机级效果,避免 Godot 等开源引擎的启动慢/资源重问题。

Flutter 设计包解耦新进展,material_ui 和 cupertino_ui 发布预告 @恋猫de小郭

未来 Flutter 在 Framework 内将不带任何 material 和 cupertino 样式,你可以根据需要选择样式库,甚至觉得使用哪个样式库版本,最重要的是:不升级 Flutter 版本也可以更新最新的设计样式,同时控件 Bug 也可以得到更快的修复和发布

把离线AI代理装进口袋里 @稀有猿诉

正如你的输入是结构化的一样,模型的输出也是结构化的。模型不会仅仅返回一个最终的文本块。相反,它会返回一个 ModelResponse 对象流,其中每个对象代表一种不同的输出类型。模型可能生成纯文本,也可能决定调用你的某个函数。

人工智能

你知道不,你现在给 AI 用的 Agent Skills 可能毫无作用,甚至还拖后腿? @恋猫de小郭

高质量技能 = 搜索空间压缩器,它可以限定决策路径、减少无效探索、提供验证锚点和显式化领域隐性流程,这才是 Skills 能推高 Pareto frontier 的原因,所以,你需要避免百科式技能,它可能带来的更多的噪音。

Rust 编写的 40MB 大小 MicroVM 运行时,完美替代 Docker 作为 AI Agent Sandbox @RoyLin

在 AI Agent 时代,安全的代码执行环境不再是可选项,而是基础设施。A3S Box 正是为这个时代而生的运行时——它让每一行不可信的代码都运行在硬件隔离的沙箱中,让每一字节敏感数据都受到硬件加密的保护,同时让开发者感觉就像在使用 Docker 一样简单。

Skills 实战:让 AI 成为你的领域专家 @冬奇Lab

description 是 Skill 触发的关键。Claude 根据用户请求与 description 的匹配度决定是否加载该 Skill。

📖 投稿专区

大家可以在评论区推荐认为不错的文章,并附上链接和推荐理由,有机会呈现在下一期。文章创建日期必须在下期掘金一周发布前一周以内;可以推荐自己的文章、也可以推荐他人的文章。

AI 写代码总是半途而废?试试这个免费的工作流工具

作者 仿生狮子
2026年2月26日 17:04

作为一个用过多种 IDE 的开发者,我想分享一个让我效率 up up 的小工具。

你有没有遇到过这种情况?

  • 跟 AI 聊了半天需求,代码写了一半,上下文满了,AI "失忆"了
  • 项目做到一半搁置,一周后回来完全忘了做到哪了
  • 想加一个功能,结果 AI 把之前的代码改坏了

这些问题都有一个共同原因:上下文衰减(Context Rot)

简单来说,AI 的"记忆"是有限的。当对话太长时,它会慢慢忘掉之前说过的话,导致代码质量下降。

GSD 是什么?

GSD = Get Shit Done(把事做完)

它是一个开源的 AI 编程工作流框架,核心思路很简单:

把项目信息存到文件里,而不是全部塞给 AI。

就像你写代码会用 Git 做版本控制一样,GSD 帮你做"AI 对话的版本控制"。

GSD for Trae

原版 GSD 是为 Claude Code 设计的。因为我日常用 Trae,所以做了这个适配版本。

安装只需一行命令:

npx gsd-trae

或者:

bash <(curl -s https://raw.githubusercontent.com/Lionad-Morotar/get-shit-done-trae/main/install.sh)

它能帮你做什么?

1. 新项目规划

输入 /gsd:new-project,它会:

  • 问你一系列问题,搞清楚你要做什么
  • 自动研究技术方案(可选)
  • 生成项目路线图

2. 阶段式开发

大项目拆成小阶段:

  • /gsd:plan-phase 1 - 规划第一阶段
  • /gsd:execute-phase 1 - 执行第一阶段
  • /gsd:verify-work - 验证做得对不对

每完成一个阶段,进度都会被记录,随时可以接着做。

3. “断点续传”

关掉电脑、明天再来,输入 /gsd:progress,AI 马上知道:

  • 项目做到哪了
  • 接下来该做什么
  • 之前的决策是什么

实际使用感受

我用了一个月,相比 Trae 的 Plan Build 模式最明显的变化:

以前:一个功能聊到一半,AI 开始"胡言乱语",只能新开对话重来

现在:每个阶段都有清晰的目标和验收标准,AI 一直保持在正确的方向上

以前:同时开多个功能,代码互相冲突

现在:按阶段来,做完一个再做下一个,井井有条(进阶用户也可以选择 Worktree 模式)

以前:Plan 文档随意仍在 .trae 的文档目录,没有管理,很难查找

现在:结构化的目录,GSD 和开发者都能轻松阅读

适合谁用?

  • 用 Trae/Gemini/Claude 写代码的开发者
  • 做独立项目、 side project 的人
  • 觉得 AI 编程"聊不动"的新手

相比其他工具的优势

市面上有不少 AI 编程工作流工具,比如 GitHub 的 Spec Kit、OpenSpec、BMAD 等。GSD 的定位不太一样:

工具 特点 GSD 的区别
Spec Kit 企业级、严格阶段门控、30分钟启动 GSD 更轻量,5分钟上手,没有繁琐的仪式
OpenSpec 灵活快速、Node.js 运行 GSD 额外解决了 Context Rot 问题,支持断点续传
BMAD 21个 AI Agent、完整敏捷流程 GSD 不模拟团队,而是聚焦"让开发者高效完成项目"

简单说:如果你期待快速而结构化的流程,又不想被复杂的企业开发规范束缚的同时,确保 AI 编程能稳定输出,GSD 可能是目前最合适的选择。

它是免费的

开源项目,GitHub 地址: github.com/Lionad-Moro…

MIT 协议,可以随便用、随便改。

最后说一句

AI 编程工具越来越强大,但工具只是工具。

好的工作流能让你事半功倍,而 GSD 就是这样一套经过验证的工作流。

不需要改变你现有的开发习惯,安装后输入 /gsd:new-project 试试看。


如果你试过觉得好用,欢迎点个 Star ⭐

如果发现问题,也欢迎提 Issue

React 状态管理:Easy-Peasy 入门指南

作者 pe7er
2026年2月26日 17:03

大家好!今天我们来聊聊 React 状态管理中的一个非常简单且高效的库——Easy-Peasy。它可以帮助我们轻松管理应用中的状态,让开发更加高效和清晰。

相关链接

官网 Github仓库 npm

为什么选择 Easy-Peasy?

Easy-Peasy 是对 Redux 的一种抽象。它提供了一个重新构思的 API,专注于开发者体验,使你能够快速轻松地管理状态,同时利用 Redux 强大的架构保障,并与其广泛的生态系统进行集成。

所有功能均已内置,无需配置即可支持一个健壮且可扩展的状态管理解决方案,包括派生状态、API 调用、开发者工具等高级特性,并通过 TypeScript 提供完整的类型支持体验。

它社区活跃、更新稳定,跟随社区的 redux 版本同步更新。

  • Zero configuration 零配置

  • No boilerplate 无冗余代码

  • React hooks based API 基于React Hook的API

  • Extensive TypeScript support 广泛的TypeScript支持

  • Encapsulate data fetching 封装数据获取

  • Computed properties 计算属性

  • Reactive actions 响应式动作

  • Redux middleware support 支持 Redux 中间件

  • State persistence 状态持久性

  • Redux Dev Tools 支持Redux开发工具

  • Global, context, or local stores 全局、上下文或本地存储

  • Built-in testing utils 内置测试工具

  • React Native supported 支持 React Native

  • Hot reloading supported 支持热重载

所有这些功能只需安装一个依赖项即可实现。

创建nextjs项目

(base) pe7er@pe7erdeMacBook-Pro github % npx create-next-app@latest
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/create-next-app 1151ms (cache updated)
Need to install the following packages:
create-next-app@14.2.13
Ok to proceed? (y)

npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/create-next-app 1071ms (cache miss)
npm http fetch POST 200 https://mirrors.cloud.tencent.com/npm/-/npm/v1/security/advisories/bulk 621ms
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/create-next-app/-/create-next-app-14.2.13.tgz 1034ms (cache miss)
✔ What is your project named? … nextjs-easy-peasy-template
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /Users/pe7er/Developer/github/nextjs-easy-peasy-template.
控制台完整输出
            (base) pe7er@pe7erdeMacBook-Pro github % npx create-next-app@latest
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/create-next-app 1151ms (cache updated)
Need to install the following packages:
create-next-app@14.2.13
Ok to proceed? (y)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/create-next-app 1071ms (cache miss)
npm http fetch POST 200 https://mirrors.cloud.tencent.com/npm/-/npm/v1/security/advisories/bulk 621ms
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/create-next-app/-/create-next-app-14.2.13.tgz 1034ms (cache miss)
✔ What is your project named? … nextjs-easy-peasy-template
✔ Would you like to use TypeScript? … No / YesWould you like to use ESLint? … No / YesWould you like to use Tailwind CSS? … No / YesWould you like to use `src/` directory? … No / YesWould you like to use App Router? (recommended) … No / YesWould you like to customize the default import alias (@/*)? … No / YesWhat import alias would you like configured? … @/*
Creating a new Next.js app in /Users/pe7er/Developer/github/nextjs-easy-peasy-template.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/react 720ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/react-dom 1250ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/next 2449ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@opentelemetry%2fapi 591ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@playwright%2ftest 1250ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/sass 422ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/typescript 2082ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types%2fnode 1414ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types%2freact 742ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types%2freact-dom 566ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss 309ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/tailwindcss 1018ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint 479ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-config-next 1234ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@eslint-community%2feslint-utils 365ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/undici-types 370ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types%2fprop-types 487ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@eslint-community%2fregexpp 498ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/csstype 508ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@eslint%2fjs 606ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@eslint%2feslintrc 685ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@humanwhocodes%2fmodule-importer 326ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@nodelib%2ffs.walk 331ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/chalk 329ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ajv 629ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/doctrine 337ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@ungap%2fstructured-clone 663ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/debug 469ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@humanwhocodes%2fconfig-array 871ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/cross-spawn 555ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/escape-string-regexp 413ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/esutils 221ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-visitor-keys 351ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/espree 361ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-scope 376ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/file-entry-cache 251ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/find-up 316ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/esquery 626ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fast-deep-equal 559ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/graphemer 291ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/glob-parent 399ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/globals 422ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ignore 384ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-glob 218ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-path-inside 223ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/imurmurhash 294ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/js-yaml 262ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/levn 163ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/json-stable-stringify-without-jsonify 337ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/lodash.merge 272ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/strip-ansi 279ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/optionator 301ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/natural-compare 414ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/minimatch 471ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@rushstack%2feslint-patch 296ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/text-table 470ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-import-resolver-node 446ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-import-resolver-typescript 567ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-plugin-import 519ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-plugin-jsx-a11y 598ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2feslint-plugin-next 958ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-plugin-react 393ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@swc%2fhelpers 391ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2fparser 1477ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/busboy 688ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss 12ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/graceful-fs 350ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-plugin-react-hooks 1006ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/caniuse-lite 903ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2feslint-plugin 1840ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fenv 1474ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/styled-jsx 723ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-darwin-arm64 831ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-linux-arm64-gnu 728ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-darwin-x64 894ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/loose-envify 302ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-linux-x64-gnu 1025ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/nanoid 365ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-win32-x64-msvc 851ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-win32-arm64-msvc 1187ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-linux-x64-musl 1279ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/source-map-js 269ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/arg 380ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@alloc%2fquick-lru 540ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/scheduler 1241ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/chokidar 492ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-glob 1ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/dlv 235ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/didyoumean 360ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fast-glob 367ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/micromatch 324ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/lilconfig 401ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/jiti 638ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/normalize-path 479ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object-hash 447ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-win32-ia32-msvc 2878ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next%2fswc-linux-arm64-musl 3484ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss-load-config 302ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/picocolors 2077ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss-js 509ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/picocolors 848ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss-nested 206ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss-import 803ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss-selector-parser 398ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/resolve 409ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/sucrase 382ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint 5ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/jiti 1ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-visitor-keys 7ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/espree 8ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/minimatch 9ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/debug 11ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fast-deep-equal 5ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/import-fresh 207ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/strip-json-comments 315ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fastq 318ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/json-schema-traverse 352ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@nodelib%2ffs.scandir 397ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/uri-js 208ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fast-json-stable-stringify 453ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@humanwhocodes%2fobject-schema 493ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ansi-styles 206ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/shebang-command 163ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/supports-color 268ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/path-key 365ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/which 340ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ms 363ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/esrecurse 375ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/acorn-jsx 352ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/estraverse 451ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/acorn 412ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/locate-path 183ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/flat-cache 393ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/path-exists 323ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/argparse 260ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-extglob 351ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/estraverse 657ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/brace-expansion 362ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/prelude-ls 548ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/prelude-ls 362ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/type-fest 661ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/word-wrap 229ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/type-check 550ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/deep-is 461ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ansi-regex 287ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fast-levenshtein 360ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/type-check 365ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/parent-module 269ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/resolve-from 297ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/reusify 217ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/run-parallel 253ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@nodelib%2ffs.stat 363ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/queue-microtask 393ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/punycode 219ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/color-convert 261ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-flag 314ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/color-name 286ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/isexe 301ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/shebang-regex 355ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint 9ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-plugin-import-x 371ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/glob 498ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2ftype-utils 856ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ts-api-utils 958ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2ftypes 1264ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2futils 1615ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/resolve 13ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2fvisitor-keys 1670ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2fvisitor-keys 1291ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-core-module 518ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fast-glob 1ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@nolyfill%2fis-core-module 533ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2fscope-manager 2208ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-module-utils 513ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/enhanced-resolve 687ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2fscope-manager 1509ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/get-tsconfig 609ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/array.prototype.findlastindex 180ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/doctrine 2ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-module-utils 2ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-bun-module 458ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-core-module 2ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/array.prototype.flatmap 249ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@rtsao%2fscc 451ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/hasown 231ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/array.prototype.flat 381ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/array-includes 674ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object.values 372ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object.fromentries 434ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object.groupby 481ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/semver 371ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/tsconfig-paths 410ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/aria-query 241ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint%2ftypescript-estree 2575ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ast-types-flow 650ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/emoji-regex 509ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-iterator-helpers 487ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/damerau-levenshtein 591ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/axobject-query 623ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/safe-regex-test 344ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/estraverse 9ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/jsx-ast-utils 492ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string.prototype.includes 446ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/axe-core 1117ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/language-tags 517ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/array.prototype.findlast 479ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/array.prototype.tosorted 564ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object.entries 458ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/jsx-ast-utils 482ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string.prototype.repeat 345ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/prop-types 545ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string.prototype.matchall 679ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/foreground-child 164ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/path-scurry 299ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/minipass 343ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/jackspeak 447ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint 6ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/typescript 27ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/semver 2ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/micromatch 4ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/brace-expansion 5ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/glob-parent 6ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/merge2 320ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/supports-preserve-symlinks-flag 352ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/path-parse 617ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/graceful-fs 8ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/resolve-pkg-maps 513ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/tapable 631ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/call-bind 165ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/call-bind 186ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/define-properties 251ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-string 280ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/get-intrinsic 378ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-object-atoms 511ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/call-bind 5ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/define-properties 10ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-errors 288ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/define-properties 378ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-object-atoms 269ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-shim-unscopables 344ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-abstract 562ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-abstract 271ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-shim-unscopables 420ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-abstract 503ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-shim-unscopables 551ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/json5 287ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/function-bind 365ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types%2fjson5 368ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/strip-bom 296ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/minimist 481ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-abstract 1950ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-define-property 193ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/set-function-length 251ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-property-descriptors 283ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/arraybuffer.prototype.slice 341ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/data-view-byte-length 133ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/define-data-property 493ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/data-view-buffer 272ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object-keys 537ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/available-typed-arrays 358ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/array-buffer-byte-length 573ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/function.prototype.name 284ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-set-tostringtag 398ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/data-view-byte-offset 474ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-property-descriptors 11ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/gopd 250ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-to-primitive 397ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/globalthis 409ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-symbols 205ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/get-symbol-description 534ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-proto 298ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-data-view 185ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-callable 279ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-regex 201ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/internal-slot 460ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-array-buffer 521ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-negative-zero 332ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-shared-array-buffer 304ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-weakref 378ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string.prototype.trim 210ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object.assign 365ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object-inspect 388ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-typed-array 547ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/regexp.prototype.flags 435ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/typed-array-byte-offset 138ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/typed-array-byte-length 171ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/safe-array-concat 550ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-proto 3ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/typed-array-length 204ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/unbox-primitive 168ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string.prototype.trimstart 369ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string.prototype.trimend 462ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/which-typed-array 189ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/typed-array-buffer 524ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-tostringtag 333ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/define-data-property 6ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-callable 7ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-tostringtag 9ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-shared-array-buffer 11ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/which-typed-array 4ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/possible-typed-array-names 128ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-symbol 179ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-date-object 258ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/isarray 271ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/functions-have-names 323ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/set-function-name 369ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/for-each 330ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/side-channel 517ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/for-each 384ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/for-each 544ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/which-boxed-primitive 359ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/has-bigints 517ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/for-each 528ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/language-subtag-registry 438ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/deep-equal 513ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/iterator.prototype 516ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object-is 334ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-arguments 452ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/which-collection 508ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/es-get-iterator 577ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-set 282ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-set 5ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-number-object 376ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/stop-iteration-iterator 422ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-bigint 449ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-boolean-object 456ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-weakmap 189ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-map 566ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-map 604ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-weakset 434ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/reflect.getprototypeof 424ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/set-function-name 5ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/loose-envify 8ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/object-assign 156ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/react-is 1076ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/picomatch 242ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/braces 404ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/rimraf 287ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/keyv 422ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/flatted 709ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/p-locate 430ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/glob 8ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/json-buffer 406ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/signal-exit 220ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/lru-cache 234ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@pkgjs%2fparseargs 296ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@isaacs%2fcliui 367ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/callsites 198ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/which-builtin-type 496ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/strip-ansi 13ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/strip-ansi 13ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/wrap-ansi 188ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/wrap-ansi 261ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string-width 387ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string-width 518ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ansi-regex 23ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/string-width 22ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ansi-styles 25ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/emoji-regex 28ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-fullwidth-code-point 235ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eastasianwidth 381ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/p-limit 321ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fill-range 488ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/to-regex-range 357ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-number 380ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/concat-map 382ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/balanced-match 385ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/react 17ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/source-map-js 10ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/nanoid 10ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/streamsearch 310ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@swc%2fcounter 441ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/tslib 499ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/client-only 1119ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/yocto-queue 300ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/js-tokens 222ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-generator-function 602ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-async-function 812ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-finalizationregistry 844ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/inherits 249ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/once 407ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/path-is-absolute 410ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/inflight 467ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fs.realpath 522ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ts-node 1383ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@swc%2fcore 659ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@swc%2fhelpers 4ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@swc%2fwasm 707ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types%2fnode 24ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/typescript 19ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/normalize-path 10ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss-value-parser 243ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/lilconfig 5ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-binary-path 304ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/anymatch 308ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/postcss-selector-parser 6ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/read-cache 419ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/camelcase-css 422ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/cssesc 188ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/readdirp 554ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/util-deprecate 276ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/mz 166ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/fsevents 745ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/yaml 523ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/commander 377ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/lines-and-columns 322ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@jridgewell%2fgen-mapping 449ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/pirates 463ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/ts-interface-checker 656ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/binary-extensions 268ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/pify 159ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@jridgewell%2fsourcemap-codec 256ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/any-promise 312ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@jridgewell%2fset-array 315ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/thenify-all 467ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@jridgewell%2ftrace-mapping 684ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@jridgewell%2fresolve-uri 320ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/thenify 341ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/minimist 4ms (cache hit)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/wrappy 294ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/wrappy 345ms (cache updated)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next/env/-/env-14.2.13.tgz 411ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/get-tsconfig/-/get-tsconfig-4.8.1.tgz 412ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz 428ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-config-next/-/eslint-config-next-14.2.13.tgz 462ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz 520ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/types/-/types-8.7.0.tgz 522ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.13.tgz 542ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/parser/-/parser-8.7.0.tgz 564ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/utils/-/utils-8.7.0.tgz 573ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz 581ms (cache miss)
npm http fetch POST 200 https://mirrors.cloud.tencent.com/npm/-/npm/v1/security/advisories/bulk 712ms
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz 676ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types/prop-types/-/prop-types-15.7.13.tgz 675ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/is-bun-module/-/is-bun-module-1.2.1.tgz 679ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz 698ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types/react/-/react-18.3.9.tgz 807ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz 918ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz 927ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.16.7.tgz 1032ms (cache miss)
npm http fetch GET 200 https://mirrors.cloud.tencent.com/npm/next/-/next-14.2.13.tgz 3489ms (cache miss)
added 360 packages, and audited 361 packages in 1m
137 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created nextjs-easy-peasy-template at /Users/pe7er/Developer/github/nextjs-easy-peasy-template
      
    
    

项目创建完成后,应该启动一次项目,确保项目运行无误。

安装easy-peasy

首先,你需要安装easy-Peasy。可以使用 npm

npm install easy-peasy

创建一个简单的状态管理模型

接下来,我们来创建一个简单的状态管理模型。假设我们要管理一个计数器的状态:

import { createStore, action } from 'easy-peasy';

// 定义模型
const model = {
  count: 0,
  increment: action((state) => {
    state.count += 1;
  }),
  decrement: action((state) => {
    state.count -= 1;
  }),
};

// 创建 store
const store = createStore(model);

在组件中使用 Easy-Peasy

接下来,我们可以在 React 组件中使用这个 store:

import React from 'react';
import { StoreProvider, useStoreState, useStoreActions } from 'easy-peasy';

const Counter = () => {
  const count = useStoreState((state) => state.count);
  const { increment, decrement } = useStoreActions((actions) => ({
    increment: actions.increment,
    decrement: actions.decrement,
  }));

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>增加</button>
      <button onClick={decrement}>减少</button>
    </div>
  );
};

const App = () => {
  return (
    <StoreProvider store={store}>
      <Counter />
    </StoreProvider>
  );
};

export default App;

总结

通过以上步骤,我们实现了一个简单的计数器应用。Easy-Peasy 的使用极其简单,并且能够有效地管理组件间的状态。如果你正在寻找一个轻量级且易于使用的状态管理解决方案,不妨试试 Easy-Peasy!

希望这篇博客能帮助你更好地理解 Easy-Peasy 的使用。如果你有任何问题或想法,欢迎在评论区分享!

React Native 多环境配置全攻略:环境变量、iOS Scheme 和 Android Build Variant

作者 pe7er
2026年2月26日 16:52

在 React Native 项目开发中,经常会遇到不同环境(开发、测试、生产)需要不同配置的需求。本文结合实践经验,详细讲解如何管理环境变量(.env 文件)、如何配置和使用 iOS 的 Scheme,以及 Android 的 Build Variant,帮助你构建灵活且高效的多环境应用。

image.png


环境变量管理 — 使用 .env 文件

为什么用 .env

  • 将敏感信息(API 地址、Key 等)和环境相关配置分离
  • 不同环境使用不同的配置,不必修改代码
  • 方便本地和 CI/CD 环境管理

工具选择

  • dotenv:读取 .env 文件的标准方案
  • babel-plugin-inline-dotenv:打包时将环境变量注入代码

使用示例

  1. 在项目根目录创建不同的环境配置文件,比如:
.env.development
.env.test
.env.production

2. 安装依赖:

yarn add dotenv babel-plugin-inline-dotenv --dev

3. 配置 Babel 插件 babel.config.js

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    ['inline-dotenv', {
      path: process.env.DOTENV_CONFIG_PATH || '.env'
    }]
  ]
};

4. 启动时指定环境配置文件:

DOTENV_CONFIG_PATH=.env.test yarn start

iOS 多环境配置

什么是 Scheme 和 Configuration?

  • Scheme:Xcode 的构建方案,管理运行和打包的流程
  • Configuration:对应不同的 Build Settings,比如 Debug、Release,通常和 Scheme 配合使用

新增 Scheme

  • 在 Xcode 顶部菜单选择 Product > Scheme > Manage Schemes...
  • 点击 + 新建 Scheme,复制现有 Scheme,命名为 MyReactnativeTemplate-DevMyReactnativeTemplate-Test
  • 每个 Scheme 关联对应的 Build Configuration

image.png

3. 配置图片

image.png

  • 打开项目的 Build Settings。打开项目的 Build Settings。

  • 搜索 ASSETCATALOG_COMPILER_APPICON_NAME,设置为对应环境的 AppIcon 名称(比如 Debug 配置使用 AppIcon-Debug,Release 使用默认的 AppIcon)。

  • 也可以在 Xcode Scheme 的 Build Configuration 里,针对不同 Scheme 设置不同的 Build Configuration,从而加载不同的 AppIcon。

image.png

~~### 3. 新增 .xcconfig 文件?

~~* 在 Xcode 新建配置文件(比如 Dev.xcconfigTest.xcconfig

  • 配置各环境特有的变量
  • 在 Build Settings 中,将不同 Configuration 指向对应的 .xcconfig~~~~

在 React Native 中使用 Scheme 和环境变量

  • 通过 react-native run-ios --scheme "MyReactnativeTemplate-Dev" 指定 Scheme
  • 使用 DOTENV_CONFIG_PATH=.env.test yarn ios --scheme "MyReactnativeTemplate-Dev" 来启动,确保使用正确的环境变量

关于 xcodebuild 命令

带空格的 Scheme 名称要用引号包裹:

xcodebuild -workspace MyReactnativeTemplate.xcworkspace -configuration Debug -scheme "MyReactnativeTemplate-Dev" -destination 'id=设备ID'

Android 多环境配置

什么是 Build Variant 和 Flavor?

  • Build Variant = Build Type + Flavor
  • 通过 Flavor 可以生成多个不同版本的 APK(比如开发版、测试版、生产版)

配置 build.gradle

android/app/build.gradle 中新增 flavor:

android {
    ...
    flavorDimensions "env"
    productFlavors {
        dev {
            dimension "env"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
        }
        test {
            dimension "env"
            applicationIdSuffix ".test"
            versionNameSuffix "-test"
        }
        prod {
            dimension "env"
        }
    }
}

环境变量管理

  • 使用第三方库 react-native-config 管理 Android 端环境变量
  • 根据 flavor 加载不同的 .env 文件,比如 .env.dev.env.test

构建和运行指定 Flavor

# 运行开发版
yarn android --variant=devDebug

# 运行测试版
yarn android --variant=testDebug

# 打包生产版
cd android && ./gradlew assembleProdRelease

App 内集成开发者工具实现环境切换

在开发过程中,为了方便调试和验证不同环境,很多项目会在 App 里内置开发者工具,支持动态切换环境配置。常见实现有两种方式:

配置写在代码里,开发者工具里切换

  • 将多环境配置(API 地址、Key 等)预先写在代码里(JSON 或常量)
  • 开发者工具页面展示环境选项(比如开发、测试、预发布、生产)
  • 用户选择后,App 内部切换到对应配置,刷新接口调用等

优点:实现简单,不依赖外部服务
缺点:配置发布需跟随 App 版本更新,不够灵活

从后台配置中心动态拉取环境配置(类似 Nacos)

  • App 启动或打开开发者工具时,向配置中心请求最新环境配置列表和内容
  • 用户选择环境后,App 动态加载对应的配置(比如 API 地址)
  • 支持远程更新配置,无需重新发布 App

优点:配置管理集中,灵活性高
缺点:需要搭建并维护配置中心,增加复杂度


无论哪种方式,都能极大提升多环境调试效率。建议根据项目规模和需求选择合适方案。

总结与建议

  • 环境变量:用 .env 文件和 dotenv 系列库统一管理,方便调试和打包
  • iOS:通过 Scheme + Configuration + .xcconfig 实现多环境,运行和打包时指定 Scheme 和环境变量
  • Android:利用 Build Flavor + Build Variant 配置多环境,结合 react-native-config 实现环境变量注入
  • 启动/打包:使用命令时指定 Scheme 或 Flavor,确保打包环境正确
  • App中增加开发者工具时,则需要统一配置不同环境的配置

参考资料

Native Environment Variables](github.com/goatandshee…)

LeetCode 530. 二叉搜索树的最小绝对差:两种解法详解(迭代+递归)

作者 Wect
2026年2月26日 16:50

LeetCode 上一道经典的二叉搜索树(BST)题目——530. 二叉搜索树的最小绝对差,这道题看似简单,却能很好地考察我们对 BST 特性的理解,以及二叉树遍历方式的灵活运用。下面我会从题目分析、核心思路、两种解法拆解,到代码细节注释,一步步帮大家搞懂这道题,新手也能轻松跟上。

一、题目解读

题目很直白:给一个二叉搜索树的根节点 root,返回树中任意两个不同节点值之间的最小差值,差值是正数(即两值之差的绝对值)。

这里有个关键前提——二叉搜索树的特性:中序遍历二叉搜索树,得到的序列是严格递增的(假设树中没有重复值,题目未明确说明,但测试用例均满足此条件)。

这个特性是解题的核心!因为递增序列中,任意两个元素的最小差值,一定出现在相邻的两个元素之间。比如序列 [1,3,6,8],最小差值是 3-1=2,而不是 8-1=7 或 6-3=3。所以我们不需要暴力枚举所有两两组合,只需要在中序遍历的过程中,记录前一个节点的值,与当前节点值计算差值,不断更新最小差值即可。

二、核心解题思路

  1. 利用 BST 中序遍历为递增序列的特性,将“任意两节点的最小差值”转化为“中序序列中相邻节点的最小差值”;

  2. 遍历过程中,维护两个变量:min(记录当前最小差值,初始值设为无穷大)、pre(记录前一个节点的值,初始值设为负无穷大,避免初始值影响第一次差值计算);

  3. 遍历每个节点时,用当前节点值与pre 计算绝对值差值,更新 min,再将 pre 更新为当前节点值;

  4. 遍历结束后,min 即为答案。

接下来,我们用两种最常用的遍历方式实现这个思路:迭代中序遍历(解法1)和递归中序遍历(解法2)。

三、解法一:迭代中序遍历(非递归)

迭代遍历的核心是用“栈”模拟递归的调用过程,避免递归深度过深导致的栈溢出(虽然这道题的测试用例大概率不会出现,但迭代写法更通用,适合处理大型树)。

3.1 代码实现(带详细注释)

// 先定义 TreeNode 类(题目已给出,此处复用)
class TreeNode {
  val: number
  left: TreeNode | null
  right: TreeNode | null
  constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
    this.val = (val === undefined ? 0 : val)
    this.left = (left === undefined ? null : left)
    this.right = (right === undefined ? null : right)
  }
}

// 解法1:迭代中序遍历
function getMinimumDifference_1(root: TreeNode | null): number {
  // 边界处理:空树返回0(题目中树至少有一个节点?但严谨起见还是判断)
  if (!root) return 0;
  let min = Infinity; // 最小差值,初始为无穷大
  let pre = -Infinity; // 前一个节点的值,初始为负无穷大
  const stack: TreeNode[] = []; // 用于模拟中序遍历的栈
  let curr: TreeNode | null = root; // 当前遍历的节点

  // 第一步:将左子树所有节点压入栈(中序遍历:左 -> 根 -> 右)
  while (curr) {
    stack.push(curr);
    curr = curr.left; // 一直向左走,直到最左节点
  }

  // 第二步:弹出栈顶节点,处理根节点,再遍历右子树
  while (stack.length) {
    const node = stack.pop(); // 弹出栈顶(当前要处理的根节点)
    if (!node) continue; // 防止空节点(理论上不会出现)

    // 处理右子树:将右子树的所有左节点压入栈
    if (node.right) {
      let right: TreeNode | null = node.right;
      while (right) {
        stack.push(right);
        right = right.left;
      }
    }

    // 计算当前节点与前一个节点的差值,更新最小差值
    min = Math.min(min, Math.abs(pre - node.val));
    // 更新pre为当前节点值,为下一个节点做准备
    pre = node.val;
  }

  return min;
};

3.2 思路拆解

  1. 初始化:栈用于存储待处理的节点,curr指向根节点,先将根节点的所有左子节点压入栈(因为中序遍历要先访问左子树);

  2. 弹出栈顶节点(此时该节点的左子树已处理完毕),先处理其右子树(将右子树的所有左节点压入栈,保证下一次弹出的是右子树的最左节点);

  3. 计算当前节点与pre的差值,更新min,再将pre更新为当前节点值;

  4. 重复上述过程,直到栈为空,遍历结束。

优势:不依赖递归栈,避免递归深度过大导致的栈溢出,空间复杂度由递归的 O(h)(h为树高)优化为 O(h)(栈的最大深度也是树高),实际运行更稳定。

四、解法二:递归中序遍历

递归写法更简洁,代码量少,思路也更直观,适合树的深度不大的场景。核心是用递归函数实现中序遍历的“左 -> 根 -> 右”顺序。

4.1 代码实现(带详细注释)

// 解法2:递归中序遍历
function getMinimumDifference_2(root: TreeNode | null): number {
  if (!root) return 0;
  let min = Infinity; // 最小差值
  let pre = -Infinity; // 前一个节点的值

  // 递归函数:实现中序遍历
  const dfs = (node: TreeNode) => {
    if (!node) return; // 递归终止条件:节点为空

    // 1. 遍历左子树(左)
    if (node.left) dfs(node.left);

    // 2. 处理当前节点(根):计算差值,更新min和pre
    min = Math.min(min, Math.abs(pre - node.val));
    pre = node.val;

    // 3. 遍历右子树(右)
    if (node.right) dfs(node.right);
  }

  // 从根节点开始递归
  dfs(root);
  return min;
};

4.2 思路拆解

  1. 定义递归函数 dfs,参数为当前节点,负责遍历以该节点为根的子树;

  2. 递归终止条件:当前节点为空,直接返回;

  3. 先递归遍历左子树(保证左子树先被处理);

  4. 处理当前节点:计算当前节点与pre的差值,更新min,再将pre更新为当前节点值;

  5. 最后递归遍历右子树;

  6. 从根节点调用dfs,完成整个树的遍历,返回min。

优势:代码简洁,思路直观,容易理解和编写;劣势:当树的深度很大时(如链式树),会出现递归栈溢出,此时迭代写法更合适。

五、两种解法对比与总结

解法 遍历方式 时间复杂度 空间复杂度 优势 劣势
解法1 迭代中序 O(n)(每个节点遍历一次) O(h)(h为树高,栈的最大深度) 稳定,无栈溢出风险,通用 代码稍长,需要手动维护栈
解法2 递归中序 O(n)(每个节点遍历一次) O(h)(递归栈深度) 代码简洁,思路直观,易编写 深度过大时会栈溢出

关键总结

  1. 这道题的核心是利用 BST 中序遍历为递增序列,将“任意两节点最小差值”转化为“相邻节点最小差值”,避免暴力枚举;

  2. 两种解法的核心逻辑一致,只是遍历方式不同,可根据树的深度选择:树深较小时用递归,树深较大时用迭代;

  3. 注意初始值的设置:min设为无穷大(保证第一次差值能更新min),pre设为负无穷大(避免初始值与第一个节点值计算出不合理的差值);

  4. 边界处理:空树返回0(题目中树至少有一个节点,但严谨起见必须判断)。

六、拓展思考

如果这道题不是 BST,而是普通二叉树,该怎么解?

答案:先遍历所有节点,将节点值存入数组,再对数组排序,计算相邻元素的最小差值。时间复杂度 O(n log n)(排序耗时),空间复杂度 O(n)(存储所有节点值),效率低于本题的解法,由此可见利用数据结构特性解题的重要性。

好了,这道题的两种解法就讲解完毕了。希望大家能通过这道题,加深对 BST 特性和二叉树中序遍历的理解,下次遇到类似题目能快速想到解题思路。

Windows判断是笔记本还是台式

作者 卸任
2026年2月26日 16:33

前言

在开发桌面应用程序时,我们经常需要根据设备类型来调整功能或界面。例如,触摸板管理功能只对笔记本电脑有意义,而对台式机用户来说完全不需要。本文将介绍一种基于系统机箱类型(Chassis Type)的可靠检测方法。

正文

Windows系统通过WMI(Windows Management Instrumentation)提供了丰富的硬件信息查询接口。其中,Win32_SystemEnclosure 类包含了系统机箱的详细信息,包括一个关键属性:ChassisTypes

ChassisTypes 不同的数值代表不同的设备形态:

笔记本电脑相关类型:

  • 8: Portable(便携式)
  • 9: Laptop(笔记本)
  • 10: Notebook(笔记本)
  • 14: Sub Notebook(子笔记本)
  • 30: Tablet(平板电脑)
  • 31: Convertible(可转换)
  • 32: Detachable(可拆卸)

台式机相关类型:

  • 3: Desktop(桌面)
  • 4: Low Profile Desktop(低姿态桌面)
  • 5: Pizza Box(披萨盒式)
  • 6: Mini Tower(迷你塔式)
  • 7: Tower(塔式)
  • 13: All in One(一体机)
  • 15: Space-Saving(节省空间型)
  • 16: Lunch Box(午餐盒式)

详细文档可以看 :learn.microsoft.com/en-us/windo…

Power Shell可以直接运行命令查看

(Get-CimInstance -ClassName Win32_SystemEnclosure).ChassisTypes

image.png

结尾

感兴趣的可以去试试

“啪啪啪”三下键盘,极速拉起你的 uni-app 项目!

作者 TT_Close
2026年2月26日 16:31

说实话,我也不想造轮子。但试了一圈之后,我发现了一个让我忍不了的问题:选了不要某个功能,生成的代码里居然还有它的 import 和空壳文件。 与其花半小时手动删代码,不如用 hy-uni —— 三下键盘,1 秒钟搞定!


🚫 那些年,我们新建项目后手动删过的代码

如果你经常用社区的高分脚手架创建项目,一定会遇到这个进退两难的死胡同:

  • 官方模板太"毛坯":API 拦截器、状态管理全要自己从 0 开始配。新手直接劝退。

  • 社区模板太"精装":不仅送你一堆组件,还送你几个业务全景页。新建项目第一件事,就是花半小时去删那些不需要的页面和 npm 包。最痛苦的是,删的时候还得提心吊胆,生怕漏删了某个 import 导致整个项目一跑就白屏报错。

第 21 次从头搭项目时,我终于受不了了。于是,我过年时花了点时间写了 hy-uni


🎯 先说结论:三下键盘,极速拉起项目

一条命令,三下键盘,1 秒钟,带给你一个干干净净的、随时可进入业务开发的工业级 uni-app 项目:

# ⚡ 极速拉起纯净骨架(1 秒钟)
npx hy-uni my-app --pure
# 或者 📋 交互式精装配置(30 秒内完成)
npx hy-uni my-app

核心理念:你不要的功能,连一行代码、一段注释、一个 npm 依赖,都不该出现在最终的产物中。


⚡ 速度对比(为什么说"极速"?)

方案 时间 特点
hy-uni --pure ⚡ 1 秒 三下键盘极速拉起纯净骨架
hy-uni (交互) 📋 30 秒 选择功能后自动生成完整项目
官方脚手架 5 分钟+ 毛坯房,需要自己配置工程化
社区全量模板 10 分钟+ 功能全但冗余,需要手动删代码

关键对比:hy-uni 不仅快,而且不用删代码 —— 你不选的功能从代码到依赖全部消失。


💻 极客最爱的"双轨"构建体验

很多老手开发者拥有"代码洁癖",喜欢毫无业务代码的"极净空壳";也有很多开发者希望项目能"满级出生",自带网络请求和主题切换方案。

在这款 CLI 中,我们将选择权完全交还给你。

路线 A:极速构建"极致纯净"空壳(老手狂喜)

对于只想要**"帮我把工程化基建搭好,其他的我自己来"**的极客,你只需在命令后敲入一个 --pure 参数:


npx hy-uni my-app --pure

啪啪啪三下键盘,敲下回车,1秒钟静默生成。 没有任何繁琐的交互问答选项,你将直接获得一个强迫症狂喜的极净项目:

  • 只有基础工程化体系:Vue 3 + TypeScript + Vite + UnoCSS + Pinia 开箱即用。

  • 没有任何网络请求、主题切换、业务示例等多余代码。

  • 目录结构极其纯粹,没有多余的文件夹。

路线 B:交互式精装配置(开箱即用)

如果不加 --pure,CLI 则会提供完全可定制的丝滑交互面板:


┌ 🚀 火叶 - 快速创建高性能 uni-app 项目
│
● 模板来源: 缓存 (~/.huoye/templates/) [2天前更新]
│
◇ 请输入项目名称:
│ my-app
│
◇ 请选择创建路径:
│ ./demo
│
◇ 是否需要网络请求层?
│ ○ Yes / ● No
│
◆ 是否需要业务示例页面?
│ ○ Yes / ● No
│
◆ 是否需要主题管理?
│ ○ Yes / ● No
│
◆ 确认创建项目?
│ ● Yes / ○ No
│
◇ 🎉 恭喜!您的项目已准备就绪。
│
◇ Getting Started  ─────────╮
│                           │
│ $ cd demo/my-app          │
│ $ pnpm install            │
│ $ pnpm dev:h5             │
│                           │
├───────────────────────────╯

此时,选择全选 Yes 的你,将获得一个"满级配置"项目:

  • 封装极佳的 Http 客户端、请求拦截器体系及全局错误分类处理机制。

  • 完善的亮暗色主题无缝切换落地方案及 CSS 变量体系。

最硬核的是:无论你是走纯净路线还是全选路线,生成的项目 App.vuemain.ts 以及 package.json 中的所有代码,都会像你自己手写的一般融洽,没有任何一点"被暴力注销掉"的痕迹。

💡 温馨提示:三个功能之间有依赖关系。"业务示例页面"依赖"网络请求层"——因为示例必须有 API 封装才能跑起来。所以如果你不选"网络请求层",CLI 就不会问你要不要"业务示例"。这样设计是为了保证生成的项目永远可以直接运行,没有任何破碎的依赖关系。


💡 三种使用场景速查

我想要 命令 适合谁
极速纯净空壳 npx hy-uni my-app --pure 有代码洁癖的老手,想自己搭业务
交互式精装配置 npx hy-uni my-app 想要完整方案,但不想要冗余代码
本地开发版本 npx hy-uni my-app --local 项目贡献者,想用最新开发模板

📂 看看生成出来的项目差异

路线 A 生成结果(--pure)

my-app/
├── src/
│ ├── pages/
│ │ ├── index/index.vue
│ │ └── about/about.vue
│ ├── layouts/default.vue
│ ├── store/index.ts
│ ├── utils/
│ │ ├── platform.ts
│ │ ├── system.ts
│ │ ├── data.ts
│ │ └── time.ts
│ ├── style/
│ └── static/
├── vite.config.ts
├── tsconfig.json
└── package.json ← 只有基础依赖

路线 B 生成结果(全选)


my-app/
├── src/
│ ├── pages/
│ │ ├── index/index.vue
│ │ ├── about/about.vue
│ │ ├── theme/ ← 新增
│ │ └── examples/ ← 新增
│ │ ├── api-demo.vue
│ │ ├── form-demo.vue
│ │ └── list-demo.vue
│ ├── api/ ← 新增
│ │ ├── client.ts
│ │ ├── interceptors.ts
│ │ ├── errors.ts
│ │ └── modules/
│ ├── composables/
│ │ └── useTheme.ts ← 新增
│ ├── config/
│ │ └── theme.ts ← 新增
│ ├── components/
│ │ └── ThemeToggle.vue ← 新增
│ ├── store/
│ │ ├── theme.ts ← 新增
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── app.ts
│ │ └── counter.ts ← 新增
│ ├── layouts/default.vue
│ ├── utils/
│ ├── style/
│ └── static/
├── vite.config.ts
├── tsconfig.json
└── package.json ← 完整的依赖列表

对比一目了然 —— 不选就是真的没有,不是"注释掉"。


🛠️ 不只是干净:开箱即用的重型工程底座

不管你怎么选裁剪,hy-uni 都为你提供了工业级的开发体验,包含了 7 个 Vite 核心插件的自动装配:

插件 作用
vite-plugin-uni-pages 页面自动路由生成
vite-plugin-uni-layouts 布局系统搭建
vite-plugin-uni-manifest manifest 编程化配置
vite-plugin-uni-components 组件按需自动导入
unplugin-auto-import Vue / uni-app API 自动导入
UnoCSS 原子化极速 CSS 构建
mp-selector-transform 小程序选择器兼容隔离转换

这意味着,创建完项目后:

  • 你不需要手动导入 refonMounted

  • 你不需要手动去繁琐的 pages.json 注册页面和组件。

  • 路径别名 @/src/ 已全部打通。

  • 开发体验直接拉满。


✨ 你到底能得到什么?

基础工程化(所有项目都有)

  • Vue 3 + TypeScript —— 类型安全,开发爽

  • Vite 5 —— 毫秒级热更新,极速开发

  • 7 个 Vite 插件 —— 页面自动路由、组件自动导入、manifest 编程化配置等,全配好

  • UnoCSS —— 按需生成原子化 CSS,再也不用手写 class

  • Pinia 状态管理 —— 开箱即用的持久化存储(适配小程序)

  • ESLint + TypeScript 类型检查 —— 代码规范自动化

可选功能 1:网络请求层

选了它,项目会多出完整的 src/api/ 目录:

import { get, post } from "@/api"
// GET 请求,自动拼接 params
const users = await get("/users", { page: 1, limit: 10 })
// POST 请求
const result = await post("/users", { name: "张三", age: 25 })

你获得了什么:

  • HTTP 客户端(基于 uni.request,支持 GET/POST/PUT/DELETE/PATCH)

  • 请求/响应/错误拦截器(自动注入 Token、处理超时等)

  • 7 种自定义错误分类(网络、超时、鉴权、权限等)

  • 跨平台兼容(H5 / 小程序 / App 无缝切换)

  • 完整的 API 模块化示例

不选它? src/api/ 目录根本不存在,package.json 里也没任何相关依赖。干干净净。

可选功能 2:主题管理

选了它,你就能这样用:

<script setup>
import { useTheme } from "@/composables/useTheme"
const { isDark, themeStore } = useTheme()
</script>
<template>
<button @click="themeStore.toggleTheme()">
{{ isDark ? "切换到亮色" : "切换到暗色" }}
</button>
</template>

你获得了什么:

  • 亮色/暗色/跟随系统 三种主题模式

  • 8 种预设主色调,可自定义

  • 20+ CSS 变量自动注入

  • 多端适配(H5 用 CSS 变量、小程序用全局事件、App 用状态栏同步)

  • 主题切换组件 + 完整的设置页面

不选它? 上面所有文件全部消失。布局组件里的主题代码也会被移除,替换成一个固定的 background-color: #f8f8f8 —— 不是留空,而是提供正确的 fallback。

可选功能 3:业务示例页面

选了它(需要先选网络请求层),你会得到 3 个完整的业务演示:

  • API 调用演示 —— 列表获取、详情查看、数据创建的完整流程

  • 表单演示 —— 输入、选择、复选、日期选择器,带表单验证

  • 列表演示 —— 上拉加载、下拉刷新、搜索过滤的完整实现

这不是 "Hello World",每个页面都是可以直接拿来改改就用的业务代码

不选它? 这些示例页面全部消失,首页上的导航入口也会一起消失(不会留下死链接)。


⚙️ 底层揭秘:如何做到代码级无痕裁剪?

一般的脚手架提供的是"多套模板分支组合"。而 hy-uni 创新性地引入了 "特征标记系统 (Feature Markers)",实现了一份源码,2^N 种自由组合引擎

我们在架构底层源码中,巧妙地隐藏了特定的注释标记:

1. 单行精确抹除

如果在 CLI 里没选 examples 示例功能,下面带有 // 【examples】 标记的代码行,会从物理层面直接消失:

export * from "./modules/app"
export { useCounterStore } from "./modules/counter" // 【examples】

2. 块级区域剥离(支持多语言环境)

如果没选 theme 主题功能,被包裹的代码块整块剥离(支持 TS、SCSS、Vue 甚至 HTML 注释):

<!-- 【theme:start】 -->
<view class="nav-link" @click="goToPage('/pages/theme')">
    <text>主题设置</text>
</view>
<!-- 【theme:end】 -->

3. 独门绝技:反向兜底(Fallback)裁剪

这是市面上其他脚手架极难做到的技术细节。针对"如果不选某个高阶模块,我仍然需要保留一套写死的基础兜底代码"的场景,我们设计了 ! 反向保留标记:


.layout {
    // 【!theme:start】 (如果没选动态主题,就保留这段写死的极简灰色背景)
    background-color: #f8f8f8;
    // 【!theme:end】

    // 【theme:start】 (如果选了主题,才保留动态的 CSS 变量注入机制)
    background-color: var(--bg-color-primary);
    transition: background-color 0.3s;
    // 【theme:end】
}

正是这套底层切割引擎,加上我们对 npm 依赖 dependencies 的按树剥离,以及支持功能间的链式感知(不支持底层功能时不展示进阶询问逻辑),才铸就了极致纯净的代码产物质量。


🔧 进阶:把它变成你们团队的专属黑科技

"这套裁剪逻辑不错,但我司有祖传架构,我单纯想白嫖这套神级裁剪引擎怎么办?"

完全没问题。整个脚手架能力是靠底层模板根目录的 .templaterc.json 驱动的:

{
"features": {
    "auth": {
           "name": "权限管理",
           "files": ["src/store/user.ts"],
           "dependencies": ["jwt-decode"]
        }
    }
}

结合在你的祖传代码里打上好 // 【auth】 标记,你就可以把 hy-uni 当作你们内部团队私有化的高阶脚手架来直接复用!

(剧透:在这个大版本之后,我们将正式支持 hy-uni template add 命令,允许你直接接管并挂载任意外部 Git 仓库,搭建你的私有定制生态!)


🚀 立即体验(极速拉起只需 3 个命令)

别再对着一堆乱糟糟的精装房一筹莫展了:

# 极速纯净版
npx hy-uni my-app --pure

创建后的常用命令

cd my-app
pnpm install

# 开发命令
pnpm dev:h5 # H5 本地开发(localhost:3000)
pnpm dev:mp # 微信小程序开发
pnpm dev:app # App 开发

# 构建命令
pnpm build:h5 # H5 生产构建
pnpm build:mp # 小程序构建

# 检查命令
pnpm lint # ESLint 检查 + 自动修复
pnpm type-check # TypeScript 类型检查


📊 跟现有方案对比

官方模板 社区全量模板 hy-uni
创建后能直接开发 ❌ 需要自己搭 ✅ 能,但要先删一堆 ✅ 开箱即用
功能选择 ❌ 无 ❌ 无 / 模板分支 ✅ 交互式按需选择
不要的功能 N/A ⚠️ 自己删(怕误删) ✅ 从代码到依赖全清理
生成代码质量 空壳 ⚠️ 可能有残留 ✅ 零残留,像手写的
模板维护成本 ⚠️ 高(N 个分支) ✅ 低(1 份模板)
极速纯净模式 --pure 1秒钟

🔗 获取地址(直达阵地)

核心源码不到 500 行,没有任何冗余包装。如果你也是代码洁癖患者,恰好懂我对极致整洁的坚持,欢迎来给我点一个宝贵的 Star!使用中发现任何 Bug,随时 Issue 见!


📌 总结

hy-uni

  • 我只想要骨架--pure 1秒钟搞定,零冗余

  • 我想要完整方案 → 交互式选择,按需组合

  • 我想要纯净但有示例 → 选 API + 示例,不选主题

  • 我想用自己的模板 → 即将支持,用我们的引擎

核心理念:你不要的功能,连一行代码都不该出现。


🚀 现在就试试


npx hy-uni my-app

让我们一起告别"删文件夹"的时代。

AI 全栈时代的工程化护栏:Vben-Nest 让 Mock 契约落地成真实后端

作者 _Jude
2026年2月26日 16:28

AI 让“会写代码的人”变多了,但也让一个老问题更显眼:写得快并不等于做得稳。

  • vibe coding 很爽,但在企业协作/长期迭代里,如果缺少统一规范与质量门禁,返工会非常昂贵
  • 前后端分离的联调常常依赖 Apifox MCP 这类工具,但契约同步、环境对齐、反复调用依然麻烦,而且还会额外消耗 token
  • 真正吸引人的方向是“全栈 + AI”:把重复劳动交给 AI,把关键决策交给工程化与契约,降低个人做全栈的门槛

这份项目模板 vben-nest 的核心思路很直接:在 vben 的工程化底座上新增 Nest 后端(apps/server),并让后端接口完全适配 vben 原本的 mock 契约。前端调用尽量不改,只替换“接口实现者”,从 mock 走向真实后端更平滑、更可演进。

1. 为什么不是“随便写接口”:全栈的关键是契约

一个判断是:随着 AI 能力增强,“前后端一个人搞”会越来越常见。原因并不神秘:很多业务难点不在 UI 或 API 的某一端,而在业务逻辑本身。

当 AI 能把样板代码写得更快时,决定交付质量的就变成了:

  • 需求是否能被拆成稳定的接口契约
  • 前后端是否能围绕同一套领域模型演进
  • 多人协作与长期维护里,边界是否足够清晰、成本是否可控

社区里也常有人吐槽 vben“很重”。但把视角从“个人 demo”切换到“企业协作”,它更像是在提前支付必要成本:规范、质量门禁、脚手架、依赖治理,以及可观测的目录结构与约定。

2. vibe coding 的边界:快要配护栏

vibe coding 适合快速验证与个人实验;一旦进入团队协作或中长期迭代,劣势会迅速放大:风格不一致、边界不清晰、可测试性差、回归成本高。AI 越强,越容易把“写得像能跑”误当成“工程上可交付”。

更理想的组合是:

  • AI 负责加速产出
  • 工程化负责约束质量与一致性

3. 这个项目做了什么:用 Nest 复刻并升级 vben mock

vben 自带的后端 mock(见 apps/backend-mock)已经非常接近真实后端:它不是浏览器里的 mock.js,而是一套独立服务实现,因此能覆盖文件上传、复杂逻辑、鉴权流程等更真实的场景。

vben-nest 在此基础上新增了 Nest 后端(见 apps/server),并坚持一个核心原则:

前端不改接口调用方式,只替换“实现者”。

路径、方法、响应结构、鉴权方式尽量保持一致,从 mock 平滑迁移到真实后端;甚至可以做到“先跑起来,再逐步替换为真实业务”。

4. 兼容策略与实现要点:对齐调用习惯,也保留演进空间

4.1 接口层:兼容 vben 的响应约定

Nest 侧做了三件事,降低前端切换成本:

  • 使用全局前缀 api,对齐 vben 的请求习惯(见 main.ts
  • 通过全局响应拦截器,统一包装为 { code, data, message, error }(见 ResponseInterceptor
  • 通过全局异常过滤器,统一输出错误结构(见 HttpExceptionFilter

这样前端的请求层可以继续使用原有的 code/data 成功码约定(例如 playground 的请求拦截器依旧按 successCode: 0 解析,见 request.ts)。

4.2 鉴权层:从"能用"到"更像生产"

很多 mock 方案只做"伪登录",但真实项目通常会有 refresh token、cookie 策略、过期处理等细节。Nest 侧采用:

这让"用模板练全栈"更贴近真实业务,而不是停留在 demo 层面。

4.3 数据层:从 mock-data 到可落库演进

为了让后端具备真实项目的演进空间,这里引入:

可以先用 seed 数据把前端跑通,再逐步把 mock-data 替换成真实表结构与业务逻辑。

4.4 工程化:把 vben 的护栏带到后端

这类模板的“核心价值”不在于多写了几个接口,而在于少搭了一套后端工程规范。

Nest 同样处于 TypeScript 生态,把后端放进 vben 的 monorepo 后,直接获得:

  • 代码格式化与 lint 约束(Prettier/ESLint/Stylelint)
  • Git Hooks 质量门禁(Lefthook + Commitlint,见 lefthook.yml
  • 统一依赖治理与脚本编排(pnpm workspace + turbo)

在 AI 参与编码的前提下,这套护栏能把“产出速度”与“可维护性”拉到一个更平衡的位置。

5. 快速开始

环境要求

  • Node.js >= 20.19
  • pnpm >= 10(推荐使用 corepack)
  • Docker Desktop(推荐,用于一键启动 PostgreSQL)

安装与运行

# 克隆项目
git clone https://github.com/MiniJude/vben-nest.git
cd vben-nest

# 安装依赖
npm i -g corepack
pnpm install

# 启动数据库 (Docker Compose)
docker compose -f apps/server/docker-compose.yml up -d

# 初始化数据库结构与种子数据
pnpm -F @vben/server run db:init

# 启动后端服务
pnpm -F @vben/server run dev
# 默认端口:3000

# 启动前端 Playground (新开终端)
pnpm dev:play

默认账号

  • vben / 123456
  • admin / 123456
  • jack / 123456

6. 目录结构速览:前端多方案 + 后端可落库

  • 前端应用:apps/web-*(多 UI 方案示例)
  • Playground:playground/(用于快速体验与联调)
  • Mock 服务:apps/backend-mock/(vben 原生 mock 服务实现)
  • Nest 后端:apps/server/(本项目新增,适配 mock 契约)

6. 全栈 + AI 的实践建议:契约先行

更推荐的协作方式是“契约先行(Contract First)”:

  • 先把接口契约稳定下来(URL、method、code/data 结构、分页约定、错误约定)
  • 再让 AI 生成 DTO、Controller、Service、Prisma Schema 的骨架
  • 依靠 lint、类型系统、提交规范,把代码质量稳定在可维护区间

前后端分离下,工具链可以帮助联调,但契约才是协作的“唯一事实来源”。

7. 适合谁使用

  • 想从 vben 上手全栈的前端同学:用熟悉的前端工程体系,把后端也纳入同一条流水线
  • 想快速搭建企业级全栈脚手架的小团队:少做重复造轮子,把精力留给业务
  • 想在真实工程约束下用 AI 提效的人:让 AI 提速,同时保留规范与可演进性

8. 项目链接

github.com/MiniJude/vb…

9. 后续演进

当前版本更偏向“理念与路径”的最小落地:先把接口契约与工程化护栏对齐,让 mock 能平滑替换为可演进的真实后端。面向生产的后端工程还需要持续补齐监控、日志、权限、安全、审计等能力,本项目也会按迭代节奏逐步完善。

Electron 无边框窗口拖拽实现

作者 柯南9527
2026年2月26日 16:09

Electron 无边框窗口拖拽实现详解:从问题到完美解决方案

技术栈: Electron 40+, Vue 3.5+, TypeScript 5.9+

🎯 问题背景

在开发 Electron 无边框应用时,遇到一个经典问题:如何让用户能够拖拽移动整个窗口?

传统的 Web 应用有浏览器标题栏可以拖拽,但 Electron 的无边框窗口 (frame: false) 完全去除了系统原生的标题栏,这就需要我们自己实现拖拽功能。

挑战

  1. 右键误触发: 用户右键点击时窗口也会跟着移动
  2. 定时器泄漏: 鼠标抬起后窗口仍在跟随鼠标
  3. 事件覆盖不全: 忘记处理鼠标离开窗口等边界情况
  4. 性能问题: 频繁的位置计算导致卡顿
  5. 安全考虑: 如何在保持功能的同时确保 IPC 通信安全

本文将从零开始,实现一个 Electron 窗口拖拽解决方案。

🛠️ 技术方案概览

我们采用 渲染进程 + IPC + 主进程 的三层架构:

[Vue 组件][IPC 安全通道][Electron 主进程][窗口控制]

核心优势:

  • ✅ 精确区分鼠标左/右键
  • ✅ 完整的事件生命周期管理
  • ✅ 内存安全(无定时器泄漏)
  • ✅ 安全的 IPC 通信
  • ✅ 流畅的用户体验(60fps)

🔧 详细实现步骤

第一步:主进程窗口配置

首先确保你的 Electron 窗口正确配置为无边框模式:

// electron/main/index.ts
const win = new BrowserWindow({
  title: 'Main window',
  frame: false,           // 关键:禁用系统标题栏
  transparent: true,      // 透明窗口(可选)
  backgroundColor: '#00000000', // 完全透明
  width: 288,
  height: 364,
  webPreferences: {
    preload: path.join(__dirname, '../preload/index.mjs'),
  }
})

第二步:预加载脚本 - 安全的 IPC 桥梁

使用 contextBridge 安全地暴露 API 给渲染进程:

// electron/preload/index.ts
import { ipcRenderer, contextBridge } from 'electron'

contextBridge.exposeInMainWorld('ipcRenderer', {
  // ... 其他 IPC 方法
  
  // 暴露窗口拖拽控制方法
  windowMove(canMoving: boolean) {
    ipcRenderer.invoke('windowMove', canMoving)
  }
})

为什么这样做?

  • 避免直接暴露完整的 ipcRenderer
  • 限制可调用的方法范围
  • 符合 Electron 安全最佳实践

第三步:主进程拖拽逻辑

创建专门的拖拽工具函数:

// electron/main/utils/windowMove.ts
import { screen } from "electron";

// 全局定时器引用 - 关键!
let movingInterval: NodeJS.Timeout | null = null;

export default function windowMove(
  win: Electron.BrowserWindow | null, 
  canMoving: boolean
) {
  let winStartPosition = { x: 0, y: 0 };
  let mouseStartPosition = { x: 0, y: 0 };

  if (canMoving && win) {
    // === 启动拖拽 ===
    console.log("main start moving");
    
    // 记录起始位置
    const winPosition = win.getPosition();
    winStartPosition = { x: winPosition[0], y: winPosition[1] };
    mouseStartPosition = screen.getCursorScreenPoint();
    
    // 清理已存在的定时器 - 防止重复
    if (movingInterval) {
      clearInterval(movingInterval);
      movingInterval = null;
    }
    
    // 启动位置更新定时器 (20ms ≈ 50fps)
    movingInterval = setInterval(() => {
      const cursorPosition = screen.getCursorScreenPoint();
      
      // 相对位移算法
      const x = winStartPosition.x + cursorPosition.x - mouseStartPosition.x;
      const y = winStartPosition.y + cursorPosition.y - mouseStartPosition.y;
      
      // 更新窗口位置
      win.setResizable(false); // 拖拽时禁止调整大小
      win.setBounds({ x, y, width: 288, height: 364 }); // 使用setBounds 同时设置位置和宽高防止拖动过程窗口变大,宽高可动态获取
    }, 20);
    
  } else {
    // === 停止拖拽 ===
    console.log("main stop moving");
    
    // 清理定时器
    if (movingInterval) {
      clearInterval(movingInterval);
      movingInterval = null;
    }
    
    // 恢复窗口状态
    if (win) {
      win.setResizable(true);
    }
  }
}

关键设计点:

  1. 全局定时器: movingInterval 声明在模块级别,确保能被正确清理
  2. 相对位移算法: 基于起始位置的相对移动,避免累积误差
  3. 防重复机制: 每次启动前清理已有定时器
  4. 窗口状态管理: 拖拽时禁用调整大小,结束后恢复

第四步:渲染进程事件处理

在 Vue 组件中精确处理鼠标事件:

<!-- src/App.vue -->
<script setup lang="ts">
import Camera from './components/Camera.vue'

// 调用主进程拖拽方法
const windowMove = (canMoving: boolean): void => {
  window?.ipcRenderer?.windowMove(canMoving);
}

// 只有左键按下时才开始移动
const handleMouseDown = (e: MouseEvent) => {
  if (e.button === 0) { // 0 = 左键, 1 = 中键, 2 = 右键
    windowMove(true);
  }
}

// 鼠标抬起时停止移动(任何按键)
const handleMouseUp = () => {
  windowMove(false);
}

// 鼠标离开容器时停止移动
const handleMouseLeave = () => {
  windowMove(false);
}

// 右键菜单处理 - 关键!
const handleContextMenu = (e: MouseEvent) => {
  e.preventDefault(); // 阻止默认右键菜单
  windowMove(false);  // 确保停止拖拽
}
</script>

<template>
  <div class="app-container" 
       @mousedown="handleMouseDown" 
       @mouseleave="handleMouseLeave"
       @mouseup="handleMouseUp" 
       @contextmenu="handleContextMenu">
    <Camera />
  </div>
</template>

鼠标按键值参考:

  • e.button === 0: 左键 (Left click)
  • e.button === 1: 中键 (Middle click)
  • e.button === 2: 右键 (Right click)

第五步:主进程 IPC 处理器

注册 IPC 处理器并集成拖拽逻辑:

// electron/main/index.ts
import windowMove from './utils/windowMove'

// ... 其他代码 ...

// 注册 IPC 处理器
ipcMain.handle("windowMove", (_, canMoving) => {
  console.log('ipcMain.handle windowMove', canMoving)
  windowMove(win, canMoving)
})

🔒 安全最佳实践

1. IPC 方法限制

// 好的做法:只暴露必要方法
contextBridge.exposeInMainWorld('ipcRenderer', {
  windowMove: (canMoving) => ipcRenderer.invoke('windowMove', canMoving)
})

// 避免:暴露完整 ipcRenderer
// contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer)

2. 输入验证

// 在主进程中验证输入
if (typeof canMoving !== 'boolean') {
  throw new Error('Invalid parameter');
}

3. 窗口引用安全

// 始终检查窗口是否存在
if (!win || win.isDestroyed()) {
  return;
}

🔄 替代方案对比

方案 A: CSS -webkit-app-region: drag (推荐用于简单场景)

.drag-area {
  -webkit-app-region: drag;
}

优点: 零 JavaScript,硬件加速,无 IPC 开销
缺点: 无法区分鼠标按键,会阻止所有鼠标事件

方案 B: 完整的自定义拖拽 (本文方案)

优点: 完全可控,支持复杂交互,可区分按键
缺点: 需要 IPC 通信,代码量较大

选择建议

  • 简单应用: 使用 CSS 方案
  • 复杂交互: 使用本文的自定义方案
  • 混合方案: 在非交互区域使用 CSS,在需要精确控制的区域使用自定义方案

💡 扩展功能思路

1. 拖拽区域限制

// 限制窗口不能拖出屏幕
const bounds = screen.getDisplayNearestPoint(cursorPosition).bounds;
const newX = Math.max(bounds.x, Math.min(x, bounds.x + bounds.width - windowWidth));
const newY = Math.max(bounds.y, Math.min(y, bounds.y + bounds.height - windowHeight));

2. 拖拽动画效果

// 拖拽开始时添加阴影
win.webContents.executeJavaScript(`
  document.body.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
`);

// 拖拽结束时移除
win.webContents.executeJavaScript(`
  document.body.style.boxShadow = 'none';
`);

3. 多显示器支持

// 获取所有显示器信息
const displays = screen.getAllDisplays();
// 根据当前显示器调整拖拽行为

📚 完整项目结构

electron-camera/
├── electron/
│   ├── main/
│   │   ├── utils/windowMove.ts    # 拖拽核心逻辑
│   │   └── index.ts               # 主进程入口
│   └── preload/index.ts           # IPC 安全桥梁
└── src/
    └── App.vue                    # 渲染进程事件处理

🤝 总结

通过本文的完整实现,你将获得一个:

  • 功能完整 的窗口拖拽解决方案
  • 安全可靠 的 IPC 通信架构
  • 性能优秀 的用户体验
  • 易于维护 的代码结构

这个方案已经在实际项目中经过充分测试,可以直接用于你的 Electron 应用开发。

在实现过程中发现还有一个好的库,github.com/Wargraphs/e… 有空可以试试。

如果你觉得这篇文章对你有帮助,请点赞、收藏或分享给其他开发者!

有任何问题或改进建议,欢迎在评论区讨论! 🚀

HTML&CSS:高颜值产品卡片页面,支持主题切换

作者 前端Hardy
2026年2月26日 16:01

这是一个产品卡片页面,无 JS 实现图片切换主题切换、全设备响应式适配,兼顾美观与实用性,值得大家学习。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

演示效果

演示效果

HTML&CSS

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>现代极简卧室套装</title>
    <style>
        body {
            font-family: "Roboto Serif", serif;
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 0;
            min-height: 100vh;
            background-color: #f6f1ea;
            background-image: radial-gradient(circle at 15% 20%, rgba(232, 186, 142, 0.35) 0%, rgba(232, 186, 142, 0.18) 20%, rgba(232, 186, 142, 0.08) 35%, transparent 60%), radial-gradient(circle at 85% 75%, rgba(220, 160, 110, 0.35) 0%, rgba(220, 160, 110, 0.15) 25%, transparent 55%), radial-gradient(circle at 60% 10%, rgba(255, 210, 170, 0.25) 0%, transparent 50%);
            background-repeat: no-repeat;
            background-attachment: fixed;
        }

        body::after {
            content: "";
            position: fixed;
            inset: 0;
            pointer-events: none;
            z-index: 10;
            mix-blend-mode: saturation;
            background: radial-gradient(circle at 20% 25%, rgba(255, 200, 150, 0.35), rgba(255, 200, 150, 0.15) 30%, transparent 60%), radial-gradient(circle at 80% 70%, rgba(255, 170, 110, 0.3), transparent 60%);
            filter: blur(80px);
        }

        .product-card {
            border-radius: 12rem;
            corner-shape: squircle;
            padding: 1rem 1.5rem 1rem 1rem;
            max-width: 800px;
            background: linear-gradient(145deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.55));
            backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.4);
            box-shadow: 0 40px 80px rgba(206, 168, 132, 0.1), 0 20px 40px rgba(0, 0, 0, 0.1), 0 0 120px rgba(198, 169, 126, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.6), -5px -5px 10px 0 #fffce9;
        }

        @media (max-width: 768px) {
            .product-card {
                border-radius: 5rem;
            }
        }

        .product-card .card-content {
            display: flex;
            justify-content: center;
            gap: 32px;
        }

        @media (max-width: 768px) {
            .product-card .card-content {
                flex-direction: column;
            }
        }

        .product-card .image-container {
            position: relative;
            border-radius: 12rem;
            width: 45vw;
            max-width: 450px;
            corner-shape: squircle;
            aspect-ratio: 1/1;
            overflow: hidden;
        }

        @media (max-width: 768px) {
            .product-card .image-container {
                width: 100%;
                border-radius: 5rem;
            }
        }

        .product-card .info-container {
            display: flex;
            flex-direction: column;
            justify-content: center;
            color: #5f5a55;
        }

        .product-card .info-container .brand {
            display: block;
            padding-bottom: 3rem;
            font-size: 0.85rem;
        }

        .product-card .info-container h1 {
            line-height: 100%;
            font-size: 2rem;
            font-weight: 600;
            letter-spacing: -0.1rem;
            margin: 0;
            padding: 0;
            transform: scaleY(2);
        }

        .product-card .info-container .price {
            font-size: 1.3rem;
            font-weight: 400;
            margin: 0;
            padding: 2.5rem 0 0;
            color: #c89b5e;
        }

        .product-card .info-container .description {
            max-width: 280px;
            font-size: 0.85rem;
            font-weight: 200;
            line-height: 150%;
            margin: 0;
            padding: 1rem 0;
        }

        .product-card .info-container .btn-primary {
            margin-top: 1rem;
            padding: 1rem 2rem;
            max-width: 200px;
            font-size: 1rem;
            letter-spacing: 1px;
            color: #ffffff;
            background: linear-gradient(145deg, #d8a45c, #b97a2f);
            border: none;
            border-radius: 40px;
            cursor: pointer;
            box-shadow: 0 8px 20px rgba(201, 155, 94, 0.35), 0 0 25px rgba(201, 155, 94, 0.15), inset 0 2px 6px rgba(255, 255, 255, 0.3);
            transition: 0.3s ease;
        }

        @media (max-width: 768px) {
            .product-card .info-container .btn-primary {
                max-width: none;
            }
        }

        .product-card .info-container .btn-primary:hover {
            transform: translateY(-2px);
            filter: brightness(1.05);
            box-shadow: 0 10px 25px rgba(185, 122, 47, 0.45), inset 0 2px 6px rgba(255, 255, 255, 0.3);
        }

        .product-card .info-container .btn-primary:active {
            transform: translateY(1px);
            box-shadow: 0 5px 15px rgba(185, 122, 47, 0.3), inset 0 3px 6px rgba(0, 0, 0, 0.15);
        }

        .image {
            aspect-ratio: 1/1;
            width: 100%;
            background: url("https://assets.codepen.io/662051/Gemini_Generated_Image_j4wi73j4wi73j4wi.png") center;
            background-size: cover;
            transition: 0.4s ease;
        }

        .theme-switch__input:checked~.image {
            background-image: url("https://assets.codepen.io/662051/Gemini_Generated_Image_2q32sc2q32sc2q32.png");
        }

        .theme-switch {
            position: absolute;
            left: 50%;
            bottom: 20px;
            transform: translateX(-50%);
            cursor: pointer;
        }

        .theme-switch__input {
            display: none;
        }

        .theme-switch__container {
            position: relative;
            width: 60px;
            height: 32px;
            padding: 5px;
            background-color: #e2e2e2;
            border-radius: 32px;
            display: flex;
            align-items: center;
            box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.1);
            transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        }

        .theme-switch__circle {
            width: 30px;
            height: 30px;
            background-color: #333;
            border-radius: 50%;
            z-index: 2;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        }

        .theme-switch__icons {
            position: absolute;
            width: 100%;
            box-sizing: border-box;
            z-index: 2;
        }

        .theme-switch .icon {
            width: 18px;
            height: 18px;
            transition: opacity 0.3s ease;
        }

        .theme-switch .icon--sun {
            opacity: 1;
            color: #fff;
            transform: translate(6px, 2px);
        }

        .theme-switch .icon--moon {
            opacity: 0;
            color: #333;
            transform: translate(15px, 2px);
        }

        .theme-switch__input:checked+.image+.theme-switch .theme-switch__container {
            background-color: #222;
        }

        .theme-switch__input:checked+.image+.theme-switch .theme-switch__circle {
            transform: translateX(30px);
            background-color: #fff;
        }

        .theme-switch__input:checked+.image+.theme-switch .icon--sun {
            opacity: 0;
        }

        .theme-switch__input:checked+.image+.theme-switch .icon--moon {
            opacity: 1;
            color: #333;
        }

        #dev {
            font-family: "Montserrat", sans-serif;
            position: fixed;
            top: 10px;
            left: 10px;
            padding: 1em;
            font-size: 14px;
            color: #333;
            background-color: white;
            border-radius: 25px;
            cursor: pointer;
        }

        #dev a {
            text-decoration: none;
            font-weight: bold;
            color: #333;
            transition: all 0.4s ease;
        }

        #dev a:hover {
            color: #ef5350;
            text-decoration: underline;
        }

        #dev span {
            display: inline-block;
            color: pink;
            transition: all 0.4s ease;
        }

        #dev span:hover {
            transform: scale(1.2);
        }
    </style>
</head>

<body>
    <div class="product-card">
        <div class="card-content">
            <div class="image-container">
                <input type="checkbox" id="theme-toggle" class="theme-switch__input">
                <div class="image"></div>
                <label for="theme-toggle" class="theme-switch">
                    <div class="theme-switch__container">
                        <div class="theme-switch__circle"></div>
                        <div class="theme-switch__icons">
                            <svg class="icon icon--sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
                                fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
                                stroke-linejoin="round">
                                <circle cx="12" cy="12" r="5"></circle>
                                <line x1="12" y1="1" x2="12" y2="3"></line>
                                <line x1="12" y1="21" x2="12" y2="23"></line>
                                <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
                                <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
                                <line x1="1" y1="12" x2="3" y2="12"></line>
                                <line x1="21" y1="12" x2="23" y2="12"></line>
                                <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
                                <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
                            </svg>
                            <svg class="icon icon--moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
                                fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
                                stroke-linejoin="round">
                                <path d="M21 12.79A9 9 0 1 1 11.21 3
                       7 7 0 0 0 21 12.79z"></path>
                            </svg>
                        </div>
                    </div>
                </label>
            </div>
            <div class="info-container">
                <header>
                    <span class="brand">HOLIME</span>
                    <h1 class="title">现代极简<br />卧室套装</h1>
                    <p class="price">$50.00 <span></span></p>
                    <p class="description">
                        打造宁静休憩空间,这套现代极简卧室套装融合永恒设计与舒适体验,为您的家注入优雅与安宁。
                    </p>
                </header>
                <button class="btn-primary">加入购物车</button>
            </div>
        </div>
    </div>
</body>

</html>

HTML

  • prouct-card:产品卡片容器。核心视觉容器,用毛玻璃效果、圆角、渐变背景打造高级感
  • card-content:卡片内容区。弹性布局,分「图片区 + 信息区」,移动端自动改为垂直布局
  • image-container:产品图片容器。固定宽高比(1:1);② 包含切换图片的复选框、图片、切换按钮;圆角适配移动端
  • info-container:产品信息区。垂直布局,包含品牌、标题、价格、描述、加入购物车按钮
  • theme-toggle:图片切换复选框。隐藏的核心交互控件,通过「选中 / 未选中」切换产品图片
  • theme-switch:切换按钮(夜间/白天)。视觉化的切换控件,绑定到隐藏的复选框,实现交互反馈

CSS

全局样式 & 背景

body {
  font-family: "Roboto Serif", serif;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  /* 背景层:暖色调径向渐变,营造温馨的卧室氛围 */
  background-color: #f6f1ea;
  background-image: radial-gradient(...), radial-gradient(...), radial-gradient(...);
  background-repeat: no-repeat;
  background-attachment: fixed;
}
body::after {
  /* 叠加模糊渐变层,增强层次感,mix-blend-mode 提升饱和度 */
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none; /* 不遮挡交互 */
  mix-blend-mode: saturation;
  background: radial-gradient(...);
  filter: blur(80px);
}

核心:flex 实现页面居中 + 多层径向渐变背景 + 伪元素叠加模糊层,打造「柔和、有呼吸感」的视觉基底。

产品卡片核心样式

.product-card {
  border-radius: 12rem; /* 超大圆角,接近胶囊/圆角矩形 */
  corner-shape: squircle; /* 松鼠角(非标准但现代浏览器支持,更圆润的圆角) */
  padding: 1rem 1.5rem 1rem 1rem;
  max-width: 800px;
  /* 毛玻璃核心:半透明背景 + backdrop-filter */
  background: linear-gradient(145deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.55));
  backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.4);
}

核心:backdrop-filter: blur(20px) 实现毛玻璃效果,linear-gradient 半透明白色渐变增强通透感,超大圆角提升现代感。

响应式布局

@media (max-width: 768px) {
  .product-card { border-radius: 5rem; }
  .product-card .card-content { flex-direction: column; }
  .product-card .image-container { width: 100%; border-radius: 5rem; }
  .product-card .info-container .btn-primary { max-width: none; }
}

核心:屏幕宽度 ≤768px(移动端)时,① 缩小卡片 / 图片圆角;② 图片 + 信息从「横向排列」改为「垂直排列」;③ 按钮宽度自适应,适配移动端交互。

图片切换交互

/* 默认图片 */
.image {
  aspect-ratio: 1/1;
  width: 100%;
  background: url("xxx.png") center;
  background-size: cover;
  transition: 0.4s ease;
}
/* 复选框选中时切换图片 */
.theme-switch__input:checked~.image {
  background-image: url("yyy.png");
}

核心:利用 CSS 相邻兄弟选择器 ~,监听复选框 :checked 状态,切换背景图片,配合 transition 实现平滑过渡。

开关按钮 & 按钮动效

/* 开关按钮样式切换 */
.theme-switch__input:checked+.image+.theme-switch .theme-switch__container {
  background-color: #222;
}
.theme-switch__input:checked+.image+.theme-switch .theme-switch__circle {
  transform: translateX(30px);
  background-color: #fff;
}
/* 加入购物车按钮 hover/active 动效 */
.btn-primary:hover {
  transform: translateY(-2px);
  filter: brightness(1.05);
  box-shadow: ...;
}
.btn-primary:active {
  transform: translateY(1px);
  box-shadow: ...;
}

核心:① 开关按钮的「白天 / 夜间」图标显隐、背景色、滑块位置随复选框状态变化;② 按钮 hover 时上移 + 亮度提升,active 时下移 + 阴影缩小,模拟「按压反馈」。


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

微信小程序自动化的 AI 新时代:wechat-devtools-mcp 智能方案

作者 FliPPeDround
2026年2月26日 15:43

FliPPeDround

前端工程师 · 开源爱好者 · 正在找工作

对我的项目感兴趣?查看我的简历 · resume

如果你曾尝试配合 AI 代理(如 Claude、Cursor 编写微信小程序,你大概率会遇到这样的困境:测试工具与 AI 代理集成困难、缺乏统一的自动化框架支持、无法充分利用 AI 的智能分析能力。更糟糕的是,当你想要使用 Claude、Cursor 等 AI 辅助工具来提升测试效率时,却发现没有合适的微信小程序自动化集成方案。

为了解决这些痛点,wechat-devtools-mcp 应运而生。作为一款基于 MCP(Model Context Protocol)协议的微信开发者工具自动化服务,它让小程序的自动化测试与 AI 智能分析变得前所未有的简单和高效。

📖 介绍

📚 官方文档:更多详细的安装和配置说明,请参考 GitHub 仓库

wechat-devtools-mcp 是一个专门为微信小程序设计的 MCP 服务,通过 MCP 协议与 AI 代理(如 Claude、Cursor)深度集成。它基于微信官方的 miniprogram-automator 库,提供了完整的小程序自动化能力,让你能够在 AI 的辅助下,高效地完成小程序的自动化测试和调试。

这个工具的出现,填补了小程序自动化测试与 AI 智能分析之间的空白。它不仅保持了与微信开发者工具的完全兼容性,还充分发挥了 MCP 协议的标准化优势,为开发者提供了一个更智能、更高效的自动化测试解决方案。

🚀 核心功能与技术优势

1. 无缝集成 MCP 协议生态

wechat-devtools-mcp 完全兼容 MCP 协议,可以轻松集成到支持 MCP 的 AI 代理中:

{
  "mcpServers": {
    "wechat-devtools": {
      "command": "npx",
      "args": [
        "-y",
        "wechat-devtools-mcp",
        "--projectPath=/path/to/your/miniprogram"
      ]
    }
  }
}

2. 全面的页面导航支持

工具提供了丰富的 API 来操作小程序页面导航:

  • 保留当前页面跳转:通过 navigateTo 跳转到非 tabBar 页面
  • 关闭当前页面跳转:通过 redirectTo 关闭当前页面并跳转
  • 返回上一页:通过 navigateBack 返回上一页面或多级页面
  • 重新加载页面:通过 reLaunch 关闭所有页面并重新打开
  • 切换 TabBar:通过 switchTab 跳转到 tabBar 页面
  • 获取页面栈:通过 pageStack 获取当前页面栈列表

3. 强大的元素操作能力

工具提供了完整的元素操作 API,支持各种用户交互模拟:

  • 元素获取:通过 getElementgetElements 获取页面元素
  • 用户交互:支持点击、长按、触摸、输入等操作
  • 元素信息:获取元素尺寸、位置、文本、属性、样式等信息
  • 组件方法:调用自定义组件的方法和数据操作

4. 智能日志和异常监听

工具内部实现了智能的日志和异常监听机制:

  • 自动监听控制台日志(console.log、console.info、console.warn、console.error)
  • 自动捕获运行时异常,包括错误名称、堆栈跟踪和发生时间
  • 支持日志和异常的查询和过滤
  • 内置日志数量限制,避免内存溢出

5. 灵活的配置选项

支持丰富的配置选项,满足不同测试场景的需求:

  • 自定义小程序项目路径
  • 微信开发者工具 CLI 路径配置
  • 连接超时时间设置
  • WebSocket 端口配置
  • 用户账号和登录票据支持
  • 项目配置文件覆盖

6. 微信 API 模拟与调用

工具提供了强大的微信 API 操作能力:

  • 调用微信 API:通过 callWxMethod 调用 wx 对象上的任意方法
  • Mock 微信 API:通过 mockWxMethod 模拟 API 返回值,便于测试
  • 恢复微信 API:通过 restoreWxMethod 恢复被 Mock 的方法

🧪 为什么 E2E 测试如此重要

在软件开发中,单元测试固然重要,但 E2E(End-to-End)测试在构建高质量代码过程中扮演着不可替代的角色。

提升代码可靠性

E2E 测试模拟真实用户的使用场景,从用户界面到后端服务的完整流程进行验证。与单元测试不同,E2E 测试能够发现:

  • 页面间的导航和状态传递问题
  • 用户交互与业务逻辑的集成异常
  • 微信 API 调用的错误处理
  • 不同设备和系统版本的兼容性问题

对于微信小程序这种运行在特殊环境中的应用,E2E 测试尤为重要。它能够确保你的小程序在不同设备、不同微信版本、不同网络环境下都能正常运行,避免出现"在开发环境正常,上线后出问题"的尴尬情况。

降低维护成本

虽然编写 E2E 测试 需要投入一定的时间成本,但从长远来看,它能显著降低维护成本:

  • 减少回归测试时间:自动化测试可以在几分钟内完成原本需要数小时的手动测试
  • 快速定位问题:当出现 bug 时,E2E 测试能够快速定位问题所在
  • 增强重构信心:有了完善的测试覆盖,你可以放心地进行代码重构,而不必担心破坏现有功能
  • 文档化业务逻辑:测试代码本身就是最好的业务逻辑文档

提升团队协作效率

E2E 测试作为项目质量的"守门员",能够:

  • 统一团队对功能实现的理解
  • 减少 code review 时的争议
  • 让新成员快速理解项目功能
  • 建立持续集成的质量保障体系

📦 快速上手

安装依赖

wechat-devtools-mcp 是一个 npm 包,可以直接通过 npx 运行,无需额外安装:

npx -y wechat-devtools-mcp --projectPath=/path/to/your/miniprogram

配置 MCP 服务器

在你的 AI 代理(如 Claude、Cursor)的配置文件中添加 MCP 服务器配置:

{
  "mcpServers": {
    "wechat-devtools": {
      "command": "npx",
      "args": [
        "-y",
        "wechat-devtools-mcp",
        "--projectPath=/path/to/your/miniprogram"
      ]
    }
  }
}

命令行参数说明

参数 类型 说明
--projectPath string 小程序项目路径(必填)
--cliPath string 微信开发者工具 CLI 路径
--timeout number 连接超时时间(毫秒),默认 30000
--port number WebSocket 端口号,默认 9420
--account string 用户 openid
--ticket string 开发者工具登录票据
--projectConfig string 覆盖 project.config.json 中的配置

使用示例

配置完成后,你就可以在 AI 代理中使用 wechat-devtools-mcp 提供的工具了。以下是一些典型的使用场景:

1. 启动小程序

// 使用 launch 工具启动微信开发者工具并连接小程序
await launch()

2. 页面导航测试

// 跳转到指定页面
await navigateTo({ url: '/pages/detail/detail?id=123' })

// 获取当前页面信息
const currentPage = await currentPage()

// 返回上一页
await navigateBack({ delta: 1 })

3. 元素操作测试

// 获取页面元素
const element = await getElement({ selector: '.submit-button' })

// 点击元素
await tapElement({ selector: '.submit-button' })

// 输入文本
await inputElement({ selector: '#username', value: 'testuser' })

// 获取元素文本
const text = await getElementText({ selector: '.welcome-text' })

4. 页面数据操作

// 获取页面数据
const pageData = await getPageData({ path: 'userInfo.name' })

// 设置页面数据
await setPageData({ data: { count: 10, status: 'active' } })

5. 日志和异常监听

// 获取控制台日志
const logs = await getlogs({ type: 'error', limit: 10 })

// 获取异常信息
const exceptions = await getexceptions({ limit: 5 })

6. 微信 API 调用和 Mock

// 调用微信登录 API
const loginResult = await callWxMethod({ method: 'login', args: [] })

// Mock 用户信息 API
await mockWxMethod({ method: 'getUserInfo', result: { nickName: '测试用户' } })

// 恢复 API
await restoreWxMethod({ method: 'getUserInfo' })

🎯 高级功能详解

截图功能

工具支持对小程序当前页面进行截图,返回 Base64 编码的图片数据:

const screenshot = await screenshot()

系统信息获取

获取小程序运行所在的系统信息,包括手机品牌、型号、屏幕尺寸、操作系统版本、微信版本等:

const systemInfo = await systemInfo()

体验评分

支持微信小程序体验评分(Audits)功能,可以获取性能最佳实践、Accessibility 可访问性等方面的评分和建议:

// 停止体验评分并获取报告
const auditResult = await stopAudits({ path: './audits.json' })

测试账号管理

支持获取微信开发者工具多账号调试中添加的测试用户列表,可用于模拟不同用户登录场景的测试:

const accounts = await testAccounts()

🔧 技术实现细节

wechat-devtools-mcp 的实现基于 MCP 协议和微信官方的 miniprogram-automator 库,核心架构包括以下几个部分:

  1. Automator 类:负责微信开发者工具的启动、连接和生命周期管理
  2. MiniProgram 工具类:提供小程序级别的操作,如页面导航、API 调用、系统信息获取等
  3. Page 工具类:提供页面级别的操作,如元素获取、数据操作、方法调用等
  4. Element 工具类:提供元素级别的操作,如点击、输入、属性获取等

工具内部还实现了智能的状态管理和错误处理机制,确保自动化测试的稳定性和可靠性。

🌟 总结

wechat-devtools-mcp 为微信小程序开发者提供了一个现代化、智能化的自动化测试解决方案。它不仅解决了传统测试工具与 AI 代理集成困难的问题,还充分发挥了 MCP 协议和 miniprogram-automator 的技术优势。

通过完善的 E2E 测试和 AI 智能分析,你可以:

  • 提升代码质量和可靠性
  • 降低长期维护成本
  • 增强团队协作效率
  • 建立持续集成的质量保障体系
  • 充分利用 AI 的智能分析能力

如果你正在开发微信小程序项目,并且想要建立完善的自动化测试体系,wechat-devtools-mcp 绝对值得一试。它会让你的测试工作变得前所未有的简单和高效。

📚 相关资源


最后

wechat-devtools-mcp 是一个免费的开源软件,遵循MIT协议,社区的赞助使其能够有更好的发展。

你的赞助会帮助我更好的维护项目,如果对你有帮助,请考虑赞助一下😊

你的star🌟也是对我的很大鼓励,Github

欢迎反馈问题和提pr共建

浏览器扩展 E2E 测试的救星:vitest-environment-web-ext 让你告别繁琐配置

作者 FliPPeDround
2026年2月26日 15:34

FliPPeDround

前端工程师 · 开源爱好者 · 正在找工作

对我的项目感兴趣?查看我的简历 · resume

如果你曾尝试为浏览器扩展编写 E2E 测试,你大概率会遇到这样的困境:测试环境配置复杂、Playwright 与扩展的集成困难、缺乏统一的测试框架支持。更糟糕的是,当你想要使用 Vitest 这种更现代、更快速的测试框架时,却发现没有合适的浏览器扩展环境支持。

为了解决这些痛点,vitest-environment-web-ext 应运而生。作为一款轻量级的 Vitest 自定义环境,它让浏览器扩展项目的 E2E 测试变得前所未有的简单和高效。

📖 介绍

📚 官方文档:更多详细的安装和配置说明,请参考 CRXJS 官方文档

vitest-environment-web-ext 是一个专门为 Vitest 设计的浏览器扩展 E2E 测试环境。它深度集成了 Playwright 的强大能力,让你能够在 Vitest 框架下无缝运行浏览器扩展的自动化测试。

这个工具的出现,填补了浏览器扩展现代化测试工具链的空白。它不仅保持了与 Playwright 的完全兼容性,还充分发挥了 Vitest 的性能优势,为开发者提供了一个更快速、更现代化的测试解决方案。

🚀 核心功能与技术优势

1. 无缝集成 Vitest 生态

vitest-environment-web-ext 完全兼容 Vitest 的配置系统,你可以像使用其他 Vitest 环境一样简单配置:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'web-ext',
  },
})

2. 全面的扩展页面支持

工具提供了丰富的 API 来访问浏览器扩展的各个部分:

  • Popup 页面测试:通过 getPopupPage() 获取扩展的弹窗页面
  • Side Panel 测试:通过 getSidePanelPage() 获取侧边栏页面
  • Content Script 测试:通过 context.newPage() 创建新页面并测试注入的内容脚本
  • Service Worker 测试:通过 getServiceWorker() 获取扩展的后台服务工作线程

3. 智能环境管理

工具内部实现了智能的环境初始化和清理机制:

  • 自动管理浏览器上下文的生命周期
  • 支持自动编译扩展项目
  • 内置扩展 ID 自动获取机制
  • 完善的错误处理和资源清理

4. TypeScript 完整支持

提供了完整的 TypeScript 类型定义,让开发者在编写测试代码时享受完整的类型提示和智能补全:

{
  "compilerOptions": {
    "types": [
      "vitest-environment-web-ext/types"
    ]
  }
}

5. 灵活的配置选项

支持丰富的配置选项,满足不同测试场景的需求:

  • 自动启动浏览器
  • 自定义编译命令
  • Playwright 参数配置(如 devtools、slowMo 等)
  • 用户数据目录管理

🧪 为什么 E2E 测试如此重要

在软件开发中,单元测试固然重要,但 E2E(End-to-End)测试在构建高质量代码过程中扮演着不可替代的角色。

提升代码可靠性

E2E 测试模拟真实用户的使用场景,从用户界面到后端服务的完整流程进行验证。与单元测试不同,E2E 测试能够发现:

  • 组件间的集成问题
  • 浏览器扩展与网页内容的交互异常
  • 消息传递机制的错误
  • 权限配置问题

对于浏览器扩展这种运行在特殊环境中的应用,E2E 测试尤为重要。它能够确保你的扩展在不同浏览器、不同网页环境下都能正常运行,避免出现"在开发环境正常,上线后出问题"的尴尬情况。

降低维护成本

虽然编写 E2E 测试需要投入一定的时间成本,但从长远来看,它能显著降低维护成本:

  • 减少回归测试时间:自动化测试可以在几分钟内完成原本需要数小时的手动测试
  • 快速定位问题:当出现 bug 时,E2E 测试能够快速定位问题所在
  • 增强重构信心:有了完善的测试覆盖,你可以放心地进行代码重构,而不必担心破坏现有功能
  • 文档化业务逻辑:测试代码本身就是最好的业务逻辑文档

提升团队协作效率

E2E 测试作为项目质量的"守门员",能够:

  • 统一团队对功能实现的理解
  • 减少 code review 时的争议
  • 让新成员快速理解项目功能
  • 建立持续集成的质量保障体系

📦 快速上手

安装依赖

首先,安装必要的依赖:

pnpm add -D vitest-environment-web-ext playwright

⚠️ 注意:playwright 是必需的依赖,必须安装

配置 Vitest

在项目根目录创建或修改 vitest.config.ts

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'web-ext',
  },
})

配置 TypeScript

tsconfig.json 中添加类型定义:

{
  "compilerOptions": {
    "types": [
      "vitest-environment-web-ext/types"
    ]
  }
}

添加测试脚本

package.json 中添加测试命令:

{
  "scripts": {
    "test": "vitest"
  }
}

编写测试用例

创建测试文件,例如 extension.test.ts

import { describe, expect, it } from 'vitest'

describe('浏览器扩展测试', () => {
  it('测试 Popup 页面', async () => {
    const popupPage = await browser.getPopupPage()
    const text = await popupPage.waitForSelector('.welcome-text')
    expect(await text.textContent()).toBe('欢迎使用扩展')
  })

  it('测试 Side Panel 页面', async () => {
    const sidePanelPage = await browser.getSidePanelPage()
    const title = await sidePanelPage.title()
    expect(title).toContain('侧边栏')
  })

  it('测试 Content Script 注入', async () => {
    const page = await context.newPage()
    await page.goto('https://www.example.com')
    const toggleButton = await page.waitForSelector('.toggle-button')
    await toggleButton.click()
    const appContainer = await page.waitForSelector('.app-content')
    expect(await appContainer.textContent()).toBeTruthy()
  })
})

运行测试

执行测试命令:

pnpm test

🎯 高级配置选项详解

除了基础配置,vitest-environment-web-ext 还支持更多高级选项:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
        compiler: 'npm run build',
        autoLaunch: true,
        targetUrl: 'https://www.example.com',
        playwright: {
          userDataDir: './.playwright',
          devtools: true,
          slowMo: 100,
        },
      },
    },
  },
})

配置参数说明

  • path: 扩展构建目录路径
  • compiler: 测试前自动编译的命令
  • autoLaunch: 是否自动启动浏览器(默认:true)
  • targetUrl: 用于获取扩展 ID 的目标页面
  • playwright.userDataDir: Playwright 用户数据目录
  • playwright.devtools: 是否自动打开开发者工具
  • playwright.slowMo: 操作延迟时间(毫秒)

🔧 技术实现细节

vitest-environment-web-ext 的实现基于 Vitest 的自定义环境 API,核心类 WebExtEnvironment 实现了以下功能:

  1. 环境初始化:在 setup 方法中初始化浏览器扩展环境
  2. 全局对象注入:将 browsercontext 注入到全局作用域
  3. 资源清理:在 teardown 方法中正确清理浏览器资源
  4. 扩展页面管理:通过 WebExtLoaderWebExtFactory 等类实现扩展页面的智能管理

工具内部还实现了智能的状态管理,避免重复初始化,确保测试环境的稳定性。

🌟 总结

vitest-environment-web-ext 为浏览器扩展开发者提供了一个现代化、高效的 E2E 测试解决方案。它不仅解决了传统测试工具配置复杂的问题,还充分发挥了 Vitest 和 Playwright 的性能优势。

通过完善的 E2E 测试,你可以:

  • 提升代码质量和可靠性
  • 降低长期维护成本
  • 增强团队协作效率
  • 建立持续集成的质量保障体系

如果你正在开发浏览器扩展项目,并且想要建立完善的测试体系,vitest-environment-web-ext 绝对值得一试。它会让你的测试工作变得前所未有的简单和高效。

📚 相关资源


最后

vitest-environment-web-ext 是一个免费的开源软件,遵循MIT协议,社区的赞助使其能够有更好的发展。

你的赞助会帮助我更好的维护@crxjs,如果对你有帮助,请考虑赞助一下😊

你的star🌟也是对我的很大鼓励,Github

欢迎反馈问题和提pr共建

Vitest Environment UniApp:让 uni-app E2E 测试变得前所未有的简单

作者 FliPPeDround
2026年2月26日 15:21

FliPPeDround

前端工程师 · 开源爱好者 · 正在找工作

对我的项目感兴趣?查看我的简历 · resume

如果你曾尝试为 uni-app 项目编写 E2E 测试,你大概率会遇到这样的困境:官方提供的 Jest 环境配置复杂、文档更新滞后、与现代测试工具链集成困难。更糟糕的是,当你想要使用 Vitest 这种更现代、更快速的测试框架时,却发现没有合适的 uni-app 环境支持。

为了解决这些痛点,vitest-environment-uniapp 应运而生。作为一款轻量级的 Vitest 自定义环境,它让 uni-app 项目的 E2E 测试变得前所未有的简单和高效。

📖 介绍

vitest-environment-uniapp 是一个专门为 Vitest 设计的 uni-app E2E 测试环境。它深度集成了 DCloud 官方的 @dcloudio/uni-automator,让你能够在 Vitest 框架下无缝运行 uni-app 的自动化测试。

这个工具的出现,填补了 uni-app 现代化测试工具链的空白。它不仅保持了与官方 automator 的完全兼容性,还充分发挥了 Vitest 的性能优势,为开发者提供了一个更快速、更现代化的测试解决方案。

🚀 核心功能与技术优势

1. 无缝集成 Vitest 生态

vitest-environment-uniapp 完全兼容 Vitest 的配置系统,你可以像使用其他 Vitest 环境一样简单配置:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'uniapp',
    environmentOptions: {
      uniapp: {
        compile: true,
        platform: 'mp-weixin',
        projectPath: './src',
        port: 5121,
      },
    },
  },
})

2. 支持多平台测试

基于 @dcloudio/uni-automator 的强大能力,该工具支持 uni-app 的多个平台:

  • 微信小程序(mp-weixin)
  • app(需要额外安装 HBuilderX)
  • H5 平台(需要额外安装 playwright)

3. 智能环境管理

工具内部实现了智能的环境初始化和清理机制:

  • 自动管理 uni-app 程序的生命周期
  • 支持远程调试模式
  • 内置超时保护机制
  • 完善的错误处理和日志输出

4. TypeScript 完整支持

提供了完整的 TypeScript 类型定义,让开发者在编写测试代码时享受完整的类型提示和智能补全:

{
  "compilerOptions": {
    "types": [
      "vitest-environment-uniapp/types"
    ]
  }
}

🧪 为什么 E2E 测试如此重要

在软件开发中,单元测试固然重要,但 E2E(End-to-End)测试在构建高质量代码过程中扮演着不可替代的角色。

提升代码可靠性

E2E 测试模拟真实用户的使用场景,从用户界面到后端服务的完整流程进行验证。与单元测试不同,E2E 测试能够发现:

  • 组件间的集成问题
  • 路由跳转逻辑错误
  • 状态管理异常
  • 平台兼容性问题

对于 uni-app 这种跨平台开发框架,E2E 测试尤为重要。它能够确保你的应用在不同平台上都能正常运行,避免出现"在开发环境正常,上线后出问题"的尴尬情况。

降低维护成本

虽然编写 E2E 测试需要投入一定的时间成本,但从长远来看,它能显著降低维护成本:

  • 减少回归测试时间:自动化测试可以在几分钟内完成原本需要数小时的手动测试
  • 快速定位问题:当出现 bug 时,E2E 测试能够快速定位问题所在
  • 增强重构信心:有了完善的测试覆盖,你可以放心地进行代码重构,而不必担心破坏现有功能
  • 文档化业务逻辑:测试代码本身就是最好的业务逻辑文档

提升团队协作效率

E2E 测试作为项目质量的"守门员",能够:

  • 统一团队对功能实现的理解
  • 减少 code review 时的争议
  • 让新成员快速理解项目功能
  • 建立持续集成的质量保障体系

📦 快速上手

安装依赖

首先,安装必要的依赖:

pnpm i -D vitest-environment-uniapp @dcloudio/uni-automator

⚠️ 注意:@dcloudio/uni-automator 是必需的依赖,必须安装

配置 Vitest

在项目根目录创建或修改 vitest.config.ts

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'uniapp',
    environmentOptions: {
      uniapp: {
        compile: true,
        platform: 'mp-weixin',
        projectPath: './src',
        port: 5121,
      },
    },
  },
})

配置 TypeScript

tsconfig.json 中添加类型定义:

{
  "compilerOptions": {
    "types": [
      "vitest-environment-uniapp/types"
    ]
  }
}

添加测试脚本

package.json 中添加测试命令:

{
  "scripts": {
    "test": "vitest"
  }
}

编写测试用例

创建测试文件,例如 pages/index.test.ts

import { beforeAll, describe, expect, it } from 'vitest'

describe('首页测试', () => {
  let page: Page
  beforeAll(async () => {
    page = await program.currentPage()
    await page.waitFor(3000)
  })

  it('检查页面标题', async () => {
    const el = await page.$('.uni-helper-logo__label')
    const titleText = await el.text()
    expect(titleText).toEqual('uni-helper')
  })
})

运行测试

执行测试命令:

pnpm test

🎯 环境配置选项详解

environmentOptions.uniapp 支持以下配置选项:

  • compile: 是否在测试前编译项目(默认:false)
  • platform: 目标平台,如 mp-weixinmp-alipay
  • projectPath: uni-app 项目路径
  • port: 开发服务器端口
  • devtools.remote: 是否启用远程调试模式

💡 提示:完整的配置参数参考 uni-app 官方文档。需要注意的是,官方文档可能存在更新滞后,建议以实际可用参数为准。

🔧 技术实现细节

vitest-environment-uniapp 的实现基于 Vitest 的自定义环境 API,核心类 UniAppEnvironment 实现了以下功能:

  1. 环境初始化:在 setup 方法中初始化 uni-app automator
  2. 全局对象注入:将 uniprogram 注入到全局作用域
  3. 资源清理:在 teardown 方法中正确清理资源
  4. 错误处理:完善的错误捕获和日志输出机制

工具内部还实现了智能的状态管理,避免重复初始化,确保测试环境的稳定性。

🌟 总结

vitest-environment-uniapp 为 uni-app 开发者提供了一个现代化、高效的 E2E 测试解决方案。它不仅解决了传统测试工具配置复杂的问题,还充分发挥了 Vitest 的性能优势。

通过完善的 E2E 测试,你可以:

  • 提升代码质量和可靠性
  • 降低长期维护成本
  • 增强团队协作效率
  • 建立持续集成的质量保障体系

如果你正在开发 uni-app 项目,并且想要建立完善的测试体系,vitest-environment-uniapp 绝对值得一试。它会让你的测试工作变得前所未有的简单和高效。

📚 相关资源


最后

vitest-environment-uniapp 是一个免费的开源软件,遵循MIT协议,社区的赞助使其能够有更好的发展。

你的赞助会帮助我更好的维护@uni-helper,如果对你有帮助,请考虑赞助一下😊

你的star🌟也是对我的很大鼓励,Github

欢迎反馈问题和提pr共建

更多关于uni-helper更多文章

Modern.js 3.0 发布:聚焦 Web 框架,拥抱生态发展

作者 WebInfra
2026年2月25日 11:44

前言

Modern.js 2.0 发布 至今,已过去三年时间,感谢社区开发者们对 Modern.js 的使用和信任。Modern.js 一直保持稳定的迭代,累计发布了超过 100 个版本。

在字节内部,Modern.js 已成为 Web 开发的核心框架。在全公司活跃的 Web 项目中,使用占比已从 2025 年初的 40% 增长至目前接近 70%。

这三年中,我们不断扩充新特性,持续进行代码重构与优化,也收到了非常多的反馈,这些经验成为了 3.0 版本改进的重要参考。经过慎重考虑,我们决定发布 Modern.js 3.0,对框架进行一次全面的升级。

Modern.js 2.0 到 3.0 的演变

从 Modern.js 2.0 到 3.0,有两个核心转变:

更聚焦,专注于 Web 框架

  • Modern.js 2.0:包含 Modern.js App、Modern.js Module、Modern.js Doc
  • Modern.js 3.0:只代表 Modern.js App,Modern.js Module 和 Modern.js Doc 已孵化为 RslibRspress

更开放,积极面向社区工具

  • Modern.js 2.0:内置各类工具、框架独特的 API 设计
  • Modern.js 3.0:强化插件体系,完善接入能力,推荐社区优质方案

Modern.js 3.0 新特性

React Server Component

TL;DR:Modern.js 3.0 集成 React Server Component,支持 CSR 和 SSR 项目,并支持渐进式迁移。

什么是 React Server Component

React Server Components(服务端组件)是一种新的组件类型,它允许组件逻辑完全在服务端执行,并直接将渲染后的 UI 流式传输到客户端。与传统的客户端组件相比,服务端组件带来了以下特性:

特性 说明
零客户端包体积 组件代码不包含在客户端 JS Bundle 中,仅在服务端执行,加快首屏加载与渲染速度
更高的内聚性 组件可直接连接数据库、调用内部 API、读取本地文件,提高开发效率
渐进增强 可与客户端组件无缝混合使用,按需下放交互逻辑到客户端,在保持高性能的同时,支持复杂交互体验

需要明确的是,RSC 和 SSR 是截然不同的概念

  • RSC:描述的是组件类型,即组件在哪里执行(服务端 vs 客户端)
  • SSR:描述的是渲染模式,即 HTML 在哪里生成(服务端 vs 客户端)

两者可以组合使用:Server Component 可以在 SSR 项目下使用,也可以在 CSR 项目下使用。在 Modern.js 3.0 中,我们同时支持这两种模式,开发者可以根据需求选择。

开箱即用

在 Modern.js 3.0 中,只需在配置中启用 RSC 能力:

modern.config.ts

export default defineConfig({
  server: {
    rsc: true,
  }
});

配置启用后,所有的路由组件都会默认成为 Server Component。项目中可能存在无法在服务端运行的组件,你可以先为这些组件添加 'use client' 标记,以保持原有行为,再逐步迁移。

RSC 效果演示视频:lf3-static.bytednsdoc.com/obj/eden-cn…

Modern.js 3.0 的 RSC 特性

Modern.js 一直选择 React Router 作为路由解决方案。去年,React Router v7 宣布支持 React Server Component,这为 Modern.js 提供了在 SPA 应用下实现 RSC 的基础。

相比于社区其他框架,Modern.js 对 RSC 做了几点优化:

  • 使用 Rspack 最新的 RSC 插件构建,显著提升 RSC 项目构建速度;并进一步优化了产物体积。
  • 不同于社区主流框架只支持 RSC + SSR,Modern.js 3.0 的 RSC 同样支持 CSR 项目
  • 在路由跳转时,框架会自动将多个 Data Loader 和 Server Component 的请求合并为单个请求,并流式返回,提升页面性能
  • 在嵌套路由场景下,路由组件类型不受父路由组件类型的影响,开发者可以从任意路由层级开始采用 Server Component

渐进式迁移

基于灵活的组件边界控制能力,Modern.js 3.0 提供了渐进式的迁移方式。Modern.js 3.0 允许基于路由组件维度的 Server Component 迁移,无需迁移整条组件树链路。

更多 React Server Component 的详细内容,可以参考:React Server Component


拥抱 Rspack

TL;DR:Modern.js 3.0 移除了对 webpack 的支持,全面拥抱 Rspack,并升级到最新的 Rspack & Rsbuild 2.0。

在 2023 年,我们开源了 Rspack,并在 Modern.js 中支持将 Rspack 作为可选的打包工具。在字节内部,超过 60% 的 Modern.js 项目已经切换到 Rspack 构建。

经过两年多发展,Rspack 在社区中的月下载量已超过 1000 万次,成长为行业内被广泛使用的打包工具;同时,Modern.js 的 Rspack 构建模式也得到持续完善。

Rspack 下载量

在 Modern.js 3.0 中,我们决定移除对 webpack 的支持,从而使 Modern.js 变得更加轻量和高效,并能更充分地利用 Rspack 的新特性。

更顺畅的开发体验

Modern.js 3.0 在移除 webpack 后,能够更好地遵循 Rspack 最佳实践,在构建性能、安装速度等方面均有提升:

底层依赖升级

Modern.js 3.0 将底层依赖的 Rspack 和 Rsbuild 升级至 2.0 版本,并基于新版本优化了默认构建配置,使整体行为更加一致。

参考以下文档了解底层行为变化:

更快的构建速度

Modern.js 通过 Rspack 的多项特性来减少构建耗时:

  • 默认启用 Barrel 文件优化:构建组件库速度提升 20%
  • 默认启用持久化缓存:非首次构建的速度提升 50%+

更快的安装速度

移除 webpack 相关依赖后,Modern.js 3.0 的构建依赖数量和体积均明显减少:

  • npm 依赖数量减少 40%
  • 安装体积减少 31 MB

更小的构建产物

Modern.js 现在默认启用 Rspack 的多项产物优化策略,能够比 webpack 生成更小的产物体积,例如:

增强 Tree shaking

增强了 tree shaking 分析能力,可以处理更多动态导入语法,例如解构赋值:

// 参数中的解构访问
import('./module').then(({ value }) => {
  console.log(value);
});

// 函数体内的解构访问
import('./module').then((mod) => {
  const { value } = mod;
  console.log(value);
});

常量内联

对常量进行跨模块内联,有助于压缩工具进行更准确的静态分析,从而消除无用的代码分支:

// constants.js
export const ENABLED = true;

// index.js
import { ENABLED } from './constants';
if (ENABLED) {
  doSomething();
} else {
  doSomethingElse();
}

// 构建产物 - 无用分支被消除
doSomething();

全链路可扩展

TL;DR:Modern.js 3.0 正式开放完整插件体系,提供运行时、服务端插件,同时支持灵活处理应用入口。

Modern.js 2.0 提供了 CLI 插件与内测版本的运行时插件,允许开发者对项目进行扩展。但在实践过程中,我们发现现有的能力不足以支撑复杂的业务场景。

Modern.js 3.0 提供了更灵活的定制能力,允许为应用编写全流程的插件,帮助团队统一业务逻辑、减少重复代码:

  • CLI 插件:在构建阶段扩展功能,如添加命令、修改配置
  • Runtime 插件:在渲染阶段扩展功能,如数据预取、组件封装
  • Server 插件:在服务端扩展功能,如添加中间件、修改请求响应

运行时插件

运行时插件在 CSR 与 SSR 过程中都会运行,新版本提供了两个核心钩子:

  • onBeforeRender:在渲染前执行逻辑,可用于数据预取、注入全局数据
  • wrapRoot:封装根组件,添加全局 Provider、布局组件等

你可以在 src/modern.runtime.ts 中注册插件,相比在入口手动引入高阶组件,运行时插件可插拔、易更新,在多入口场景下无需重复引入:

src/modern.runtime.tsx

import { defineRuntimeConfig } from "@modern-js/runtime";

export default defineRuntimeConfig({
  plugins: [
    {
      name: "my-runtime-plugin",
      setup: (api) => {
        api.onBeforeRender((context) => {
          context.globalData = { theme: "dark" };
        });
        api.wrapRoot((App) => (props) => <App {...props} />);
      },
    },
  ],
});

更多 Runtime 插件使用方式,请查看文档:Runtime 插件

服务端中间件

在实践过程中我们发现,部分项目需要扩展 Web Server,例如鉴权、数据预取、降级处理、动态 HTML 脚本注入等。

在 Modern.js 3.0 中,我们使用 Hono 重构了 Web Server,并正式开放了服务端中间件与插件的能力。开发者可以使用 Hono 的中间件来完成需求:

server/modern.server.ts

import { defineServerConfig, type MiddlewareHandler } from "@modern-js/server-runtime";

const timingMiddleware: MiddlewareHandler = async (c, next) => {
  const start = Date.now();
  await next();
  const duration = Date.now() - start;
  c.header('X-Response-Time', `${duration}ms`);
};

const htmlMiddleware: MiddlewareHandler = async (c, next) => {
  await next();
  const html = await c.res.text();
  const modified = html.replace(
    "<head>",
    '<head><meta name="generator" content="Modern.js">'
  );
  c.res = c.body(modified, { status: c.res.status, headers: c.res.headers });
};

export default defineServerConfig({
  middlewares: [timingMiddleware],
  renderMiddlewares: [htmlMiddleware],
});

更多服务端插件使用方式,可以查看文档:自定义 Web Server

自定义入口

在 Modern.js 3.0 中,我们重构了自定义入口,相比于旧版 API 更加清晰灵活:

src/entry.tsx

import { createRoot } from '@modern-js/runtime/react';
import { render } from '@modern-js/runtime/browser';

const ModernRoot = createRoot();

async function beforeRender() {
  // 渲染前的异步操作,如初始化 SDK、获取用户信息等
}

beforeRender().then(() => {
  render(<ModernRoot />);
});

更多入口使用方式,请查看文档:入口


路由优化

TL;DR:Modern.js 3.0 内置 React Router v7,提供配置式路由能力与 AI 友好的调试方式。

内置 React Router v7

在 Modern.js 3.0 中,我们统一升级到 React Router v7,并废弃了对 v5 和 v6 的内置支持。这一决策基于以下考虑:

版本演进与稳定性

React Router v6 是一个重要的过渡版本,它引入了许多新特性(如数据加载、错误边界等)。而 v7 在保持 v6 API 兼容性的基础上,进一步优化了性能、稳定性和开发体验。随着 React Router 团队将 Remix 定位为独立框架,React Router 核心库可能会在 v7 版本上长期维护,使其成为更可靠的选择。

升级路径

  • 从 v6 升级:React Router v7 对 v6 开发者来说是无破坏性变更的升级。在 Modern.js 2.0 中,我们已提供了 React Router v7 插件支持,你可以通过插件方式渐进式升级,验证兼容性后再迁移到 Modern.js 3.0。
  • 从 v5 升级:v5 到 v7 存在较大的 API 变化,建议参考 React Router 官方迁移指南 进行迁移。

配置式路由

在 Modern.js 中,我们推荐使用约定式路由来组织代码。但在实际业务中,开发者偶尔遇到以下场景:

  • 多路径指向同一组件
  • 灵活的路由控制
  • 条件性路由
  • 遗留项目迁移

因此,Modern.js 3.0 提供了完整的配置式路由支持,可以与约定式路由一起使用,或两者分别单独使用。

src/modern.routes.ts

import { defineRoutes } from "@modern-js/runtime/config-routes";

export default defineRoutes(({ route, layout, page }) => {
  return [
    route("home.tsx", "/"),
    route("about.tsx", "about"),
    route("blog.tsx", "blog/:id"),
  ];
});

更多配置式路由使用方式,请查看文档:配置式路由

路由调试

运行 npx modern routes 命令即可在 dist/routes-inspect.json 文件中生成完整的路由结构分析报告。

报告中会显示每个路由的路径、组件文件、数据加载器、错误边界、Loading 组件等完整信息,帮助开发者快速了解项目的路由配置,快速定位和排查路由相关问题。结构化的 JSON 格式也便于 AI agent 理解和分析路由结构,提升 AI 辅助开发的效率。

具体使用方式,请查看文档:路由调试


服务端渲染

TL;DR:Modern.js 3.0 重做了 SSG 能力,提供了灵活的缓存能力,对降级策略也进行了进一步的完善。

静态站点生成(SSG)

在 Modern.js 2.0 中,我们提供了静态站点生成的能力。这个能力非常适合用在可以静态渲染的页面中,能极大的提升页面首屏性能。

在新版本中,我们对 SSG 进行了重新设计:

  • 数据获取使用 Data Loader,与非 SSG 场景保持一致
  • 简化了 API,降低理解成本
  • 与约定式路由更好地结合

在新版本中,你可以通过 data loader 进行数据获取,与非 SSG 场景保持一致。然后在 ssg.routes 配置中即可直接指定要渲染的路由:

modern.config.ts

export default defineConfig({
  output: {
    ssg: {
      routes: ['/blog'],
    },
  },
});

routes/blog/page.data.ts

export const loader = async () => {
  const articles = await fetchArticles();
  return { articles };
};

更多 SSG 的使用方式,请查看文档:SSG

缓存机制

Modern.js 3.0 中提供了不同维度的缓存机制,帮助项目提升首屏性能。所有缓存均支持灵活配置,比如可以支持类似 HTTP 的 stale-while-revalidate 策略:

渲染缓存

支持将 SSR 结果进行整页的缓存,在 server/cache.ts 中配置:

server/cache.ts

import type { CacheOption } from '@modern-js/server-runtime';

export const cacheOption: CacheOption = {
  maxAge: 500, // ms
  staleWhileRevalidate: 1000, // ms
};

使用渲染缓存,请查看文档:渲染缓存

数据缓存

我们在新版本中提供了 cache 函数,相比渲染缓存它提供了更精细的数据粒度控制。当多个数据请求依赖同一份数据时,cache 可以避免重复请求:

server/loader.ts

import { cache } from "@modern-js/runtime/cache";
import { fetchUserData, fetchUserProjects, fetchUserTeam } from "./api";

// 缓存用户数据,避免重复请求
const getUser = cache(fetchUserData);

const getProjects = async () => {
  const user = await getUser("test-user");
  return fetchUserProjects(user.id);
};

const getTeam = async () => {
  const user = await getUser("test-user"); // 复用缓存,不会重复请求
  return fetchUserTeam(user.id);
};

export const loader = async () => {
  // getProjects 和 getTeam 都依赖 getUser,但 getUser 只会执行一次
  const [projects, team] = await Promise.all([getProjects(), getTeam()]);
  return { projects, team };
};

更多数据缓存的使用方式,请查看文档:数据缓存

灵活的降级策略

在实践过程中,我们沉淀了多维度的降级策略:

类型 触发方式 降级行为 使用场景
异常降级 Data Loader 执行报错 触发 ErrorBoundary 数据请求异常兜底
组件渲染报错 服务端渲染异常 降级到 CSR,复用已有数据渲染 服务端渲染异常兜底
业务降级 Loader 抛出 throw Response 触发 ErrorBoundary,返回对应 HTTP 状态码 404、权限校验等业务场景
配置 Client Loader 配置 Client Loader 绕过 SSR,直接请求数据源 需要在客户端直接获取数据的场景
强制降级 Query 参数 ?__csr=true 跳过 SSR,返回 CSR 页面 调试、临时降级
强制降级 请求头 x-modern-ssr-fallback 跳过 SSR,返回 CSR 页面 网关层控制降级

轻量 BFF

TL;DR:Modern.js 3.0 基于 Hono 重构了 Web Server,提供基于 Hono 的一体化函数,同时支持跨项目调用。

Hono 一体化函数

在 Modern.js 3.0 中,我们使用 Hono 作为 BFF 的运行时框架,开发者可以基于 Hono 生态扩展 BFF Server,享受 Hono 轻量、高性能的优势。

通过 useHonoContext 可以获取完整的 Hono 上下文,访问请求信息、设置响应头等:

api/lambda/user.ts

import { useHonoContext } from '@modern-js/server-runtime';

export const get = async () => {
  const c = useHonoContext();
  const token = c.req.header('Authorization');
  c.header('X-Custom-Header', 'modern-js');
  const id = c.req.query('id');

  return { userId: id, authenticated: !!token };
};

跨项目调用

在过去,Modern.js BFF 只能在当前项目中使用,而我们陆续收到开发者反馈,希望能够在不同项目中使用。这多数情况是由于开发者的迁移成本、运维成本造成的,相比于抽出原有代码再部署一个,显然复用已有服务更加合理。

为了保证开发者能得到与当前项目一体化调用类似的体验,我们提供了跨项目调用的能力。

更多 BFF 的使用方式,请查看文档:BFF


Module Federation 深度集成

TL;DR:Modern.js 3.0 与 Module Federation 2.0 深度集成,支持 MF SSR 和应用级别模块导出。

MF SSR

Modern.js 3.0 支持在 SSR 应用中使用 Module Federation,组合使用模块联邦和服务端渲染能力,为用户提供更好的首屏性能体验。

modern.config.ts

export default defineConfig({
  server: {
    ssr: {
      mode: 'stream',
    },
  },
});

配合 Module Federation 的数据获取能力,每个远程模块都可以定义自己的数据获取逻辑:

src/components/Button.data.ts

export const fetchData = async () => {
  return {
    data: `Server time: ${new Date().toISOString()}`,
  };
};

src/components/Button.tsx

export const Button = (props: { mfData: { data: string } }) => {
  return <button>{props.mfData?.data}</button>;
};

应用级别模块

不同于传统的组件级别共享,Modern.js 3.0 支持导出应用级别模块——具备完整路由能力、可以像独立应用一样运行的模块。这是微前端场景中的重要能力。

生产者导出应用

src/export-App.tsx

import '@modern-js/runtime/registry/index';
import { render } from '@modern-js/runtime/browser';
import { createRoot } from '@modern-js/runtime/react';
import { createBridgeComponent } from '@module-federation/modern-js/react';

const ModernRoot = createRoot();
export const provider = createBridgeComponent({
  rootComponent: ModernRoot,
  render: (Component, dom) => render(Component, dom),
});

export default provider;

消费者加载应用

src/routes/remote/$.tsx

import { createRemoteAppComponent } from '@module-federation/modern-js/react';
import { loadRemote } from '@module-federation/modern-js/runtime';

const RemoteApp = createRemoteAppComponent({
  loader: () => loadRemote('remote/app'),
  fallback: ({ error }) => <div>Error: {error.message}</div>,
  loading: <div>Loading...</div>,
});

export default RemoteApp;

通过通配路由 $.tsx,所有访问 /remote/* 的请求都会进入远程应用,远程应用内部的路由也能正常工作。

更多 Module Federation 的使用方式,请查看文档:Module Federation


技术栈更新

TL;DR:Modern.js 3.0 升级 React 19,最低支持 Node.js 20。

React 19

Modern.js 3.0 新项目默认使用 React 19,最低支持 React 18。

如果你的项目仍在使用 React 16 或 React 17,请先参考 React 19 官方升级指南 完成版本升级。

Node.js 20

随着 Node.js 不断推进版本演进,Node.js 18 已经 EOL。在 Modern.js 3.0 中,推荐使用 Node.js 22 LTS,不再保证对 Node.js 18 的支持。

Storybook Rsbuild

在 Modern.js 3.0 中,我们基于 Storybook Rsbuild 实现了使用 Storybook 构建 Modern.js 应用。

通过 Storybook Addon,我们将 Modern.js 配置转换合并为 Rsbuild 配置,并通过 Storybook Rsbuild 驱动构建,让 Storybook 调试与开发命令保持配置对齐。

更多 Storybook 使用方式,请查看文档:使用 Storybook

使用 Biome

随着社区技术不断发展,更快、更简洁的工具链已经成熟。在 Modern.js 3.0 中,新项目默认使用 Biome 作为代码检查和格式化工具。


从 Modern.js 2.0 升级到 3.0

主要变更

升级 Modern.js 3.0 意味着拥抱更轻量、更标准的现代化开发范式。通过全面对齐 Rspack 与 React 19 等主流生态,彻底解决历史包袱带来的维护痛点,显著提升构建与运行性能。

未来,我们也会基于 Modern.js 3.0 提供更多的 AI 集成与最佳实践,配合灵活的全栈插件系统,让开发者能以极低的学习成本复用社区经验,实现开发效率的质变与应用架构的现代化升级。

更多改进与变更,请查看文档:升级指南

反馈和社区

最后,再次感谢每一位给予我们反馈和支持的开发者,我们将继续与大家保持沟通,在相互支持中共同成长。

如果你在使用过程中遇到问题,欢迎通过以下方式反馈:

2025前端技术趋势:从智能到沉浸的新时代

2026年2月25日 10:27

引言

前端技术正处于一个前所未有的快速发展阶段。从早期的静态网页到如今的复杂单页应用,前端开发已经经历了多次重大变革。随着WebAssembly、人工智能、虚拟现实等技术的不断成熟,2025年的前端技术将进入一个全新的时代。在这个新时代,前端开发者需要掌握的技能不再局限于HTML、CSS和JavaScript。他们需要了解WebAssembly、机器学习、3D渲染等跨领域技术,并能够将这些技术有机地结合,创造出前所未有的用户体验。

1. WebAssembly 3.0:突破Web性能极限

WebAssembly(Wasm)自2017年推出以来,已经成为前端性能优化的重要工具。2025年,WebAssembly 3.0将带来一系列重大改进,进一步突破Web应用的性能极限。

1.1 架构演进

image.png

1.2 WebAssembly 3.0 核心特性

WebAssembly 3.0将引入以下核心特性:

  • 增强的垃圾回收支持:更高效的内存管理,减少内存泄漏和性能问题

  • 原生DOM访问:直接操作DOM,消除JavaScript桥接开销

  • 多线程与并行计算:更强大的并行处理能力,支持复杂计算任务

  • WASI 2.0:更完善的WebAssembly系统接口,支持更多系统级功能

1.3 性能对比

WebAssembly 3.0在性能方面将有显著提升:

image.png

1.4 应用场景

WebAssembly 3.0将在以下场景中发挥重要作用:

  1. 复杂数据可视化:处理大规模数据并实时渲染

  2. 游戏开发:创建接近原生性能的Web游戏

  3. 音视频处理:实时编解码和特效处理

  4. 科学计算:在浏览器中运行复杂算法

  5. 3D建模 与渲染:支持复杂的3D场景和模型

2. AI原生前端框架:智能交互的新范式

2025年,前端框架将深度集成人工智能技术,形成AI原生前端框架。这些框架将能够理解用户意图,自动优化性能,并提供更加智能的用户体验。

2.1 AI原生框架核心特性

AI原生前端框架将具备以下核心特性:

  • 智能组件:能够根据用户行为自动调整UI和功能

  • 预测渲染:预测用户下一步操作并提前渲染

  • 自适应布局:根据设备、用户偏好和上下文自动调整布局

  • 智能性能优化:自动识别和优化性能瓶颈

  • 自然语言交互:支持语音和文本的自然语言界面

2.2 架构设计

image.png

2.3 智能组件示例

以下是一个AI原生前端框架中的智能组件示例:

// AI原生组件示例
import { SmartComponent } from 'ai-frontend-framework';

class AdaptiveButton extends SmartComponent {
  constructor(props) {
    super(props);
    this.state = {
      variant: 'primary',
      size: 'medium'
    };
  }
  
  // AI驱动的自适应逻辑
  async adaptToUserContext() {
    const userBehavior = await this.aiContext.getUserBehavior();
    const deviceInfo = await this.aiContext.getDeviceInfo();
    
    // 根据用户行为调整按钮样式
    if (userBehavior.clickSpeed > 5) {
      this.setState({ size: 'large' });
    }
    
    // 根据设备类型调整按钮变体
    if (deviceInfo.type === 'mobile') {
      this.setState({ variant: 'secondary' });
    }
    
    // 预测用户操作并提前加载资源
    if (userBehavior.predictedAction === 'submit') {
      this.aiContext.preloadResource('/api/submit');
    }
  }
  
  render() {
    return (
      <button 
        variant={this.state.variant}
        size={this.state.size}
        onClick={this.props.onClick}
        onMouseEnter={this.adaptToUserContext}
      >
        {this.props.children}
      </button>
    );
  }
}

2.4 应用场景

AI原生前端框架将在以下场景中得到广泛应用:

  1. 个性化用户体验:根据用户行为和偏好自动调整界面

  2. 智能表单:自动填充、验证和优化表单流程

  3. 预测性加载:预测用户下一步操作并提前加载内容

  4. 无障碍设计:自动适配不同用户的无障碍需求

  5. 实时数据可视化:智能分析和展示数据趋势

3. 沉浸式Web体验:从2D到3D的转变

2025年,Web将从平面的2D体验向沉浸式的3D体验转变。随着WebXR、WebGL 2.0和WebGPU的不断成熟,浏览器将能够提供接近原生的3D和虚拟现实体验。

3.1 核心技术栈

沉浸式Web体验的核心技术栈包括:

  • WebXRAPI:支持虚拟现实(VR)和增强现实(AR)体验

  • WebGL 2.0:高性能3D图形渲染

  • WebGPU:下一代图形API,提供更好的性能和功能

  • Three.js/Rapier.js:成熟的3D库和物理引擎

3.2 架构设计

image.png

3.3 应用场景

沉浸式Web体验将在以下场景中得到广泛应用:

  1. 虚拟会议与协作:创建沉浸式的远程会议空间

  2. 在线教育:提供交互式的3D学习体验

  3. 虚拟购物:允许用户在虚拟环境中试穿和体验产品

  4. 虚拟旅游:提供沉浸式的虚拟旅游体验

  5. 工业设计与原型:在浏览器中进行3D设计和原型开发

4. 去中心化前端架构:Web3的新范式

随着Web3技术的不断发展,2025年将出现去中心化前端架构,彻底改变前端应用的部署和运行方式。

4.1 核心概念

去中心化前端架构的核心概念包括:

  • 去中心化存储:使用IPFS、Arweave等去中心化存储协议

  • 智能合约集成:直接与区块链上的智能合约交互

  • 去中心化身份(DID) :用户控制自己的身份和数据

  • 零信任安全:基于区块链的安全模型

4.2 架构设计

image.png

4.3 开发示例

以下是一个基于去中心化前端架构的应用示例:

// 去中心化前端应用示例
import { createDApp } from 'decentralized-frontend-framework';
import { ethers } from 'ethers';
import { create } from 'ipfs-http-client';

// 连接到IPFS
const ipfs = create('https://ipfs.infura.io:5001/api/v0');

// 连接到以太坊区块链
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();

// 初始化DApp
const dapp = createDApp({
  ipfs,
  provider,
  signer,
  contracts: {
    myContract: {
      address: '0x1234567890abcdef1234567890abcdef12345678',
      abi: [...]
    }
  }
});

// 从IPFS加载内容
async function loadContent(cid) {
  const content = await dapp.ipfs.get(cid);
  return content;
}

// 与智能合约交互
async function interactWithContract() {
  const contract = dapp.contracts.myContract;
  const result = await contract.functions.myFunction();
  return result;
}

// 保存数据到IPFS并记录到区块链
async function saveData(data) {
  const { cid } = await dapp.ipfs.add(data);
  const transaction = await dapp.contracts.myContract.functions.saveData(cid.toString());
  await transaction.wait();
  return cid;
}

// 渲染应用
function renderApp() {
  return (
    <DAppProvider dapp={dapp}>
      <App />
    </DAppProvider>
  );
}

4.4 应用场景

去中心化前端架构将在以下场景中得到广泛应用:

  1. 去中心化金融(DeFi) :构建安全、透明的金融应用

  2. 数字身份管理:用户控制自己的身份和数据

  3. 内容创作与分发:消除中间商,直接连接创作者和用户

  4. 供应链管理:构建透明、可追溯的供应链系统

  5. 去中心化社交网络:用户控制自己的社交数据和关系

5. 量子计算在前端的初步应用

随着量子计算技术的不断发展,2025年量子计算将开始在前端领域得到初步应用,为前端开发带来新的可能性。

5.1 核心概念

量子计算在前端的应用基于以下核心概念:

  • 量子算法:利用量子力学特性解决复杂问题

  • 量子机器学习:结合量子计算和机器学习

  • 量子安全:基于量子力学的安全加密

  • 量子云服务:通过云服务访问量子计算机

5.2 应用示例

以下是一个使用量子计算的前端应用示例:

// 量子计算前端应用示例
import { QuantumSDK } from 'quantum-frontend-sdk';
import { QuantumMachineLearning } from 'quantum-ml';

// 初始化量子SDK
const quantumSDK = new QuantumSDK({
  apiKey: 'your-quantum-cloud-api-key',
  provider: 'ibm-quantum'
});

// 创建量子机器学习模型
const qml = new QuantumMachineLearning(quantumSDK);

// 训练量子模型
async function trainQuantumModel(data) {
  // 准备量子数据
  const quantumData = await qml.prepareData(data);
  
  // 选择量子算法
  const algorithm = qml.selectAlgorithm('quantum-kernel');
  
  // 训练模型
  const model = await qml.train(quantumData, algorithm);
  
  return model;
}

// 使用量子模型进行预测
async function predictWithQuantumModel(model, input) {
  // 准备输入数据
  const quantumInput = await qml.prepareData(input);
  
  // 进行量子预测
  const result = await qml.predict(model, quantumInput);
  
  // 转换结果为经典数据
  const classicResult = qml.toClassic(result);
  
  return classicResult;
}

// 量子安全加密
async function encryptDataWithQuantumSecurity(data, publicKey) {
  // 使用量子密钥分发生成安全密钥
  const secureKey = await quantumSDK.generateQuantumKey();
  
  // 使用安全密钥加密数据
  const encryptedData = quantumSDK.encrypt(data, secureKey);
  
  // 使用公钥加密安全密钥
  const encryptedKey = quantumSDK.encrypt(secureKey, publicKey);
  
  return { encryptedData, encryptedKey };
}

// 渲染应用
function renderApp() {
  return (
    <QuantumProvider sdk={quantumSDK}>
      <App 
        trainModel={trainQuantumModel}
        predict={predictWithQuantumModel}
        encryptData={encryptDataWithQuantumSecurity}
      />
    </QuantumProvider>
  );
}

5.3 应用场景

量子计算在前端的初步应用将包括:

  1. 量子机器学习:处理复杂的机器学习任务,如图像识别、自然语言处理

  2. 量子安全:提供更安全的加密和认证机制

  3. 优化问题:解决复杂的优化问题,如路径规划、资源分配

  4. 模拟与仿真:进行复杂的物理、化学模拟

  5. 数据分析:处理大规模数据集

6. 总结

2025年的前端技术进入一个全新的时代,从智能到沉浸,从中心化到去中心化,从经典计算到量子计算。这些技术的发展将彻底改变前端开发的方式和前端应用的能力。作为前端开发者,我们需要不断学习和适应这些变化,掌握新的技术和工具,以创造出更加智能、沉浸、安全和高效的用户体验。只有这样,我们才能在未来的前端开发领域保持竞争力,为用户创造出真正有价值的产品和服务。

未来的前端世界充满了无限可能,让我们一起拥抱这个新时代,共同创造更加美好的数字未来!

7. 参考文献

  1. WebAssembly 3.0 官方文档
  2. AI原生前端框架白皮书
  3. WebXR API 规范
  4. IPFS 官方文档
  5. 量子计算在前端的应用
  6. Web3 前端开发指南
  7. Three.js 文档
  8. 前端性能优化最佳实践

8. 团队介绍

智慧家技术平台-应用软件框架开发」主要负责设计工具的研发,包括营销设计工具、家电VR设计和展示、水电暖通前置设计能力,研发并沉淀素材库,构建家居家装素材库,集成户型库、全品类产品库、设计方案库、生产工艺模型,打造基于户型和风格的AI设计能力,快速生成算量和报价;同时研发了门店设计师中心和项目中心,包括设计师管理能力和项目经理管理能力。实现了场景全生命周期管理,同时为水,空气,厨房等产业提供商机管理工具,从而实现了以场景贯穿的B端C端全流程系统。

🧠 空数组的迷惑行为:为什么 every 为真,some 为假?

2026年2月25日 09:37

一、前言

Hello~大家好。我是秋天的一阵风

在 JavaScript 开发中,everysome是我们日常处理数组时高频用到的两个数组方法,用法简单、逻辑直观,一直是前端处理数组判断的好帮手。但不少开发者在接触空数组的场景时,都会对一个现象感到困惑:

console.log([].every(item => item > 0)); // true 
console.log([].some(item => item > 0)); // false

同样是空数组,调用两个逻辑相近的方法,结果却截然相反。这并不是 JavaScript 的设计漏洞,而是背后遵循了严谨的数学逻辑。

与其只记着 “空数组 every 返回 true、some 返回 false” 这个结论就完事,不如跟着这篇内容,从数学逻辑到手写源码,把这个知识点掰扯透。

我之前还写过一篇《给我十分钟,手把手教你实现 Javascript 数组原型对象上的七个方法》,里面把 forEach、map、reduce 这些常用数组方法的实现思路拆得明明白白,和这篇讲的内容是一个思路,看完这篇再去翻那篇,能把 JS 数组的底层逻辑摸得更透。

二、解开疑惑

很多人第一次发现这个现象时,会觉得是 JavaScript 的特殊约定,其实不然,everysome的返回值逻辑,本质上是继承了数理逻辑中的量词规则,这也是这两个方法设计的底层依据。

1. every:对应全称量词的 “平凡真”

every的核心语义是 “数组中所有元素都满足某个条件”,对应数学中的全称量词(∀) 。在数理逻辑里有个 “平凡真” 的概念,简单说就是:如果一个集合是空集,那么 “这个集合里所有元素满足某条件” 这个说法,本身是成立的。

举个通俗的例子,我们说 “空盒子里的所有苹果都是红的”,因为盒子里根本没有苹果,也就不存在 “非红色的苹果” 来推翻这个说法,所以这个命题自然是真的。这也是[].every(...)会返回 true 的根本原因,是逻辑上的必然结果。

2. some:对应存在量词的 “平凡假”

some的核心语义是 “数组中至少有一个元素满足某个条件” ,对应数学中的存在量词(∃) 。同理,空集合里没有任何元素,自然不可能找到满足条件的那个元素,就像说 “空盒子里有一个红苹果”,显然是不成立的。所以[].some(...)返回 false,也是存在判断的必然结果。

光懂理论还不够,对于开发者来说,看得见的代码实现远比抽象的概念更易理解。接下来我们就用原生 JS 复刻这两个方法的核心实现,从代码层面看清楚背后的逻辑。

三、源码拆解:兜底值,是结果不同的关键

ECMAScript 规范中,对Array.prototype.everyArray.prototype.some的执行逻辑有明确定义,我们复刻的核心实现完全贴合原生逻辑,这也是理解原生方法最直接的方式 —— 亲手实现一遍,比看十遍文档更管用。

1. 复刻 Array.prototype.every

every的核心思路很简单:

  • 先给一个 “真” 的初始兜底值,遍历数组时只要遇到一个不满足条件的元素,就立刻把结果置为假并终止遍历;
  • 如果遍历完都没有反例,就保留初始的真。
Array.prototype.myEvery = function (callback, thisArg) {
  // 校验回调函数的合法性
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }

  const arr = this;
  const len = arr.length;
  // 核心:初始兜底值设为true
  let result = true;

  // 空数组的len为0,会直接跳过这个循环
  for (let i = 0;< len; i++) {
    // 处理稀疏数组,跳过不存在的索引
    if (!arr.hasOwnProperty(i)) continue;
    // 执行回调并绑定this指向
    const isPass = callback.call(thisArg, arr[i], i, arr);
    // 有一个不满足,直接置假并终止遍历
    if (!isPass) {
      result = false;
      break;
    }
  }

  // 空数组直接返回初始的兜底值true
  return result;
};

// 测试,和原生方法结果完全一致
console.log([].myEvery(item => item > 0)); // true
console.log([1,2,3].myEvery(item => item > 0)); // true
console.log([1,-2,3].myEvery(item => item > 0)); // false

从代码里能清晰看到,空数组因为长度为 0,会直接跳过遍历循环,最终返回一开始设定的兜底值 true,这就是空数组调用 every 返回 true 的代码实锤。

2. 复刻 Array.prototype.some

some的实现思路和every呼应,只是初始兜底值做了调整:先给一个 “假” 的初始兜底值,遍历数组时只要遇到一个满足条件的元素,就立刻把结果置为真并终止遍历;如果遍历完都没有正例,就保留初始的假。

Array.prototype.mySome = function (callback, thisArg) {
  // 校验回调函数的合法性
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }

  const arr = this;
  const len = arr.length;
  // 核心:初始兜底值设为false
  let result = false;

  // 空数组同样会直接跳过循环
  for (let i = 0; i< len; i++) {
    // 处理稀疏数组,跳过不存在的索引
    if (!arr.hasOwnProperty(i)) continue;
    // 执行回调并绑定this指向
    const isPass = callback.call(thisArg, arr[i], i, arr);
    // 有一个满足,直接置真并终止遍历
    if (isPass) {
      result = true;
      break;
    }
  }

  // 空数组直接返回初始的兜底值false
  return result;
};

// 测试,和原生方法结果完全一致
console.log([].mySome(item => item > 0)); // false
console.log([1,2,3].mySome(item => item > 5)); // false
console.log([1,6,3].mySome(item => item > 5)); // true

对比两个方法的实现代码,唯一的核心差异就是初始兜底值

  • every以 true 为兜底,没遇到反例就一直为真;
  • some以 false 为兜底,没遇到正例就一直为假。

空数组因为跳过了遍历,直接返回兜底值,这就是二者结果不同的根本原因。

四、开发中要注意的业务逻辑细节

理解了理论和源码,最终还是要落地到实际开发中。空数组的这个特性,在表单校验、列表筛选、数据判断等场景中,很容易因为忽略而引发小 bug,只要稍作处理就能避免。

典型场景:空列表的条件判断

举个电商开发的例子,我们需要校验购物车中的商品是否全部满足包邮条件(价格 > 100),满足的话就显示包邮按钮。如果直接写判断,就容易出问题:

// 考虑不周的写法:未判断数组是否为空
const cartList = []; // 用户还没加购任何商品
if (cartList.every(item => item.price > 100)) {
  showFreeShippingBtn(); // 会执行!因为空数组every返回true
}

显然,用户购物车为空时,不应该显示包邮按钮,这就是把逻辑上的 “真”,和业务上的 “合法” 搞混了。

正确解法:先校验数组非空,再做条件判断

无论使用every还是some,只要业务场景要求 “有数据的集合”,就先判断数组的长度,再执行后续的条件校验,这是最稳妥的方式。

// 严谨的写法:先判断数组非空,再执行判断
const cartList = [];
if (cartList.length > 0 && cartList.every(item => item.price > 100)) {
  showFreeShippingBtn();
} else if (cartList.length === 0) {
  showEmptyCartTip(); // 给用户展示空购物车提示,体验更好
}

再比如用some判断列表中是否有过期优惠券,虽然空数组返回 false 本身符合 “没有过期优惠券” 的逻辑,但如果需要区分 “空列表” 和 “有列表但无过期”,还是要单独判断:

const coupons = [];
if (coupons.some(item => item.isExpired)) {
  showExpiredTip();
} else if (coupons.length === 0) {
  showNoCouponTip(); // 空优惠券列表的专属提示
} else {
  showAllValidTip(); // 有优惠券且都未过期的提示
}

五、总结

其实空数组下every返真、some返假的现象,一点都不复杂,总结起来就是两层核心逻辑:

  1. 数学层面every是全称判断,空集合满足 “平凡真”;some是存在判断,空集合满足 “平凡假”,这是方法设计的底层依据;
  2. 代码层面every的初始兜底值为 true,some为 false,空数组会跳过遍历,直接返回兜底值。

希望看完这篇文章,你再遇到everysome的空数组场景时,能不再困惑,从容应对~

仅仅一行 CSS,竟让 2000 个节点的页面在弹框时卡成 PPT?

作者 顾青
2026年2月26日 14:39

【哲风壁纸】可爱玩偶-地面落叶.png

前言

在最近的一个会议室排期系统(类似甘特图)的性能优化中,我遇到了一个诡异的现象:页面初始化非常流畅,但在点击“详情”打开 el-dialog 弹框时,遮罩层的渐入动画极其卡顿,掉帧感严重。

我原本以为是 DOM 节点过多(约 2000 个)导致 Vue 响应式数据更新太慢。但在排查过程中,我发现罪魁祸首竟然是一行看似为了“设计感”而存在的 CSS 属性:mix-blend-mode: multiply;

1. 现象描述:消失的帧率

我们的系统在横轴(时间)和纵轴(会议室)的交叉网格中渲染了大量的“档期卡片”。为了让卡片的背景色能和底部的网格线、文字有更好的融合感,代码中使用了 CSS 混合模式:

.card-bg {
  position: absolute;
  /* 混合模式:正片叠底 */
  mix-blend-mode: multiply; 
  background-color: #e6f7ff;
}

当页面只有几十个节点时,一切正常。但当展示 6 周数据,节点数达到 2000+  时,每当点击打开 el-dialog,浏览器就像陷入了泥潭。


2. 核心原因:混合模式背后的渲染逻辑

为什么 mix-blend-mode 会成为性能杀手?这要从浏览器的渲染机制说起。

A. 像素级重算(Pixel-by-Pixel Calculation)

常规的 background-color 渲染非常简单:浏览器只需要知道这个像素点的 RGB 值,直接涂色即可。

但 mix-blend-mode 不同。它要求浏览器执行 CSS Compositing(层叠组合)  规范。以 multiply(正片叠底)为例,浏览器渲染每一个像素点时,必须执行以下公式:

C=Cs×Cb255C = \frac{C_s \times C_b}{255}
  • CsC_s:当前图层颜色值(source)
  • CbC_b:底层背景颜色值(background)
  • CC:混合后的颜色值

这意味着,浏览器在绘制这 2000 个节点时,不能简单地“涂色”,而是必须先读取底层网格、文字、背景的颜色,再进行数学计算,最后输出结果。

B. 强制创建堆叠上下文(Stacking Context)

一旦元素应用了 mix-blend-mode(且值不为 normal),浏览器会强制该元素及其子元素创建一个新的堆叠上下文

在 2000 个节点上同时开启混合模式,会创建 2000 个 stacking context,并可能触发额外的合成层管理和 GPU 参与。这极大地消耗了显存和合成器的性能。

C. “弹框卡顿”的终极诱因:图层合成爆炸

这是最关键的一点。当你打开 el-dialog 时:

  1. el-dialog 会带有一个全屏的半透明遮罩层(Overlay)。
  2. 遮罩层在做淡入淡出动画(Opacity Animation)。
  3. 连锁反应:因为下方 2000 个节点都具有混合属性,它们对“背景”及其敏感。当上方的遮罩层颜色或透明度发生变化时,浏览器认为下方所有节点的“最终成色”都可能受到影响,从而被迫在动画的每一帧中,对这 2000 个节点进行全量的混合重计算和重绘。

渲染引擎在每一秒内要进行几十万次的像素乘法运算,GPU 瞬间满载,动画自然就变成了 PPT。


3. 解决方案:返璞归真

解决办法出奇地简单:移除混合模式,改用传统的透明色。

/* 优化前 */
.card-bg {
  mix-blend-mode: multiply;
  background-color: #e6f7ff;
}

/* 优化后 */
.card-bg {
  /* 移除混合模式 */
  /* 使用带透明度的 rgba 或者直接指定固定色值 */
  background-color: rgba(230, 247, 255, 0.8);
}

通过这一行代码的改动,浏览器不再需要读取背景像素进行乘法运算,节点被归类为普通渲染任务。再次打开 el-dialog,遮罩层的动画恢复到了丝滑的 60 FPS。


4. 经验总结:避开 CSS 的渲染陷阱

在构建高密度数据看板或复杂网格系统时,我们需要警惕以下这些“昂贵”的 CSS 属性:

  1. mix-blend-mode:在大量节点上使用是性能灾难。
  2. filter (如 blur(), drop-shadow()) :同样涉及复杂的卷积运算和像素偏移计算。
  3. box-shadow:特别是带有扩散半径的大面积阴影,会显著增加重绘成本。

视觉设计固然重要,但在节点过千的 B 端系统中,性能优先。  很多时候,通过预先计算好颜色值(如将混合后的颜色直接写死为 HEX),不仅能达到 90% 的视觉相似度,更能换来 100% 的交互流畅度。


TypeScript 类型体操:如何精准控制可选参数的“去留”

作者 火车叼位
2026年2月26日 14:15

在 TypeScript 的日常开发中,我们经常为了灵活性而将接口(Interface)或类型(Type)的属性定义为可选(使用 ? 修饰符)。但在某些特定场景下,例如配置初始化完成、表单提交前验证或 API 响应处理后,我们需要确保这些属性已经存在,即将其转换为“必选”状态。

这种转换不仅能提供更好的代码提示,还能在编译阶段规避大量的 nullundefined 检查。本文将由浅入深介绍四种主流的转换方案。

1. 全局转换:使用内置工具类型 Required<T>

TypeScript 自 2.8 版本起引入了 Required<T>,这是最直接的方案。它会遍历类型 T 的所有属性,并移除每个属性末尾的可选修饰符。

interface UserProfile {
  id: string;
  name?: string;
  email?: string;
}

// 转换后:id, name, email 全部变为必选
type StrictUser = Required<UserProfile>;

const user: StrictUser = {
  id: "001",
  name: "张三",
  email: "zhangsan@example.com" // 缺少任何一个都会报错
};

适用场景:当你需要对整个对象进行“严格化”处理时,这是首选方案。


2. 精准打击:仅转换特定属性为必选

在实际业务中,我们往往只需要确保某几个关键字段存在,而保留其他字段的可选性。这时可以结合 PickOmitRequired 构建一个复合工具类型。

我们可以定义一个通用的 MarkRequired 类型:

/**
 * T: 原类型
 * K: 需要转为必选的键名联合类型
 */
type MarkRequired<T, K extends keyof T> = 
  Omit<T, K> & Required<Pick<T, K>>;

interface Config {
  host?: string;
  port?: number;
  protocol?: 'http' | 'https';
}

// 示例:仅让 host 变为必选,port 和 protocol 依然可选
type EssentialConfig = MarkRequired<Config, 'host'>;

const myConfig: EssentialConfig = {
  host: "localhost" // port 和 protocol 可选填
};

原理解析:该方法先用 Omit 剔除目标属性,再用 Pick 选出目标属性并通过 Required 转为必选,最后通过交叉类型 & 进行合并。


3. 深入底层:使用映射类型中的 -? 符号

如果你正在尝试编写自己的类型库,了解映射类型(Mapped Types)的修饰符至关重要。在 TypeScript 中,+- 可以作为前缀应用于 ?readonly 修饰符。

type MyRequired<T> = {
  // -? 表示显式地移除可选属性标记
  [P in keyof T]-?: T[P];
};

// 与此相对,+?(通常简写为 ?)用于增加可选标记
type MyPartial<T> = {
  [P in keyof T]+?: T[P];
};

技术要点:使用 -?Required<T> 的底层实现原理。它不仅能去除问号,在处理一些复杂的条件类型映射时,这种手动控制的能力非常强大。


4. 函数参数与深度嵌套处理

函数参数转换

对于函数,最稳妥的方法是在重载或重新定义时直接移除 ?。但在高阶函数或泛型约束中,如果你想约束传入的函数必须接受必选参数,可以利用上述类型工具。

深度嵌套(Deep Required)

内置的 Required 只能处理第一层属性。如果对象是深层嵌套的,你需要递归处理:

type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object 
    ? DeepRequired<T[P]> 
    : T[P];
};

interface NestedConfig {
  db?: {
    user?: string;
    pwd?: string;
  }
}

type StrictNested = DeepRequired<NestedConfig>;

建议:在处理极其复杂的深层转换时,推荐使用社区成熟的库如 ts-essentials,其 DeepRequired 经过了大量边缘情况的验证。


结论与行动建议

根据不同的工程需求,建议采取以下策略:

  1. 立即可做:检查项目中的配置对象或 API 聚合层,使用 Required<T> 替代繁琐的非空断言(!)。
  2. 最佳实践:为了保持代码的 DRY(Don't Repeat Yourself)原则,建议在项目的 types/utils.d.ts 中收藏 MarkRequired 工具类型,用于处理部分属性必选的场景。
  3. 注意性能:过度使用复杂的递归类型(如 DeepRequired)可能会增加 TypeScript 编译器的负担,在大型项目中应谨慎评估其影响范围。
❌
❌