普通视图

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

毫秒级响应:前端本地搜索的“降维打击”

2026年2月27日 07:18

你一定被原生 IndexedDB 的查询限制气笑过:它只支持前缀匹配IDBKeyRange.bound),想搜“中间字符”?对不起,原生没有 LIKE %keyword%

在 AI Prompt Manager 场景下,用户可能搜“前端”、“报告”、“审计”,这些词可能出现在标题中间。如果直接 getAll() 拿出来在 JS 里 filter,数据量上万时,内存占用主线程卡顿会直接让你的“资深”头衔蒙尘。


1. 为什么原生查询不行?

原生 IndexedDB 的索引是 B-Tree 结构,它能极快地找到“以某字符开头”的数据,但无法处理“包含某字符”。

  • 低级做法getAll() + Array.prototype.filter。数据量 10k+ 时,解析 JSON 的开销会让 UI 瞬间掉帧。
  • 高级做法倒排索引(Inverted Index)

2. 方案一:引入 FlexSearch (极致性能的首选)

FlexSearch 是目前 Web 端最快的全量搜索库,它的速度比 Fuse.js 快一个数量级。

实战集成:PromptDB + FlexSearch

JavaScript

import { Index } from "flexsearch";

class SearchablePromptDB extends PromptDB {
  constructor() {
    super();
    // 创建内存索引,开启“模糊匹配”(suggest)
    this.index = new Index({
      tokenize: "forward", // 适合前缀+中间搜索
      resolution: 9,
      cache: true
    });
  }

  // 1. 同步索引:在数据存入 DB 的同时,存入 FlexSearch
  async addWithIndex(prompt) {
    await this.set(prompt);
    this.index.add(prompt.id, `${prompt.title} ${prompt.content}`);
  }

  // 2. 毫秒级搜索
  search(query) {
    const results = this.index.search(query, { limit: 20 });
    // results 返回的是 id 数组,再去 DB 拿具体对象(或从内存缓存拿)
    return results; 
  }
}

3. 方案二:手写“倒排索引” (不依赖库的深度方案)

如果你不想增加包体积,可以利用 IndexedDB 的 multiEntry 特性。

核心技巧:词根索引化

将 Prompt 的标题和标签拆分成字/词,存入一个专门的 searchTerms 数组字段。

JavaScript

// 存入时
const prompt = {
  id: 'p1',
  title: '金融审计助手',
  // 手动分词:['金', '融', '审', '计', '助', '手', '金融', '审计']
  searchKeywords: splitWords('金融审计助手') 
};

// 在 DB 初始化时,为这个数组字段开启 multiEntry
store.createIndex('keywords_idx', 'searchKeywords', { multiEntry: true });

// 查询时
const range = IDBKeyRange.only('审计');
const request = index.getAll(range); // 瞬发响应,因为它是原生索引

注:multiEntry: true 会为数组中的每个元素在 B-Tree 中创建一个独立的指针。


4. 性能瓶颈的终极解决方案:Web Worker

即便搜索算法再快,当数据量达到 10 万级,字符串分词索引构建依然会占用主线程。

架构设计:将搜索推入边缘

  1. 主线程:只负责接收用户输入和渲染结果。
  2. Worker 线程:持有 FlexSearch 实例,监听 IndexedDB 的变化。
  3. 流程:输入 -> PostMessage -> Worker 搜索 -> 返回 ID 列表 -> 主线程渲染。

JavaScript

// search.worker.js
self.onmessage = ({ data: { type, payload } }) => {
  if (type === 'SEARCH') {
    const results = flexIndex.search(payload);
    self.postMessage(results);
  }
};

5. 优化建议

  1. 分词策略:中文搜索最简单有效的是 “二元分词” 。例如“我的代码”,拆分为 ['我', '的', '代', '码', '我的', '的代码']
  2. 防抖 (Debounce) :搜索框必须加 150-300ms 防抖,避免用户打字太快导致 Worker 任务堆积。
  3. 结果高亮:搜索是毫秒级的,但千万别忘了在 UI 上用 <mark> 标签高亮匹配字符,这才是“体感”流畅的关键。
  4. 分片加载:搜索结果如果太多,只取前 20 条,配合滚动加载。

存储配额:用 navigator.storage.estimate() 预判浏览器什么时候会删你的数据

2026年2月27日 07:18

你一定知道浏览器是个“无情的房东”。当磁盘空间不足时,浏览器会自动启动 驱逐机制(Eviction) ,而你的 IndexedDB 数据往往是第一批被清理的对象,且清理前没有任何弹窗提示

为了不让用户的 AI Prompt 模板一夜之间消失,我们需要利用 navigator.storage API 进行“生存预判”。


1. 核心数据:Quota vs. Usage

通过 navigator.storage.estimate(),我们可以获取到当前域名的存储状态:

  • usage: 你已经占用了多少字节(Byte)。
  • quota: 浏览器分配给你的最高额度。通常是磁盘剩余空间的 80% ,但这只是“软上限”。

实战代码封装

JavaScript

async function checkStorageHealth() {
  if (!navigator.storage || !navigator.storage.estimate) {
    return { status: 'unsupported' };
  }

  const { usage, quota } = await navigator.storage.estimate();
  
  // 转换为更直观的 GB/MB
  const usageMB = (usage / (1024 * 1024)).toFixed(2);
  const quotaMB = (quota / (1024 * 1024)).toFixed(2);
  const percentUsed = ((usage / quota) * 100).toFixed(2);

  return {
    usageMB: `${usageMB}MB`,
    quotaMB: `${quotaMB}MB`,
    percentUsed: `${percentUsed}%`,
    isRisk: percentUsed > 80 // 占用超过 80% 即为高风险
  };
}

// 在 AI Prompt Manager 初始化时调用
checkStorageHealth().then(console.table);

2. 存储策略:最佳努力 (Best-effort) vs. 持久化 (Persistent)

默认情况下,所有的 Web 存储都是 “最佳努力(Best-effort)” 。这意味着当用户电脑没空间时,浏览器会根据 LRU(最近最少使用)原则删掉你的数据库。

作为资深开发,你应该在用户存储了重要数据后,申请 “持久化存储权限”

JavaScript

async function requestPersistence() {
  if (navigator.storage && navigator.storage.persist) {
    // 检查是否已经持久化
    const isPersisted = await navigator.storage.persisted();
    if (isPersisted) return true;

    // 申请持久化
    const granted = await navigator.storage.persist();
    return granted; // 返回 true 表示浏览器承诺:除非用户手动清理,否则绝不删除
  }
  return false;
}

注意: 浏览器(尤其是 Chrome)会自动根据网站的“活跃度”决定是否授予持久化权限。如果你的应用被用户频繁访问,获批概率极大。


3. 浏览器如何决定删谁?(驱逐逻辑)

不同的房东有不同的驱逐规矩:

浏览器 存储上限 (Quota) 驱逐触发条件
Chrome / Edge 共享磁盘剩余空间的 80% 全局磁盘空间不足 10% 或 2GB 时
Firefox 磁盘剩余空间的 80% 超过总额度的 10% 时开始 LRU 清理
Safari 严格限制(通常 1GB 或更少) 7 天不使用即可能被清理(移动端更严)

4. “预判避坑”指南

  1. 静默失败的处理:不要等到 set() 报错 QuotaExceededError 才行动。在那之前,通过 estimate() 监控,当 percentUsed > 70% 时,在 UI 上给用户一个“清理旧数据”的黄色警告。

  2. 垃圾回收的滞后:当你删除了 100MB 的 IndexedDB 数据后,usage 不会立刻减少。浏览器需要时间进行内部清理(Compaction),所以不要在 delete 之后立刻测 estimate

  3. 计算并不精准estimate() 返回的是字节数,但 IndexedDB 存储时会有额外的索引开销和数据库元数据。实际占用通常比数据本身大 10% 到 20%

  4. 金融数据备份建议:既然涉及到金融级别的安全性,永远不要把浏览器存储作为唯一的真理来源。

    • 低频:将重要 Prompt 同步到后端云端。
    • 高频:提供本地导出 .json 的功能作为手动备份。

BEM、OOCSS、SMACSS、ITCSS、AMCSS、SUITCSS:CSS命名规范简介

作者 漂流瓶jz
2026年2月26日 23:37

本来是希望讲一下CSS组件化发展历史上的技术,但所有内容放到一个文章中描述太长了,因此对各类技术分开写一下。这篇文章讲一下CSS命名规范。

在前端开发中,不同组件/模块的class类名都是公用的,假设两个组件中起了同样的类名,那么就会出现样式污染。既然问题出在名字,那么让不同组件的类名不同就能解决问题了。因此,社区中出现了一些CSS命名规范,希望使用规范将CSS的冲突污染减少,同时通过命名起到和HTML标签关系更紧密,封装公共CSS样式,以及一些其它作用。

BEM

BEM介绍

BEM是最知名的CSS命名规范,由Yandex团队开发。BEM的全称为Block Element Modifier,翻译成和中文就是块,元素和修饰符。BEM使用这三种层级来规范CSS的命名:

  • Block 区块 表示页面中一个独立可复用的模块或者组件
  • Element 元素 表示区块中的一个组成元素
  • Modifier 修饰符 修饰元素的状态或者行为

每个层级内部使用串行命名法(kebab-case),中间分隔单词使用单连字符-。元素前的分隔符为双下划线__,修饰符前的分隔符为双连字符--。元素不能独立存在,必须依附于区块内。修饰符则必须跟在元素或者区块后面。因此可以这样组合命名:

  • block 单区块
  • block__element 区块+元素
  • block--modifier 区块+修饰符
  • block__element--modifier 区块+元素
<div class="container">
  <input class="container__input" />
  <button class="container__button--primary">提交<button>
</div>

<style>
.container {}
.container__input {}
.container__button--primary {}
</style>

在上面的例子中,container是区块,input和button是元素,primary则是修饰符。这样每个元素都有自己的类型,不需要考虑名称冲突的问题,而且这样命名是有页面结构含义在的,即通过命名就知道这个元素属于哪个组件,有什么用处。因此,BEM也不推荐使用嵌套选择器。

BEM的应用和优缺点

BEM的应用比较广泛,很多项目都是使用它来命名class,还有一些项目利用了他的命名思路。这里我们以Vue3的组件库Element-Plus为例,来看一下BEM的应用:

css-name-1.png

这里是一个复合型输入框组件,名称叫做el-input-group。组件包含左边的前置展示元素和右边的输入框,其中组件结构和以BEM方式命名的class如下:

  • el-input-group--prepend: 区块 el-input-group 修饰符 prepend
    • el-input-group__prepend: 区块 el-input-group 元素 prepend
    • el-input__wrapper: 区块 el-input 元素 wrapper
      • el-input__inner: 区块 el-input 元素 inner

通过这种方式,Element-Plus有着清晰的元素class名,不仅组件内部开发使用,使用组件库的用户也可以使用这些类名来覆盖组件库样式。下面我们来总结一下BEM命名规范的优缺点:

  • 优点
    • 清晰的类名,只看class就能知道元素的作用和归属,不会发生混淆
    • 组件和组件之间的名称是独立的,不会样式污染
    • 提供了命名规范,团队协作开发时命名不会混乱,也可以提供给外部使用
  • 缺点
    • 对于包含很多元素的复杂组件,仅仅三个层级,命名可能并不够用
    • 组件名称太长,对开发者并不方便

这些优缺点不仅仅是BEM的优缺点,也是大部分CSS命名规范的优缺点了。

OOCSS

面对对象简介

OOCSS的全称为Object Oriented CSS,即为面对对象的CSS。接触过编程的同学大多知道,Object Oriented即面对对象,是一种编程模式,是将一些数据属性和对应的方法结合起来,抽象成一个类,类可以生成实例对象。面对对象还有继承,封装,多态等特性。这里举个简单的例子:

类别 类名 属性 方法
基类 水果 名称 重量 体积 切开水果
子类 继承水果 苹果 甜度 做苹果派
子类 继承水果 橘子 酸度 作陈皮

每一种类都封装了属性和方法。苹果和橘子都是水果的子类,继承了水果的属性和方法。子类可以有自己独立的方法,也能调用父类的方法。调用父类的方法时,可以有子类自己的实现,这是多态。例如苹果和橘子都可以使用切开水果这个方法,但切开的效果不一样。一个类可以生成很多个实例对象,每个对象可以有不同的数据。

JavaScript中也有面对对象相关的方法,老方法有原型链,ES6中直接提供了class关键字,并且在逐渐完善面对对象相关的语法。但CSS并不是编程语言,无法提供直接提供面对对象语法,只能在概念上简单模拟一下。OOCSS就是利用CSS,对面对对象的概念进行了简单的模拟。

分离结构和皮肤

按照OOCSS的设想,CSS样式可以分为结构structure和皮肤skin。结构表示它的尺寸/位置/边距等内容;皮肤表示颜色,字体,背景等。因为皮肤可能会根据不同的场景变化,而且皮肤可能被多个组件所公用,因此分开作为两个类来处理。这里我们举个例子,首先是不使用OOCSS的做法,两个CSS类独立互相没有依赖:

<div>
  <button class="btn-small">jzplp按钮1</button>
  <button class="btn-large">jzplp按钮2</button>
<div>
<style>
.btn-small {
  width: 20px;
  height: 20px;
  Padding: 5px;
  color: red;
  background: blue;
}
.btn-large {
  width: 200px;
  height: 200px;
  Padding: 50px;
  color: red;
  background: blue;
}
</style>

这样写会造成一些重复属性存在,例如这里的skin相关属性就是重复的,我们将他抽象出来作为单独的skin共享:

<div>
  <button class="btn-small btn-skin">jzplp按钮1</button>
  <button class="btn-large btn-skin">jzplp按钮2</button>
<div>
<style>
.btn-skin {
  color: red;
  background: blue;
}
.btn-small {
  width: 20px;
  height: 20px;
  Padding: 5px;
}
.btn-large {
  width: 200px;
  height: 200px;
  Padding: 50px;
}
</style>

这样皮肤的样式就可以在不同的元素中复用了。如果要修改皮肤,修改一个位置就统一修改了所有元素的皮肤。

分离容器和内容

很多人在写CSS时,遇到容器和内容这样组合的HTML结构,经常会把CSS也写为组合的样式,例如与HTML一样也保持了父子的结构。但OOCSS认为,这样限制了这些CSS的引用场景,不利于其它元素复用这些CSS代码。需要将它们分开撰写。这里举个例子,首先依然是嵌套CSS的场景:

<div class="container">
  <div>jzplp内容1</div>
  <div>jzplp内容2</div>
<div>
<style>
.container {
  width: 100%;
  height: 200px;
  div {
      width: 30px;
      margin-right: 10px;
      height: 100%;
  }
}
</style>

假设有其它场景只希望复用内部div的CSS代码,是没有办法的,因为嵌套的结构限制了这里的使用场景。因此按照OOCSS的设想,应该不使用嵌套结构,将CSS代码解耦:

<div class="container">
  <div class="content">jzplp内容1</div>
  <div class="content">jzplp内容2</div>
<div>
<style>
.container {
  width: 100%;
  height: 200px;
}
.content {
  width: 30px;
  margin-right: 10px;
  height: 100%;
}
</style>

OOCSS的优缺点

除了上面OOCSS的两个原则“分离结构和皮肤/分离容器和内容”之外,OOCSS最核心的原则其实是:拆开元素的CSS样式,变为更方便复用,更独立的样式。上面两个原则是这个核心原则的部分具体做法。

这时候有些同学会问,这些原则和面对对象有什么关系?实话说我也觉得关系确实不大。但按照OOCSS的说法,我们定义的类选择器就是面对对象中的类。将这个类的提供给HTML元素,就相当于将这个类实例化。使用OOCSS的原则,拆开的可复用CSS样式相当于基类,那些拆开后依然无法复用的CSS样式称为子类。(例如前面btn-small是子类,btn-skin是父类)。

如果这样抽象的话,即使不了解OOCSS的开发者,肯定也无意间使用过OOCSS的原则,也用过“面对对象方法”组织过CSS。这里我们总结一下OOCSS的优缺点:

  • 优点
    • 复用已有的CSS规则更方便(这也是OOCSS的核心原则)
    • CSS文件更少,可提高页面加载速度(这也是复用程度高造成的)
    • 有利于CSS规则更新和扩展(只改一个CSS规则,所有位置都可以生效)
  • 缺点
    • 一个元素上可能挂多个类名,可能造成属性混乱
    • 如何拆分抽象公共CSS规则需要根据业务设计与平衡
    • 结构和皮肤有时候时互相关联的,有时候并不容易区分
    • 部分CSS本身就要求父子有联系,例如flex,grid布局等等,必须要求父子元素独立可能并不适合

总之,OOCSS只是一个组织CSS的思路,我们不需要教条化的拆分,而是根据具体场景拆分和抽象公共CSS规则。

SMACSS

SMACSS的全称叫做Scalable and Modular Architecture for CSS,意思是可扩展和模块化的CSS结构。他与OOCSS类似,也是制定了一些CSS组织的规范,但比OOCSS更细致。这两个命名规范的思想上有很多相似之处。SMACSS将页面的CSS规则分为五种类型,下面我们将分别介绍:

  • Base 基础样式
  • Layout 布局样式
  • Module 模块样式
  • State 状态样式
  • Theme 主题样式

Base基础样式

基础样式是整个页面通用的公共样式。一个常用的例子是CSS reset样式表。在CSS优先级,没有想的那么简单!全面介绍影响CSS优先级的各类因素中我们介绍过,浏览器会提供一些预置的默认样式,叫做“用户代理样式表”。但是很多用户不希望使用这些默认样式,因此使用一个全局的CSS reset样式表处理这些默认样式。

除了reset样式表之外,基础样式还可以包含一些对于所有元素通用的样式,例如标题样式,默认链接样式,页面背景等。SMACSS不推荐在基础样式中使用类或者ID选择器。例如:

body, form {
  margin: 0;
  padding: 0;
}
a {
  color: #039;
}
a:hover {
  color: #03F;    
}
body {
  background-color: red;
}

Layout布局样式

布局指的是将页面划分为几个大部分,这几个部分的样式作为布局样式。例如页面可以划分为头部、主内容区、底部、侧边栏等。这些样式通常是全局样式,一个布局元素中可以包含很多个模块。如果布局元素确定只出现一次,甚至可以使用ID选择器。可以使用l-或者layout-前缀来表示是布局样式,但也可以不使用。这里举几个例子:

#header, #article, #footer {
    width: 960px;
    margin: auto;
}
.sidebar {
    float: right;
}

Module模块样式

SMACSS中的模块和其它CSS命名规范中模块的含义一致,都是页面中独立可复用的模块,也就是组件。模块中的规则避免使用ID选择器或者元素选择器,而使用类名。为了规则不发生冲突,每个模块内部可以用模块名称本身作为前缀,例如.module-。

.card { padding: 5px; }
.card-top { font-size: 10px; }

State状态样式

SMACSS中的状态类似于BEM中的修饰符modifier,它表示模块或者布局在某些状态下的外观或者行为。但SMACSS中的状态样式倾向于是全局使用的,即多个模块和布局都可以使用。状态样式也可以是依赖JavaScript驱动的,例如点击或者其它操作展示的效果。状态样式可以用is-作为前缀。因为要覆盖元素本身的默认样式,因此允许使用!important。

.is-collapsed {
  width: 10px;
}
.is-selected {
  color: red !important;
}
/* 仅供模块使用的状态规则,可以添加模块前缀 */
.is-card-selected {
  color: yellow !important;
}

Theme主题样式

主题描述了模块或布局的外观样式,一些小的页面不要求主题样式,但有些页面有特殊要求,甚至要求换肤。将皮肤抽象出来作为的独立样式,方便抽象和更改。这里和OOCSS的皮肤规则有点像。

.normal {
  color: blue;
  background: grey;
}

.primary {
  color: red;
  background: white;
}

SMACSS的优缺点

SMACSS不仅描述了五种CSS规则类型,还包含很多规范说明,比如:类名规范、选择器使用规范和性能优化、字体、页面状态变化、嵌套选择器、与HTML5集成,与CSS预处理器集成、特殊CSS规则、甚至是CSS代码缩进等等。这里我们总结一下SMACSS的优缺点:

  • 优点
    • 提供了比较详尽的CSS组织规范
    • 考虑到了各种类型的公共样式,组件/模块的独立样式,可复用和隔离能力相对平衡
    • 由于比较详尽,更有利于团队协作开发
  • 缺点
    • 规范比较落后,没有适应现在前端框架的发展,有些想法也过时了
    • Layout也经常以模块/组件的形式组织
    • 规范太详尽,导致经常出现不符合实际情况的场景
    • 虽然说了不要死板套用,但如果不符合的场景太多,那还是需要重新定义自己的规范

ITCSS

ITCSS的全称为Inverted Triangle Cascading Style Sheets,翻译成中文为倒三角CSS。ITCSS把CSS规则分成了七层,并且把这七层展示为了一个倒三角的形式。

css-name-2.png

倒三角的形式指的是从上到下CSS规则的普遍性减少,特殊性增加,即越往下,影响范围和可复用性越低。这里我们说明一下每一层的内容:

  • Settings 预先定义的颜色变量,数值变量等
  • Tools 全局使用的mixins和函数等
  • Generic 全局标准化样式,例如CSS reset样式表
  • Elements HTML元素的通用样式
  • Objects 整个工程的布局样式,但不包含外观属性
  • Components 具体的组件样式
  • Trumps 可以覆盖的辅助样式,可以接受!important

可以看到,前两层都没有真正的CSS规则代码;三四层是不带类选择器的CSS规则。ITCSS利用了CSS预处理的特性,例如mixins和函数等。

AMCSS

AMCSS的全称为Attribute Modules for CSS,即使用属性作为模块的CSS。它与其它CSS命名规范都不相同:其它命名规范主要使用HTML的class属性作为选择器,而它则采用自定义HTML属性作为选择器。

  • Modules 模块
    • 类似于BEM中区块和元素的概念
    • 使用HTML属性描述,属性名称采用大驼峰命名法BlockName,如果嵌套子模块名使用连字符-
  • Variations 变体
    • 类似于BEM中的修饰符,表示模块中变化的部分,用来新增和覆盖部分属性
    • 使用HTML属性值描述,多个用空格分隔
  • Traits 特征
    • 一组某个用途的CSS规则,可以用来描述一些公共的CSS
    • 同一组特征的HTMl属性相同,值不同。特征的属性名采用小驼峰式命名法featureName

上面讲的有点晦涩,这里还是要用实际例子说明一下。AMCSS要求属性名添加前缀,推荐am-,其它前缀也可以。

<div am-MainCard>
</div>
<div am-Card>
  <div am-Card-Container> jzplp1 </div>
</div>
<div am-Card="sp1 primary"> 
  <div am-textType="title"> jzplp2 </div>
</div>

<style>
  /* 仅模块名 */
  [am-Card] { color: red; }
  /* 模块名采用大驼峰命名法 */
  [am-MainCard] { color: red; }
  /* 子模块名使用连字符- */
  [am-Card-Container] { color: red; }
  /* 变体使用属性 */
  [am-Card~="primary"] { color: red; }
  /* 特征名使用小驼峰式命名法 */
  [am-textType] { color: red; }
  /* 特征名和限制特征值 */
  [am-textType~="title"] { color: red; }
</style>

可以看到,AMCSS实际上就是将类选择器的那一套用法搬到了属性选择器上面,属性选择器的~=符号同样支持多个属性值。而且由于属性有属性名和属性值两种,因此相比于class名更灵活也更清晰。这种属性命名方式并不是推荐的HTML规范,但也可以正常使用。

SUITCSS

SUITCSS是一套组件化的样式工具。它不仅包含CSS命名规范,而且也提供了一些CSS预设包,构建工具,预处理器(实际上是PostCSS的插件集合),测试工具等。这里我们主要描述一下命名规范:

  • 公共样式: 表示一些公共样式
    • 命名规则 u-[sm-|md-|lg-]<utilityName>
    • 使用-u开头,后面跟骆驼命名法。中间也可以加响应式规则sm-|md-|lg-
  • 组件样式:描述独立组件内部的样式
    • 命名规则 [<namespace>-]<ComponentName>[-descendentName][--modifierName]
    • namespace 可选的命名空间,例如组件库中的组件避免与业务组件冲突,可以加前缀,例如 el-label, el-tag等。
    • ComponentName 组件名称,用Pascal命名法。组件名称需要与其他组件不同。
    • descendentName 组件内后代的名称,即为组件内部组成元素的类名,使用骆驼命名法。
    • modifierName 组件修饰符,修饰元素的状态或者行为。使用骆驼命名法,且前面有两个连字符。

SUITCSS命名规范中还规定了组件的设计原则,CSS变量名的命名方式,预置公共样式,甚至是代码风格等。

总结

即使没有了解过这些命名方案,其中的部分思想在我们的开发中也不知不觉会用到一些。这些命名规范确实能够解决很多问题,在前端发展的历史中起到过很多作用,也引导和启发了后续CSS组件化和工程化的发展。

但这些命名规范需要“手工处理”:手工定义各种名称,手工抽象CSS文件等。一个人开发还好,如果是多人协作团队开发,还要让每个人遵守规则,检查代码,这就成了一个麻烦的问题(少量规范有工具)。另外规范给出的类名大多很长,虽然更容易识别代码含义,但也造成了代码冗长,代码传输速度慢。

另外很多命名规范都有这样一个冲突:如果规范将CSS代码分类和组织的太过明确,这会造成应用范围小,很多工程根本不适用。如果规范将CSS代码分类和组织的太模糊,那代码就太随心所欲了,与没定义差不多。因此我们最好根据每个工程的具体实际情况定义合适的规范和抽象。

还有很多CSS命名规范比较老,跟不上时代发展。有些老旧的规范并不适应部分新内容:例如新的CSS布局方案,CSS变量,前端框架,CSS Modules,CSS代码格式规范(有自动化工具)等。CSS命名规范也存在互相吸收想法和思路的,晚出的方案相对更完善一些,但没有早出的方案更知名。

参考

AI辅助开发实战:会问问题比会写代码更重要

作者 牛奶
2026年2月26日 21:22

AI辅助开发实战:会问问题比会写代码更重要

系列第二篇。我想聊聊怎么用好 AI 这个工具。不是教你怎么敲代码,而是教你,怎么真正用好AI辅助开发工具。


原文地址

墨渊书肆/AI辅助开发实战:会问问题比会写代码更重要


你有没有过这样的经历?

打开Cursor(或者TraeCopilot),对着空白编辑器发了半天呆,不知道该让AI帮你干什么。

或者你问了一句「帮我写个登录功能」,AI 噼里啪啦写了一大堆代码,你看都看不懂,最后只能硬着头皮复制粘贴。

再或者,你问 AI:「这个报错是什么意思?」它回了一堆你看不懂的术语,你更迷茫了。

如果你有以上任何一种经历,这篇文章就是写给你的。


会问问题,比会写代码更重要

这是我最近一年用 AI 辅助开发最大的感悟。

以前我觉得,AI 嘛,就是个更聪明的搜索引擎。我不会的代码问它,它告诉我怎么写呗。

后来发现不是这么回事。

同样一个问题,不同的问法,AI 给出的答案质量可以差十倍。

AI 不会读心术。你得把自己的需求翻译成 AI 能理解的语言。

举两个例子感受一下:

第一种问法:「帮我写个登录功能。」

AI给你一个标准答案:用户名密码输入框、提交按钮、后端接口、数据库查询。看起来很全,但放到你的项目里可能完全不适用。你要改吧,改到猴年马月。不要吧,扔掉又可惜。

第二种问法:「我的项目用Next.jsPrisma,用户表字段是 email 和 passwordHash。请帮我写一个登录API,要支持邮箱密码登录,密码用 bcrypt 加密,返回 JWT token,7天有效期。」

AI给你的代码,直接就能用。稍微调一下就能跑。

这就是差距。好的 Prompt 不是更长的Prompt,而是更精确的 Prompt。


几个基本概念

在开始讲技巧之前,先简单说几个你经常会遇到的术语:

LLM:Large Language Model,大语言模型。你可以把LLM理解为"大脑",GPT、Claude、DeepSeek 都是 LLM。ChatGPT、Cursor背后的 AI 都是LLM在驱动。

Prompt:提示词,你给AI说的话。「帮我写个登录功能」就是一个Prompt。

Agent:你可以理解为"能自己干活"的AI。传统AI是你问一句它答一句,Agent 是你告诉它一个目标,它自己规划步骤去执行。Cursor 的 Agent 模式就是这个原理。

MCP:Model Context Protocol,模型上下文协议。这是 2024 年出来的一个标准,让 AI 能统一地访问外部工具和数据。比如 AI 可以通过 MCP 直接读取你电脑上的文件、查询你的数据库、控制浏览器。2026年的 Cursor 已经支持 MCP,用起来很方便。

Token:你可以理解为 AI 处理文字的"计量单位"。英文约4个字符=1个 Token,中文约1-2个汉字=1个 Token。

为什么要注意 Token?因为 AI API 是按 Token 收费的。你输入的文字要花钱,AI输出的文字也要花钱。知道这些就够了,继续往下看。


我的AI辅助开发经验

2026年了,AI辅助开发工具已经成为程序员的标配。CursorTraeCopilotOpenCode……不管你用哪个,核心技巧都是互通的。

我用了一年多,从一开始的「这有啥」到现在的「真香」,总结了一些真正有用的经验。

1. 搞清楚什么时候用什么模式

Cursor 有两个核心模式:AgentChat。用对了,效率翻倍;用错了,就是折磨。

Chat模式:你问一句,它答一句。像跟人聊天一样。

我一般用来:

  • 问具体问题:「这个报错是什么意思?」
  • 查知识点:「PostgreSQL的索引类型有哪些?」
  • 解释代码:「这个函数做了什么?」
  • 帮我想名字:「帮我给这个函数起个名字」

Agent模式:你描述一个任务,它自己去分析和改代码。威力更大,但需要把需求说清楚。

我一般用来:

  • 帮我重构整个模块:「把这个登录从JWT改成Session」
  • 帮我修bug:「登录一直返回401,帮我看看是什么原因」
  • 帮我转换代码:「把这个JavaScript文件改成TypeScript」
  • 帮我实现一个功能:「帮我实现用户注册功能,包含表单验证、数据库存储、发送欢迎邮件」

简单说:小问题用Chat,大任务用Agent。

2. 喂上下文是有技巧的

Cursor 最强的地方是它能理解你的整个项目。你打开一个文件,问它这个组件是做什么的,它能根据文件名、代码内容、项目结构给你答案。

但有时候它也会犯傻——给你一些牛头不对马嘴的回答。

这时候,你得学会喂上下文

我犯过的错误:

「怎么优化这个查询?」

AI回了半天,什么加索引、分页、缓存讲了一套,我根本不知道它说的是什么,因为我连我的表结构都没告诉它。

后来我学乖了:

「我的Prisma查询是这样的:prisma const users = await prisma.user.findMany() 数据量大概10万条,现在查询要3秒,请问怎么优化?」

这次AI直接告诉我:1. 加索引 2. 用select只查需要的字段 3. 考虑分页。

我的习惯是:至少告诉AI三件事

  1. 我用的技术栈是什么(Next.js + Prisma + PostgreSQL)
  2. 当前代码长什么样(贴上代码)
  3. 遇到了什么问题(查询慢、报错、不知道怎么做)

3. Tab键补全真的好用

大部分 AI 辅助开发工具都有代码补全功能,会预测你下一行要写什么。按 Tab 键直接采纳预测。

刚开始我还不太信这个功能,觉得 AI 哪有那么聪明。后来真香了。

我经常这样用:

  • 写TypeScript类型定义,AI能猜到我要的类型
  • 写React组件props,AI能帮我补全大部分
  • 写数据库schema,AI知道我想要什么字段
  • 写import语句,AI知道我要导入什么

10次有8次是准的,能省很多打字的时间。

4. 选中代码让AI帮你改(核心技巧)

选中一段代码,让AI帮你修改。这是一个通用技巧,大部分工具都支持,只是快捷键不太一样。

这是我最常用的功能,没有之一。

比如我选中一个函数,这样用:

「请帮我添加错误处理和类型定义」

AI直接在原代码基础上帮我改好了,我只需要确认一下就行。

比让它生成一段新代码然后我再替换,效率高很多。

再举几个我常用的场景:

  • 选中一段面条式代码:「请帮我重构这段代码」
  • 选中一个API接口:「请帮我添加参数校验」
  • 选中一个组件:「请把这个组件改成响应式」

5. 打开对话窗口做复杂任务

有时候你想让AI帮你做比较复杂的任务,比如生成一个完整的组件。

选中代码后打开对话窗口,可以详细描述你的需求。

我经常这样用:

  1. 选中一段代码
  2. 打开对话窗口
  3. 详细描述我要做什么
  4. AI生成代码,我可以逐行确认

这个功能特别适合:

  • 生成一个新组件
  • 实现一个复杂功能
  • 写测试用例

6. @符号引用文件

@符号引用特定内容。

  • @File :引用当前打开的文件
  • @components/UserCard.tsx :直接引用某个文件
  • @Folder :引用整个文件夹
  • @Docs :引用官方文档
  • @Search :搜索项目内的代码

最常用的场景:

@components/UserCard.tsx 请帮我在这个组件里添加一个编辑用户信息的功能

AI直接读取文件内容,在正确的位置帮我添加代码。

@Docs 请帮我查一下Next.js的metadata怎么用来做SEO

AI直接读官方文档,给我准确的答案。

7. 设置好项目规范

我在每个项目都会设置Rules。这是Cursor的一个特色功能,其他工具类似功能还在发展中。

在项目根目录创建.cursor/rules/目录,放.mdc文件:

---
name: 项目规范
description: Next.js 15 App Router 项目规范
---

# 技术栈
- 框架:Next.js 15 App Router
- 语言:TypeScript strict
- 样式:Tailwind CSS
- 数据库:PostgreSQL + Prisma

# 目录结构
- app/:页面
- components/:组件
- lib/:工具函数
- prisma/:数据库schema

# 代码规范
1. 默认使用 Server Components
2. 客户端用 'use client' 标记
3. API错误格式:{ success: boolean, error?: string }

设置好之后,Cursor每次生成代码都会自动遵循这些规范。

举个例子:我不用每次都说「API错误要返回success和error字段」,Cursor自己就知道。

而且Rules是可以复用的。我做了几个模板:

  • Next.js项目规范
  • NestJS项目规范
  • React组件规范

每次建新项目,直接复制过来改一下就行。

8. 用好Skills,让AI更专业

如果说Rules是「项目规范」,那Skills就是「专业能力」。

你可以在.cursor/skills/目录放一些专业技能文件:

# .cursor/skills/database.md

你是一个数据库专家,精通 PostgreSQL 和 Prisma。

在回答数据库相关问题时:
1. 优先考虑查询性能,避免 N+1 问题
2. 合理使用索引,解释为什么加这个索引
3. 更新用 update,删除用 delete,别用 updateMany

回答时先解释原理,再给代码示例。

用的时候告诉AI:「请用数据库专家的角度,帮我审查以下Prisma查询...」

它回答的专业度明显比普通模式高。

我目前积累了几个Skills:

  • database.md:数据库专家
  • security.md:安全专家
  • performance.md:性能优化专家
  • typescript.md:TypeScript专家

9. MCP让AI更强大

前面提到了MCP,这是2026年特别值得关注的特性。

简单说,MCP 让 AI 能从"只懂训练数据"变成"能操作真实世界":

  • MCP + 文件系统:AI 可以直接读取、修改你本地项目的代码
  • MCP + 数据库:AI 可以直接查询你的数据库
  • MCP + 浏览器:AI 可以控制浏览器,帮你填表单、截图
  • MCP + 搜索:AI 可以帮你搜Google、搜文档

Cursor、Trae 等新一代工具已经开始支持 MCP,装好对应的插件就能用。

装好 MCP 插件后,我可以直接问AI:「帮我查询数据库里最近注册的10个用户」

AI真的会去查数据库,然后给我结果。

这个功能还在快速进化中,未来能做的事情会越来越多。

10. 节省Token是有技巧的

前面提到了 Token 的概念。Token 是 AI 处理文字的计量单位,AI API 是按 Token 收费的。

这是我总结的节省 Token 经验:

  1. 别一上来就贴全栈代码:只贴和问题相关的代码片段,AI不需要看你的整个项目才能回答问题。

  2. 问完一个问题可以开新会话:如果新问题和上一个问题不相关,别在同一个会话里继续聊。AI需要记住之前对话的内容,这些也会算Token。

  3. 让AI一次性完成:比如你要写一个组件,别分开问「先帮我写HTML」「再帮我写样式」「再加个交互」。直接说「帮我写一个登录组件,包含表单验证、错误提示、暗色模式支持」,一次搞定。

  4. 精简你的Prompt:Prompt不是越长越好,是越精确越好。把无关的废话去掉,AI能更专注,Token也花得值。

  5. 用@引用代替复制粘贴:用@File引用文件,AI会自己去读,比你复制粘贴一长串代码省Token。


这些场景我天天用AI

1. 读报错信息

以前遇到报错,我要把错误信息复制到Google搜半天。

现在直接问Cursor:「这个报错是什么意思?TypeError: Cannot read properties of undefined (reading 'map')」

它会告诉我:错误原因是什么、最可能出在哪个地方、怎么修复。

80%的情况下,它能帮我省掉搜索的时间。

有时候我甚至直接截图给它看,它也能分析个大概。

2. 代码Review

以前代码Review都是同事做。现在我先让AI Review一遍,发现低级问题,再交给同事。

效率高很多,而且有些话AI说得,我作为开发者反而不好开口。

「请审查以下代码,指出:1. 潜在安全问题 2. 性能问题 3. 代码规范问题」

它会从安全性、性能、代码规范等角度帮我分析一遍。

3. 重构代码

觉得某段代码写得烂,但不知道怎么改?

问AI:「请帮我重构以下代码,要求:1. 使用TypeScript类型 2. 提取可复用逻辑 3. 增加错误处理」

AI会给一个全新的版本,我可以参考它的思路自己改,也能学到东西。

有时候我还会让它用不同的方式重构,让我对比学习。

4. 帮我想名字

我经常让AI帮我给变量、函数起名字。

「我有一个函数,接受用户ID,返回用户名、邮箱、头像、最后登录时间。请帮我想一个合适的函数名」

AI会给三四个建议:

  • getUserById
  • fetchUserDetails
  • getUserProfile

我会选一个最合适的。

比自己想半天强多了。而且AI起的名字通常都比较规范,符合命名习惯。

5. 写测试

写测试很枯燥,但很重要。

我会让AI帮我:

「请为以下函数编写单元测试,覆盖:正常情况、空输入、错误输入」

AI生成测试代码,我再根据需要调整。能省不少时间。

有时候我还会让它帮我补充边界情况的测试。

6. 查文档

以前遇到问题,我先去 Google 搜,然后看 Stack Overflow,最后实在不行才去翻文档。

现在我直接问 Cursor:

`@Docs 请帮我查一下Next.js 15怎么做密码重置」

或者

`@Docs Vercel AI SDK怎么实现流式响应」

AI直接从文档里给我准确的答案,比我自己搜快多了。

7. 帮我写SQL

有时候我需要写一些复杂的SQL查询,直接问AI:

「帮我写一个SQL,查询过去7天每天的新增用户数,按日期排序」

AI会给我SQL,我稍微改改就能用。

8. 帮我理解别人的代码

接手别人的项目,看不懂代码怎么办?

问AI:

`@components/OldCode.tsx 请帮我解释这个组件做了什么」

AI会把代码逻辑梳理一遍,比我自己看快多了。


积累自己的Prompt模板库

这是我想聊的最后一个话题。

有些 Prompt 我会反复使用,慢慢就积累了一套模板:

// 解释代码
请用三句话解释以下代码做了什么

// 解释报错
这个报错是什么意思?{报错信息}

// 生成类型
请为以下接口生成 TypeScript 类型定义

// 代码审查
请审查以下代码,指出:1. 潜在问题 2. 性能优化点 3. 代码规范问题

// 重构
请重构以下代码,要求:{你的要求}

// 写测试
请为以下函数编写测试用例,覆盖:{场景1}、{场景2}、{场景3}

// 查文档
@Docs {你的问题}

我保存在一个markdown文件里,用的时候直接复制粘贴,稍微改改就能用。

我的经验总结

用多了,你会发现有些规律:

  • 1. 模板要简单通用

    我的模板都很简单,就是一个开头。比如「请用三句话解释」,这个模板可以用在任何代码上。

    不要把模板写得太具体,比如「帮我写一个登录表单要包含用户名、密码、验证码」。这样反而不好复用。

  • 2. 遇到好的Prompt就保存下来

    有时候你会发现,同样的问题,不同的问法,AI回答的质量差很多。

    遇到好的Prompt,就把它保存下来。下次遇到类似的问题,直接用或者改改再用。

  • 3. Rules模板可以复用

    Rules模板也是一样的道理。

    我做了几个模板:

    • Next.js项目规范
    • NestJS项目规范
    • React组件规范

    每次建新项目,直接复制过来改一下就能用。做到第三个项目,你会发现很多规范是可以复用的。

  • 4. 定期整理和迭代

    我的模板库每个月会整理一次。把不用的删掉,好的留下来。

    有时候会发现之前写的模板不够好,就改改。

    这是一个持续迭代的过程,不用急。


写在最后

回到开头的问题:会问问题,为什么比会写代码更重要?

因为 AI 时代,写代码的门槛会越来越低。但提问的能力——把模糊的需求翻译成精确的描述——这个能力反而越来越值钱。

你能不能清楚地描述你想要什么?能不能给 AI 足够的上下文?能不能判断 AI 给出的答案对不对?

这些才是 AI 时代真正的核心竞争力。

AI 辅助开发工具会越来越好用,Cursor、Trae、Copilot、OpenCode……不管你用哪个,核心技巧都是互通的。用好工具的人,永远是那些懂得思考的人。

下一篇文章,我们会开始真正的技术内容:《全栈开发环境搭建:Git + monorepo + 开发工具链》。

感兴趣的话,下一篇见。

为什么2026年还要学全栈?

作者 牛奶
2026年2月26日 21:17

为什么2026年还要学全栈?

系列开篇,写给想要真正做事的人。


原文地址

墨渊书肆/为什么2026年还要学全栈


你有没有过这样的经历?

做了一套很酷的前端界面,发到群里求赞。朋友问:「能线上访问吗?」你愣了一下:「还在本地跑着呢。」

搭建了一个API接口,测试数据跑得好好的。放到线上就开始报错,你对着日志看了半天,不知道是数据库连接问题还是CORS没配好。

买了个云服务器,SSH连上后对着黑屏发呆——接下来该干什么?域名怎么绑定?HTTPS怎么配置?

如果你有过类似的经历,说明你和我一样,曾经被困在某个技术边界里。

前端会一点,后端也懂一点,但真的要把一个想法变成线上能用的东西,总是差了那么一口气。

我想聊聊这件事。


全栈这件事,被误解了很多年

一提到「全栈工程师」,很多人脑海里浮现的是这样一个形象:什么框架都会,什么语言都能写,数据库也能碰,服务器也能捣鼓。

换句话说,「什么都会一点」。

这种理解,在五年前或许还能成立。那时候做Web开发,确实需要前后端都懂一点才能混得下去。

但2026年了,这种理解该过时了。

真正的全栈,不是「什么都会一点」,而是「能独立交付一个完整的、可运行的互联网产品」。

这两个定义有本质区别。

「什么都会一点」说的是技术广度,你掌握了ABCDE各种技术。 「能交付完整产品」说的是能力深度,你能够从0到1,把想法变成现实。

前者是堆砌,后者是整合。

这十年,全栈经历了什么

让我简单回顾一下这段历史,你可能会更有感触。

  • 2010-2015年:全栈的黄金时代

那时候,一个创业者想要做个网站,真的需要一个人搞定所有事情。PHP就是最典型的全栈语言——一个文件,从数据库到HTML全写了。

没有选择,只能全栈。

  • 2015-2020年:前后端分离,全栈「衰落」

前端技术越来越复杂,React、Vue、Angular各自一套生态。后端技术也在深化,微服务、容器化、云原生,一个领域比一个领域深。

很多人开始专注于一个方向。全栈这个词渐渐变成了「什么都会一点,什么都不精」的代名词。

我见过很多前端工程师,后端代码一行都不敢改。也见过很多后端工程师,写个表单样式就头皮发麻。

技术栈在变宽,人在变窄。

  • 2020年至今:AI时代,全栈复兴

转折来自两个力量:

一是Serverless和全栈框架的成熟。Next.jsSupabase让一个人能覆盖的场景越来越广。

二是AI的爆发。代码可以自动生成了,一个人能做的事情边界再次扩大。

但这次不一样。

这次的全栈,不是回到过去那种「什么都会一点」的状态,而是有了AI的辅助,你可以更专注于「整合」而非「实现」

你不需要记住每个API的用法,AI可以帮你查。但你需要知道一个系统需要哪些模块、它们怎么配合。

这才是2026年「全栈」的真正含义。


我见过太多「会技术」但「做不出东西」的人

我自己也是这么走过来的。

刚学编程的那几年,我痴迷于学新东西。React出来了,学React。Vue火了,学Vue。Node.js流行,学Node。Docker热门,学Docker。

感觉自己越来越厉害,简历上技术栈越来越长。

但有一次,我做一个个人博客系统,前前后后做了俩个月。

不是技术难,而是我在每个环节都卡住:

  • 前端写到一半,发现后端API设计不合理,推倒重来
  • 数据库表结构改了三版,每次都要改前端对应的字段
  • 好不容易做完了,部署上线又折腾了一周
  • 刚上线就被别人注册了一堆垃圾数据,才发现自己没做接口限流

一个看似简单的博客系统,真正从零做到上线,才发现之前学的那些技术都是散的,根本连不起来。

后来我反思:不是我技术不够,而是我从来没有站在「完整产品」的角度去规划一个系统。

这就是问题所在。

但现在,在春节前,我使用 AI 辅助开发和腾讯云的轻量服务器,3天就成功上线了我的个人博客站。

————墨渊书肆

后面,也会根据这个博客站,和我在开发的另一个出海产品,分享我的实战经验。


全栈到底学什么?

说了这么多,你可能想问:所以全栈到底要学什么?

我的回答是:不是学更多技术,而是理解技术之间的关系。

举两个例子。

第一个例子。

你想实现「用户登录后可以评论」这个功能。你需要懂:

  • 前端表单验证
  • 后端接口设计
  • 数据库表结构
  • 密码怎么加密存储
  • Token怎么验证
  • HTTPS怎么配
  • Rate Limiting怎么加

每一项单拎出来都不难。但如果你不懂它们之间的关系,就会出现:前端验证了后端没验证、密码存明文了、Token没过期时间、接口被人刷爆等各种问题。

第二个例子。

你做一个博客系统。要发文章、要看文章、要评论、要搜索、要做SEO、要做推荐。

每个功能你都能找到对应的技术方案。但关键问题是:

  • 先做哪个后做哪个?
  • 数据库表之间怎么关联?
  • 哪些数据要缓存哪些不用?
  • 搜索要做全文检索还是简单like查询?

这些问题没有标准答案,需要你根据实际需求去权衡去决策。

全栈的核心能力,就是理解这些技术怎么配合,然后做出合理的决策。


2026年的全栈技术图谱

既然说到全栈,我把一个现代 Web 应用涉及到的技术领域整理一下。不用全部记住,但需要知道大概有哪些东西,以及每个部分是干嘛的。


前端部分 —— 用户能看到的一切

前端就是用户打开浏览器能看到的所有东西。按钮能不能点、页面好不好看、表单能不能提交,这些都归前端管。

框架:用来构建用户界面。React是现在最主流的选择,Vue在国内用得也比较多,Next.js比较特殊,它既是前端框架,又自带后端能力,属于「全栈框架」。

样式:让界面好看。Tailwind CSS是现在的主流,因为它不用写单独的CSS文件,直接在HTML里写样式,很方便。

状态管理:管理页面数据。比如用户登录了,他的信息存在哪里?购物车有几件商品?这些数据的变化需要统一管理,Zustandmobx是轻量级的选择,Redux功能更全但也 更重。

UI组件:现成的界面零件。shadcn/ui现在特别火,它不是传统意义上的组件库,而是提供代码让你自己修改,这样你可以完全控制样式。


后端部分 —— 用户看不到但每天在用的

后端是服务器上运行的代码,你看不见它,但它在默默处理各种请求。用户登录、提交订单、查询数据——这些都需要后端来处理。

运行时:JS 可以在服务器上运行了,这就是Node.js,目前最成熟。Bun更快,Deno更现代(Node.js的原作者重新写的)。

框架:写后端代码的工具。Next.js API Routes是前后端一起写的方式,适合小项目。Hono非常轻量,而且天然支持 Edge 部署(边缘计算,后面会讲)。NestJS是企业级的,结构更严谨,适合大项目。

数据库:存数据的地方。PostgreSQL是目前最强悍的关系型数据库,MySQL是老牌稳定选手。简单理解:重要数据放数据库。

ORM:数据库和代码之间的翻译官。Prisma用起来很舒服,Drizzle更快且更轻, typeORM 功能更全。它们让你用 JS 的语法去操作数据库,不用写原始SQL。


基础设施 —— 让你的应用能跑起来

这部分是很多前端开发者最头疼的——代码写完了,怎么让它能被所有人访问?这就是基础设施要解决的问题。

服务器:一台24小时开机的电脑。国内的阿里云腾讯云,国外的VercelNetlify,都是提供服务器的服务商。

容器:把应用和它依赖的所有东西打包,这样在任何环境下都能跑。Docker是标配,Docker Compose用来在本机编排多个服务。

CDN:让用户访问更快。CDN就是一堆分布在世界各地的服务器,用户访问时从最近的服务器拿资源,速度会快很多。国际首选Cloudflare,国内用阿里云CDN

域名和SSL:域名是网站的地址,SSL是让访问变成https://的那个加密协议。Let's Encrypt提供免费SSL,Cloudflare可以自动帮你处理HTTPS。


运维监控 —— 保障服务稳定运行

应用上线了,怎么知道用户访问快不快?出错了怎么知道?这些就是运维监控要做的。

日志:记录系统发生了什么。ELK(Elasticsearch + Logstash + Kibana)是经典方案,Loki更轻量。现在很多云服务也自带日志功能。

监控:看系统健康不健康。Sentry专门追踪错误,谁的代码出错了第一时间知道。Prometheus + Grafana是看指标的,比如服务器CPU用了多少、数据库响应多快。

CI/CD:自动化部署。代码提交后自动测试、自动部署到服务器。GitHub Actions最常用,国内有阿里云效腾讯云CODING


安全 —— 保护你的应用

不做安全防护的应用,就像没装门的房子,谁都能进来。

前端安全:XSS是别人在你的页面里注入恶意脚本,CSRF是别人伪造你的身份发请求,CSP是限制页面能加载哪些资源。

后端安全:SQL注入是通过输入框往数据库里塞恶意代码,参数校验是确保用户传的数据是你期望的,Rate Limiting是限制一个人1分钟内只能发10次请求,防止被刷。

数据安全:HTTPS加密传输是最基本的,敏感数据(比如密码)要加密存储,密钥不要写在代码里。


AI能力 —— 新时代的必备技能

2026年了,如果你说自己是全栈但不懂 AI 用法,就像做前端不会用Git一样说不过去。

集成框架Vercel AI SDK是最流行的AI功能集成框架,支持流式响应(就是 ChatGPT 那种一个字一个字蹦出来的效果),对接各种模型很方便。

模型提供商:国外用OpenAI(GPT)、Anthropic(Claude),国内用硅基流动DeepSeek。国内外使用体验和成本差异很大,后面实战会分别讲。

向量数据库:AI场景专用。传统数据库存文字,向量数据库存「意思」。比如你搜「苹果」,它不仅能匹配到「苹果」,还能匹配到「iPhone」、「水果」,因为它理解「苹果」的含义。PineconeMilvus是代表。


这就是现代全栈的完整图谱。你不需要每样都精通,但需要知道它们各自负责什么,以及什么时候该用什么。


AI时代,全栈反而更重要了

我知道你可能会有疑问:现在AI这么强,Cursor敲几下代码就出来了,我还需要学全栈吗?

我的答案是:恰恰相反。

AI可以帮你写一个登录API,但它不知道:

  • 你的产品需不需要短信验证码登录
  • 你的用户数据存储在哪里
  • 你要不要支持微信登录
  • 登录失败几次要锁号
  • Token过期时间设多长

AI可以帮你写一个数据库查询,但它不知道:

  • 你的数据量级需要什么索引
  • 哪些查询需要加缓存
  • 读写分离怎么做

AI可以帮你部署上线,但它不知道:

  • 选择Vercel还是阿里云
  • 国内用户访问慢怎么办
  • 怎么做成本优化

AI擅长的是「点」,你需要的是「面」。

你告诉AI「帮我写个用户登录」,它会给你写一个标准答案。但具体怎么设计,这是你需要决策的事情。

而且,只有当你真正理解一个系统是怎么运转的,你才能:

  • 准确描述你想要什么(而不是永远在改需求)
  • 发现AI写的代码哪里有问题(而不是全盘接受)
  • 把不同模块组合在一起(而不是拼都拼不起来)

这才是整合能力的价值。

AI不是取代你,而是放大你。你原本只能做前端,AI帮你写了后端,你就能做全栈。但前提是,你本来就具备全栈思维,知道一个完整的产品需要什么。


怎么学?T型发展

说了这么多,到底怎么学?我的建议是「T型发展」:

先广度,后深度。

首先,对全栈技术有个整体认知。前端、后端、数据库、运维、安全……每个领域都了解一下,知道它们各自负责什么、解决什么问题。

这个阶段不需要深入,掌握概念就够了。

然后,选择一个方向深挖。

如果你对前端感兴趣,就深入React/Next.js。如果你对后端感兴趣,就深入Node.js/PostgreSQL。深入到能独立完成一个完整项目的程度。

最后,按需补充。

在实际项目中遇到什么问题,就去学什么。需要做支付,就去学Stripe。需要做搜索,就去学Elasticsearch。需要做 AI 功能,就去学Vercel AI SDK

这种「实战驱动」的学习方式,效率最高。


这个系列想带你做什么

市面上不缺技术教程。React入门、Node.js实战、Docker部署——这种内容一搜一大把。

但我发现很多人学完这些教程,还是做不出东西。

因为技术是散的,需要一条线把它们串起来。

这个系列我想带你做的事情很简单:从零开始,构建一个真正能上线的产品。

不是demo,不是练习,而是真实的、可访问的、能在生产环境跑的系统。

我会分成这几个阶段:

  • 第一阶段:认知重建

先理解全栈到底要学什么,怎么学(就是这篇)。

  • 第二阶段:基础设施

服务器、域名、CDN、Docker、日志、监控——那些「不太技术」但非常重要的东西。

  • 第三阶段:前端开发

React、Next.js、TypeScript、UI体系。

  • 第四阶段:后端开发

API设计、数据库、认证、缓存。

  • 第五阶段:AI集成

Vercel AI SDK、流式响应。

  • 第六阶段:部署上线

国内(阿里云)和国外(Vercel)两套方案。

  • 第七阶段:安全与性能

生产环境必须注意的那些事。

  • 第八阶段:实战

两个完整项目,从0到上线的全过程。

在这个过程中,你会看到我踩过的坑、做过的错误决策、总结出的经验。我不是为了告诉你「这个技术怎么用」,而是告诉你「这个系统该怎么搭」。


写在最后

回到开头的问题。

你是不是经常感觉学了很多技术,但真正要用的时候还是不知道从哪里开始?

这很正常。

技术本身不是目的,产品才是。

2026年了,AI 可以帮你写代码,但不能帮你交付产品。能做到这一点的人,永远有市场。

而这,就是我们这个系列要一起做的事情。

下一篇文章,我会讲讲AI辅助开发这件事——怎么用好CursorTraeOpenCode,以及一个更重要的道理:会问问题比会写代码更重要。

感兴趣的话,下一篇见。

昨天 — 2026年2月26日掘金 前端

「寻找年味」 沸点活动|获奖名单公示🎊

作者 掘金酱
2026年2月26日 18:15

image.png

🎉2026年 「寻找年味」 沸点活动正式落幕啦!

这个春节,我们在沸点里看见了无数动人瞬间:有故乡的烟火,有团圆的温柔,也有坚守岗位的专注与光芒。

每一条沸点,都是最真实、最可爱、最有温度的新年风景。

感谢每一位技术er,用文字、照片与 AI 创作,记录独属于程序员的春节时光,让技术与年味温暖相融。

由于获奖用户较多,详细的名单公示如下:[2026年 寻找年味沸点活动_获奖名单]

如何快速找到自己:进入飞书表格后,使用 Ctr+F 搜索自己的用户名或 用户id,选择“所有工作表”( 用户 id 即掘金主页 juejin.cn/XXXXXX 最后的一串数字)。之前打开过本表同学请刷新表单,奖项或有增补,以最新的表格为准。

image.png

活动奖品

沸点将根据评审团综合打分互动量得分加权计算。

✅ 常规内容赛道:TOP10 赢 SZCOOL 无线蓝牙音箱

✅ AI 内容赛道: TOP10 拿下「窝在一起」瓦楞猫窝

✅阳光普照奖:凡是符合#寻找年味主题的且有互动(排除作者自己点赞和评论)的沸点内容皆可获得10w 矿石

领奖方式

  • 获得上述奖项的掘友近期请注意 [系统消息] (预计2026年2月26日23:00前下发),请于 2026 年 3 月 4 日 23 点 之前在相关问卷中填写信息,过期将视为放弃奖品。
  • 常规内容赛道/AI 内容赛道 奖品将于问卷截止日期后的 30 个工作日内完成发放。
  • 阳光普照奖矿石将于2026年3月5日(申述时间截止后)发放。

若对获奖名单有疑问,请先自查沸点,确认无以下原因后,可点击联系 爱专研的安东尼 进行申诉。 申诉处理时间2026年2月26日-2026年3月4日,过时维持原结果。

  • 参赛沸点互动量按「点赞数 + 评论数」总和核算;

  • 同一用户发布多条内容,仅取数据最优一条参与排名获奖;

  • 无意义水评论、刷赞等违规行为,剔除获奖资格;

  • 数据统计截止至 2026 年 2 月 23 日 23:59,逾期新增互动不计入;

  • 禁止引战/制造冲突/谩骂内容,或者发现引战内容剔除获奖资格;

  • 评审团会依据沸点内容质量进行打分排序;

  • 非AI相关内容都归类为常规内容赛道,两个赛道不重复获奖,阳光普照奖可叠加赛道获奖;

再次感谢所有掘友的热情参与,愿大家新的一年代码无 Bug、技术节节高、万事皆顺遂!

常见的内存泄漏有哪些?

2026年2月26日 17:34

在 JavaScript 中,内存泄漏指的是应用程序不再需要某块内存,但由于某种原因,垃圾回收机制(GC, Garbage Collection)无法将其回收,导致内存占用持续升高,最终可能引发性能下降或崩溃。

以下是 JavaScript 中导致内存泄漏的最常见情况及示例:

1. 意外的全局变量

在 JavaScript 中,如果未声明的变量被赋值,它会自动成为全局对象的属性(浏览器中是 window,Node.js 中是 global)。全局变量在页面关闭前永远不会被垃圾回收。

function leak() {
  // 忘记了使用 let/const/var
  secretData = "这是一段敏感数据"; // 变成了 window.secretData
}
leak();

解决方案:

  • 使用严格模式 ('use strict') 来避免意外的全局变量。
  • 使用完后手动设置为 null

2. 被遗忘的定时器或回调函数

如果代码中设置了 setIntervalsetTimeout,但忘记清除(clear),且定时器内部引用了外部变量,那么这些变量无法被释放。

const someResource = hugeData(); // 很大的数据

setInterval(function() {
  // 这个回调引用了 someResource
  console.log(someResource);
}, 1000);

// 如果没有调用 clearInterval,someResource 会一直留在内存中

解决方案:

  • 在组件卸载或页面关闭时,清除定时器:clearInterval(id)

3. 闭包(Closures)的不当使用

闭包是 JavaScript 的强大特性,但如果闭包长期持有父函数的变量,而这些变量又很大,就会造成泄漏。

function outer() {
  const largeArray = new Array(1000000).fill('data');

  return function inner() {
    // inner 函数引用了 outer 作用域的 largeArray
    // 只要 inner 函数还存在,largeArray 就无法被回收
    console.log(largeArray.length);
  };
}

const innerFunc = outer(); // largeArray 被保留
// 如果后续没有释放 innerFunc,内存就会泄漏

解决方案:

  • 确保不再需要的函数被释放(innerFunc = null)。
  • 在闭包外尽量避免引用大对象。

4. DOM 引用未被清理

当把 DOM 元素存储为 JavaScript 对象或数据结构时,即使该元素已从 DOM 树中移除,只要 JS 中还有引用,该 DOM 元素连同其事件监听器就不会被释放。

const elements = {
  button: document.getElementById('button')
};

function removeButton() {
  document.body.removeChild(document.getElementById('button'));
  // 注意:elements.button 仍然指向那个 DOM 对象,所以它无法被回收
}

解决方案:

  • 移除 DOM 节点后,同时将变量设置为 null

5. 事件监听器未移除

向 DOM 元素添加了事件监听器,但在移除该元素前没有移除监听器。现代浏览器(尤其是针对原生 DOM 的监听器)处理得比以前好,但在单页应用(SPA, Single Page Application)中,如果频繁添加和移除元素,累积的监听器仍会导致泄漏。

const element = document.getElementById('button');
element.addEventListener('click', onClick);

// 如果后来 element 被移除了,但没有 removeEventListener
// 并且 onClick 函数引用了外部变量,就会造成泄漏

解决方案:

  • 在移除元素前调用 removeEventListener
  • 使用框架(如 React、Vue)时,框架的生命周期通常会自动处理,但要注意在 useEffect 的清理函数中移除原生监听器。

6. 脱离 DOM 树的引用(DOM 树内部引用)

这通常发生在给 DOM 元素添加自定义属性时。如果两个 DOM 元素相互引用,即使从文档流中移除,也可能因为循环引用导致泄漏(在老版本 IE 中常见,现代浏览器有所改进,但仍需注意)。

7. Map 或 Set 的不当使用

使用对象作为 MapSet 的 key,如果只把 key 置为 null,而没有从 Map 中删除它,key 依然被 Map 引用着,无法被回收。

let obj = {};
const map = new Map();
map.set(obj, 'some value');

obj = null; // 这里 obj 被置为 null
// 但 map 里仍然有对原对象的引用,所以原对象无法被回收

解决方案:

  • 使用 WeakMapWeakSet。它们的 key 是弱引用,不会阻止垃圾回收。

8. console.log 的影响

在开发环境调试时打印对象,如果线上环境忘记删除 console.log,控制台会一直持有对象的引用(特别是打印复杂对象时),导致对象无法被回收。现代浏览器在处理 console.log 时有所优化,但仍需注意。

建议:

  • 生产环境打包时移除所有 console.log

总结:如何避免内存泄漏?

  1. 使用 WeakMapWeakSet 存储对象引用。
  2. 及时清理:清除定时器、取消订阅、解绑事件。
  3. 避免全局变量,使用 let/const 和严格模式。
  4. 合理使用闭包,避免在闭包中持有大量数据的引用。
  5. 善用工具
    • 使用 Chrome DevTools 的 Memory 面板拍摄堆快照(Heap Snapshot),分析内存占用。
    • 使用 Performance 面板监控内存变化。

JavaScript 基础入门

2026年2月26日 17:19

如果把网页比作一个人:

  • HTML 是骨架:决定哪里是头、哪里是手。
  • CSS 是皮肤和衣服:决定长得好不好看。
  • JavaScript(简称 JS)是肌肉和神经:决定网页能不能“动”起来。

没有 JS,网页就是一张静态海报;有了 JS,网页就变成了可以互动的游戏、能验证密码的表单、能滑动加载的瀑布流。

趣味科普:1995 年,网景公司的程序员仅用 10天 就写出了 JS 的初版。为了蹭当时 Java 语言的热度,起名叫 JavaScript,但实际上它俩毫无关系(就像“雷锋”和“雷峰塔”)。如今,JS 已经成为能写网页、做手机 APP、写后端服务的“全栈霸主”。


核心前置知识:小白必懂的 3 个概念

在动手写代码前,必须先搞懂这 3 个“绕不开”的基础概念。

1. 变量:装数据的“带标签盒子”

变量的作用是给数据起个名字,方便反复使用。

声明关键字 特点 适用场景 示例
const 不可变(常量) 声明后不会再改变的数据 const name = "张三";
let 可变(变量) 声明后可能会被修改的数据 let age = 20; age = 21;

2. DOM:网页的“结构图纸”

DOM(文档对象模型) 是浏览器把 HTML 转换成的一棵“对象树”。 JS 看不懂 HTML 代码,但它能看懂 DOM 树。JS 拿着这张图纸,就能精准找到网页上的任何元素并修改它。

graph TD
    HTML((html)) --> HEAD((head))
    HTML --> BODY((body))
    HEAD --> TITLE[title: 网页标题]
    BODY --> H1[h1: 标题文字]
    BODY --> IMG[img: 图片]
    BODY --> BUTTON[button: 按钮]
    
    style HTML fill:#f9f2f4,stroke:#d04437,stroke-width:2px
    style BODY fill:#e6f2ff,stroke:#007bff,stroke-width:2px
    style H1 fill:#d9ead3,stroke:#93c47d
    style IMG fill:#d9ead3,stroke:#93c47d

3. 事件监听:给网页安上“传感器”

让 JS 盯着用户的动作(点击、滑动、输入),一旦触发,就执行对应的代码。

  • 核心语法addEventListener("事件类型", 触发后执行的代码)

综合实战:打造你的第一个交互网页

我们将通过一个综合案例,一次性掌握 改文字、换图片、存数据 三大核心技能。

步骤 1:搭建 HTML 骨架

新建 index.html,注意 <script> 标签里的 defer 属性,这是新手避坑的关键!

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>我的交互网页</title>
  <!-- 引入 JS。defer 表示“后台加载,等 HTML 渲染完再执行”,防止 JS 找不到元素 -->
  <script defer src="main.js"></script>
</head>
<body>
  <h1>原来的标题</h1>
  <img src="images/firefox-icon.png" alt="火狐图标" id="myImg">
  <button id="changeUserBtn">切换用户</button>
</body>
</html>

步骤 2:编写 JS 代码

新建 main.js,把下面的代码复制进去。我们分三步来实现交互:

魔法 1:自动修改标题(DOM 操作)

// 1. 找元素:用 querySelector 找到 <h1>
const myHeading = document.querySelector("h1");

// 2. 改内容:修改它的文字属性
myHeading.textContent = "Hello World!";

魔法 2:点击切换图片(事件监听 + 属性修改)

// 1. 找元素:找到图片
const myImage = document.querySelector("#myImg");

// 2. 听事件:监听点击动作
myImage.addEventListener("click", () => {
  // 3. 做处理:获取当前图片路径
  const currentSrc = myImage.getAttribute("src");
  
  // 4. 更页面:判断并切换路径
  if (currentSrc === "images/firefox-icon.png") {
    myImage.setAttribute("src", "images/firefox2.png"); // 换新图
  } else {
    myImage.setAttribute("src", "images/firefox-icon.png"); // 换回原图
  }
});

魔法 3:记住用户的名字(本地存储 + 弹窗)

这里我们会用到浏览器的“小仓库”——localStorage。它能把数据存在用户电脑上,刷新网页也不会丢。

const myButton = document.querySelector("#changeUserBtn");

// 定义一个设置名字的函数
function setUserName() {
  // prompt 会弹出一个输入框
  const userName = prompt("请输入你的名字:");
  
  if (!userName) { 
    setUserName(); // 如果没输入,就重新弹窗要求输入
  } else {
    // 存数据:把名字存进本地仓库,贴上标签叫 "savedName"
    localStorage.setItem("savedName", userName);
    // 模板字符串:用反引号 ` 和 ${} 把变量塞进句子里
    myHeading.textContent = `JS 太酷了,${userName}!`;
  }
}

// 页面刚打开时的判断逻辑
if (!localStorage.getItem("savedName")) {
  setUserName(); // 没存过,弹窗让用户输入
} else {
  const savedName = localStorage.getItem("savedName"); // 存过,直接取出来
  myHeading.textContent = `JS 太酷了,${savedName}!`;
}

// 点击按钮时,重新设置名字
myButton.addEventListener("click", () => {
  setUserName();
});

总结:JS 交互的“万能 4 步曲”

不管是做简单的按钮点击,还是复杂的网页游戏,JS 的核心交互逻辑永远逃不开这 4 步。把这张图印在脑子里,你就算真正入门了!

graph TD
    A[1. 找元素<br>querySelector] -->|找到目标| B[2. 听事件<br>addEventListener]
    B -->|用户触发| C[3. 做处理<br>逻辑判断/存取数据]
    C -->|计算完毕| D[4. 更页面<br>修改 DOM/样式]
    
    style A fill:#ffe6cc,stroke:#ff9900
    style B fill:#e6f2ff,stroke:#007bff
    style C fill:#e6ffe6,stroke:#28a745
    style D fill:#f9f2f4,stroke:#d04437

新手避坑指南

  1. 报错 Cannot read properties of null(找不到元素)
    • 原因:JS 执行时,HTML 还没加载完。
    • 解决:检查 <script> 标签有没有加 defer 属性,或者把 <script> 放到 </body> 标签的前面。
  2. 名字存了但刷新后不显示
    • 原因:存数据(setItem)和取数据(getItem)时,用的“标签名(key)”拼写不一致。
    • 解决:仔细检查字符串,比如是不是把 savedName 错拼成了 saveName

call、apply、bind 原理与实现

作者 二二四一
2026年2月26日 17:19

📍 第一章:先搞清楚 this 是什么

在讲 call/apply/bind 之前,必须理解 this 的指向规则:

// 1. 默认绑定:独立函数调用,this 指向全局(非严格模式)
function foo() { console.log(this); }
foo(); // window(浏览器)或 global(Node)

// 2. 隐式绑定:作为对象方法调用,this 指向该对象
const obj = { foo };
obj.foo(); // obj

// 3. 显式绑定:call/apply/bind 强制指定 this
foo.call(obj); // obj

// 4. new 绑定:构造函数,this 指向新创建的对象
new foo(); // foo 的实例

call/apply/bind 的作用:强行指定函数的 this,让函数执行时指向你给的对象。

🔧 第二章:call 方法深度拆解

2.1 call 的语法

fn.call(thisArg, arg1, arg2, ...)

2.2 call 的原理

一句话原理:把函数临时挂载到目标对象上执行,执行完再删除。

// 假设我们想让 fn 的 this 指向 obj
const obj = { name: 'obj' };
function fn() { console.log(this.name); }

// 1. 正常的 this 指向
fn(); // undefined(this 指向全局)

// 2. call 的魔法:obj.fn = fn; obj.fn(); delete obj.fn;
// 这就是 call 的底层原理!

2.3 手写实现

Function.prototype.myCall = function(context = window) {
    // ----- 第1步:处理 context -----
    // 为什么要用 Object()?
    // 如果 context 是原始值(如 123、'abc'、null、undefined),需要转成对象
    // null/undefined 会被转成空对象,原始值会被包装成对象
    context = Object(context);
    // 示例:
    // myCall(123)  → context = Number {123}
    // myCall(null) → context = {}
    
    // ----- 第2步:创建唯一属性名 -----
    // 为什么要用 Symbol?
    // 防止覆盖 context 上已有的属性
    // 比如 context 本来就有 fn 属性,直接用 'fn' 会覆盖
    const fnSymbol = Symbol('fn');
    
    // ----- 第3步:把函数挂到对象上 -----
    // 这里的 this 是谁?是调用 myCall 的那个函数!
    // fn.myCall(obj)  → this = fn
    context[fnSymbol] = this;
    
    // ----- 第4步:处理参数 -----
    // arguments 是所有参数的类数组对象
    // fn.myCall(obj, 1, 2, 3) → arguments = [obj, 1, 2, 3]
    const args = Array.from(arguments).slice(1);
    // slice(1) 去掉第一个参数(context)
    
    // ----- 第5步:执行函数 -----
    // 判断是否有参数,用扩展运算符传参
    const result = args.length 
        ? context[fnSymbol](...args)   // 有参数
        : context[fnSymbol]();          // 无参数
    
    // ----- 第6步:清理临时属性 -----
    // 用完就删,保持对象原样
    delete context[fnSymbol];
    
    // ----- 第7步:返回结果 -----
    return result;
};

2.4 执行过程可视化

function greet(greeting) {
    console.log(`${greeting}, ${this.name}`);
    return 'done';
}

const person = { name: '张三' };

// 调用
greet.myCall(person, 'Hello');

// 执行过程:
// 第1步:context = Object(person) → { name: '张三' }
// 第2步:fnSymbol = Symbol('fn')
// 第3步:context[fnSymbol] = greet
//       person 变成了:{ name: '张三', [Symbol(fn)]: greet }
// 第4步:args = ['Hello']
// 第5步:执行 context[fnSymbol]('Hello') → this 指向 person
// 第6步:delete context[fnSymbol] → person 恢复原样:{ name: '张三' }
// 第7步:返回 'done'

2.5 边界情况处理

// 情况1:context 是原始值
function test() { console.log(this); }
test.myCall(123); // Number {123}  ✅

// 情况2:context 是 null/undefined
test.myCall(null); // {}  ✅(非严格模式下转成全局对象)

// 情况3:函数有返回值
function add(a, b) { return a + b; }
console.log(add.myCall(null, 1, 2)); // 3 ✅

// 情况4:无参数
function logThis() { console.log(this); }
logThis.myCall(); // window ✅

📊 第三章:apply 方法深度拆解

3.1 apply 与 call 的唯一区别

// call:参数列表
fn.call(obj, 1, 2, 3);

// apply:参数数组
fn.apply(obj, [1, 2, 3]);

3.2 手写实现

Function.prototype.myApply = function(context = window, args = []) {
    // ----- 第1步:处理 context -----
    context = Object(context);
    
    // ----- 第2步:创建唯一属性名 -----
    const fnSymbol = Symbol('fn');
    context[fnSymbol] = this;
    
    // ----- 第3步:执行函数(唯一区别在这里)-----
    // args 是数组,直接用扩展运算符展开
    const result = args.length 
        ? context[fnSymbol](...args) 
        : context[fnSymbol]();
    
    // ----- 第4步:清理 -----
    delete context[fnSymbol];
    
    return result;
};

3.3 为什么要有 apply?

// 场景1:处理类数组对象
function sum() {
    // arguments 是类数组,不能直接用数组方法
    // 用 apply 传参
    const nums = Array.prototype.slice.apply(arguments);
    return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6

// 场景2:配合 Math.max/min
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 7
// ES6 可以用扩展运算符:Math.max(...numbers)

// 场景3:合并数组
const arr1 = [1, 2];
const arr2 = [3, 4];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4]

🔗 第四章:bind 方法深度拆解

4.1 bind 的核心特性

fn.bind(thisArg, arg1, arg2, ...)

三大特性

  1. 不立即执行:返回一个新函数
  2. 永久绑定 this:bind 后的函数 this 不能再用 call/apply 改变
  3. 支持柯里化:可以预置参数

4.2 基础版实现

Function.prototype.myBind = function(context = window, ...boundArgs) {
    // 保存原函数
    const originalFn = this;
    
    // 返回新函数
    return function(...callArgs) {
        // 合并参数:bind 时传的 + 调用时传的
        const allArgs = [...boundArgs, ...callArgs];
        
        // 用 apply 改变 this
        return originalFn.apply(context, allArgs);
    };
};

// 测试
function introduce(hobby, age) {
    console.log(`我是${this.name},喜欢${hobby},今年${age}岁`);
    return 'done';
}

const person = { name: '李四' };
const boundIntroduce = introduce.myBind(person, '编程');
const result = boundIntroduce(18); 
// 输出: 我是李四,喜欢编程,今年18岁
console.log(result); // done

4.3 进阶:考虑 new 的情况(完整版)

如果 bind 返回的函数被 new 调用,this 应该指向新创建的对象

Function.prototype.myBind = function(context = window, ...boundArgs) {
    const originalFn = this;
    
    // 返回的函数
    function boundFunction(...callArgs) {
        const allArgs = [...boundArgs, ...callArgs];
        
        // 关键判断:是否通过 new 调用
        // this instanceof boundFunction 为 true 说明用了 new
        const isNewCall = this instanceof boundFunction;
        
        // 如果是 new 调用,this 指向新对象;否则指向绑定的 context
        return originalFn.apply(
            isNewCall ? this : context,
            allArgs
        );
    }
    
    // 维护原型链:让返回的函数继承原函数的原型
    // 这样 new boundFunction() 创建的对象才能继承 originalFn.prototype
    boundFunction.prototype = Object.create(originalFn.prototype);
    
    return boundFunction;
};

4.4 new 场景测试

function Person(name, age) {
    this.name = name;
    this.age = age;
    console.log('构造函数执行了');
}

Person.prototype.sayHi = function() {
    console.log(`Hi, I'm ${this.name}`);
};

// bind 预置 name
const BoundPerson = Person.myBind(null, '王五');

// 用 new 调用
const p = new BoundPerson(25);
console.log(p); // Person { name: '王五', age: 25 } ✅
p.sayHi(); // Hi, I'm 王五 ✅(原型链也保留了)

// 如果不处理 new 的情况:
// p 会是 {},name/age 都挂到了 BoundPerson 上,原型链也断了 ❌

4.5 bind 的特性验证

// 特性1:永久绑定(一旦 bind,不能再用 call/apply 改变)
function fn() { console.log(this.name); }
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };

const boundFn = fn.bind(obj1);
boundFn(); // obj1 ✅

// 尝试用 call 改变
boundFn.call(obj2); // 还是 obj1 ✅(bind 优先级最高)

// 特性2:支持柯里化(预置参数)
function add(a, b, c) {
    return a + b + c;
}

const add5 = add.bind(null, 5);    // 预置 a = 5
const add5And10 = add5.bind(null, 10); // 预置 b = 10
console.log(add5And10(15)); // 5 + 10 + 15 = 30

// 特性3:this 优先级
// new > bind > call/apply > 隐式绑定 > 默认绑定

🎯 第五章:三者的优先级关系

// this 绑定优先级(从高到低)
// 1. new 绑定
// 2. bind 绑定
// 3. call/apply 绑定
// 4. 隐式绑定(对象.方法)
// 5. 默认绑定(独立调用)

function test() { console.log(this.name); }

const obj = { name: 'obj' };
const obj2 = { name: 'obj2' };

// bind 优先级 > call/apply
const bound = test.bind(obj);
bound.call(obj2); // obj(不是 obj2) ✅

// new 优先级 > bind
function Person(name) { this.name = name; }
const BoundPerson = Person.bind({ name: 'bindObj' });
const p = new BoundPerson('newObj');
console.log(p.name); // newObj(不是 bindObj)✅

💡 第六章:常见题解析

实现一个可以链式调用的 call

// 题目:让 fn.call.call(obj) 这种写法生效
function fn() { console.log(this); }

// 解析:fn.call.call(obj) 等价于
// (fn.call).call(obj)
// 即 Function.prototype.call 作为函数被调用

// 理解:
// fn.call 本身是一个函数(Function.prototype.call)
// .call(obj) 把 fn.call 的 this 指向 obj
// 所以执行的是 obj 上的 call 方法

bind 之后的函数 length 属性

function fn(a, b, c) {}
console.log(fn.length); // 3

const bound = fn.bind(null, 1);
console.log(bound.length); // 2(预置了一个参数,剩余 2 个)

// 原理:bind 返回的函数的 length = 原函数 length - 预置参数个数

实现函数的柯里化

// 用 bind 实现
function curry(fn, ...args) {
    return fn.length <= args.length
        ? fn(...args)
        : curry.bind(null, fn, ...args);
}

function sum(a, b, c) {
    return a + b + c;
}

const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6

📝 第七章:完整代码汇总

// call 完整实现
Function.prototype.myCall = function(context = window) {
    context = Object(context);
    const fnSymbol = Symbol();
    context[fnSymbol] = this;
    const args = Array.from(arguments).slice(1);
    const result = args.length ? context[fnSymbol](...args) : context[fnSymbol]();
    delete context[fnSymbol];
    return result;
};

// apply 完整实现
Function.prototype.myApply = function(context = window, args = []) {
    context = Object(context);
    const fnSymbol = Symbol();
    context[fnSymbol] = this;
    const result = args.length ? context[fnSymbol](...args) : context[fnSymbol]();
    delete context[fnSymbol];
    return result;
};

// bind 完整实现(含 new 处理)
Function.prototype.myBind = function(context = window, ...boundArgs) {
    const originalFn = this;
    
    function boundFunction(...callArgs) {
        const allArgs = [...boundArgs, ...callArgs];
        return originalFn.apply(
            this instanceof boundFunction ? this : context,
            allArgs
        );
    }
    
    boundFunction.prototype = Object.create(originalFn.prototype);
    return boundFunction;
};

🎓 第八章:总结对比

特性 call apply bind
执行时机 立即 立即 延迟
参数形式 列表 数组 列表(可分批)
返回值 函数结果 函数结果 新函数
this 永久性 一次性 一次性 永久
柯里化 不支持 不支持 支持
new 调用 无效 无效 有效(原函数可被 new)
实现难点 Symbol 防冲突 参数数组处理 new 判断 + 原型链

call 是立即执行+参数列表,apply 是立即执行+参数数组,bind 是返回新函数+永久绑定+支持柯里化

什么是事件循环?调用堆栈和任务队列之间有什么区别?

2026年2月26日 17:18

事件循环 (Event Loop)

事件循环是 JavaScript 运行时处理异步操作的核心机制,它使得 JavaScript 虽然是单线程的,但能够非阻塞地处理 I/O 操作和其他异步任务。

主要组成部分

  1. 调用堆栈 (Call Stack)

    • 一个后进先出(LIFO)的数据结构
    • 用于跟踪当前正在执行的函数
    • 当函数被调用时,会被推入堆栈;执行完毕后弹出
  2. 任务队列 (Task Queue)

    • 一个先进先出(FIFO)的数据结构
    • 存储待处理的消息(异步操作的回调)
    • 包括宏任务队列和微任务队列

调用堆栈 vs 任务队列

特性 调用堆栈 (Call Stack) 任务队列 (Task Queue)
结构 LIFO (后进先出) FIFO (先进先出)
内容 同步函数调用 异步回调函数
执行时机 立即执行 等待调用堆栈为空时才执行
优先级
溢出 可能导致"栈溢出"错误 不会溢出,但可能导致内存问题

事件循环的工作流程

  1. 执行调用堆栈中的同步代码
  2. 当调用堆栈为空时,事件循环检查任务队列
  3. 如果有待处理的任务,将第一个任务移到调用堆栈执行
  4. 重复这个过程

微任务队列 (Microtask Queue)

  • 比普通任务队列优先级更高
  • 包含 Promise 回调、MutationObserver 等
  • 在当前任务完成后、下一个任务开始前执行
  • 会一直执行直到微任务队列为空
console.log('1'); // 同步代码,直接执行

setTimeout(() => console.log('2'), 0); // 宏任务,放入任务队列

Promise.resolve().then(() => console.log('3')); // 微任务,放入微任务队列

console.log('4'); // 同步代码,直接执行

// 输出顺序: 1, 4, 3, 2

理解事件循环和这些队列的区别对于编写高效、无阻塞的 JavaScript 代码至关重要。

处理 I/O 操作的含义

I/O(Input/Output,输入/输出)操作是指程序与外部资源进行数据交换的过程。在JavaScript中,处理I/O操作特别重要,因为JavaScript是单线程的,而I/O操作通常是阻塞的(需要等待响应)。

常见的I/O操作类型

  1. 文件系统操作

    • 读写文件
    • 例如Node.js中的fs.readFile()
  2. 网络请求

    • HTTP/HTTPS请求
    • WebSocket通信
    • 例如fetch()XMLHttpRequest
  3. 数据库操作

    • 查询或更新数据库
    • 例如MongoDB、MySQL等数据库操作
  4. 用户输入

    • 键盘输入
    • 鼠标点击等交互事件

JavaScript如何处理I/O操作

JavaScript通过异步非阻塞方式处理I/O:

  1. 非阻塞特性

    • 发起I/O请求后,不等待结果立即继续执行后续代码
    • 避免线程被阻塞
  2. 回调机制

    • I/O完成后通过回调函数处理结果
    • 例如:
      fs.readFile('file.txt', (err, data) => {
        if (err) throw err;
        console.log(data);
      });
      
  3. Promise/async-await

    • 更现代的异步处理方式
    • 例如:
      async function fetchData() {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        console.log(data);
      }
      

为什么需要特殊处理I/O

  1. 性能考虑:I/O操作通常比CPU操作慢得多

    • 磁盘读取:毫秒级(10^-3秒)
    • 网络请求:可能达到秒级
  2. 单线程限制:JavaScript只有一个主线程

    • 如果同步等待I/O,整个程序会卡住
  3. 用户体验:在浏览器中,阻塞会导致页面无响应

事件循环中的I/O处理

当I/O操作完成时:

  1. 相应的回调函数被放入任务队列
  2. 事件循环在调用栈为空时从队列中取出回调执行
  3. 这使得JavaScript能够高效处理大量并发I/O
console.log('开始请求'); // 同步代码

// 异步I/O操作
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log('收到数据:', data)); // 回调

console.log('请求已发起,继续执行其他代码'); // 立即执行

// 可能的输出顺序:
// 开始请求
// 请求已发起,继续执行其他代码
// 收到数据: {...}

这种机制使得JavaScript特别适合I/O密集型应用(如Web服务器),能够高效处理大量并发请求而不需要创建多个线程。

防抖和节流:解决高频事件性能

作者 苑若轻航
2026年2月26日 17:10

在前端开发中,经常会遇到高频触发的事件,搜索框输入、页面滚动、按钮点击等,如果不对这些事件进行处理,频繁执行回调函数,可能会导致页面卡顿、请求次数过多,影响用户体验和系统性能。

防抖(Debounce)和节流(Throttle) 就是解决高频事件的秘密武器。它们的实现原理是闭包,用法相似,使用场景不同。应该根据实际场景,选择合适的方法进行处理。

为什么需要防抖节流

先从一个实际的例子入手,百度搜索,当你在搜索框输入关键字时,每次输入,都会触发键盘事件,若直接绑定网络请求,就会出现高频请求的问题。

如果不做任何处理,就会出现两个核心问题:

  • 执行次数过多:如果用户输入速度快,会在短时间内多次触发网络请求,网络次数过多,不仅服务器压力大,也会浪费前端性能。
  • 用户体验失衡:请求过快,频繁发送请求可能会导致响应混乱、页面卡顿;请求过慢,会导致联想建议延迟,影响使用体验

类似的场景还有很多,页面滚动加载、按钮重复提交,这些高频触发事件,都要通过防抖或节流来优化,避免“性能浪费”

这一切的实现,都离不开闭包的支持,利用闭包保留定时器ID,上一次执行时间等状态,让函数能够记住之前的执行状态,从而实现精准的触发控制,这就是防抖节流的核心底层逻辑。

防抖(Debounce): 多次触发,只执行最后一次

什么是防抖

防抖的核心逻辑:**在规定时间内,无论事件触发多少次,都只执行最后一次回调。**触发高频事件之后,函数不会立即执行,而是等待一段时间(延迟时间)后再执行;若在这段时间内事件再次被触发,则重新计时,只有当事件停止触发并且超过要延迟时间,函数才会执行一次。

防抖实现关键

/**
 * 防抖函数
 * @param {Function} fn 函数
 * @param {Number} delay 间隔
 */
function debounce(fn, delay) {
  var timer = null; // 变量(闭包核心):保存定时器ID
  return function(args) {
    if(timer) clearTimeout(timer); // 每次触发事件,先清除之前的定时器,重置倒计时
    var that = this; // 保存当前this指向,避免定时器内this丢失
    timer = setTimeout(function(){
      fn.call(that, args); // 推迟执行:延迟delay毫秒后,执行目标函数(最后一次触发的回调)
    }, delay);
  }
}

防抖应用场景

  • 搜索框输入联想:用户不断输入,等待用户停止输入后才触发联想请求;
  • 输入框实时校验:手机号、邮箱格式等,等待用户输入完成之后再校验,避免输入过程中频繁提示;
  • 按钮防止重复提交:例如表单提交按钮,避免用户连续点击发送多次请求;
  • 滚动条滚动检测:无需滚动过程中频繁检测,等待滚动停止后,判断滚动条位置。

节流(Throttle):每隔一段时间,只执行一次

什么是节流

节流的核心逻辑:**在规定时间内,无论事件触发多少次,都只执行一次回调。**触发高频事件,限制函数在指定时间间隔内只执行一次,无论事件触发多少次,都会按照固定的频率执行函数。

节流实现关键

/**
 * 节流函数
 * @param {Function} fn 函数
 * @param {Number} interval 间隔
 */
function throttle(fn, interval) {
    var enterTime = Date.now(); //触发时间
    return function() {
        var that = this, currentTime = Date.now(); // 当前时间
        if (currentTime - enterTime >= interval) { //判断是否到了指定间隔
            fn.apply(that, args);
            enterTime = Date.now(); //更新触发时间
        }
    }
}

节流应用场景

  • 按钮点击:点赞、刷新按钮,限制用户操作频率,防止恶意刷赞;
  • 滚动加载更多:用户滚动要页面,设置间隔,检测滚动位置,判断是否需要加载更多数据;
  • 鼠标移动:拖拽元素,固定频率更新元素位置,避免更新过于频繁,造成页面卡顿。

防抖和节流的区别

特性 防抖(Debounce) 节流(Throttle)
核心逻辑 等待最后一次触发后,延迟执行一次 固定时间间隔内,只执行一次
执行时机 事件停止触发后(延迟结束) 事件触发过程中(按固定频率)
触发频率 取决于事件停止触发的时间,可能很久执行一次 固定频率执行,不受事件触发频率影响
核心目的 过滤无效触发,保留最后一次结果 控制执行频率,避免过度执行
通俗比喻 电梯关门(有人进就重新计时) 水龙头滴水(固定时间滴一滴)

总结

防抖和节流解决问题的核心目的是一致:优化高频事件的性能,避免复杂任务频繁执行,两者的核心区别在于防抖只执行最后一次,节流是固定时间间隔内只执行一次。在实际运用过程中,需要根据不同场景合理选择使用防抖或节流,提升页面性能,优化用户体验。

【uniapp】小程序端解决分包的uni_modules打包后产物进入主包中的问题

2026年2月25日 11:42

配置

分包优化

需要在 mainfest.json 指定小程序节点下添加如下配置,例如:

{
  "mp-weixin": {
         "optimization": {
            "subPackages": true
          },
        "usingComponents": true
  }
}

主包分包的 uni_modules

首先,主包的 uni_moudles 要放在主包的根目录下,分包的 uni_moudles 要放在分包的根目录下

sub.jpg

然后,在 pages.json 中配置组件 easycom 引入规则,这一步是为了避免同一个组件库被主包分包都使用,出现识别错误的问题,例如,我在 uniappx 项目中使用了 rice-ui 组件库,可以这样配置

{
  "easycom": {
        "autoscan": true,
        "custom": {
            "^rice-(.*)": "uni_modules/rice-ui/components/rice-$1/rice-$1.uvue",
            "^sub-rice-(.*)": "sub/uni_modules/rice-ui/components/rice-$1/rice-$1.uvue"
        }
    }
}

这样,分包用组件就写 sub-rice-avatar,主包就是 rice-button

main.jpg

示例项目

测试项目在这个帖子末尾的附件 ask.dcloud.net.cn/article/423…

从 8 个实战场景深度拆解:为什么资深前端都爱柯里化?

2026年2月25日 09:21

你一定见过无数臃肿的 if-else 和重复嵌套的逻辑。在追求 AI-Native 开发的今天,代码的“原子化”程度直接决定了 AI 辅助重构的效率。

柯里化(Currying) 绝不仅仅是面试时的八股文,它是实现逻辑复用、配置解耦的工业级利器。通俗地说,它把一个多参数函数拆解成一系列单参数函数:f(a,b,c)f(a)(b)(c)f(a, b, c) \rightarrow f(a)(b)(c)

以下是 8 个直击前端实战痛点的柯里化应用案例。


1. 差异化日志系统:环境与等级的解耦

在web系统中,我们经常需要根据不同环境输出不同等级的日志。

JavaScript

const logger = (env) => (level) => (msg) => {
  console.log(`[${env.toUpperCase()}][${level}] ${msg} - ${new Date().toLocaleTimeString()}`);
};

const prodError = logger('prod')('ERROR');
const devDebug = logger('dev')('DEBUG');

prodError('支付接口超时'); // [PROD][ERROR] 支付接口超时 - 10:20:00

2. API 请求构造器:预设 BaseURL 与 Header

不用每次请求都传 Token 或域名,通过柯里化提前“锁死”配置。

JavaScript

const request = (baseUrl) => (headers) => (endpoint) => (params) => {
  return fetch(`${baseUrl}${endpoint}?${new URLSearchParams(params)}`, { headers });
};

const apiWithAuth = request('https://api.finance.com')({ 'Authorization': 'Bearer xxx' });
const getUser = apiWithAuth('/user');

getUser({ id: '888' }); 

3. DOM 事件监听:优雅传递额外参数

在 Vue 或 React 模板中,我们常为了传参写出 () => handleClick(id)。柯里化可以保持模板整洁并提高性能。

JavaScript

const handleMenuClick = (menuId) => (event) => {
  console.log(`点击了菜单: ${menuId}`, event.target);
};

// 模板中直接绑定:@click="handleMenuClick('settings')"

4. 复合校验逻辑:原子化验证规则

将复杂的表单校验拆解为可组合的原子。

JavaScript

const validate = (reg) => (tip) => (value) => {
  return reg.test(value) ? { pass: true } : { pass: false, tip };
};

const isMobile = validate(/^1[3-9]\d{9}$/)('手机号格式错误');
const isEmail = validate(/^\w+@\w+.\w+$/)('邮箱格式错误');

console.log(isMobile('13800138000')); // { pass: true }

5. 金融汇率换算:固定基准率

在处理多币种对账时,柯里化能帮你固定变动较慢的参数。

JavaScript

const convertCurrency = (rate) => (amount) => (amount * rate).toFixed(2);

const usdToCny = convertCurrency(7.24);
const eurToCny = convertCurrency(7.85);

console.log(usdToCny(100)); // 724.00

6. 动态 CSS 类名生成器:样式逻辑解耦

配合 CSS Modules 或 Tailwind 时,通过柯里化快速生成带状态的类名。

JavaScript

const createCls = (prefix) => (state) => (baseCls) => {
  return `${prefix}-${baseCls} ${state ? 'is-active' : ''}`;
};

const navCls = createCls('nav')(isActive);
const btnCls = navCls('button'); // "nav-button is-active"

7. 数据过滤管道:可组合的 Array 操作

在处理海量 AI Prompt 列表时,将过滤逻辑函数化,方便链式调用。

JavaScript

const filterBy = (key) => (value) => (item) => item[key].includes(value);

const filterByTag = filterBy('tag');
const prompts = [{ title: 'AI助手', tag: 'Finance' }, { title: '翻译机', tag: 'Tool' }];

const financePrompts = prompts.filter(filterByTag('Finance'));

8. AI Prompt 模板工厂:多层上下文注入

为你正在开发的 AI Prompt Manager 设计一个分层注入器:先注入角色,再注入上下文,最后注入用户输入。

JavaScript

const promptFactory = (role) => (context) => (input) => {
  return `Role: ${role}\nContext: ${context}\nUser says: ${input}`;
};

const financialExpert = promptFactory('Senior Financial Analyst')('Analyzing 2026 Q1 Report');
const finalPrompt = financialExpert('请总结该季报风险点');

Bun 全景指南:下一代 All-in-One 运行时详解与实战

2026年2月25日 08:47

1. 什么是 Bun?真正的 All-in-One

对于习惯了 Node.js 生态的开发者来说,Bun 不仅仅是另一个 JavaScript 运行时,它是一次对现代前端工程化的重构整合

Bun 是一个为速度而生的 All-in-one JavaScript 工具箱。它使用 Zig 语言从头构建。与 Node.js 不同,Bun 不仅仅是一个运行时(Runtime),它将前端开发所需的核心工具链全部内置到了一个二进制文件中:

  1. 运行时 (Runtime): 替代 Node.js,支持 TS/JS,基于 JavaScriptCore。
  2. 包管理器 (Package Manager): 替代 npm/yarn/pnpm,安装速度极快。
  3. 测试运行器 (Test Runner): 替代 Jest/Vitest,开箱即用,无需配置。
  4. 打包器 (Bundler): 替代 Webpack/Rollup/Esbuild,用于构建生产环境代码。

这种设计哲学旨在消除碎片化。你不再需要为了一个简单的项目去配置 package.json 中的十几个 devDependencies,也不用担心 ts-nodebabeljestwebpack 之间的版本冲突。Bun 给你提供了一个统一、标准且极速的开发环境。

2. 核心特性与使用实战

2.1 原生 TypeScript 支持

在 Node.js 中运行 TS 通常需要 ts-node 或构建步骤。在 Bun 中,TypeScript 是一等公民

# 直接运行 TS 文件,无需配置 tsconfig.json (虽然它也支持读取配置)
bun run index.ts

Bun 会在运行时直接转译 TypeScript(速度极快),无需预编译。注意:Bun 仅移除类型注解,不进行类型检查。生产构建时建议配合 tsc --noEmit 进行静态检查。

2.2 极速包管理器 (bun install)

Bun 的包管理器旨在替代 npm/yarn/pnpm。它利用全局缓存和硬链接(类似 pnpm),安装速度通常快 10-100 倍

bun install
bun add react
bun remove lodash

2.3 内置测试运行器 (bun test)

你不再需要 Jest 或 Vitest。Bun 内置了一个与 Jest 兼容的测试运行器。

// test.ts
import { expect, test } from "bun:test";

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

运行测试:

bun test

2.4 Web 标准 API 与内置服务器

Bun 实现了标准的 Web API,如 fetch, Request, Response, WebSocket。这意味着你在浏览器中学到的知识可以直接在服务端使用。

启动一个高性能 HTTP 服务器极其简单:

// server.ts
Bun.serve({
  port: 3000,
  fetch(req) {
    return new Response("Hello from Bun!");
  },
});

2.5 内置打包器 (bun build)

Bun 还是一个打包工具,旨在替代 Webpack/Rollup/Esbuild。

bun build ./index.tsx --outdir ./out --target browser

3. 为什么 Bun 这么快?

Bun 的“快”是全方位的,主要体现在三个维度:

3.1 启动速度 (Startup Time)

这是 Bun 最显著的优势。

  • 现象: 运行 bun run script.ts 几乎是瞬时的。
  • 原因:
    • JavaScriptCore (JSC): Bun 使用了 Safari 的 JS 引擎。JSC 针对移动端设备进行了极致优化,优先保证快速启动低内存占用。相比之下,V8 (Node.js) 更侧重于长时间运行后的 JIT 优化。
    • Zig 语言: Bun 的底层代码是用 Zig 编写的。Zig 允许手动管理内存,没有垃圾回收(GC)的暂停,且编译器能生成高度优化的机器码。

3.2 运行时性能 (Runtime Performance)

虽然 V8 在极致的 JIT 优化上非常强大,但 Bun 在很多 I/O 密集型任务上依然超越了 Node.js。

  • HTTP 服务器: Bun.serve() 基于 uWebSockets 库(C++编写),处理并发请求的能力远超 Node.js 的 http 模块。
  • SQLite: bun:sqlite 是直接嵌入在运行时中的,比 Node.js 通过桥接调用的 SQLite 快数倍。

3.3 工具链速度 (Tooling Speed)

  • 安装依赖: bun install 使用了二进制层面的优化,利用系统调用(System Calls)快速读写文件,并采用全局缓存 + 硬链接策略。
  • 转译/打包: Bun 的转译器是用 Zig 手写的,不依赖 Babel 或其他 JS 实现的 AST 解析器,因此速度极快。

3.4 理性看待:Bun 的局限性与 V8 的反击

回答一个常见疑问:Bun 真的在所有场景下都完胜 Node.js 吗? 答案是否定的。

  1. 启动快 vs 跑得快 (Startup vs Peak Performance)

    • Bun (JSC): 赢在起跑线。由于 JavaScriptCore 针对移动端(Safari)优化,它的冷启动速度极快,内存占用极低。这使得 Bun 非常适合CLI 工具Serverless 函数脚本等短生命周期任务。
    • Node.js (V8): 赢在长跑。V8 引擎拥有强大的 TurboFan 优化编译器。在长时间运行的计算密集型服务中,V8 的 JIT 优化往往能产生比 JSC 更高效的机器码,峰值吞吐量可能更高。
  2. 兼容性的“恐怖谷”

    • Bun 虽然实现了 90% 的 Node.js API,但在剩下的 10% 中隐藏着魔鬼。特别是涉及到C++ Addons、一些冷门的 fschild_process 边缘行为时,Bun 可能会表现出与 Node.js 不一致,导致现有项目迁移受阻。
  3. 生态系统的护城河

    • Node.js 经过 10+ 年的打磨,其稳定性是企业级的。Bun 虽然迭代快,但作为新生代,Bug 率相对较高。对于金融级或核心业务系统,Node.js 依然是更稳妥的选择。

4. 路径之争:前端工程化的未来

Node.js 生态目前正在经历一场“性能革命”,主要有两种演进路径:

4.1 路径 A:渐进式改良 (Node.js + Rust/Go)

这是目前的主流趋势,即在保留 Node.js 运行时的基础上,用高性能语言重写上层工具链。

  • 代表: Vite (使用 esbuild/Rollup), SWC (Rust版 Babel), Biome (Rust版 Prettier/ESLint)。
  • 优势: 兼容性好。用户依然运行在 Node.js 上,迁移成本低,生态惯性大。
  • 劣势: 碎片化依旧。底层 Node.js 的启动速度瓶颈依然存在,且工具链之间的数据交换(JS <-> Rust/Go)存在序列化开销。

4.2 路径 B:革命式重构 (Bun)

这是 Bun 选择的道路,从底层 Runtime 到上层 Tooling 全部推倒重来。

  • 代表: Bun。
  • 优势: 极致性能与体验。消除了所有中间环节的开销,统一了标准,实现了真正的 All-in-One。
  • 劣势: 兼容性挑战。虽然 Bun 努力兼容 Node.js API,但在 C++ Addons 和一些边缘 API 上仍需时间完善。

4.3 另一个挑战者:Deno

提到新的运行时,不得不提 Deno。它由 Node.js 之父 Ryan Dahl 创立,旨在“修复 Node.js 的设计遗憾”。

特性 Node.js Deno Bun
核心哲学 稳定、生态丰富 安全、Web 标准、修正设计 极致性能、无缝兼容
JS 引擎 V8 V8 JavaScriptCore (启动更快)
编写语言 C++ Rust Zig
兼容性 基准 逐步兼容 npm 优先兼容 Node API
优势 庞大生态 安全性 (默认无权限)、TS 开箱即用 速度 (启动/安装/运行)、All-in-One

核心区别总结

  • Deno (理想主义): 强调安全性Web 标准。它试图纠正 Node.js 的历史包袱(如 node_modules),推崇 URL 导入和显式权限管理。虽然现在也支持 npm,但其初衷是建立一个更“正确”的运行时。
  • Bun (实用主义): 强调速度兼容性。它不试图改变开发习惯,而是致力于让你现有的 npm 项目跑得飞快。对于大多数希望“无痛提速”的开发者来说,Bun 的迁移阻力更小。

4.4 结论

Bun 是一条鲶鱼。它的出现已经迫使 Node.js 社区加速进化(Node 最近也在添加内置测试运行器、.env 支持等)。

对于开发者,现在是学习 Bun 的最佳时机。它代表了前端工程化向“高性能、统一化”发展的未来方向。即使你不立即在生产环境使用它,它的设计理念和对 Web 标准的拥抱也能让你对现代 JavaScript 运行时有更深的理解。

5. 市场现状与数据 (2024)

虽然 Bun 在技术圈的讨论度极高,但客观地看,它仍处于早期快速增长阶段

5.1 开发者调研数据

根据 State of JS 2024 (最新发布) 的数据:

  • 使用率: Bun 的关注度和使用率持续上升。在“运行时”类别中,虽然 Node.js 依然占据统治地位,但 Bun 已成为增长最快的挑战者。
  • 关注度: Bun 连续两年在开发者调查中获得极高的“感兴趣” (Interest) 评分,这表明它在开发者群体中拥有极高的 Mindshare(心智占有率)。
  • Stack Overflow 2024: 在更广泛的开发者调查中,Bun 被列为增长最快的运行时之一,特别是在追求高性能的 Web 框架(如 ElysiaJS, Hono)社区中采用率极高。

5.2 实际落地场景

目前 Bun 的核心用户群并非直接替换大型企业的核心业务后端,而是集中在以下领域:

  1. CI/CD 流水线: 利用 bun install 极速安装依赖,显著缩短构建时间。
  2. Serverless/Edge: 利用其毫秒级的冷启动速度(比 Node.js 快 3-5 倍),在 AWS Lambda、Cloudflare Workers 等场景下降低延迟和成本。
  3. 新一代框架: 像 ElysiaJS 这样的框架专门为 Bun 优化,性能霸榜,吸引了大量追求极致性能的开发者。
  4. PaaS 支持: 主流云平台如 Railway, Vercel, Netlify 均已原生支持 Bun,降低了部署门槛。

总结: Bun 目前的市场份额虽然无法与 Node.js 抗衡,但其20%+ 的早期采用率证明了它不仅仅是个玩具。它正在通过“农村包围城市”的策略(先占领工具链和边缘计算,再渗透核心运行时)逐步扩大版图。

6. 实战案例:Opencode 如何使用 Bun

6.1 关于 Opencode

在深入技术细节之前,先简单介绍一下案例背景。Opencode 是一个开源的现代 AI 编程助手。与传统的 IDE 插件不同,Opencode 采用 Client-Server 架构,旨在通过 Agent(智能体)自主完成复杂的编程任务。

  • 核心能力: 它可以理解自然语言指令,通过工具(Tools)自主执行终端命令、读写文件、运行测试,甚至进行复杂的代码重构。
  • 技术栈: 项目完全基于 TypeScript 构建,采用 Monorepo 结构,而 Bun 正是其底层的运行时基石。

Opencode 选择 Bun 并非偶然,而是为了满足 AI 编程助手对低延迟高吞吐环境一致性的苛刻要求。

6.2 精确的依赖管理 (Lockfile Policy)

在 [bunfig.toml] 中,Opencode 配置了 save-exact = true

[install]
exact = true
  • 作用: 这意味着当你运行 bun add react 时,Bun 会在 package.json 中写入 "react": "18.2.0" 而不是 "^18.2.0"
  • 收益: 这在大型工程中至关重要,它锁死了所有依赖的确切版本,消除了“在我机器上能跑,在你机器上报错”的幽灵依赖问题,确保了团队开发环境的一致性。

6.2 高性能 HTTP Server (Hono + Bun)

Opencode 的核心是一个 HTTP Server,用于处理 CLI 命令和 AI 会话。它使用了 Hono 框架,并直接运行在 Bun.serve 上。

参考代码 [server.ts]:

import { Hono } from "hono"
import { websocket } from "hono/bun" // 直接使用 Bun 原生 WebSocket

const app = new Hono()

// Hono 会自动检测并使用 Bun.serve 启动
export default {
  port: 4096,
  fetch: app.fetch,
  websocket // 利用 Bun 的高性能 WebSocket 实现
}
  • 收益: 相比于 Node.js + Express,这种组合的吞吐量通常高出 3-5 倍,且内存占用极低。这对于需要常驻后台的 Agent 服务来说非常重要。

6.3 极速测试 (Bun Test)

Opencode 抛弃了 Jest,直接使用 bun test。在 [package.json] 中可以看到:

"scripts": {
  "test": "bun test --timeout 30000"
}
  • 收益: 测试启动几乎是瞬时的。对于包含大量单元测试的 Monorepo,这意味着 CI 流水线的时间可以从几分钟缩短到几十秒。

6.4 Monorepo 工作空间

Opencode 是一个多包项目 (packages/opencode, packages/sdk, packages/util)。Bun 原生支持 Workspace 协议:

"dependencies": {
  "@opencode-ai/sdk": "workspace:*",
  "@opencode-ai/util": "workspace:*"
}
  • 收益: Bun 会自动将本地包链接在一起,修改 sdk 的代码后,opencode 能立即感知,无需重新构建或 npm link

附录:技术选型深度解析

1. 为什么选择 Zig?

Zig 是一门现代系统编程语言,旨在替代 C。

  • 手动内存管理,但更安全: Zig 没有垃圾回收(GC),这让 Bun 能精确控制内存行为,避免了 GC 带来的不可预测的暂停。同时它提供了比 C 更好的内存安全检查。
  • Comptime (编译时执行): Zig 允许在编译阶段执行代码。Bun 利用这一点在编译时预计算了很多数据结构,减少了运行时的开销。
  • C 互操作性: Zig 可以直接引用 C 头文件并链接 C 库。这对于 Bun 集成 JavaScriptCore(它是 C++ 写的,但在 Zig 中调用非常方便)至关重要。

2. 为什么选择 JavaScriptCore (JSC) 而非 V8?

这是 Bun 最具争议也最核心的决策之一。

特性 V8 (Chrome/Node.js) JavaScriptCore (Safari/Bun)
主要优化目标 桌面端/服务器的高性能计算 移动端的快速启动省电
JIT 策略 激进的 TurboFan,预热后代码运行极快 分层编译,优先保证启动速度,后续再优化热点代码
启动时间 较慢(需加载庞大的优化编译器) 极快
内存占用 较高 较低

Bun 的选择逻辑: 现代前端开发(以及 Serverless/Edge 场景)最大的痛点往往是 "启动慢"(运行一个简单的 CLI 命令都要等几秒)。JSC 的架构天然适合解决这个问题。虽然在长时间运行的纯计算任务上 V8 可能略胜一筹,但在 99% 的 Web 场景(I/O 密集型、短生命周期脚本)中,JSC 的快速启动和低内存占用带来的体验提升更为明显。

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…)

❌
❌