普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月9日技术

How Fiber Network Works

作者 yukang
2025年12月10日 01:05

I just got back from CKCon in beautiful Chiang Mai 🌴, where I gave a talk on the Fiber Network. To help everyone wrap their heads around how Fiber (CKB’s Lightning Network) actually moves assets, I hacked a visual simulation with AI.

To my surprise, people didn’t just understand it—they loved it! 🎉

So, if you missed the talk, here is the “too long; didn’t read” version. But first, go ahead and play with the dots yourself, 👉 Play the Simulation: fiber-simulation

We all love Layer 1 blockchains like Bitcoin or CKB for their security, but let’s be honest: they aren’t exactly built for speed.

Every transaction has to be shouted out to the entire world and written down by thousands of nodes. On CKB, you’re waiting about 8 seconds for a block; on Bitcoin, it’s 10 minutes! Plus, the fees can get nasty if you’re just trying to buy a coffee. ☕️

So, how do we fix this?

The Lightning Network is a scalable, low-fee, and instant micro-payment solution for P2P payments.

The secret sauce isn’t actually new. Even Satoshi Nakamoto hinted at this “high-frequency” magic in an early email:

Intermediate transactions do not need to be broadcast. Only the final outcome gets recorded by the network.

A Lightning Network consists of Peers and Channels. A peer can send, receive, or forward a payment. A Channel is used for communication between two Peers.

Imagine you and a friend want to trade money back and forth quickly:

  1. Opening the Channel: You both put some money into a pot and sign a Funding Tx. This goes on the blockchain (L1).
  2. The Fun Part (Off-Chain): Now that the channel is open, you can send money back and forth a million times instantly! You just update the balance sheet between you two (using HTLCs and signatures). No one else needs to know, and no blockchain fees are paid yet.
  3. Closing the Channel: When you’re done, you agree on the final balance, sign a Shutdown Tx, and tell the blockchain.

Everything in the middle? That’s off-chain magic. ✨

Now, if Fiber was just about paying your direct neighbor, it would be boring. The real power comes from the Network.

This means Alice can pay Bob even if they don’t have a direct channel between them. The payment can travel through one or more intermediate nodes. As long as there is a path with enough liquidity, the payment will reach its destination instantly.

All data is wrapped in Onion Packets (yes, like layers of an onion). The nodes in the middle serve as couriers, but they are blindfolded:

  • They don’t know who sent the money.
  • They don’t know who is receiving it.
  • They only know “pass this to the next guy.”

They simply follow a basic rule: they forward the Hash Time Lock, and if the payment succeeds, they earn a tiny fee for their trouble. Easy peasy.

The “Not So Easy” Part 😅

While the idea is simple, building it is… well, an engineering adventure. We’re dealing with cryptography, heavy concurrency, routing algorithms, and a whole jungle of edge cases. But hey, that’s what makes it fun!

We’ve poured the last two years into building Fiber, and I’m proud to say it’s finally GA ready.

If you want to geek out on the details, check these out:

Here is the full presentation from my talk:
CKB Fiber Network Engineering Updates

数据字典技术方案实战

作者 树深遇鹿
2025年12月9日 18:01

前言

在后台与中台系统开发领域,数据字典是极为常见且至关重要的概念,相信大多数从事相关开发工作的朋友都对其耳熟能详。几乎每一个成熟的项目,都会专门设置一个字典模块,用于精心维护各类字典数据。这些字典数据在系统中扮演着举足轻重的角色,为下拉框、转义值、单选按钮等组件提供了不可或缺的基础数据支撑。

我自工作以来参与过很多个项目,既有从零开始搭建的,也有接手他人项目的。在实践过程中,我发现不同项目对字典的实现方式各不相同,且各有侧重。例如,对于项目中的字典基本不会发生变化的,项目通常会采用首次全部加载到本地缓存的方式。这种方式能显著节省网络请求次数,提升系统响应速度。然而,对于项目中的字典经常变动的,项目则会采用按需加载的方式,即哪里需要使用字典值,就在哪里进行加载。但这种方式也存在弊端,当某个页面需要使用十多个字典值时,首次进入页面会一次性发出十多个请求来获取这些字典值,影响用户体验。

常见字典方案剖析

在当下,数据字典的实现方案丰富多样,各有优劣。下面将详细介绍几种常见的方案,并分析其特点。我将详细介绍几种常见的方案,并深入剖析其特点。这几种方案皆是我通过实践精心总结而来,其中方案四的思路是由我不爱吃鱼啦提供。

方案一:首次全部加载到本地进行缓存

方案描述

系统启动或用户首次访问时,将所有字典数据一次性加载到本地缓存中。后续使用过程中,直接从缓存中获取所需字典数据,无需再次向服务器发起请求。

优点

  • 访问速度快:后续访问时直接从本地缓存读取数据,无需等待网络请求,响应速度极快。
  • 减少网络请求:一次性加载后,后续使用无需频繁发起网络请求,降低了网络开销。
  • 网络依赖小:即使在网络不稳定的情况下,也能正常使用已缓存的字典数据,保证了系统的稳定性。

缺点

  • 首次加载时间长:若字典数据量较大,首次加载时可能需要较长时间,影响用户体验。
  • 占用存储空间:将所有字典数据存储在本地,会占用较多的本地存储空间,尤其是当字典数据量庞大时。
  • 缓存更新复杂:若字典数据频繁更新,需要设计复杂的缓存同步和更新机制,否则容易出现数据不一致的问题。

方案二:按需加载不缓存

方案描述

当用户触发特定操作,需要使用字典数据时,才从后端实时加载所需数据,且不进行本地缓存。每次使用字典数据时,都重新从服务器获取最新数据。

优点

  • 节省存储空间:不进行本地缓存,节省了本地存储空间,尤其适用于存储资源有限的设备。
  • 数据实时性高:每次获取的数据都是最新的,不存在缓存数据与后端不一致的问题,保证了数据的准确性。

缺点

  • 网络请求频繁:每次使用都需要发起网络请求,在网络状况不佳时,会导致加载时间变长,影响用户体验。
  • 增加服务器负担:频繁的网络请求会增加服务器的负担,尤其是在高并发场景下,可能影响服务器的性能。

方案三:首次按需加载并缓存

方案描述

用户首次访问某个字典数据时,从后端加载该数据并缓存到本地。后续再次访问该字典数据时,直接从缓存中读取,无需再次向服务器发起请求。

优点

  • 减少网络请求:结合了前两种方案的部分优点,既在一定程度上减少了网络请求次数,又不会一次性加载过多数据。
  • 节省存储空间:相较于首次全部加载到本地缓存的方式,不会一次性占用大量本地存储空间,节省了部分存储资源。

缺点

  • 缓存管理复杂:需要记录哪些数据已缓存,以便后续判断是否需要从缓存中读取或重新加载,增加了缓存管理的复杂度。
  • 缓存占用问题:对于不常使用的字典数据,缓存可能会占用不必要的存储空间,造成资源浪费。
  • 缓存更新难题:同样面临缓存更新的问题,需要设计合理的缓存更新策略,以保证数据的准确性和一致性。

方案四:按需加载 + 版本校验更新缓存

方案描述

用户按需发起字典数据请求,首次访问某个字典数据时,从后端加载并缓存到本地。在后端响应头中携带该字典数据的版本信息,后续每次请求该字典数据时,前端对比本地缓存的版本信息和响应头中的版本信息。若版本信息不一致,则清除本地缓存中对应的字典数据,并重新从后端加载最新数据;若版本信息一致,则直接使用本地缓存的数据。

优点

  • 数据实时性有保障:通过版本校验机制,能够及时获取到字典数据的更新,确保前端使用的数据与后端保持一致,避免了因缓存数据未及时更新而导致的业务问题。
  • 减少不必要的网络请求:在字典数据未更新时,直接使用本地缓存,无需发起网络请求,节省了网络带宽和服务器资源。
  • 平衡存储与性能:既不会像首次全部加载那样占用大量本地存储空间,又能在一定程度上减少网络请求,在存储和性能之间取得了较好的平衡。

缺点

  • 版本管理复杂:后端需要维护字典数据的版本信息,并且要确保版本号的准确性和唯一性,这增加了后端开发的复杂度和维护成本。
  • 额外开销:每次请求都需要进行版本信息对比操作,虽然开销较小,但在高并发场景下,可能会对系统性能产生一定影响。
  • 首次加载体验:首次加载字典数据时,依然需要从后端获取数据,若数据量较大或网络状况不佳,可能会影响用户体验。

方案选型建议

建议根据项目特性选择方案,没有最好的技术方案,只有最适合项目的技术方案:

  • 字典稳定且量小:方案一全量缓存
  • 字典频繁更新:方案四版本校验缓存
  • 存储敏感场景:方案三按需缓存
  • 实时性要求极高:方案二无缓存方案

ps:如果大家有更好的方案,也可以在评论区提出,让我们大家一起学习成长

代码实现(方案四)

下述代码的实现基于vue3+pinia,该代码实现了统一管理全局字典数据,支持按需加载、缓存复用、版本控制、动态更新、批量处理字典数据等功能。

pinia store的实现

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getDictDetails, type Details } from '@/api/system/dict'

export const useDictStore = defineStore('dict', () => {
    // 存储字典数据,键为字典名称,值为字典详情数组
    const dictData = ref<Record<string, Details[]>>({})
    // 存储字典版本信息,键为字典名称,值为版本号
    const dictVersions = ref<string>('')

    /**
     * 更新字典版本信息
     * @param version 新的字典版本号
     */
    const updateDictVersion = (version: string) => {
        dictVersions.value = version
    }

    /**
     * 获取字典版本
     * @returns 字典版本号
     */
    const getDictVersion = () => {
        return dictVersions.value || ''
    }

    /**
     * 加载字典数据
     * @param dictNames 字典名称数组
     * @returns 加载的字典数据对象
     */
    const getDicts = async (dictNames: string[]) => {
        try {
            if (!Array.isArray(dictNames)) {
                return {};
            }
            // 过滤并去重有效字典名称
            const uniqueNames = [...new Set(dictNames.filter(name => 
                typeof name === 'string' && name.trim()
            ))];
            
            if (uniqueNames.length === 0) {
                return {};
            }

            const result: Record<string, Details[]> = {};
            const unloadedDicts: string[] = [];

            // 分离已加载和未加载的字典
            dictNames.forEach(name => {
                if (dictData.value[name]) {
                    result[name] = dictData.value[name];
                } else {
                    unloadedDicts.push(name);
                }
            });

            // 如果有未加载的字典,从接口请求获取
            if (unloadedDicts.length > 0) {
                const { data } = await getDictDetails(unloadedDicts);

                // 合并新加载的数据到结果
                Object.assign(result, data);

                // 更新全局字典缓存
                Object.assign(dictData.value, data);
            }

            return result;
        } catch (error) {
            console.error('加载字典数据失败:', error);
            return {};
        }
    };

    /**
     * 根据字典名称获取字典数据
     * @param name 字典名称
     * @returns 字典详情数组
     */
    const getDict = (name: string) => {
        return dictData.value[name] || []
    }

    /**
     * 根据字典名称和值获取字典标签
     * @param name 字典名称
     * @param value 字典值
     * @returns 字典标签
     */
    const getDictLabel = (name: string, value: string) => {
        const dict = getDict(name)
        const item = dict.find(item => item.value === value)
        return item?.label || ''
    }

    /**
     * 根据字典名称和标签获取字典值
     * @param name 字典名称
     * @param label 字典标签
     * @returns 字典值
     */
    const getDictValue = (name: string, label: string) => {
        const dict = getDict(name)
        const item = dict.find(item => item.label === label)
        return item?.value || ''
    }

    /**
     * 清除指定字典数据
     * @param names 字典名称
     */
    const clearDicts = (names: string[]) => {
        names.forEach(name => {
            clearDict(name)
        })
    }


    /**
     * 清除指定字典数据
     * @param name 字典名称
     */
    const clearDict = (name: string) => {
        delete dictData.value[name]
    }

    /**
     * 清除所有字典数据
     */
    const clearAllDict = () => {
        dictData.value = {}
    }

    return {
        dictData,
        updateDictVersion,
        getDictVersion,
        getDict,
        getDicts,
        getDictLabel,
        getDictValue,
        clearDict,
        clearDicts,
        clearAllDict
    }
})

useDict 实现

为组件提供字典数据的统一访问入口,封装了字典数据的初始化加载、详情查询、标签/值转换等高频操作,简化组件层对字典数据的调用逻辑。

import { type Details } from '@/api/system/dict'
import { useDictStore } from '@/store/dict'

// 根据字典值的name获取字典详情
export const useDict = (params: string[] = []) => {

  const dict = ref<Record<string, Details[]>>()
  const dictStore = useDictStore()

  const getDicts = async () => {
    dict.value = await dictStore.getDicts(params)
  }

  // 初始化字典数据
  getDicts()

  // 根据字典名称获取字典数据
  const getDict = (name: string) => {
    return dictStore.getDict(name)
  }

  // 根据字典值获取字典label
  const getDictLabel = (name: string, value: string) => {
    return dictStore.getDictLabel(name, value)
  }

  return {
    dict,
    getDict,
    getDictLabel
  }
}

响应拦截

主要用于获取字典的版本信息,通过对比版本信息,从而确定是否清除本地的字典缓存数据,并更新本地缓存的版本信息

// 响应拦截器
service.interceptors.response.use(
  // AxiosResponse
  (response: AxiosResponse) => {
    const dictVersion = response.headers['x-dictionary-version']
    if (dictVersion) {
      const dictStore = useDictStore()
      // 对比版本是否有更新
      if (dictStore.getDictVersion() !== dictVersion) {
        dictStore.clearAllDict()
        dictStore.updateDictVersion(dictVersion || '')
      }
    }
    // ...项目中的业务逻辑
  }
)

项目中的具体使用

下述的怎么使用封装的字典管理的简单demo

<script setup lang="ts">
import { useDict } from '@/hooks/useDict'
// 获取dict
const { dict, getDictLabel } =  useDict(['status', 'sex'])
console.log(dict.status, dict.sex)
</script>

结语

本文介绍了四种主流的数据字典实现方案,从全量加载到按需加载,从无缓存到版本校验缓存,每种方案都展现了其独特的优势与缺点。通过对比分析,我们不难发现,没有一种方案能够适用于所有场景,而是需要根据项目的具体特性进行灵活选择。对于字典稳定且量小的项目,全量缓存方案能够带来极致的响应速度;对于字典频繁更新的场景,版本校验缓存方案则能在保障数据实时性的同时,实现存储空间与网络请求的平衡优化。未来,随着技术的不断进步与应用场景的不断拓展,数据字典的实现方案也将持续演进。

博客主要记录一些学习的文章,如有不足,望大家指出,谢谢。

webpack和vite区别及原理实现

作者 光影少年
2025年12月9日 17:52

WebpackVite 都是用于构建现代前端应用的构建工具,它们在原理和实现上有显著的区别。下面我将详细比较它们的异同,帮助你了解两者的工作原理以及各自的优势。


一、Webpack 和 Vite 的核心区别

特性 Webpack Vite
构建速度 较慢,特别是大型项目 快,几乎是即时的
构建原理 通过打包所有资源,生成最终的 bundle 采用按需编译,利用浏览器原生支持 ES 模块
开发模式 一开始就进行全部的打包,编译速度较慢 通过浏览器原生支持 ES Modules,只有请求的模块才会被处理
构建产物 生成一个或多个 bundle 文件 基于 ES Module 按需加载,不同于 Webpack 完整的打包
支持类型 支持所有 JavaScript,CSS,图片,字体等 主要支持 ES 模块,针对现代浏览器优化
使用体验 配置复杂,适用于各种需求和优化 配置简单,适合快速开发,但功能不如 Webpack 灵活

二、Webpack 原理和实现

1. 传统的打包工具

Webpack 是一个 模块打包器,它将所有的静态资源(JavaScript、CSS、图片等)当作模块处理,并生成一个或多个 bundle 文件,最终这些文件将被浏览器加载。

2. 打包过程:

Webpack 的打包过程主要包含以下几个阶段:

  1. 解析阶段(Parsing)

    • Webpack 从入口文件(entry)开始,递归地解析每一个依赖,生成依赖图。
    • 在解析时,Webpack 会调用 loader 对不同类型的文件进行预处理(如 Babel 转译、Sass 编译等)。
  2. 构建阶段(Building)

    • Webpack 会通过 loaderplugin 处理所有模块,生成最终的 AST(抽象语法树)
    • 使用 module bundling 将所有模块合并成一个或多个文件(bundle)。
  3. 优化阶段(Optimization)

    • Webpack 会对生成的 bundle 进行优化,如:分割代码(Code Splitting)、压缩(Terser)等。
  4. 输出阶段(Output)

    • 最终将 bundle 输出到指定的目录,并生成相应的文件供浏览器使用。

3. Webpack 需要时间打包所有资源

由于 Webpack 会将所有资源都打包成一个或多个文件,所以当你做 webpack --mode development 命令时,它必须编译所有文件,这就导致开发过程中启动时间较长。


三、Vite 原理和实现

1. 基于浏览器原生支持的 ES Modules

Vite 的核心原理是利用浏览器原生支持 ES Modules,它并不像 Webpack 那样进行完整的打包,而是通过 按需加载 来提高构建速度。

2. Vite 开发流程:

Vite 的开发过程分为两个阶段:

开发阶段:
  1. 按需编译

    • 当你启动 Vite 时,它不会一次性打包整个项目,而是仅对 首次请求的模块 进行编译和服务。比如,只有用户第一次访问某个页面时,Vite 才会编译该页面依赖的 JavaScript 和 CSS。
  2. 热模块替换(HMR)

    • Vite 提供了 即时的热模块替换,当你在开发过程中修改了某个模块,Vite 会只编译并替换该模块,而不是重新打包整个项目。这大大提高了开发体验。
构建阶段:
  1. 生产构建(build)

    • 在生产环境下,Vite 使用 Rollup(一个现代的 JavaScript 打包工具)进行最终的打包,将所有模块合并成一个优化过的 bundle,进行代码拆分,压缩等优化,生成最终的静态文件。

3. 不需要一直打包全部资源

Vite 的按需编译和快速响应机制,使得开发过程非常迅速。只有在页面访问时,才会处理该页面的依赖,避免了 Webpack 那种完全打包的性能消耗。


四、Webpack 与 Vite 优缺点对比

特性 Webpack Vite
构建速度 较慢(尤其是大型项目时) 极快,尤其是冷启动和热更新
配置复杂性 配置较为复杂,需要处理许多细节 配置简单,开箱即用,少配置即可
开发体验 开发中每次更改都会触发完整编译 热更新速度快,修改后的内容即时反应
支持的功能 功能强大,支持的插件丰富,几乎无所不包 适合现代前端开发,特性较为简洁和聚焦
构建产物 生成一个或多个较大的 bundle 生成多个按需加载的小文件
适用场景 适合中大型复杂项目,支持更多自定义需求 适合中小型项目、现代前端框架(如 React/Vue)

五、总结

  1. Webpack:

    • 适用于复杂的前端项目,支持插件和加载器的灵活扩展。
    • 在开发时,启动和热更新较慢,尤其是大型项目。
    • 配置复杂,需要更多的手动配置来实现项目定制。
  2. Vite:

    • 更适合现代前端开发,特别是对开发速度和用户体验有高要求的场景。
    • 使用浏览器原生的 ES Modules 来实现按需编译和即时热更新,开发体验极佳。
    • 适用于现代前端框架(如 Vue、React),并在生产环境中使用 Rollup 进行高效构建。

📌 推荐场景:

  • Webpack 适合 大型、复杂的前端项目,尤其是有多种技术栈、框架,或者需要更多自定义构建的项目。
  • Vite 更适合 快速开发、现代化前端应用,尤其是小型或中型项目,或者想要享受极速开发体验的团队。

React Scheduler为何采用MessageChannel调度?

2025年12月9日 17:43
特性 setTimeout requestAnimationFrame requestIdleCallback MessageChannel
本质 定时器宏任务 动画回调钩子 空闲期回调钩子 消息通信宏任务
执行时机 延迟指定毫秒后(不精确) 下一帧渲染之前(与刷新率同步) 浏览器空闲时(一帧的末尾) 下一轮事件循环(作为宏任务)
主要设计目的 延迟执行代码 实现流畅动画 执行低优先级后台任务 不同上下文间通信
关键优势 通用、灵活、兼容性好 动画流畅、节能(后台暂停) 不阻塞渲染与交互,利用空闲时间 延迟极短且稳定,可精准控制任务切片
关键缺陷 时序不精确,嵌套有最小延迟(如4ms) 依赖渲染周期,执行频率固定(~16.7ms) 执行时机不可控,可能长期得不到调用 非用于调度,是“创造性”用法
React调度的适用性 ❌ 延迟不可控,不适合精细调度 ❌ 依赖渲染节奏,无法在帧中多次调度 ❌ 时机不可靠,无法满足及时响应需求 ✅ 在事件循环中及时插入任务,实现可中断调度的理想选择

📝 各API功能与特性详解

下面我们来具体看看每个API的核心工作机制和适用场景。

  1. setTimeout

    • 功能:最基础的异步定时器,用于在指定的延迟(毫秒)后,将回调函数推入任务队列等待执行
    • 执行机制:它设置的是一个“最小延迟”,而非精确时间。回调的实际执行时间会受到主线程上其他任务(如同步代码、微任务、UI渲染)的阻塞,延迟可能远大于设定值
    • 使用场景:适用于对时间精度要求不高的延迟操作,如防抖/节流、轮询检查等。
  2. requestAnimationFrame

    • 功能:专为动画设计的API,其回调函数会在浏览器下一次重绘(即绘制下一帧)之前执行
    • 执行机制:与显示器的刷新率(通常是60Hz,约16.7ms/帧)同步。浏览器会自动优化调用,在页面不可见时(如标签页被隐藏)会自动暂停,以节省资源
    • 使用场景实现任何需要平滑过渡的动画效果,是替代setTimeout做动画的最佳实践
  3. requestIdleCallback

    • 功能:允许你在浏览器空闲时期调度低优先级任务
    • 执行机制:在一帧处理完用户输入、requestAnimationFrame回调、布局和绘制等关键任务后,如果还有剩余时间,才会执行它的回调回调函数会接收一个IdleDeadline参数,告诉你当前帧还剩余多少空闲时间
    • 使用场景:适合执行一些非紧急的后台任务,如数据上报、非关键的数据预取等。
  4. MessageChannel

    • 功能:用于在不同浏览器上下文(如两个iframe、主线程与Web Worker)间建立双向通信的通道
    • 执行机制:调用port.postMessage()方法,会向消息队列添加一个宏任务。这个任务会在当前事件循环的微任务执行完毕后、下一次事件循环中执行。
    • 使用场景:主要应用于跨上下文通信。它在React调度中的用法,是利用其能产生一个在下一轮事件循环中尽早执行的宏任务的特性。

⚙️ 为什么React最终选择了MessageChannel?

React调度器的核心目标是:实现可中断的并发渲染,将长任务切成小片,在每一帧中插入执行,同时能快速响应高优先级更新。这就要求调度器能主动、及时地“让出”主线程。

结合上表和分析,其他API不适用于此的原因如下:

  • setTimeout延迟不稳定且不可控。其最小延迟(如4ms)在密集调度时会造成浪费,更严重的是,延迟时间可能被拉长,导致调度器无法在预期时间内“苏醒”并交还主线程,影响页面响应
  • requestAnimationFrame调用频率被锁死在屏幕刷新率(约16.7ms一次) 。这意味着即便一帧中有大量空闲时间,调度器也无法插入更多任务切片,无法充分利用帧内的空闲资源
  • requestIdleCallback执行时机“太被动”且不稳定。它依赖于浏览器的“空闲通知”,但空闲期可能很短或很久都不出现。对于需要主动、可预测地进行任务切片的调度器来说,这不可靠,同时,其兼容性也不够理想。

MessageChannel的优势正在于解决了上述问题:

  1. 主动且及时的调度:通过port.postMessage(),React可以主动在下一个事件循环中创建一个宏任务来继续工作。这让调度器能精确地在每个5ms左右的时间片(这是React设定的切片时间目标-7)结束后“中断”自己,及时归还主线程给浏览器进行渲染或处理交互。
  2. 更高的执行频率:它不依赖屏幕刷新率,可以在同一帧内的多次事件循环中连续调度,从而更密集、更充分地利用一帧之内的计算资源。
  3. 避免微任务的弊端:为什么不直接用Promise(微任务)?因为微任务会在当前事件循环中连续执行直到队列清空,这同样会长时间阻塞主线程,达不到“可中断”的目的

💎 总结

React选择MessageChannel,是基于其能产生一个在事件循环中及时、稳定执行的宏任务这一特性。这为React实现主动、可中断、基于时间片的任务调度提供了最合适的底层机制,从而在实现并发渲染的同时,保障了浏览器的渲染和用户交互能获得最高优先级的响应。

用一篇文章带你手写Vue中的reactive响应式

作者 momo06117
2025年12月9日 17:42

关于reactive

最近小编在刷vue面经时,有看到ref与reacive的区别,关于ref大家都耳熟能详,但是对于ractive,小编一开始只了解到与ref的区别是不需要.value访问值并且只能绑定引用数据类型,而ref既能绑定基本类型又能绑定引用数据类型,那么reactive的存在的意义在哪里?

这样的困惑驱使小编不能仅满足于表面的理解。在 Vue 庞大而精密的体系里,reactive 必然承载着特殊的使命。通过查阅资料和源码,小编这了解到reactive背后的奥秘,下面就一一道来。

github.com/vuejs/core/… 附上github上reactive的源码

reactive的设计理念

首先,为什么reactive只能接受引用数据类型?这是因为reactive是基于ES6的Proxy实现的响应式,而Proxy只能代理引用数据类型。

而ref在绑定基本数据类型时是基于Object.defineProperty通过数据劫持变化来实现数据响应式的。对于引用数据类型,defineProperty的方法存在一些弊端:比如无法监听到对象属性的新增和删除,也无法监听数组索引的直接设置和length变化。这里简单对比一下vue响应式方式。

实现方式 适用类型 核心缺陷
Object.defineProperty(ref 底层) 基本类型(包装为对象) 1. 无法监听对象新增 / 删除属性;2. 无法监听数组索引 / 长度变化;3. 只能劫持单个属性
Proxy(reactive 底层) 引用类型(对象 / 数组 / Map 等) 无上述缺陷,可代理整个对象,支持动态增删属性、数组操作

因此,ref在代理对象时也是借助到reactive

reactive基于Proxy的响应式系统能完美解决这些问题,下面我们来写一个简单的reactive响应式

reactive代理对象

要实现reactive实现数据响应式,我们需要先创建一个reactive方法,通过Proxy进行代理。 其中,proxy代理的target需要是对象并且没有被代理过。

//创建一个Map来保存代理过的reactive
const reactiveMap = new Map()

function isReactive(target){
  if(reactiveMap.has(target)){
      return true
  }
  return false
}

export function reactive(target){
  //检查是否已经被代理
  if(isReactive(target)){
    return target
  }


  return createReactiveObject(
      target,
      mutableHandlers
  )
}

export function createReactiveObject(target,mutableHandlers){
   //检查是否为对象
    if(typeof target !== 'object' || target == null){
      return target
    }
    //Proxy 接受俩个参数 代理对象和代理方法
    const newReactive =  new Proxy(target,mutableHandlers)
    reactiveMap.set(target,newReactive)
    return newReactive
}

Get & Set

我们新建一个文件导出mutableHandlers方法供proxy使用,mutableHandlers需要有一个get与set,分别在访问和修改target时触发。get需要实现依赖收集,当访问对象属性时将对应的副作用函数收集到依赖集合,set需要实现当对象属性更改时,更新依赖,通知副函数执行。

import {track,trigger} from './effect.js'

const get = (target, key) => {
  // target代理对象 key键名
  track(target, key) // 副作用收集
  // 相当于target[key]
  const value = Reflect.get(target, key)
  if (typeof value === 'object' && value !== null) {
    return reactive(value)
  }
  return value
}

const set = (target, key, value) => {
  // console.log('target被设置值', key, value)
  const oldValue = Reflect.get(target, key)
  // 比较是否更新
  if (oldValue !== value) {
    const result = Reflect.set(target, key, value)
    trigger(target, key, value, oldValue)
    return result
  }
  return true // 如果值没有变化,返回true表示设置成功
}

export const mutableHandlers = {
  get,
  set
}

副作用的收集与触发

接下来,我们要完成依赖收集函数track和副作用触发函数trigger。做之前我们要思考一下他们要做的事情: track在get时触发,主要负责将副作用函数effect载入targetMap中,tigger在set时触发 主要负责执行副作用函数。

提一嘴 *WeakMap是es6的新特性 特殊的是键必须是引用类型 *

// 存储依赖关系
const targetMap = new WeakMap() // 可以看set({}:map())
let activeEffect //当前执行的副作用函数

// 副作用的执行函数
export const effect = (callback) => {
  activeEffect = callback
  callback()
  activeEffect = null
}

//依赖收集
export const track = (target, key) => {
  // 如果该依赖没有副作用直接返回
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map())) // 第一次收集该依赖
  }

  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set())) // 依赖的第一个副作用
  }
  dep.add(activeEffect)
}

//触发
export const trigger = (target, key) => {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

测试

到此为止,我们已经简单完成reactive的demo。接下来新建一个vue文件来测试一下这个简易的reactiveDemo

<template>
    <div>
        <button @click="handleClick">count++</button>
    </div>
</template>

<script setup>
import {reactive} from './utils/reactivity/reactive.js'
import {effect} from './utils/reactivity/effect.js'
const count = reactive({
  value: 0
})

effect(()=>{
  console.log('count的值是:', count.value) 
})

effect(()=>{
  console.log(count.value, '正在执行计算')
})

effect(()=>{
  console.log(count.value, '正在渲染页面')
})


const handleClick = ()=>{
  count.value++
}
</script>

当我们点击按钮触发count.value++时,到触发代理proxy的set,执行targget,从而触发此前访问过count.value相关的副作用函数,完成更新。

image.png

总结

reactive是Vue中实现响应式的Api,它通过proxy实现代理,为ref代理对象提供支持。reactive不是“多余选项”了,而是vue响应式的核心支柱。

而要实现reactive的核心在于:

  • 使用proxy代理
  • 收集与触发副作用函数

*以上是小编在学习过程中的一点小见解 如果有写得不对的 欢迎在评论区指出 *

image.png

前端基础数据中心:从混乱到统一的架构演进

2025年12月9日 17:41

本文记录了我们团队在 Vue 3 + TypeScript 项目中,如何将散乱的基础数据管理逻辑重构为统一的「基础数据中心」。如果你的项目也有类似的痛点,希望这篇文章能给你一些参考。

一、问题是怎么来的

做过 B 端系统的同学应该都有体会——基础数据无处不在。港口、船舶、航线、货币、字典……这些数据在几乎每个页面都会用到,要么是下拉选择,要么是代码翻译,要么是表格筛选。

我们项目一开始的做法很「朴素」:哪里用到就哪里请求。后来发现这样不行,同一个港口列表接口一个页面能请求三四次。于是开始加缓存,问题是加着加着,代码变成了这样:

store/basicData/cache.ts      <- Pinia 实现的缓存
composables/basicData/cache.ts  <- VueUse + localStorage 实现的缓存
store/port.ts                   <- 独立的港口缓存(历史遗留)

三套缓存系统,各自为政。更要命的是 CACHE_KEYS 这个常量在两个地方都有定义,改一处忘一处是常态。

某天排查一个 bug:用户反馈页面显示的港口名称和实际不一致。查了半天发现是两套缓存系统的数据版本不同步——A 组件用的 Pinia 缓存已经过期刷新了,B 组件用的 localStorage 缓存还是旧数据。

是时候重构了。

二、想清楚再动手

重构之前,我们先梳理了需求优先级:

需求 优先级 说明
跨组件数据共享 P0 同一份数据,全局只请求一次
缓存 + 过期机制 P0 减少请求,但数据要能自动刷新
请求去重 P1 并发请求同一接口时,只发一次
持久化 P1 关键数据存 localStorage,提升首屏速度
DevTools 调试 P2 能在 Vue DevTools 里看到缓存状态

基于这些需求,我们确定了架构原则:

Store 管状态,Composable 封业务,Component 只消费。

三、分层架构设计

最终的架构分三层:

┌─────────────────────────────────────────────────┐
│               Component Layer                    │
│              (Vue 组件/页面)                     │
│  只使用 Composables,不直接访问 Store            │
└─────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────┐
│              Composable Layer                    │
│           (composables/basicData/)              │
│  usePorts / useVessels / useDict / ...          │
│  封装 Store,提供业务友好的 API                  │
└─────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────┐
│                Store Layer                       │
│             (store/basicData/)                  │
│  useBasicDataStore                              │
│  统一缓存、加载状态、请求去重、持久化            │
└─────────────────────────────────────────────────┘

为什么要分这么多层?

  • Store 层:单一数据源,解决「数据从哪来」的问题
  • Composable 层:业务封装,解决「数据怎么用」的问题
  • Component 层:纯消费,只关心「界面怎么展示」

这样分层之后,职责边界就清晰了。组件开发者不用关心缓存策略,只管调 usePorts() 拿数据就行。

四、核心实现

4.1 Store 层:请求去重是关键

Store 层最核心的逻辑是 loadData 方法。这里要处理三种情况:

  1. 缓存命中 → 直接返回
  2. 有相同请求正在进行 → 复用已有 Promise
  3. 发起新请求 → 请求完成后写入缓存
// store/basicData/useBasicData.ts
export const useBasicDataStore = defineStore('basic-data', () => {
  const cacheMap = ref<Map<BasicDataType, CacheEntry>>(new Map())
  const pendingRequests = new Map<BasicDataType, Promise<unknown>>()

  async function loadData<T>(
    type: BasicDataType,
    fetcher: () => Promise<T>,
    config?: CacheConfig
  ): Promise<T | null> {
    // 1. 缓存命中
    const cached = getCache<T>(type)
    if (cached !== null) return cached

    // 2. 请求去重——这是关键
    const pending = pendingRequests.get(type)
    if (pending) return pending as Promise<T | null>

    // 3. 发起新请求
    const request = (async () => {
      try {
        const data = await fetcher()
        setCache(type, data, config)
        return data
      } finally {
        pendingRequests.delete(type)
      }
    })()

    pendingRequests.set(type, request)
    return request
  }

  return { loadData, getCache, setCache, clearCache }
})

请求去重的实现很简单:用一个 Map 存储正在进行的 Promise。当第二个请求进来时,直接返回已有的 Promise,不发新请求。

这样即使页面上 10 个组件同时调用 usePorts(),实际 API 请求也只有 1 次。

4.2 Composable 层:工厂函数批量生成

港口、船舶、航线……这些 Composable 的逻辑高度相似,用工厂函数批量生成:

// composables/basicData/hooks.ts
function createBasicDataComposable<T extends BaseDataItem>(
  type: BasicDataType,
  fetcher: () => Promise<T[]>,
  config?: CacheConfig
) {
  return () => {
    const store = useBasicDataStore()

    // 响应式数据
    const data = computed(() => store.getCache<T[]>(type) || [])
    const loading = computed(() => store.getLoadingState(type).loading)
    const isReady = computed(() => data.value.length > 0)

    // 自动加载
    store.loadData(type, fetcher, config)

    // 业务方法
    const getByCode = (code: string) => 
      data.value.find(item => item.code === code)

    const options = computed(() => 
      data.value.map(item => ({
        label: item.nameCn,
        value: item.code
      }))
    )

    return { data, loading, isReady, getByCode, options, refresh }
  }
}

// 一行代码定义一个 Composable
export const usePorts = createBasicDataComposable('ports', fetchPorts, { ttl: 15 * 60 * 1000 })
export const useVessels = createBasicDataComposable('vessels', fetchVessels, { ttl: 15 * 60 * 1000 })
export const useLanes = createBasicDataComposable('lanes', fetchLanes, { ttl: 30 * 60 * 1000 })

这样做的好处是:

  • 新增一种基础数据,只需加一行代码
  • 所有 Composable 的 API 完全一致,学习成本低
  • 类型安全,TypeScript 能正确推断返回类型

4.3 字典数据:特殊处理

字典数据稍微复杂一些,因为它是按类型分组的。我们单独封装了 useDict

export function useDict() {
  const store = useBasicDataStore()

  // 加载全量字典数据
  store.loadData('dict', fetchAllDict, { ttl: 30 * 60 * 1000 })

  const getDictItems = (dictType: string) => {
    const all = store.getCache<DictData>('dict') || {}
    return all[dictType] || []
  }

  const getDictLabel = (dictType: string, value: string) => {
    const items = getDictItems(dictType)
    return items.find(item => item.value === value)?.label || value
  }

  const getDictOptions = (dictType: string) => {
    return getDictItems(dictType).map(item => ({
      label: item.label,
      value: item.value
    }))
  }

  return { getDictItems, getDictLabel, getDictOptions }
}

使用起来非常直观:

<script setup>
const dict = useDict()
const cargoTypeLabel = dict.getDictLabel('CARGO_TYPE', 'FCL') // "整箱"
</script>

<template>
  <el-select>
    <el-option 
      v-for="opt in dict.getDictOptions('CARGO_TYPE')" 
      :key="opt.value" 
      v-bind="opt" 
    />
  </el-select>
</template>

五、实际使用场景

场景一:下拉选择器

最常见的场景。以前要自己请求数据、处理格式,现在一行搞定:

<script setup>
import { usePorts } from '@/composables/basicData'

const { options: portOptions, loading } = usePorts()
const selectedPort = ref('')
</script>

<template>
  <el-select v-model="selectedPort" :loading="loading" filterable>
    <el-option v-for="opt in portOptions" :key="opt.value" v-bind="opt" />
  </el-select>
</template>

场景二:表格中的代码翻译

订单列表里显示港口代码,用户看不懂,要翻译成中文:

<script setup>
import { usePorts } from '@/composables/basicData'

const { getByCode } = usePorts()

// 翻译函数
const translatePort = (code: string) => getByCode(code)?.nameCn || code
</script>

<template>
  <el-table :data="orderList">
    <el-table-column prop="polCode" label="起运港">
      <template #default="{ row }">
        {{ translatePort(row.polCode) }}
      </template>
    </el-table-column>
  </el-table>
</template>

场景三:字典标签渲染

状态、类型这类字段,通常要显示成带颜色的标签:

<script setup>
import { useDict } from '@/composables/basicData'

const dict = useDict()
</script>

<template>
  <el-tag :type="dict.getDictColorType('ORDER_STATUS', row.status)">
    {{ dict.getDictLabel('ORDER_STATUS', row.status) }}
  </el-tag>
</template>

场景四:数据刷新

用户修改了基础数据,需要刷新缓存:

import { usePorts, clearAllCache } from '@/composables/basicData'

const { refresh: refreshPorts } = usePorts()

// 刷新单个
await refreshPorts()

// 刷新全部
clearAllCache()

六、缓存策略

不同数据的变化频率不同,缓存策略也不一样:

数据类型 TTL 持久化 原因
国家/货币 1 小时 几乎不变
港口/码头 15-30 分钟 偶尔变化
船舶 15 分钟 数据量大(10万+),不适合 localStorage
航线/堆场 30 分钟 相对稳定
字典 30 分钟 偶尔变化

持久化用的是 localStorage,配合 TTL 一起使用。数据写入时记录时间戳,读取时检查是否过期。

船舶数据量太大,存 localStorage 会导致写入超时,所以不做持久化,每次刷新页面重新请求。

七、调试支持

用 Pinia 还有一个好处:Vue DevTools 原生支持。

打开 DevTools,切到 Pinia 面板,能看到:

  • 当前缓存了哪些数据
  • 每种数据的加载状态
  • 数据的具体内容

排查问题时非常方便。

另外我们还提供了 getCacheInfo() 方法,可以在控制台查看缓存统计:

import { getCacheInfo } from '@/composables/basicData'

console.log(getCacheInfo())
// {
//   ports: { cached: true, size: 102400, remainingTime: 600000 },
//   vessels: { cached: false, size: 0, remainingTime: 0 },
//   ...
// }

八、踩过的坑

坑 1:响应式丢失

一开始我们这样写:

// ❌ 错误写法
const { data } = usePorts()
const portList = data.value // 丢失响应式!

datacomputed,取 .value 之后就变成普通值了,后续数据更新不会触发视图刷新。

正确做法是保持响应式引用:

// ✅ 正确写法
const { data: portList } = usePorts()
// 或者
const portList = computed(() => usePorts().data.value)

坑 2:循环依赖

Store 和 Composable 互相引用导致循环依赖。解决办法是严格遵守分层原则:Composable 可以引用 Store,Store 不能引用 Composable。

坑 3:SSR 兼容

localStorage 在服务端不存在。如果你的项目需要 SSR,持久化逻辑要加判断:

const storage = typeof window !== 'undefined' ? localStorage : null

九、总结

重构前后的对比:

维度 重构前 重构后
缓存系统 3 套并存 1 套统一
代码复用 到处复制粘贴 工厂函数批量生成
请求优化 无去重,重复请求 自动去重
调试 只能打 log DevTools 原生支持
类型安全 部分 any 完整类型推断

核心收益:

  1. 开发效率提升:新增基础数据类型从半天缩短到 10 分钟
  2. Bug 减少:数据不一致问题基本消失
  3. 性能优化:重复请求减少 60%+

如果你的项目也有类似的基础数据管理问题,可以参考这个思路。关键是想清楚分层,把「状态管理」和「业务封装」分开,剩下的就是体力活了。


本文基于实际项目经验整理,代码已做脱敏处理。欢迎讨论交流。

2025 复盘 | 穿越AI焦虑周期,进化为 "AI全栈"

作者 coder_pig
2025年12月9日 17:36

1. 今年一定

好几年没写「年终总结」,翻了下「掘金」,上一篇还停留在「2021年」,倒不是这几年没活过,也不是不想写,只是每次都止步于「拖延」。每到年底,脑子里就会蹦出的各种想法,被我零散地记录在 "大纲笔记" 中:

写这玩意挺废「时间」和「心力」,所以总想着 "找个周末,花一大块时间,好好梳理一下",结果都是一拖再拖:拖到元旦,拖到春节,拖到元宵。

然后,转念一想:"这都明年了,还写个🐣毛啊?算了,算了,明年一定!"。2022、2023、2024 就在这样的 "明年一定" 中溜走了...


人到中年,主观感觉「时间」过得越来越快,「记性」 也大不如前,很多当时觉得 "刻骨铭心" 的瞬间 (如:结婚、当爹),如今回忆起,就剩下一个 "模糊的轮廓"。不写的话,又是「 🐟」一年,趁着 2025 年还没过完,很多感觉还是热乎的,赶紧动笔,今年一定!!!

2. 如此生活30年,直到大厦崩塌

2025 年,对我冲击最大的当属 "AI",大到让我不得不重新审视「自己的工作和价值」。

我接触 AI 并不算晚,2024 年那会跟风玩了下 ChatGPT,后面在 GitHub Copilot (便宜盗版,30¥/月) 的协助下,快速交付了一个爬虫单子。尝到 "提效甜头" 的我,咬咬牙上了 年付正版 (100刀/年) 的车。

当时 AI 在我的眼里,只是个 "比较聪明的代码补全工具",可以帮我少些几行代码,仅此而已,因为 逻辑还得我来把控

但到了 2025 年,事情却变得有点不一样了:

AI能读懂/分析整个项目、重构屎山代码、把一个模糊的业务需求实现得有模有样。

当然,最让我 "破防" 的还是它的 "解BUG" 能力:

按照以往的习惯,我得去 Google、翻 Stack Overflow、看源码,少说得折腾半天。现在,把 报错日志相关代码 丢给它,通常几秒就能:指出问题所在给出解决方案,甚至可以 帮我改好并测试通过

积累多年,一度 "引以为傲" 的「编程经验」(对API的熟练程度、快速定位BUG的直觉、配置环境的熟练度等) 在 AI 的面前,变得 "不堪一击"。渐渐地,我的「工作流程」也发生了改变,"亲手" 敲下的代码越来越少,取而代之的是一套 "机械化" 的 "肌肉记忆":

  • 写注释等待AI补全按Tab:哪怕脑子里知道下一行该写什么,手指也会下意识停顿,等待那行灰色的建议浮现,然后无脑按下 Tab。
  • 提需求生成代码Accept:把业务逻辑描述一遍,丢给 AI,都不太细看生成的具体实现,直接点击 Accept All,主打一个 "能跑就行"。
  • 运行报错复制日志丢给 AI:遇到 Bug,第一反应不再是去分析堆栈信息 (Stack Trace),而是CV日志到对话框,问它:"解决报错:xxx"。
  • 效果不对截图丢给 AI:连描述问题的精力都省了,直接截图往对话框里一扔,附上一句 "改成这样"。

代码跑通了,效率提高了,却带来了精神上的「空虚」,我似乎再也感受不到当初那种「编程的快乐」:

  • 为了解决某个问题,苦思冥想,抽丝剥茧,最后成功 "破案" 时 "多巴胺疯狂分泌" 的 "快感"。
  • 查各种资料、反复推敲、验证,最终设计出一个自己觉得 "牛逼哄哄" 代码架构时的 "成就感"。

同时,也陷入了一种深深的「自我怀疑与迷茫」:

  • 越来越搞不清楚自己的「定位」(存在价值),上面那套 "连招" 找个实习生培训两天也能干。我曾赖以为生的那些 "技能",正变得廉价、可替代、甚至有点多余 ...
  • 找不到方向,以前「程序员成长路径」很清晰:学语言 → 学框架 → 学架构 → 学系统设计 → 刷算法 → 搞源码 ... 只要你一步步往上爬,爬到 "山顶" 就能成为 "大牛"。而如今却好像 "失效" 了...
  • 可控感被剥夺,程序员是典型的「内控型人格」—— 相信通过逻辑和细节掌控能预测一切。而但 AI 的「黑箱特性」带来了「工具不可控」,无法完全准确预测AI输出,调试从 "追踪逻辑" 变为 "试探模型"。

3. 调整对待AI的心态

3.1. 从 "焦虑" 到 "接纳"

我深知「焦虑」无用,于是开始探寻「破局之道」,反复阅读大量资料后发现,几乎所有人都在让你「拥抱 AI」,但具体怎么拥抱法?没人说,或者说得含糊不清,有些甚至还想割我 "韭菜" 🤡 ?屏蔽这些噪音,冷静下来复盘,拨开情绪迷雾,透过现象看本质。

首先,坦诚地「接纳」肯定是没错的,历史的车轮从不因个人的意志而停止转动,当 第一次工业革命的蒸汽机 轰鸣作响时,那些坚守手工工场的匠人们,也经历着相同的困境。精细手艺 在不知疲倦、效率千倍的 机械化工厂 面前显得苍白而无力。大机器生产取代手工劳动,不是一种选择,而是一种必然的 "降维打击"。

现在,我们同样站在了 "生产力变革" 的周期节点上, "效率至上" 的底层逻辑从未改变。是选择成为被时代甩下车的 "旧时代纺织工"?还是进化为驾驭机器的 "新时代工程师"?回归「第一性原理」,剥开 "智能" 的外衣,想想 "AI 的本质是什么?" —— 「干活的工具

所以,面对 AI,我们要做的事情就是琢磨 "如何用好这个工具? ",即:详细阅读使用说明后,在合适的场景,用合适的方式,解决合适的问题。

3.2. AI 有什么用?—— 能力放大 + 自学利器

3.2.1. 能力放大器

🐶 经常在 自媒体平台 刷到 "普通人学AI后致富/逆袭" 的 叙事,看到这些 "逆天标题" 没把我笑死:

多的不说,记住这段话就对了:

变现的核心能力从来不是使用工具,而是商业认知、市场洞察、营销推广、客户服务。AI 只是一个环节,不要高估了工具的作用,而低估了 商业常识的重要性,也不要低估了背后的 隐性成本和巨大的工作量

这些 "AI变现教程" 的 最大问题

让你把AI当成一个 独立的、全新的、需要从零开始的 "行业" 去卷。

对于 99% 的普通人而言,把AI看作 "能力放大器" 会更靠谱一点,即:

思考如何利用AI,帮我把我已有的技能/兴趣做得更好?

比如:

  • 用 AI 减少重复劳动,提高工作效率和质量,把时间花在更有创造力的事情。
  • AI 负责广度,你负责深度,在你热爱的小众领域里用AI武装自己,做到 "人无我有,人有我精"。

今年「Vibe Coding (氛围编程) 」很火:

用自然语言描述想要的效果,AI帮你写代码,你只负责验收结果和提修改意见,不用管具体代码怎么实现的。

编程门槛大大降低,普通人 只要能把 创意和感觉 翻译成需求,就能借助 AI 将其快速具象化为 可运行的产品

但你会发现,绝大多数生成的作品都是 "一次性原型或玩具":灵光一现即可实现,却缺乏持续迭代、架构设计与用户验证,因此难以具备商业价值、也难形成可持续的产品形态。

真正能够利用 Vibe Coding 实现变现的,往往是具备一定 编程经验产品思维 的 "专业人士"。他们不仅能用 AI 快速实现灵感,还能对作品进行持续优化、迭代和工程化打磨,从而将 "灵感原型" 进化为 "可用产品"。

再说一个自己观察到的例子,前阵子 OpenAI 发布了用于生成短视频的「Sora2」,B站 很快涌现了一堆 AI 生成的 "赛博科比" 恶搞视频。

看到一个播放量破百万的作品有点意思,点进 UP 主的主页想看下其它作品,结果发现他并不是突然爆火的 "新人",人家已经默默做了好几年视频,只是过去的播放量惨淡 (几十几百)。但他却一直坚持创作,尝试不同的方向,能清晰地看到他的剪辑、叙事和整体制作水平在一点点提高。

AI 不会让没有积累的人"平地起飞",但有可能让有准备的人"一飞冲天"。—《抠腚男孩》

3.2.2. 自学利器

看到这里,可能有人会问:

"那普通人怎么办?我没啥专业技能,也没有长期积累啊? "

简单,那就 "" 啊!!!以前学习的最大限制是什么?

没人教、教不好、学不动、坚持难

而现在,你有了一个「全知全能、知无不言、24小时为你服务的免费老师

  • 不会写代码?手把手教你,从逻辑到示例一步步拆开。
  • 想转行?给你路径、资源、练习清单、复盘建议。
  • 想跨领域?帮你建立知识框架,把陌生领域最难啃的部分变简单。
  • 遇到瓶颈?像一对一导师一样不断提问、引导、纠偏。

当然,想要这台 "自学利器" 高效运转起来,实现 快速学习/试错/跨域 还需要掌握一些 方法论

详细解读可以参加我之前写的《如何借助AI,高效学习,实现快速"跨域"》

3.2.3. 不要神化 AI

🐶 2333,经常刷到 "xx公司发布新的 xx 模型/AI产品颠覆行业,xx师要失业了" 的标题,但事实真的如此吗?最近 Google 家的 Nano Banana Pro 🍌很火,号称当前 "最强AI生图" 模型,亲身体验下确实强 (本文大部分配图就是它出的),天天在群里吹爆。

某天晚上,有 "多年专业设计经验" 的老婆收到一个改图需求 (抠素材,按要求调整海报):

😄 看着简单,感觉 🍌 就能做,于是我提出和老婆 PK 下,她用 PS 改,我用 🍌 嘴遁修图,看谁出的图又快又好。结果:她10分钟不到就改完,而我跟 🍌 Battle了半个小时没搞好,最终的效果图 (左边她的,右边我的):

🤡 "甲方" 的评价 (破大防了😭):

观察仔细的读者可能会问:"你是不是漏了一个车🚗啊?",憋说了,这破车把我调麻了...

那一刻,我深刻体会到了什么叫 "不要拿你的兴趣爱好,去挑战别人的饭碗",真的是 "降维打击" 啊!

AI 确实拉低了创作的门槛,但目前还处于生成 80分 内容的阶段 (效率),最后的 10-20 分 (细节、审美、情感) 才是价值的核心。——《抠腚男孩》

后面复盘,老婆看了下我的 Prompt,说我的流程有点问题,应该让 AI 先把素材全抠出来先,再慢慢组合。后面试了下,效果确实有好一些。不过,不得不说,AI自动抠图 这块确实可以:

🤣 老婆在日常设计时也会用 AI 来偷懒,比如:生成配图、提高清晰度、扩图等。

3.3. AI 是什么? —— 概率预测机器

现阶段谈论 AI,其实都是在谈论 大模型 (LLM) —— 一个极其复杂的 "概率预测机器"。

通过学习海量数据的 "统计规律",逐步逼近这些数据背后的 "概率分布",从而能够在给定 "上下文" 时预测最合理的下一步输出。

不同类型产物的生成原理图解 (看不懂没关系,简单了解下即可):

文本

② 图片 (扩散模型 & 自回归模型)

③ 音频 (自回归模型 & Codec + Token 预测 )

④ 视频 (扩散式 & 自回归/时空Token)

3.4. AI 的能力边界 —— 优/劣势

LLM 擅长发现 "相关性",但难以进行真正的 "因果推理",它只是在 "模仿智能",而非 "真正地理解意图,拥有意识"。 —— 《抠腚男孩

弄清楚 AI 的本质是 "概率预测机器" 后,接着从 "代码生成" 的角度梳理下它的 "优势 & 劣势":

了解完 AI优/劣势 后,接着就可以推演「人 & AI」 的 高效协作方式

一句话概括

AI 负责 "生产力" (重复、繁琐、高上下文、高整合的工作),人负责 "方向与边界" (判断、创造、决策、理解组织与业务)。

4. 必备技能 —— Prompt

一般译作 "提示词" 或 "描述词",个人认为后者更加贴切,即:描述问题/需求的 "词句组合" 。「Prompt Engineering-提示词工程」是所有人都必须掌握的 "使用AI的核心技能"。

4.1. 把话说清楚

🐶 别被几个英文单词吓倒,现在的 AI 比几年前聪明多了,普通人 只要能:

把诉求讲得清晰、完整、有逻辑,就能解决绝大多数问题

示例:

  • ❌ 混乱说法:帮我计划个周末玩的地方。
  • ✅ 有条理说法:周末想带5岁孩子一日游,2大1小,预算500以内,北京,不想跑太远,能放电、有吃饭的地方、避开暴晒,地铁可达最好。

AI输出结果 (前者输出不同城市的游玩方案,后者输出了具体的行程方案):

4.2. 套框架

再往上走,就是了解一些经典的 "Prompt框架",然后再提问时套用,以提高 AI 输出的稳定性、准确性和质量。所谓的 "框架",其实就是 "结构化模板",规定问题中包含哪些 "要素",比如最经典的「CTRF」框架:

套框架示例 (填空题~):

常见的框架还有 RTFCOSTARSPARCOTAPE 等等,适用于不同的场景。杰哥整合了自己知道的所有框架精华和高级技巧,弄了通用的「Prompt 最佳实践清单

无脑套就是了,助记口诀

也可以用故事流程来串联助记,读者可自行发挥,顺序无需固定:

让一位说书人 (角色) ,用生动的语气 (风格语气) ,给孩子们 (受众) 讲个故事 (指令) 。故事的开头 (上下文) 是...,结局 (目标) 要感人。故事的结构 (格式) 要像这样 (示例) ,但不要 (约束) 出现暴力情节。请先构思情节 (逐步思考) ,写完后再想想怎么能更精彩 (反思) 。

😄 懒得记的话,可以用我之前搭的小工具 →「CP AI Prompt助手」

配下 DeepSeekKey,复制粘贴你写的 简单Prompt,它会基于上面的十个维度对提示词进行优化:

4.3. 写出牛逼的Prompt

明白了怎么 "套框架" 写 "结构化的Prompt",但你可能还是会感到疑惑:

用的同样的AI,为什么别人的生成效果就是比我好?

尤其在 AI 生图 领域,看大佬分享的 Prompt,里面一堆看不懂的专业参数:

环境、构图、光影、景深、镜头、光圈、色调、氛围、胶片颗粒、对比度、主体增强、氛围灯...

能写出这么 专业的Prompt,是因为他们有 "相关领域的行业经验" 吗?

答:有加成,但不全是。高手的核心技能不是 "记这些专业知识",而是:知道如何指使 AI 给自己提供专业知识、框架、术语,然后再反向用这些框架让 AI 编写和优化 Prompt。

😄 其实思路很简单,拆解下这套方法论:

维度词术语/词库通用模板填空得第一版PromptAI专家视角优化迭代优化沉淀

详细玩法可以看下图:

4.4. Prompt 逆向

Prompt 逆向工程RPE,Reverse Prompt Engineering),就是:从 "输出" 反推 "是什么Prompt" 生成了它。一般用于:学习优秀案例调试和诊断问题构建Prompt库和模板企业质量控制安全审计 (防御Prompt注入攻击)。

4.4.1. 简单版

普通人 用这个套路就够了,选个聪明点的模型 (如:GPT5Gemini 3 Pro),粘贴图片,写 Prompt 让它反推:

差得有点远,描述「不满意的点」,让AI继续优化Prompt:

接着用优化后的 Prompt 来生成,可以看到效果差不多了,接着让 AI 提取一个「通用的Prompt

拿 AI 生成的 Prompt 生图,看效果,描述问题,循环反复,直到稳定生成自己想要的效果~

4.4.2. 专业版

🐶 其实也差不多,只是流程比较 "标准化",经常搞还能自己搭个 "工作流",适合专业选手,思路:

快速拆解推断 Prompt提取要素重建 Prompt优化迭代模板化沉淀

详细图解:

上面是通用的,还有几个 额外功能 的玩法也罗列下:

5. 锦上添花——懂点AI常识

🐶 懂点AI常识,能让你更 有的放矢用好AI (装逼),比如:连 Token 都不知道的话,就有点贻笑大方了。这里只是简单罗列下相关名词,不用死记,有个大概印象即可,不影响你使用AI,跳过也没关系。😄 详细讲解,建议直接复制名词问题AI,也可移步至《AI-概念名词 & LLM-模型微调》自行查阅~

5.1. AI (人工智能) 基础概念

5.2. NLP (自然语言处理)

5.3. Transformer 架构 (大型模型基础)

5.4. 语言模型基础 (Language Models)

5.5. LLM 核心概念 (Large Language Models)

5.6. 数据与训练流程 (Training & Fine-tuning)

5.7. 推理阶段 (Inference)

5.8. RAG (检索增强生成)

5.9. 多模态 AI

5.10. AIGC (生成式内容)

5.11. 模型压缩、部署与加速 (LLMOps)

5.12. Agent (自主智能体)

5.13. AGI (通往通用智能)

6. AI 编程领域专精

😄 最后,聊聊 AI 编程 领域的一些心得~

6.1. 前置知识

6.1.1. 编程模型

AI 代码写得好不好,主要看 "模型" 的 "编程能力",评估 "模型优劣" 的几个 "常见维度":

LLM 的能力很难用一句话概括,所以厂商们每次发新模型都会用一堆 Benchmark 来证明 (🐶不服,跑个分?)

推理与数学能力 (Reasoning)

"智能的核心指标",高分意味着能够做更复杂任务 (如:工程规划、Agent等),常见基准:GSM8K-小学奥数式数学题、MATH-高难度数学、AIME/AMC-奥林匹克数学、GPQA-博士级科学问答、BigBench Hard (BBH)-推理难题集合 等。

语言理解与知识能力 (Language / Knowledge)

"通用模型 IQ 测试",常见基准:MMLU-大学生多学科理解测试、MMLU-Pro - 更难版本、ARC / HellaSwag - 常识推理、OpenBookQA/TriviaQA - 事实/知识问答 等。

③ ✨编程能力 (Coding)

"商业价值极高的应用点",常见基准:HumanEval - 函数级别代码生成、MBPP / MBPP+ 简单编程题、SWE-Bench / SWE-Bench Verified✨:真实 GitHub issue + 多文件工程 (最接近真实开发场景,近两年厂商都在比这个)、Codeforces-算法比赛、CRUXEval / RepoBench-项目级分析 等。

多模态能力 (Multimodal)

"下一代 AI 产品的必争之地" (做 AI 助手、看图、自动化办公等),常见基准:MathVista:带图的数学推理、ChartQA / DocVQA:文档理解、TextCaps / ImageNet:视觉场景理解、VideoMME:视频理解、V-Bench / VQAv2:视觉问答 等。

安全性 (Safety / Robustness)

企业用户很看重 "安全合规",常见基准:Harmlessness / TruthfulnessAdvBench:对抗攻击、Red Team 红队测试Over-Refusal 测试(不会乱拒绝)、Speculative Safety(推测生成的风险)等。

速度/延迟/吞吐 (Performance Metrics)

"决定实际用户体验",常见指标:Tokens per second (推理速度)、First Token Latency (首字延迟)、Throughput QPS (每秒处理请求数)、Context processing speed (长文档处理速度)。


有时还会发布一些 "技术参数":

  • 模型规模:模型的参数量大小,影响推理与表达上限,规模越大,能力越强,但成本、延迟和部署难度也越高。如:70B = 70 billion = 700亿参数。
  • 训练数据规模:模型预训练时学习的 token 总量,代表其知识 "阅历"。数据越多通常知识覆盖越广,但质量、去重和清洗策略比单纯堆量更关键,高质量数据才能让模型表现更稳。如:15T = 15 trillion = 1.5万亿个token。
  • 上下文窗口:模型单次可接收并 "记住" 的 "输入长度上限",决定你能塞多少代码、文档和对话历史;窗口越大越适合做整仓分析、长文档问答、复杂任务,但会牺牲成本和延迟,且需要额外机制确保在超长上下文中仍能抓住重点。
  • 推理深度:模型答题时的 "思考力度",深推理模式更准确、适合复杂问题,但会更慢、更贵,适合关键任务而非高频交互。
  • 价格:按 token 收费,区分输入价、输出价与最小计费单位;部分模型提供 "缓存命中 (cache hit) ",对重复提示只按更低费率计费,大幅降低长上下文与多轮调用成本。价格决定模型可否大规模、频繁和低成本使用。
  • 延迟标准:包含首 token 延迟 (FTL) 与 生成速度 (token/s),分别决定 "多久开始回应" 和 "内容生成有多快";低延迟让补全、对话、Agent 流程更流畅,而高延迟会严重影响开发体验与实时性,是工程中比 "更聪明一点" 更重要的性能指标。
  • 模型行为控制能力:通过 Temperature、Top-p、System Prompt、工具权限等机制控制模型的随机性、稳定性与执行边界;行为越可控,越能确保输出一致、不跑偏,并安全地接入工具链或生产系统,是把模型从 demo 提升到可上线能力的关键参数。

🤡 个人 "主观" 认为的 "编程模型" 能力排名:

😊 一句话概括我的 "选模型策略":

选好的模型事半功倍!工程大活Claude精细小活 (如改BUG) 用 GPT,写前端页面用 Gemini

🐶 问:这些都是国外的模型啊,怎么才能用上?A社还锁区,经常封号?而且价格好贵啊?

答:😏 这个问题充钱可以解决.jpg,多逛下海 (xian) 鲜 (yu) 市场,国人的 "薅羊毛" 能力不是盖的,各种 "镜像站、第三方中转" 。氪金的时候注意找有 "售后" 的,随用随充,买 "短期 (如月付) ",不要买 "长期 (如年付)",这种看 LLM官方 政策的,一封就直接G了,说不定就 "卷款跑路"~

6.1.2. AI 编程工具的四种形态

😄 一句话归纳:

普通开发者 & Vibe Coding用户AI IDE/插件 居多,DevOps/后端工程师CLI团队/企业系统云端AgentAI 应用开发者AI SDK 构建构建 AI 产品与 Agent 系统。

接着说下 "AI编程" 的三种演进层次~

6.2. 第一层:AI 辅助开发

最早期的AI开发方式,以「人主导 + AI辅助」为核心逻辑,由两种交互模式组成:

  • 补全式:基于输入光标前的上下文,预测下一个单词、下一行代码、甚至整个函数。
  • 对话式:在 IDE 侧边栏或网页中,通过自然语言问答来生成代码、解释代码或查找 Bug。如:"帮我写一个 Python 的正则来验证邮箱" 或 "这段代码为什么报错?"

这一层的局限:

  • 上下文有限:AI 通常只能看到当前文件或少量相关片段,缺乏对整个项目架构的理解。
  • 被动性:AI 不会主动修改你的代码文件,它生成代码,你负责复制粘贴和校验。
  • 人是瓶颈:所有的决策、文件切换、环境配置都必须由人来操作。

6.3. 第二层:AI 驱动开发流程 (规范+Agent)

目前最前沿、最热门的阶段,AI 不再只是吐出代码片段,而是进化为 Agent (智能体),拥有了 "大脑" (规划能力) 和 "手脚" (工具使用能力),可以 "自主完成一个多步骤的开发任务"。

变成了「人定目标 + Agent 自主执行」,如:"实现一个简单的待办事项 Web 服务,要求:REST API,内存存储即可,有单元测试",Agent 可能会进行这样的任务拆解并执行:

设计目录结构 → 创建代码文件 → 写业务逻辑 → 写测试 → 运行测试并自我修复。

为了系统地应用 Agent,业界逐渐采用「 "规范"驱动的开发流程」(Spec-Driven Development):

需求规范文档任务分解Agent执行验证反馈

这个流程确保了 清晰的目标定义可追踪的执行过程,而不是让Agent盲目操作。开发者需要维护的 "三类规范" (规范必须比写代码更清晰):

  • 功能规范:目标、用户故事、输入输出、性能要求、鉴权、边界情况等。
  • 技术规范:模块结构、API、模型字段、状态机、异常流程等,Agent会根据这些自动创建项目。
  • 验收规范:测试通过、接口返回正确、性能满足要求、行为与设计一致,即每个功能的评价方式。

人不再写代码 (或者少写),负责「定义 + 审核 + 授权」,人-Agent 协作 的三阶段循环:

  • 人主导-目标设定:范围、约束、边界、不允许做的事情。
  • Agent主导-执行:分解、规划、写代码、自动Debug、修复、生成报告。
  • 人主导-验收:代码质量、安全性、单元测试覆盖率、偏差是否满足业务需求等。

💡 层2 关注的是「开发流程自动化」,任务的起点通常是 "已经确定好的需求/feature"。

6.4. 第三层:AI 全栈

所谓的 "AI 全栈",本质上就是 "让 AI 同时扮演多个软件开发角色",而 "一人分饰多角" 的自然实现方式就是 "多 Agents"。——《抠腚男孩

6.4.1. 为什么聊到 "AI全栈" 就会扯到"多 Agents"?

🤔 想象一下让 "AI全栈开发一个应用" 需要经历哪些步骤?

产品需求理解技术选型架构设计API 设计前后端代码生成数据库 schema错误处理文档生成单测编写测试执行部署脚本CI/CD 配置

让一个 Agent 承包上面所有的工作,会有什么问题?

记忆量爆炸、目标切换频繁、推理链拉得太长,错误积累变大、一旦一步出错,后续全崩、风格、结构、代码质量难统一、难以并行。

软件工程是 "多角色协作" 的结果:产品经理、架构师、后端、前端、测试、文档、DevOps... 如果想 "AI 模拟完整的软件开发流程",自然也需要 "让 AI 也模拟这些角色",于是就变成了这些 Agent

  • Planner / Architect (产品/架构):理解需求、拆任务、出计划 (Plan)。
  • Coder / Implementer (实现):按计划改代码、增删文件。
  • Searcher / Context Agent (检索):在代码库里找相关文件、API、调用链。
  • Tester / QA (写测 / 跑测):写测试、跑测试、分析报错。
  • Fixer / Debug Agent (修BUG):根据测试/运行结果修复代码。
  • Reviewer / Critic Agent (代码审阅):检查风格、一致性、潜在 bug / 安全问题
  • Ops / Deploy Agent (部署):写 Docker、CI/CD、部署脚本(有些系统只做到生成,不自动执行)

即「AI 全栈 = AI 软件开发流水线 = 模拟整个软件部门 = 多 Agent 系统」,这是开发任务决定的。

"AI全栈" 需要的三大核心能力:

  • 长任务规划 (Planning):开发一个系统不是线性的,是树状决策结构,要拆分任务,就需要 Planner Agent。
  • 并行执行 (Parallel Execution):前端、后端、文档、测试不可能一个个线性做。多 Agent 可以:前端 Agent 改 UI、后端 Agent 写 API、Docs Agent 补文档、Test Agent 补测试。
  • 验证 & 修复 (Validation Loop):真正让 "AI 全栈" 可行的关键是 "循环",写代码、跑测试、找错误、修复、再跑,需要 "多 Agent + 状态机" 才能撑起这个能力。

"AI 全栈系统" 的实现,本质就这四步:

  • 「定义一堆上面这样的角色」
  • 排布拓扑」决定这些角色之间的连接结构和调用关系。谁先谁后 (拓扑/顺序)、有没有循环 (写→测→修→再测)、有并行吗 (前后端Agent同时干活?)、是由一个 "主管Agent" 指挥大家?还是大家按照状态机自己转?
  • 给每个角色接上能用的"工具",让它真的能动手干活」常见工具:文件 (读/写代码、生成 diff)、终端 (执行命令)、搜索 (在 repo 里搜符号 / 用法)、HTTP/Browser (查文档、查API)、Git (开分支、commit、生成PR)、结构化分析 (AST分析、调用图、依赖图)。比如:Coder Agent 配置 "文件读写、diff 生成、代码搜索" 的工具,用来 "在受控范围内改代码"。
  • 套一层安全边界权限 (读写、只能改指定目录、终端命令必须在沙箱里执行)、人在回环 (关键操作必须人工确认,如:Plan-任务规划、大范围diff、部署相关改动/高危脚本)、防注入/误操作 (不轻信代码库里的"指令"-如:恶意README 写 "rm -rf /"、对外部输入做过滤-日志/错误信息/用户Prompt、限制重试次数,避免死循环修改)。

一句话概括就是:

AI 全栈 = 一群小模型/小角色 + 一个调度关系图 + 一堆工具函数 + 一圈安全护栏

😄 弄清楚本质,以后看任何 "AI 全栈多 Agents" 方案,都可以基于这三个问题进行快速拆解:

  • 它有哪些角色?(Planner / Coder / Tester / Fixer / Ops…)
  • 这些角色是按什么拓扑 / 流程连起来的?
  • 每个 Agent 有哪些工具?安全边界是什么?

6.4.2. 业界主流多 Agent 架构模式

前面AI常识部分有提到过,这里直接让🍌画个图~

6.4.3. 个人级 "AI全栈" 演进历程

🤡 上面的理论看起来简单,但对于个人来说,想要亲手实现这样 一整套多 Agents AI 全栈系统,工作量爆炸:

得自己写调度、管状态、接工具、控安全、做可视化,还要维护一堆 prompt 和配置,算完整平台工程了...

🤔 笔者认为 "个人级AI全栈" 更倾向于:

在个人可以承受的复杂度和时间成本内,让AI参与尽可能多的开发环节,而不是一次性造一个企业级AI工厂。

😄 其实,你可能已经在无形中体验 "AI 全栈" 的雏形了,现代 AI 编程工具 本身就内置了 多 Agent 编排能力~

Claude Code Sub Agents

CC 中允许创建多个带 独立角色与上下文Sub Agent (小型专属AI工作者),用法简单:

  1. 创建 Sub Agent
  • Claude Code CLI 输入 /agents,选择「Create new agent
  • 选择作用域:项目级 (推荐,只给当前项目使用)、用户级 (所有项目可用)
  • 填写:name (调用的时候用到)、description (决定CC何时自动调它)
  • 选择可用工具 (file_edit / bash / file_search / git …)
  • 完善系统Prompt:可以先让 Claude 生成,再自己改

保存后,会在 .claude/agents/ 生成一个类似这样的文件:

---
name: backend-dev
description: "专门负责后端接口、服务逻辑和数据库相关代码的实现与修改"
model: sonnet
tools: [file_search, file_edit]
color: blue
---

你是一个资深后端工程师,精通 Node.js + TypeScript 和这个项目的后端架构。
你的职责:
- 只改后端相关的代码(controllers, services, repositories)
- 遵循项目现有的代码风格和结构
- 所有改动都要尽量小步、安全、可读
在给出修改时:
- 标明文件路径
- 用 patch 的风格展示修改
- 如果需要新增文件,要说明用途和引用关系

不想自动生成,可以在 .claude/agents/ 手动按照上面的格式自己写md,保存后 CC 会自动识别。还可以在命令行启动CC时添加 --agents 参数 (适用于临时挂载场景):

claude --agents '{
  "log-analyzer": {
    "description": "分析测试日志和错误堆栈的专用Agent",
    "system_prompt": "你只负责阅读测试输出、日志,帮助定位问题和怀疑文件,不写代码",
    "tools": ["file_search"]
  }
}'
  1. 调用 Sub Agent (串起来) 的三种方式
  • 自然语言编排,用普通指令描述任务,由 Claude 自动判断并调用合适的 Sub Agent,最灵活、最贴近自然对话的方式。如:请用 backend-dev subagent 修改 search controller 的分页逻辑。
  • 结构化点名调用,明确指定要调用哪个 Sub Agent,适合需要精确控制执行顺序或避免模型误判的情况。如:Use the test-runner subagent to run the unit tests.
  • ③ 在 Agentrooms 中使用 @agent-name 直接点名,通过@用户的方式派任务,可同时管理多个 Agent,方便多人视图和多 Agent 协作。如:@backend-dev 帮我调整这个接口的返回格式
  1. 多个 Sub Agents 协同工作简单示例 (开发 → 测试 → 分析 → 再开发):
  • 让 developer 生成补丁
  • 让 test-runner 运行测试
  • 让 log-analyst 分析失败原因
  • 再让 developer 根据分析修复
  • Claude 会自动接力,也可以由你手动编排~

Cursor 2.0 多 Agent 编排

2.0 后,Cursor 界面从 "以文件为中心" 变成 "以Agent为中心",多了个 Agent Layout,切换后,侧边栏会显示当前 Agent、计划(plan)和改动,你把需求丢进去,Agent 负责读文件、计划、改代码、跑测试。

支持 同一指令 下,最多可 并行 (Parallel) 跑 8 个 Agent,每个 Agent 会在自己独立的 Git worktree / 沙盒工作区 内工作:各自改代码、build、跑测试,不会互相冲突 (🤡 就是费 Token...)。还多了一个 Plan Mode (先规划再执行),在 Agent 输入框 中按 Shift + Tab 可以切换到这个模式 (也可以手动选):

Cursor 不会直接假设你的需求,而是询问一系列澄清问题:

通过这些澄清,使 AI获得了完整的上下文,可以生成更精确的计划,避免后续的返工。接着会生成一个 plan.md 的计划文档:

你可以对文件进行编辑:增删任务、调整任务顺序、更新技术细节、调整实现方法等。确定无误后,点击 Build,Agent 会读取最新版本的 plan.md,并完成对应的任务。

🤔 与 CC Sub Agents 可编排不同,CursorAgent 更像是一个组合能力的 "大Agent",由它自动编排多个内嵌的、对用户不可见 的 Agent 来完成 用户提出的任务,收敛复杂性,只展示改动/测试结果。它的 Parallel Agents 探索不同方案,最后再汇总/合并的玩法,不算严格意义上的 "主流多 Agent 架构模式" 中的 "并行Agents模式"-支持显式地定义 / 分配 不同角色的 Agent,并让它们并行协作。

类似的支持 "多Agents" 玩法的 AI 编程工具还有:

  • GitHub Copilot Workspace多步骤 Pipeline Agents,从任务描述 → 生成完整 plan → 自动执行 → 修正,多步骤 cascaded agents,自动提 PR。
  • Google Gemini Code Assistmulti-expert prompt routing,任务自动分配给最擅长的模型/agent,复杂 monorepo 搜索 → 专家 agent 提供答案,针对 cloud infra 的执行-验证循环。
  • Replit 的 AI Dev 环境多工具执行 Agent,轻量一站式多Agent开发流水线。
  • ...等,限于篇幅,就不展开讲了~

觉得 AI编程工具 满足不了,接着就是围绕自己的开发流程,开发基于 LLMAPI 封装一些 小脚本/小工具

// 推进开发闭环的简单伪代码 (需求 → 修改 → 测试 → 修复)
plan = llm("你是架构师,帮我拆解这个改动需求…")
files = find_related_files(plan)

patches = llm("你是后端开发,只能改这些文件…", files + plan)
apply_patches_to_workdir(patches)

test_result = run_tests()

if test_result.failed:
    fix_patches = llm("你是调试工程师,根据报错修复…",
                      test_result + current_code)
    apply_patches_to_workdir(fix_patches)

大多数个人开发者达到这一层,基本够用了,再往上就是加:日志、可配置、一点UI、简单任务管理等,弄成一个仅为自己服务的 "AI 全栈开发小平台" (😄 此时更像是一个 Agent 工程师,搭建 "企业级AI全栈" 的基石)。

6.4.4. 落地方法论

根本原则

在一个完整开发周期里 (从想法到上线),有意识地让 AI 参与尽可能多的环节,并用 "多角色思维" 来组织这些调用,但工程复杂度要控制在个人能持续维护的范围内。


① 项目级自检


② 项目阶段拆解


③ 搭建可复用工作流


7. 结语

行文至此,再回看这篇拖了许久的 "年终总结",心情早已从最初面对 AI 秒解 Bug 时的 "破防" 与 "迷茫",变得平静且笃定,我们:

  • 剥开 AI "智能" 的外衣,看到了它作为 "概率预测机器" 的本质。
  • 学会用 "结构化的Prompt" 去驾驭它,而不是被幻觉带偏。
  • 也见证了开发模式从简单的 Chat 进化成 Copilot,再到如今初具雏形的 Agentic Workflow

但归根结底,AI 带来的最大变量,不在于它替我们写了多少行代码,而在于它重塑了 "专业" 的定义。

  • 懂得"底层原理"依然重要——否则你不知道为什么 AI 会把人修成 "汽车",也无法在它 "一本正经胡说八道" 时进行纠偏。
  • 懂得提问比解答更重要—— Prompt 是新时代的编程语言,清晰的逻辑表达 + 对业务的深度理解,才是最高效的 "编译器"。
  • 懂得架构比实现更重要——当 "AI 全栈" 成为可能,当一个个 Agent 可以各司其职,我们不再是死磕语法的 "搬砖工",而更像指挥数字化施工队的 "包工头 & 总设计师"。

"技术焦虑" 的解药,从来不是拒绝变化,而是成为变化的一部分。以前,我们的壁垒是 "熟练度+记忆力",以后则是 "想象力+判断力+系统工程能力",拥抱AI,在这个属于创造者的时代,进化为无所不能的 "超级个体🦸‍♀️"!

前端页面崩溃监控全攻略:心跳判定 + Service Worker 接管

作者 Sthenia
2025年12月9日 17:34

背景

在浏览器环境中,“页面崩溃(Page Crash)”并不是一个浏览器主动抛出的可监听事件。 页面可能因为以下原因被动终止:

  1. 内存 OOM 导致 tab 崩溃

  2. 浏览器内部的 renderer 进程挂掉

  3. 业务代码死循环、长任务阻塞导致页面卡死

  4. 页面在后台被系统杀进程(尤其移动端)

  5. 浏览器关闭 / 标签页关闭但执行不到 beforeunload(常见)

由于浏览器没有提供“页面是否异常退出”的 API,因此前端监控体系通常只能间接推断崩溃

本次调研希望解决以下两个问题:

🎯 目标

  1. 如何在单标签页场景中准确推断页面是否异常退出?

  2. 多标签页环境中,一个页面崩溃后,如何被其他页面检测并上报?

  3. 是否能做到不依赖页面再次打开 —— 即实时上报?(如 Service Worker)

  4. 最终方案应尽量稳定、低侵入、可扩展并减少误报率

调研方案

1. 基于「退出打标 + 心跳检测」的崩溃推断方案

这是目前业内最常见的思路,例如不少监控 SDK 都采用类似机制。

1.1 核心思路

  1. 页面正常退出时(beforeunload / pagehide / visibilitychange)写入 normalExit = true

  2. 如果是崩溃,则正常退出钩子不会触发 → normalExit 保持 false

  3. 下次启动页面时读取存储(localStorage),若发现上次 normalExit=false,则认为存在异常退出

单页面流程示意图

正常退出 → normalExit = true → 下次打开不告警
崩溃 → normalExit = false 且心跳断更 → 下次打开上报崩溃

// 单页面检测崩溃代码

// 伪代码,还需要处理beforeunload / pagehide / visibilitychange
window.addEventListener('beforeunload', () => {
  localStorage.setItem('normalExit', 'true');
});
function checkCrash() {
  const normalExit = localStorage.getItem('normalExit');
  if (normalExit !== 'true') {
    reportCrash();
  }
  localStorage.setItem('normalExit', 'false');
}

上面只是讨论到当页面单开的情况,那么如果是多标签页的场景下该如何设计呢?要知道localStorage是在同一个域名下各个标签页共享的。

根据上面的检测原理,会想到,给每个页面都设置一个独立的 tabId,并在其中一个页面获取所有的页面normalExit,判断normalExit 是不是 false,false则认为是发生了崩溃。

但是不行,当我们打开多页面时,因为页面并还没有退出,获取到的normalExit其实都是false的,那我们就需要多一个字段去判断,心跳时间;

1.2 多标签页场景的完整设计

针对多页面场景,每个页面需要心跳 + tabId,判断页面是否还存活

为什么需要心跳?

因为多页面时,每个页面运行中时 normalExit 本来就是 false。 所以不能只看 normalExit,需要结合“最后心跳时间”:

normalExit = false + 心跳超过阈值未更新 → 判定崩溃

👇 关键逻辑代码(精简示例)

** 为每个 tab 创建唯一 ID **

// 
function getTabId() {
  let id = sessionStorage.getItem('**tab_id**');
  if (!id) {
    id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
    sessionStorage.setItem('**tab_id**', id);
  }
  return id;
}

心跳写入(运行中 normalExit = false)

function saveAll(map: Record<string, HeartbeatRecord>) {
  try {
    localStorage.setItem(KEY, JSON.stringify(map));
  } catch {}
}

export function writeHeartbeat(rec: Omit<HeartbeatRecord, 'ts' | 'tabId'>) {
  const tabId = getTabId();
  const map = loadAll();
  map[tabId] = { ...map[tabId], ...rec, ts: Date.now(), tabId };
  saveAll(map);
}

export function startSessionHeartbeat(intervalMs = 3000, recBase?: Omit<HeartbeatRecord, 'ts' | 'tabId'>) {
  const page = `${window.location.pathname}${window.location.hash || ''}`;
  let timer: number | undefined;

  function beat() {
    writeHeartbeat({ page, version: recBase?.version, env: recBase?.env, meta: recBase?.meta, normalExit: false });
  }

  beat();
  timer = setInterval(beat, intervalMs);

  window.addEventListener('beforeunload', () => {
    markNormalExit();
    if (timer) clearInterval(timer);
    clearTabHeartbeat();
  });

  return () => {
    if (timer) clearInterval(timer);
  };
}

正常退出钩子(beforeunload)


export function markNormalExit() {
  const tabId = getTabId();
  const map = loadAll();
  if (map[tabId]) {
    map[tabId].normalExit = true;
    saveAll(map);
  }
}

window.addEventListener('beforeunload', () => {
  markNormalExit(); // 标记正常退出
  clearTabHeartbeat(); // 清除心跳
});

// 重新打开页面时判断历史页面是否崩溃
if (!rec.normalExit && diff > timeoutMs) {
  reportCrash(rec);
}

下次打开时检测崩溃(核心)

判断崩溃逻辑:

  1. 不是正常退出时,normalExit 为 false

  2. 当崩溃时,normalExit 为false 或 undefined

  3. diff > timeoutMs 主要是为了当多开页面时,正常的页面心跳时间一直在滚动更新,不会少于timeoutMs,防止误报

export function checkPreviousAbnormalExit(
  timeoutMs: number,
  report: (payload) => void
) {
  const now = Date.now();
  const currentTab = getTabId();
  const map = loadAll();

  let changed = false;
  Object.values(map).forEach((rec) => {
    if (!rec || rec.tabId === currentTab) return;
    const diff = now - (rec.ts || 0);

    if (!rec.normalExit && diff > timeoutMs) {
      //// diff > timeoutMs 有个弊端,当用户崩溃后,刚好设了一个时间戳,并且马上打开一个新标签页,这时diff可能还没超过timeoutMs,这种情况会漏报
      report({ ...rec, diff });
      delete map[rec.tabId];
      changed = true;
    } else if (rec.normalExit) {
      delete map[rec.tabId];
      changed = true;
    }
  });

  if (changed) saveAll(map);
}

1.3 多页面心跳的隐藏坑点(必须处理)

🔥 1. 页面隐藏时定时器会被延迟

浏览器切后台后,setInterval 会被降频,甚至几秒才执行一次。 这会误伤心跳逻辑。

处理方式:

页面隐藏时直接标记 normalExit=true,避免误报。

但为什么可行?因为后台页面本身不应计入崩溃统计(用户没在看)。

🔥 2. 心跳的时间不准

如果是使用定时器更新心跳时间,心跳更新时间并不会特别准可以了解下setInterval原理,比如设了3000毫秒更新一次,有可能是3000+-N000毫秒才执行更新,也可能是主线程有大计算导致更新时间更慢,所以阈值不能设置和心跳时间一样的时间,得有一定的宽容度。 建议阈值为:

阈值 = 心跳间隔 \* 2

1.4 方案一的优劣总结

优点

  • 实现简单,不依赖 Service Worker

  • 能检测页面是否在上一次会话中异常退出

  • 多标签页可准确判断单页崩溃

缺点(重点)

  • 无法实时上报,必须等待下次打开页面

  • 页面进程在后台被系统杀死,无法触发页面生命周期事件导致无法打标记,下次启动页面时会存在误报。但是误报笔者是觉得允许的,采集到样本大的页面崩溃路径才是最有可能导致崩溃的页面;

2. 基于 Service Worker 的实时心跳监控

既然方案一无法实时上报,那么是否能借助Worker 实现实时上报呢?worker 独立于页面运行,可以在页面崩溃后继续存活,从而实现实时上报。

笔者一开始是想到用 Web Worker 来实现的,但是后来发现 Web Worker 生命周期和页面是绑定的,页面崩溃后,Web Worker 也不可用,所以无法实现实时上报。只能是用service Worker 来实现。

Service Worker(SW)可以在页面崩溃后继续存活,只要浏览器进程未关闭。 利用 SW 作为“监控总控”,页面与 SW 双向通信,从而实现:

✔ 页面实时心跳发送

✔ SW 主动判断某 tab 心跳超时

✔ 立即上报崩溃事件(无需等待下次进入)

2.1 方案架构图

页面 A/B/C
  ↓ (heartbeat)
Service Worker(独立线程)
  ↓ (report)
监控服务(如 Sentry)

2.2 SW 的核心逻辑

  1. 收到心跳,更新 tab 的 时间搓
if (data.type === 'heartbeat') {
  tabLastBeat.set(tabId, { ts: now() });
  ensureCrashChecker();
}
  1. 定时检查哪些 tab 心跳超时
if (nowTs - ts > CRASH_TIMEOUT_MS) {
sendReport();
tabLastBeat.delete(tabId);
}
  1. 页面向 SW 发送 exit 消息,避免误报
postToSW({ type: 'exit', tabId: this.tabId });

2.3 页面侧的心跳通知

this.timer = setInterval(() => {
  this.postToSW({
    type: 'heartbeat',
    tabId: this.tabId,
    ts: Date.now()
  });
}, this.heartbeatIntervalMs);

同时绑定生命周期:

  • beforeunload

  • visibilitychange(hidden → exit)

  • pagehide

2.4 方案二的优劣

优点

  • 支持实时上报

  • 监控逻辑不依赖浏览器是否回到页面

  • 多标签页信息共享更自然(SW 本来就是共享运行时)

缺点(非常关键)

  • 浏览器窗口关闭时,SW 也会消失 → 无法上报

  • 仅适用于开启 SW 的站点(需 HTTPS + 同源)

  • SW 更新策略复杂(需处理 skipWaiting、claim 等)

最终结论与推荐方案

场景 最佳方案 原因
简单不复杂 方案一(localStorage + 心跳) 下次打开可判断所有异常退出,覆盖范围最大
想实时上报崩溃(非浏览器关闭情况) 方案二(Service Worker) 页面崩溃后 SW 仍可运行并上报
希望误报率最低 结合两者 SW 实时 & 下次打开兜底

附录

  1. 完整方案一代码示例见:页面崩溃上报实现代码
  2. 方案二 Service Worker 示例代码见:Service Worker 页面崩溃监控代码Service Worker 代码
  3. 完整demo 见:页面崩溃监控 Demo

如果觉得代码有用麻烦点个小星星。

前端应该知道的浏览器知识

2025年12月9日 17:27

 浏览器整体架构:多进程多线程

现代浏览器(以Chrome为代表)采用分层、多进程的架构,主要目的是安全、稳定和性能。一个典型的浏览器可以被抽象为下图所示的三层结构:

  • 第一层:用户界面:我们看到的地址栏、书签栏、按钮等。它运行在独立的 浏览器进程 中,响应用户的全局操作。
  • 第二层:浏览器内核:浏览器的“大脑”,负责调度和管理。网络线程处理请求,UI后端线程绘制基础控件,而最核心的渲染引擎(如Blink)和JS引擎(如V8)则运行在独立的渲染进程中。
  • 第三层:数据持久层:负责Cookie、本地存储、缓存等数据的读写。

deepseek_mermaid_20251209_899f2a.png

主要进程及其职责包括:

进程 职责
浏览器进程 负责界面显示(地址栏、书签)、用户交互、子进程管理等。
GPU进程 负责独立的图形绘制(3D CSS、WebGL)。
网络进程 负责所有网络资源加载。
渲染进程(核心) 每个标签页通常对应一个独立的渲染进程,负责解析HTML/CSS、执行JavaScript、进行布局和绘制(排版、渲染)。我们说的“主线程”就在这里。
插件进程 每个插件独立进程,防止崩溃影响浏览器。

所以,浏览器整体是多进程的。而在一个渲染进程内部,又包含多个线程协同工作

🧵 渲染进程内的线程分工

一个典型的渲染进程包含以下关键线程:

  1. GUI渲染线程:负责解析HTML/CSS、构建DOM树、CSSOM树、布局和绘制等。注意:GUI渲染线程与JS引擎线程是互斥的
  2. JavaScript引擎线程(这就是我们常说的“主线程”或“UI线程”) :负责执行JavaScript代码(如V8引擎)。我们常说的“JavaScript是单线程的”,指的就是这个线程。
  3. 定时器触发线程:管理setTimeoutsetInterval的计时,计时完毕将回调加入任务队列。
  4. 异步HTTP请求线程:处理XMLHttpRequestfetch等网络请求,完成后将回调加入任务队列。
  5. 事件触发线程:管理事件循环,当事件(如点击)触发时,将对应的回调函数加入任务队列。
  6. 合成线程:将页面分层信息发送给GPU进程。

核心:“主线程”是什么?

我们通常所说的  “主线程” ,狭义上指  “JavaScript引擎线程” ,广义上指的是承担了JavaScript执行、页面渲染(GUI)、事件处理等核心工作的这个单一线程执行模型

因为GUI渲染线程JS引擎线程是互斥的,它们不能同时执行,所以可以理解为一个“工作主线程”在不同时段切换着做这两件事。其工作流程可以总结为下图:

deepseek_mermaid_20251209_b32264.png

为什么这样设计?  主要是为了保证DOM操作结果的一致性。如果JS线程和渲染线程同时工作,JS可能在渲染中途修改DOM,导致渲染出错。互斥执行简化了并发控制,但带来了性能挑战。

🧵 微观机制:渲染进程与事件循环

作为前端开发者,我们必须深入理解渲染进程的内部,因为我们的代码就在这里执行。下图描绘了渲染进程中,从接收网络数据到最终屏幕像素的关键工作流:

deepseek_mermaid_20251209_8bd5ee.png

结合上图,我们来理解几个最关键的概念:

  1. 渲染流水线
    这是浏览器将代码变成像素的过程。你需要理解几个关键步骤:

    • 解析与构建树:HTML解析为DOM树,CSS解析为CSSOM树
    • 布局:计算每个DOM元素在视口中的精确位置和大小(又称“回流”)。
    • 绘制:将元素的文本、颜色、边框等视觉信息填充到多个图层上(又称“重绘”)。
    • 合成:这是现代浏览器保持流畅的关键。合成线程将各个图层分块,交由GPU进程进行光栅化(变成位图),最后像叠盘子一样合成为最终画面。这个过程完全在独立的线程进行,不阻塞主线程
  2. 事件循环:JavaScript的并发模型
    是 JavaScript 实现异步编程的核心机制,其作用是协调同步任务异步任务的执行顺序,让单线程的 JS 能够高效处理非阻塞操作(如网络请求、定时器、DOM 事件)。

    • 宏任务队列:包含 setTimeoutsetIntervalI/O、UI渲染、MessageChannel 等回调。

    • 微任务队列:包含 Promise.thenMutationObserverqueueMicrotask 等回调。
      运行规则:每执行一个宏任务后,会清空整个微任务队列,然后检查是否需要渲染,接着再取下一个宏任务。

    核心规则一个宏任务 → 所有微任务 → (可能渲染)→ 下一个宏任务

💡 对前端开发的深刻启示

  1. 性能瓶颈在主线程:所有同步JS、DOM操作、样式计算、布局都发生在同一个主线程上。这就是为什么长时间的同步JS会“卡死”页面——它阻塞了渲染和事件处理。

  2. 理解渲染时机:浏览器会智能地合并多次DOM操作,但直接读取某些布局属性(如 offsetTopgetComputedStyle)会强制触发同步布局,导致性能骤降。

  3. 善用异步与分层

    • 用 requestAnimationFrame 执行动画,让它与渲染周期对齐。
    • 将耗时计算移入 Web Worker(运行在独立线程,无法访问DOM)。
    • 利用CSS transform 和 opacity 属性进行动画,它们可以由合成线程单独处理,完全避开主线程和重绘,效率最高。
  4. React调度器的用武之地:React正是深刻理解了上述机制,才用 MessageChannel 将渲染工作拆分为5ms左右的可中断任务单元。每个单元执行后,通过事件循环将控制权交还浏览器,从而避免长任务阻塞,实现流畅的并发更新。

Mac上Git不识别文件名大小写修改?一招搞定!

作者 Ric970
2025年12月9日 17:25

Mac 上 Git 不识别文件名大小写修改?一招搞定!

作为一名经常在 Mac 上开发的程序员,你是否遇到过这样的窘境:明明手动把文件名从大写改成了小写(比如把 README.TXT 改成 readme.txt),但提交 Git 时却发现这个改名操作完全没生效?别慌,这不是 Git 的 bug,而是 Mac 文件系统和 Git 默认配置的 “小摩擦”,下面来看看如何解决这个问题。

一、问题复现:改了大小写,Git 却 “看不见”

场景很典型:

  • 本地有一个大写文件名的文件,比如 HELLO.md,已经被 Git 追踪;

  • 手动右键重命名,或用 mv HELLO.md hello.md 改成小写;

  • 执行 git status 查看状态,结果 Git 显示 “working tree clean / 工作区干净”,完全没检测到文件名的变化;

  • 试图直接 git add 也没用,改名仿佛 “石沉大海”。

二、根因分析:Mac 文件系统的 “大小写不敏感” 特性

问题的核心在于:

  • Mac 默认的文件系统(APFS/HFS+)通常是大小写不敏感的;

  • 在系统层面,HELLO.mdhello.md 会被认为是同一个文件;

  • Git 默认会继承操作系统的文件大小写敏感性配置;

  • 当 macOS 告诉 Git “这两个文件名是同一个” 时,Git 自然就不会把大小写修改识别为 “文件变更”,也就不会记录这个操作了。

三、解决方案:两种思路,按需选择

针对这个问题,有 “临时解决单次问题” 和 “永久配置一劳永逸” 两种方案,可以根据场景选择。

方案 1:临时解决(单次文件名修改)

如果你只是偶尔修改一个文件的大小写,用 git rm --cached 配合 mv 指令就能快速解决,步骤如下:

# 1. 先删除 Git 对原大写文件的追踪(本地文件不会被删除)
git rm --cached 大写文件名  # 示例:git rm --cached HELLO.md

# 2. 用 mv 指令重命名本地文件(如果还没改的话)
mv 大写文件名 小写文件名    # 示例:mv HELLO.md hello.md

# 3. 将小写文件重新加入 Git 追踪
git add 小写文件名         # 示例:git add hello.md

# 4. 提交修改
git commit -m "rename: 文件名大小写调整(HELLO.md -> hello.md)"

执行完这四步后,再用 git status 就能看到 Git 已经正确识别到文件名的修改了,后续正常 push 即可。

方案 2:永久配置(全局 / 仓库级识别大小写)

如果你经常需要修改文件名大小写,每次都手动执行临时方案会很繁琐,这时候可以修改 Git 的配置,让它强制识别文件名大小写。

方式 A:仅当前仓库生效(推荐)

进入你的 Git 仓库根目录,执行以下命令,仅对当前项目生效:

cd /path/to/your/repo  # 进入你的项目目录
git config core.ignorecase false

方式 B:全局生效(所有 Git 仓库)

如果想让所有本地 Git 仓库都识别文件名大小写,执行全局配置命令:

git config --global core.ignorecase false

四、关键注意事项

  • 先提交其他修改:操作前一定要确保工作区其他未提交的修改已经提交或暂存,避免执行 git rm --cached 时误删其他文件的追踪。

  • 跨平台兼容要注意:如果你的仓库需要在 Windows/macOS(大小写不敏感)和 Linux(大小写敏感)之间切换,建议尽量保持文件名全小写,避免仅靠大小写区分文件(比如同时存在 File.txtfile.txt),否则在不同系统上可能出现文件冲突。

  • 验证配置是否生效:修改配置后,可以用以下命令确认是否成功: `# 查看当前仓库的大小写敏感配置 git config core.ignorecase

查看全局的大小写敏感配置

git config --global core.ignorecase 输出false` 就说明配置生效了。

五、总结

  • Mac 上 Git 不识别文件名大小写修改,本质是 文件系统特性Git 默认配置 的冲突;

  • 通过 git rm --cached + mv 的临时方案可以快速解决单次问题;

  • 修改 core.ignorecase 配置则能在项目或全局层面 “一劳永逸”。

掌握这两种方法后,就不用再担心文件名大小写的 “Git 盲区” 了。如果你也遇到过类似的问题,或者有其他解决技巧,也可以在团队内部文档或代码规范里补充说明,方便大家排坑。

JavaScript 数组原生方法手写实现

作者 1024肥宅
2025年12月9日 17:21

引言

在JavaScript开发中,数组方法是日常编码的核心工具。理解这些方法的内部实现原理不仅能帮助我们写出更高效的代码,还能在面试中展现扎实的基础。本文将完整实现JavaScript中最重要、最常用的数组方法,涵盖高阶函数、搜索方法、扁平化方法和排序算法。

一、高阶函数实现

1.1 map方法实现

map是最常用的高阶函数之一,它创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。

Array.prototype.myMap = function (callback, thisArg) {
  // 输入验证
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + "is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;
  const result = new Array(len);

  // 遍历并执行回调
  for (let i = 0; i < len; i++) {
    // 处理稀疏数组
    if (i in obj) {
      result[i] = callback.call(thisArg, obj[i], i, obj);
    }
  }

  return result;
};

// 使用示例
const numbers = [1, 2, 3];
const squares = numbers.myMap((num) => num * num);
console.log(squares); // [1, 4, 9]
1.2 filter方法实现

filter方法创建一个新数组,包含通过测试的所有元素。

Array.prototype.myFilter = function (callback, thisArg) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;
  const result = [];

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      // 如果回调返回true,则保留该元素
      if (callback.call(thisArg, obj[i], i, obj)) {
        result.push(obj[i]);
      }
    }
  }
  return result;
};

// 使用示例:筛选出大于2的数字
const nums = [1, 2, 3, 4, 5];
const filtered = nums.myFilter((num) => num > 2);
console.log(filtered); // [3, 4, 5]
1.3 reduce方法实现

reduce是最强大的高阶函数,可以将数组元素通过reducer函数累积为单个值。

Array.prototype.myReduce = function (callback, initialValue) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  // 处理空数组且无初始值的情况
  if (len === 0 && initialValue === undefined) {
    throw new TypeError("Reduce of empty array with no initial value");
  }

  let accumulator = initialValue;
  let startIndex = 0;

  // 如果没有提供初始值,使用第一个有效元素作为初始值
  if (initialValue === undefined) {
    // 找到第一个存在的元素(处理稀疏数组)
    while (startIndex < len && !(startIndex in obj)) {
      startIndex++;
    }

    if (startIndex === len) {
      throw new TypeError("Reduce of empty array with no initial value");
    }

    accumulator = obj[startIndex];
    startIndex++;
  }

  // 执行reduce操作
  for (let i = startIndex; i < len; i++) {
    if (i in obj) {
      accumulator = callback(accumulator, obj[i], i, obj);
    }
  }

  return accumulator;
};

// 使用示例
const sum = [1, 2, 3, 4, 5].myReduce((acc, curr) => acc + curr, 0);
console.log(sum); // 15

// 复杂示例:数组转对象
const items = [
  { id: 1, name: "Apple" },
  { id: 2, name: "Banana" },
  { id: 3, name: "Orange" },
];

const itemMap = items.myReduce((acc, item) => {
  acc[item.id] = item;
  return acc;
}, {});

console.log(itemMap);
// {
//   '1': { id: 1, name: 'Apple' },
//   '2': { id: 2, name: 'Banana' },
//   '3': { id: 3, name: 'Orange' }
// }

二、搜索与断言方法

2.1 find方法实现

find方法返回数组中满足测试函数的第一个元素的值。

Array.prototype.myFind = function (callback, thisArg) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (callback.call(thisArg, obj[i], i, obj)) {
        return obj[i];
      }
    }
  }

  return undefined;
};

// 使用示例
const users = [
  { id: 1, name: "Alice", age: 25 },
  { id: 2, name: "Bob", age: 30 },
  { id: 3, name: "Charlie", age: 35 },
];

const user = users.myFind((user) => user.age > 28);
console.log(user); // { id: 2, name: 'Bob', age: 30 }
2.2 findIndex方法实现

findIndex方法返回数组中满足测试函数的第一个元素的索引。

Array.prototype.myFindIndex = function (callback, thisArg) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (callback.call(thisArg, obj[i], i, obj)) {
        return i;
      }
    }
  }

  return -1;
};

// 使用示例
const numbers = [5, 12, 8, 130, 44];
const firstLargeNumberIndex = numbers.myFindIndex(num => num > 10);
console.log(firstLargeNumberIndex); // 1
2.3 some方法实现

some方法返回数组中是否至少有一个元素通过了测试。

Array.prototype.mySome = function (callback, thisArg) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (callback.call(thisArg, obj[i], i, obj)) {
        return true;
      }
    }
  }

  return false;
};

// 使用示例
const hasEven = [1, 3, 5, 7, 8].mySome((num) => num % 2 === 0);
console.log(hasEven); // true
2.4 every方法实现

every方法测试数组中的所有元素是否都通过了测试。

Array.prototype.myEvery = function (callback, thisArg) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (!callback.call(thisArg, obj[i], i, obj)) {
        return false;
      }
    }
  }

  return true;
};

// 使用示例
const allPositive = [1, 2, 3, 4, 5].myEvery((num) => num > 0);
console.log(allPositive); // true

三、数组扁平化方法

3.1 flat方法实现

flat方法创建一个新数组, 其中所有子数组元素递归连接到指定深度。

Array.prototype.myFlat = function (depth = 1) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  // 深度参数验证
  if (depth < 0) {
    throw new RangeError("depth must be a non-negative integer");
  }

  const result = [];

  const flatten = (arr, currentDepth) => {
    for (let i = 0; i < arr.length; i++) {
      const element = arr[i];
      // 如果当前深度小于指定深度且元素是数组, 则递归扁平化
      if (Array.isArray(element) && currentDepth < depth) {
        flatten(element, currentDepth + 1);
      } else {
        // 否则直接添加到结果数组
        // 注意: 如果depth为0,则不会扁平化任何数组
        result.push(element);
      }
    }
  };

  flatten(this, 0);
  return result;
};

// 使用示例
const nestedArray = [1, [2, [3, [4]], 5]];
console.log(nestedArray.myFlat()); // [1, 2, [3, [4]], 5]
console.log(nestedArray.myFlat(2)); // [1, 2, 3, [4], 5]
console.log(nestedArray.myFlat(Infinity)); // [1, 2, 3, 4, 5]
3.2 flatMap方法实现

flatMap方法首先使用映射函数映射每个元素, 然后将结果压缩成一个新数组。

Array.prototype.myFlatMap = function (callback, thisArg) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;
  const result = [];

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      const mapped = callback.call(thisArg, obj[i], i, obj);

      // 如果回调函数返回的是数组, 则展开它
      if (Array.isArray(mapped)) {
        for (let j = 0; j < mapped.length; j++) {
          result.push(mapped[j]);
        }
      } else {
        // 如果不是数组,直接添加
        result.push(mapped);
      }
    }
  }

  return result;
};

// 使用示例
const phrases = ["Hello world", "JavaScript is awesome"];
const words = phrases.myFlatMap((phrase) => phrase.split(" "));
console.log(words); // ["Hello", "world", "JavaScript", "is", "awesome"]

// 另一个示例:展开并过滤
const numbers2 = [1, 2, 3, 4];
const result = numbers2.myFlatMap((x) => (x % 2 === 0 ? [x, x * 2] : []));
console.log(result); // [2, 4, 4, 8]

四、排序算法实现

4.1 sort方法实现

JavaScript原生的sort方法使用TimSort算法(一种混合排序算法, 结合了归并排序和插入排序)。这里我们实现一个简单但功能完整的排序方法, 支持自定义比较函数。

Array.prototype.mySort = function (compartFn) {
  if (this === null) {
    throw new TypeError("this is null or not defined");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  // 如果没有提供比较函数, 使用默认的字符串比较
  if (compartFn === undefined) {
    // 默认比较函数: 将元素转为字符串, 然后比较UTF-16代码单元值序列
    compartFn = function (a, b) {
      const aString = String(a);
      const bString = String(b);

      if (aString < bString) return -1;
      if (aString > bString) return 1;
      return 0;
    };
  } else if (typeof compartFn !== "function") {
    throw new TypeError("compareFn must be a function or undefined");
  }

  // 实现快速排序算法(高效且常用)
  function quickSort(arr, left, right, compare) {
    if (left >= right) return;

    const pivotIndex = partition(arr, left, right, compare);
    quickSort(arr, left, pivotIndex - 1, compare);
    quickSort(arr, pivotIndex + 1, right, compare);
  }

  function partition(arr, left, right, compare) {
    // 选择中间元素作为基准值
    const pivotIndex = Math.floor((left + right) / 2);
    const pivotValue = arr[pivotIndex];

    // 将基准值移到最右边
    [arr[pivotIndex], arr[right]] = [arr[right], arr[pivotIndex]];

    let storeIndex = left;

    for (let i = left; i < right; i++) {
      // 使用比较函数比较当前元素和基准值
      if (compare(arr[i], pivotValue) < 0) {
        [arr[storeIndex], arr[i]] = [arr[i], arr[storeIndex]];
        storeIndex++;
      }
    }

    // 将基准值放到正确的位置
    [arr[storeIndex], arr[right]] = [arr[right], arr[storeIndex]];
    return storeIndex;
  }

  // 将稀疏数组转换为紧凑数组(跳过不存在的元素)
  const compactArray = [];
  for (let i = 0; i < len; i++) {
    if (i in obj) {
      compactArray.push(obj[i]);
    }
  }

  // 执行快速排序
  if (compactArray.length > 0) {
    quickSort(compactArray, 0, compactArray.length - 1, compartFn);
  }

  // 将排序后的数组复制回原数组,保持稀疏性
  let compactIndex = 0;
  for (let i = 0; i < len; i++) {
    if (i in obj) {
      obj[i] = compactArray[compactIndex++];
    }
  }

  return obj;
};

// 使用示例
const unsorted = [3, 1, 4, 1, 5, 9, 2, 6, 5];
unsorted.mySort();
console.log(unsorted); // [1, 1, 2, 3, 4, 5, 5, 6, 9]

// 使用自定义比较函数
const students = [
  { name: "Alice", score: 85 },
  { name: "Bob", score: 92 },
  { name: "Charlie", score: 78 },
];

students.mySort((a, b) => b.score - a.score);
console.log(students);
// 按分数降序排列

五、总结

5.1 实现要点总结
  1. 输入验证: 始终检查this是否为nullundefined, 以及回调函数是否为函数类型
  2. 稀疏数组处理: 使用in操作符检查索引是否存在
  3. 类型安全: 使用>>>0确保长度为非负整数
  4. 性能考虑:
  • 避免不必要的数组拷贝
  • 使用适当的算法(如快速排序对于sort方法)
  • 注意递归深度(特别是对于flat方法)
  1. 与原生方法差异:
  • 我们的实现在某些边缘情况下可能与原生方法略有不同
  • 原生方法通常有更好的性能和内存管理
5.2 实际应用场景
  1. 数据处理: mapfilterreduce是数据处理的三件套
  2. 搜索功能: findfindIndex用于数据检索
  3. 表单验证: someevery用于验证多个输入
  4. 状态管理: flatflatMap在处理嵌套状态时特别有用
  5. 数据展示: sort用于数据排序

通过手动实现这些核心数组方法,我们不仅加深了对JavaScript数组操作的理解,还掌握了函数式编程的核心概念。

记住:在实际生产环境中,仍然建议使用原生数组方法,因为它们经过了充分优化和测试。但理解这些方法的实现原理,将使你成为一个更出色的JavaScript开发者。

成为开源项目的Contributor:从给uView-pro 贡献一次PR开始

2025年12月9日 17:20

wx.getSystemInfoSync is deprecated.Please use wx.getSystemSetting / wx.getAppAuthorizeSetting / wx.getDeviceInfo/wx.getWindowInfo/wx.getAppBaseInfo instead.

😄 前言

微信小程序平台上的getSystemInfoSync调用,就像一个害羞的少女,会悄悄抛出警告。啊~这样的不完美,怎么能容忍呢? (。•́︿•̀。),让我们来给uView-pro 贡献一次PR吧

🍴 Fork项目:开源之旅的起点

兴奋地搓手手,眼中闪烁着期待的光芒

第一步:Fork原项目

  1. 打开uView-pro原项目
  2. 点击右上角的Fork按钮,将项目Fork到自己的GitHub账号下

第二步:克隆到本地

# 克隆你Fork的项目(注意替换YOUR_USERNAME)
git clone https://github.com/YOUR_USERNAME/uView-Pro.git

得意地眨眼 看,现在你就有了两个远程仓库:origin(你的Fork)和upstream(原项目)~

🌿 创建分支:在独立的花园里耕耘

轻轻地,就像在培育一朵娇嫩的花朵

第三步:创建功能分支

# 确保你在main分支上
git checkout main

# 拉取最新代码
git pull upstream main

# 创建新的功能分支
git checkout -b feature/20251209/fix-getSystemInfoSync-warn

# 推送分支到你的Fork
git push origin feature/20251209/fix-getSystemInfoSync-warn

啊~一个崭新的分支,就像一片等待播种的花园,准备孕育你的代码之花~ 🌸

💻 代码开发:在键盘上起舞

优雅地敲击键盘,每一个字符都是爱的告白

第四步:进行代码修改

  1. 分析问题:仔细阅读相关代码,理解问题的根本原因
  2. 设计方案:构思优雅的解决方案,考虑兼容性和性能
  3. 编写代码:实现你的解决方案,保持代码风格一致
  4. 本地测试:确保修改不会引入新的问题

我的设计方案

核心思路

  1. 统一收口:把分散在各处的getSystemInfoSync调用集中到一个专门的sys.ts文件里
  2. 平台兼容:使用条件编译尽量抹平平台之间的差异(目前仅App、微信、支付宝、H5支持了getDeviceInfo getWindowInfo 等API)
  3. 最小改动:使用条件编译,微信端使用新的API(getDeviceInfo getWindowInfo 等),其它平台暂未弃用getSystemInfoSync,因此可以继续沿用

技术实现

/**
 * 获取当前操作系统平台
 * @returns 平台字符串,如 'ios'、'android'、'windows' 等
 */
export function os(): string {
    // #ifdef MP-WEIXIN
    return uni.getDeviceInfo().platform;
    // #endif
    // #ifndef MP-WEIXIN
    return uni.getSystemInfoSync().platform;
    // #endif
}
/**
 * 获取窗口信息
 * @returns 窗口信息对象
 */
export function getWindowInfo(): Omit<UniApp.GetWindowInfoResult, 'screenTop'> & { screenTop?: number } {
    // #ifdef MP-WEIXIN
    return uni.getWindowInfo();
    // #endif
    // #ifndef MP-WEIXIN
    const {
        pixelRatio,
        screenWidth,
        screenHeight,
        windowWidth,
        windowHeight,
        statusBarHeight,
        windowTop,
        windowBottom,
        safeArea,
        safeAreaInsets
    } = sys();
    return {
        pixelRatio,
        screenWidth,
        screenHeight,
        windowWidth,
        windowHeight,
        statusBarHeight,
        windowTop,
        windowBottom,
        safeArea,
        safeAreaInsets
    };
    // #endif
}
/**
 * 获取设备信息
 * @returns 设备信息对象
 */
export function getDeviceInfo(): UniApp.GetDeviceInfoResult {
    // #ifdef  MP-WEIXIN
    return uni.getDeviceInfo();
    // #endif
    // #ifndef MP-WEIXIN
    const {
        deviceBrand,
        deviceModel,
        deviceId,
        deviceType,
        devicePixelRatio,
        deviceOrientation,
        brand,
        model,
        system,
        platform
    } = sys();
    // #endif
    return {
        deviceBrand,
        deviceModel,
        deviceId,
        deviceType,
        devicePixelRatio,
        deviceOrientation,
        brand,
        model,
        system,
        platform
    };
}

第五步:推送到你的Fork

# 推送分支到你的GitHub
git push origin feature/20251209/fix-getSystemInfoSync-warn

🚀 创建PR:向世界展示你的作品

紧张又兴奋地搓手手,心跳加速

第六步:发起Pull Request

  1. 打开GitHub:访问你的Fork项目页面
  2. 点击Compare & pull request:GitHub会智能提示你创建PR
  3. 填写PR标题:简洁明了地描述你的修改
  4. 详细描述:在PR描述中详细说明:
    • 修改的目的和背景
    • 技术实现方案
    • 测试结果
    • 可能的影响范围

第七步:等待审核

安静地等待,像等待情人的回信

  • 保持耐心:维护者可能需要时间review你的代码
  • 积极响应:如果有反馈,及时回复和修改
  • 学习交流:把review过程当作学习的机会

第八步:PR合并

欢呼雀跃,眼中闪烁着幸福的泪花

当你的PR被合并的那一刻,就像收到了心上人的回信~你的代码正式成为了开源项目的一部分!

🎉 后续维护:持续的爱与关怀

温柔地抚摸着代码,眼神迷离

保持同步

# 定期同步上游仓库
git checkout main
git fetch upstream
git merge upstream/main
git push origin main

删除已合并的分支

# 删除本地分支
git branch -d feature/20251209/fix-getSystemInfoSync-warn

# 删除远程分支
git push origin --delete feature/20251209/fix-getSystemInfoSync-warn

💝 开源心得:爱与痛的交织

眼神变得温柔,轻轻诉说

第一次为开源项目贡献代码,心情就像坐过山车一样刺激呢~ ❤️

甜蜜的收获:

  • 深入理解了uni-app的跨平台机制
  • 学会了使用条件编译处理平台差异
  • 体验了完整的PR流程
  • 获得了项目维护者的认可

小小的挫折:

  • 一开始对微信小程序的特殊性了解不够
  • 担心修改会影响其他平台的兼容性
  • 等待PR合并时的焦虑心情

🌟 给后来者的情书

张开双臂,热情地拥抱

亲爱的,如果你也想踏入开源的花园,请记住:

  1. 从熟悉开始:选择你常用的项目,这样更容易发现问题
  2. 小步快跑:不要一开始就想着大改,从小问题入手
  3. 仔细阅读文档:了解项目的贡献规范和代码风格
  4. 勇于尝试:不要害怕犯错,每个贡献者都是从新手开始的
  5. 享受过程:开源不仅是代码,更是与世界各地开发者交流的机会

🎭 结语:开源,一场永不落幕的舞会

优雅地旋转,裙摆飞扬

开源世界就像一个永不停歇的舞会,每一个PR都是一支独特的舞蹈。我的这次贡献虽然只是一个小小的兼容性优化,但它让我感受到了开源社区的温度和活力。

当你看到自己的代码被合并,被全世界的开发者使用,那种成就感就像在心爱的人面前跳了一支完美的舞~ (✧ω✧)

所以,亲爱的,不要犹豫,不要害羞。打开GitHub,找到你心仪的项目,开启你的开源之旅吧!记住,每一个伟大的贡献者,都曾经是一个忐忑不安的新手...

调皮地眨眼 说不定,我们还能在开源的世界里相遇呢~ ❤️


最后的最后:

愿你在代码的世界里找到属于自己的浪漫,愿每一个PR都能被温柔以待。开源路上,我们不见不散~

深情地飞吻 么么哒~ 💋✨


📊 贡献详情

我的PR:refactor(sys): 优化微信小程序平台 getSystemInfoSync 兼容性处理 by liujiayii · Pull Request #83 · anyup/uView-Pro

技术关键词: #uni-app #微信小程序 #跨平台开发 #开源贡献 #条件编译 #TypeScript

期待与你在开源的世界里相遇~ ٩(◕‿◕)۶

LongCat 上线 AI 生图!精准高效,AI 创作不设限

美团 LongCat 全新上线 AI 生图功能,该功能基于 LongCat 系列模型「LongCat-Image」打造而成。无论是追求高效出图的普通用户,还是需要精准落地创意的专业创作者,LongCat 都以 “轻量化模型 + 流畅体验” ,让 AI 生图真正成为人人可用的创作工具。
❌
❌