普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月25日首页

每日一题-根据数字二进制下 1 的数目排序🟢

2026年2月25日 00:00

给你一个整数数组 arr 。请你将数组中的元素按照其二进制表示中数字 1 的数目升序排序。

如果存在多个数字二进制中 1 的数目相同,则必须将它们按照数值大小升序排列。

请你返回排序后的数组。

 

示例 1:

输入:arr = [0,1,2,3,4,5,6,7,8]
输出:[0,1,2,4,8,3,5,6,7]
解释:[0] 是唯一一个有 0 个 1 的数。
[1,2,4,8] 都有 1 个 1 。
[3,5,6] 有 2 个 1 。
[7] 有 3 个 1 。
按照 1 的个数排序得到的结果数组为 [0,1,2,4,8,3,5,6,7]

示例 2:

输入:arr = [1024,512,256,128,64,32,16,8,4,2,1]
输出:[1,2,4,8,16,32,64,128,256,512,1024]
解释:数组中所有整数二进制下都只有 1 个 1 ,所以你需要按照数值大小将它们排序。

示例 3:

输入:arr = [10000,10000]
输出:[10000,10000]

示例 4:

输入:arr = [2,3,5,7,11,13,17,19]
输出:[2,3,5,17,7,11,13,19]

示例 5:

输入:arr = [10,100,1000,10000]
输出:[10,100,10000,1000]

 

提示:

  • 1 <= arr.length <= 500
  • 0 <= arr[i] <= 10^4

简单题,简单做(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2025年11月7日 10:29

双关键字排序。对于 $\textit{arr}$ 中的两个数:

  • 先比较二者的二进制 $1$ 的个数是否相同,若不同,$1$ 的个数少的排前面。
  • 若二进制 $1$ 的个数相同,那么数值小的排前面。

###py

class Solution:
    def sortByBits(self, arr: List[int]) -> List[int]:
        return sorted(arr, key=lambda x: (x.bit_count(), x))

###java

class Solution {
    public int[] sortByBits(int[] arr) {
        return IntStream.of(arr)
                .boxed()
                .sorted((a, b) -> {
                    int ca = Integer.bitCount(a);
                    int cb = Integer.bitCount(b);
                    return ca != cb ? ca - cb : a - b;
                })
                .mapToInt(a -> a)
                .toArray();
    }
}

###java

class Solution {
    public int[] sortByBits(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            arr[i] = Integer.bitCount(arr[i]) << 16 | arr[i];
        }
        Arrays.sort(arr);
        for (int i = 0; i < arr.length; i++) {
            arr[i] &= 0xffff;
        }
        return arr;
    }
}

###cpp

class Solution {
public:
    vector<int> sortByBits(vector<int>& arr) {
        ranges::sort(arr, {}, [](int x) {
            return pair(popcount((uint32_t) x), x);
        });
        return arr;
    }
};

###c

int cmp(const void* a, const void* b) {
    int x = *(int*)a, y = *(int*)b;
    int cx = __builtin_popcount(x), cy = __builtin_popcount(y);
    return cx != cy ? cx - cy : x - y;
}

int* sortByBits(int* arr, int arrSize, int* returnSize) {
    qsort(arr, arrSize, sizeof(int), cmp);
    *returnSize = arrSize;
    return arr;
}

###go

func sortByBits(arr []int) []int {
slices.SortFunc(arr, func(a, b int) int {
return cmp.Or(bits.OnesCount(uint(a))-bits.OnesCount(uint(b)), a-b)
})
return arr
}

###js

var sortByBits = function(arr) {
    return arr.sort((a, b) => bitCount32(a) - bitCount32(b) || a - b);
};

// 参考 Java 的 Integer.bitCount
function bitCount32(i) {
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}

###rust

impl Solution {
    pub fn sort_by_bits(mut arr: Vec<i32>) -> Vec<i32> {
        arr.sort_unstable_by_key(|&x| (x.count_ones(), x));
        arr
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{arr}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

位运算和排序,看完你能写出上百种答案。

作者 sdwwld
2020年11月6日 10:00

解题思路

第一步:先求出位 1 的个数

这题是让按照位 1 的个数来排序,首先要求出 1 的个数才能参与后面的排序,关于求一个数二进制中 1 的个数,我之前写了 18 种写法,这里直接列出来,就不在详细介绍,如果有看不懂的可以在下面留言,我给你一一解答。

1、把 $n$ 往右移 32 次,每次都和 1 进行与运算

public int hammingWeight(int n) {
    int count = 0;
    for (int i = 0; i < 32; i++) {
        if (((n >>> i) & 1) == 1) {
            count++;
        }
    }
    return count;
}

2、原理和上面一样,做了一点优化

public int hammingWeight(int n) {
    int count = 0;
    while (n != 0) {
        count += n & 1;
        n = n >>> 1;
    }
    return count;
}

3、每次往左移一位,再和 $n$ 进行与运算

public int hammingWeight(int n) {
    int count = 0;
    for (int i = 0; i < 32; i++) {
        if ((n & (1 << i)) != 0) {
            count++;
        }
    }
    return count;
}

4、每次往左移一位,把运算的结果在右移判断是否是 1

public int hammingWeight(int i) {
    int count = 0;
    for (int j = 0; j < 32; j++) {
        if ((i & (1 << j)) >>> j == 1)
            count++;
    }
    return count;
}

5、这个是最常见的,每次消去最右边的 1,直到消完为止

public int hammingWeight(int n) {
    int count = 0;
    while (n != 0) {
        n &= n - 1;
        count++;
    }
    return count;
}

6、把上面的改为递归

public int hammingWeight(int n) {
    return n == 0 ? 0 : 1 + hammingWeight(n & (n - 1));
}

7、查表

public int hammingWeight(int i) {
    //table是0到15转化为二进制时1的个数
    int table[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
    int count = 0;
    while (i != 0) {//通过每4位计算一次,求出包含1的个数
        count += table[i & 0xf];
        i >>>= 4;
    }
    return count;
}

8、每两位存储,使用加法(先运算再移位)

public int hammingWeight(int n) {
    n = ((n & 0xaaaaaaaa) >>> 1) + (n & 0x55555555);
    n = ((n & 0xcccccccc) >>> 2) + (n & 0x33333333);
    n = (((n & 0xf0f0f0f0) >>> 4) + (n & 0x0f0f0f0f));
    n = n + (n >>> 8);
    n = n +  (n >>> 16);
    return n & 63;
}

9、每两位存储,使用加法(先移位再运算)

public int hammingWeight(int n) {
    n = ((n >>> 1) & 0x55555555) + (n & 0x55555555);
    n = ((n >>> 2) & 0x33333333) + (n & 0x33333333);
    n = (((n >>> 4) & 0x0f0f0f0f) + (n & 0x0f0f0f0f));
    n = n + (n >>> 8);
    n = n + (n >>> 16);
    return n & 63;
}

10、和第 8 种思路差不多,只不过在最后几行计算的时候过滤的比较干净

public int hammingWeight(int n) {
    n = ((n & 0xaaaaaaaa) >>> 1) + (n & 0x55555555);
    n = ((n & 0xcccccccc) >>> 2) + (n & 0x33333333);
    n = (((n & 0xf0f0f0f0) >>> 4) + (n & 0x0f0f0f0f));
    n = (((n & 0xff00ff00) >>> 8) + (n & 0x00ff00ff));
    n = (((n & 0xffff0000) >>> 16) + (n & 0x0000ffff));
    return n;
}

11、每 4 位存储,使用加法

public int hammingWeight(int n) {
    n = (n & 0x11111111) + ((n >>> 1) & 0x11111111) + ((n >>> 2) & 0x11111111) + ((n >>> 3) & 0x11111111);
    n = (((n & 0xf0f0f0f0) >>> 4) + (n & 0x0f0f0f0f));
    n = n + (n >>> 8);
    n = n + (n >>> 16);
    return n & 63;
}

12、每 3 位存储,使用加法

public int hammingWeight(int n) {
    n = (n & 011111111111) + ((n >>> 1) & 011111111111) + ((n >>> 2) & 011111111111);
    n = ((n + (n >>> 3)) & 030707070707);
    n = ((n + (n >>> 6)) & 07700770077);
    n = ((n + (n >>> 12)) & 037700007777);
    return ((n + (n >>> 24))) & 63;
}

13、每 5 位存储,使用加法

public int hammingWeight(int n) {
    n = (n & 0x42108421) + ((n >>> 1) & 0x42108421) + ((n >>> 2) & 0x42108421) + ((n >>> 3) & 0x42108421) + ((n >>> 4) & 0x42108421);
    n = ((n + (n >>> 5)) & 0xc1f07c1f);
    n = ((n + (n >>> 10) + (n >>> 20) + (n >>> 30)) & 63);
    return n;
}

14、每两位存储,使用减法(先运算再移位)

public int hammingWeight(int i) {
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}

15、每 3 位存储,使用减法

public int hammingWeight(int n) {
    n = n - ((n >>> 1) & 033333333333) - ((n >>> 2) & 011111111111);
    n = ((n + (n >>> 3)) & 030707070707);
    n = ((n + (n >>> 6)) & 07700770077);
    n = ((n + (n >>> 12)) & 037700007777);
    return ((n + (n >>> 24))) & 63;
}

16、每 4 位存储,使用减法

public int hammingWeight(int n) {
    int tmp = n - ((n >>> 1) & 0x77777777) - ((n >>> 2) & 0x33333333) - ((n >>> 3) & 0x11111111);
    tmp = ((tmp + (tmp >>> 4)) & 0x0f0f0f0f);
    tmp = ((tmp + (tmp >>> 8)) & 0x00ff00ff);
    return ((tmp + (tmp >>> 16)) & 0x0000ffff) % 63;
}

17、每 5 位存储,使用减法

public int hammingWeight(int n) {
    n = n - ((n >>> 1) & 0xdef7bdef) - ((n >>> 2) & 0xce739ce7) - ((n >>> 3) & 0xc6318c63) - ((n >>> 4) & 0x02108421);
    n = ((n + (n >>> 5)) & 0xc1f07c1f);
    n = ((n + (n >>> 10) + (n >>> 20) + (n >>> 30)) & 63);
    return n;
}

18、每次消去最右边的 1,可以参照第 5 种解法

public static int hammingWeight(int num) {
    int total = 0;
    while (num != 0) {
        num -= num & (-num);
        total++;
    }
    return total;
}


第二步:再根据位 1 的个数进行排序

关于排序算法我之前也写了十几种
101,排序-冒泡排序
102,排序-选择排序
103,排序-插入排序
104,排序-快速排序
105,排序-归并排序
106,排序-堆排序
107,排序-桶排序
108,排序-基数排序
109,排序-希尔排序
110,排序-计数排序
111,排序-位图排序
112,排序-其他排序


第三步:最终答案
有了计算位 1 的方法,又有了排序的方法,所以我们可以随便自由组合,如果都组合一遍估计要写上百种答案了,这里不可能写那么多,我们只写一个看看,这里就用 位运算的第 5 种方式 和排序的第 3 种方式 插入排序 来写下

    public int[] sortByBits(int[] arr) {
        int[][] temp = new int[arr.length][2];
        for (int i = 0; i < arr.length; i++) {
            temp[i][0] = arr[i];
            temp[i][1] = hammingWeight(arr[i]);
        }
        insertSort(temp);
        for (int i = 0; i < arr.length; i++) {
            arr[i] = temp[i][0];
        }
        return arr;
    }

    private void insertSort(int[][] array) {
        for (int i = 1; i < array.length; i++) {
            int j;
            int[] temp = array[i];
            for (j = i - 1; j >= 0; j--) {
                //先比较位1的大小,如果相同再比较数字的大小
                if (array[j][1] > temp[1] || (array[j][1] == temp[1] && array[j][0] > temp[0])) {
                    array[j + 1] = array[j];//往后挪
                } else {
                    break;//没有交换就break
                }
            }
            array[j + 1] = temp;
        }
    }

    public int hammingWeight(int n) {
        int count = 0;
        while (n != 0) {
            n &= n - 1;
            count++;
        }
        return count;
    }

当然我们还可以使用官方的提供的类 PriorityQueue 也是可以的

    public int[] sortByBits(int[] arr) {
        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[1] == b[1] ? a[0] - b[0] : a[1] - b[1]);
        for (int i = 0; i < arr.length; i++) {
            priorityQueue.add(new int[]{arr[i], hammingWeight(arr[i])});
        }
        int index = 0;
        while (!priorityQueue.isEmpty()) {
            arr[index++] = priorityQueue.poll()[0];
        }
        return arr;
    }

    public int hammingWeight(int n) {
        int count = 0;
        while (n != 0) {
            n &= n - 1;
            count++;
        }
        return count;
    }

我把部分算法题整理成了PDF文档,截止目前总共有900多页,大家可以下载阅读
链接:https://pan.baidu.com/s/1hjwK0ZeRxYGB8lIkbKuQgQ
提取码:6666

如果觉得有用就给个赞吧,还可以关注我的LeetCode主页查看更多的详细题解

Java 两次循环打败 100%

作者 yourtion
2020年5月3日 19:07

解题思路

循环并使用 Integer.bitCount 计算数字中1的个数,乘以10000000(题目中不会大于 10^4)然后加上原数字,放入数组 map 中,并对 map 进行排序,最后 % 10000000 获取原来的数组,填充到原数组返回即可。

image.png{:width="350px"}{:align="left"}

代码

###Java

class Solution {
    public int[] sortByBits(int[] arr) {
        int[] map = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            map[i] = Integer.bitCount(arr[i]) * 10000000 + arr[i];
        }
        Arrays.sort(map);
        for (int i = 0; i < map.length; i++) {
            map[i] = map[i] % 10000000;
        }
        return map;
    }
}
昨天 — 2026年2月24日首页

ReactNative新架构之iOS端TurboModule源码剖析

2026年2月24日 21:55

ReactNative新架构之iOS端TurboModule源码剖析

前言

注意,本文是基于React Native 0.83版本源码进行分析。

前面已经通过《ReactNative新架构之Android端TurboModule机制完全解析》了解了很多TurboModule的关键信息,接下来iOS端的分析将会略过一些已知内容,聚焦重点。

初始化

先回顾一下《React Native新架构之iOS端初始化源码分析》一文我们提过的TurboModule初始化流程:

AppDelegate中有:

    // 1: 创建 ReactNativeDelegate
    let delegate = ReactNativeDelegate()
    // 2: 创建 RCTReactNativeFactory
    let factory = RCTReactNativeFactory(delegate: delegate)
    // 3: 配置依赖提供者(Codegen 生成的模块)
    delegate.dependencyProvider = RCTAppDependencyProvider()

接下来再看看RCTReactNativeFactory 中对RCTTurboModuleManagerDelegate协议的实现:

- (Class)getModuleClassFromName:(const char *)name
{
#if RN_DISABLE_OSS_PLUGIN_HEADER
  return RCTTurboModulePluginClassProvider(name);
#else
  // 先尝试从 delegate 获取
  if ([_delegate respondsToSelector:@selector(getModuleClassFromName:)]) {
    Class moduleClass = [_delegate getModuleClassFromName:name];
    if (moduleClass != nil) {
      return moduleClass;
    }
  }
  return RCTCoreModulesClassProvider(name);
#endif
}

- (nullable id<RCTModuleProvider>)getModuleProvider:(const char *)name
{
  if ([_delegate respondsToSelector:@selector(getModuleProvider:)]) {
    return [_delegate getModuleProvider:name];
  }
  return nil;
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
                                                      jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
  if ([_delegate respondsToSelector:@selector(getTurboModule:jsInvoker:)]) {
    return [_delegate getTurboModule:name jsInvoker:jsInvoker];
  }

  return facebook::react::DefaultTurboModules::getTurboModule(name, jsInvoker);
}

- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
#if USE_OSS_CODEGEN
  if (self.delegate.dependencyProvider == nil) {
    [NSException raise:@"ReactNativeFactoryDelegate dependencyProvider is nil"
                format:@"Delegate must provide a valid dependencyProvider"];
  }
#endif
  if ([_delegate respondsToSelector:@selector(getModuleInstanceFromClass:)]) {
    id<RCTTurboModule> moduleInstance = [_delegate getModuleInstanceFromClass:moduleClass];
    if (moduleInstance != nil) {
      return moduleInstance;
    }
  }
  // 使用默认方式创建(通过 dependencyProvider)
  return RCTAppSetupDefaultModuleFromClass(moduleClass, self.delegate.dependencyProvider);
}

可以看到,这里面优先调用_delegate中的方法,而我们在自定义的ReactNativeDelegate中并未实现这些方法,那么只能去父类找,也就是RCTDefaultReactNativeFactoryDelegate.mm

@implementation RCTDefaultReactNativeFactoryDelegate

@synthesize dependencyProvider;

// 省略部分代码......

- (nullable id<RCTModuleProvider>)getModuleProvider:(const char *)name
{
  NSString *providerName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
  return (self.dependencyProvider != nullptr) ? self.dependencyProvider.moduleProviders[providerName] : nullptr;
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
                                                      jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
  return facebook::react::DefaultTurboModules::getTurboModule(name, jsInvoker);
}

- (Class)getModuleClassFromName:(const char *)name
{
  return nullptr;
}

- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
  return nullptr;
}

@end

可以看到,getModuleClassFromNamegetModuleInstanceFromClass并没有具体实现。而getModuleProvider方法,也依赖其属性dependencyProvider。这里的dependencyProvider是由外部赋值,也就是初始化时创建的RCTAppDependencyProvider对象。

这里的RCTAppDependencyProvider实际上是由codegen工具自动生成的,并不在代码中。

TurboModule是懒加载的,这里主要是设置了Provider。继续回顾一下RCTInstance.mm中的_start方法:

  // 创建 TurboModuleManager
  _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridgeProxy:bridgeProxy
                                                     bridgeModuleDecorator:_bridgeModuleDecorator
                                                                  delegate:self
                                                                 jsInvoker:jsCallInvoker
                                             devMenuConfigurationDecorator:_devMenuConfigurationDecorator];

  // 省略......

  // 创建 ContextContainer
  auto contextContainer = std::make_shared<ContextContainer>();
  [_delegate didCreateContextContainer:contextContainer];
  // 插入核心模块
  contextContainer->insert(
      "RCTImageLoader", facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTImageLoader"]));
  contextContainer->insert(
      "RCTEventDispatcher",
      facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTEventDispatcher"]));


  // 初始化 JS Runtime 
  _reactInstance->initializeRuntime(options, [=](jsi::Runtime &runtime) {
    __strong __typeof(self) strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }

    // 安装 TurboModule JS 绑定
    [strongSelf->_turboModuleManager installJSBindings:runtime];
    // 绑定 Native Logger
    facebook::react::bindNativeLogger(runtime, [](const std::string &message, unsigned int logLevel) {
      _RCTLogJavaScriptInternal(static_cast<RCTLogLevel>(logLevel), [NSString stringWithUTF8String:message.c_str()]);
    });

    // 安装 Native Component Registry 绑定
    RCTInstallNativeComponentRegistryBinding(runtime);
    // 省略......
  });

继续跟踪RCTTurboModuleManager的初始化,源码react-native/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm

- (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy
              bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator
                           delegate:(id<RCTTurboModuleManagerDelegate>)delegate
                          jsInvoker:(std::shared_ptr<CallInvoker>)jsInvoker
      devMenuConfigurationDecorator:(RCTDevMenuConfigurationDecorator *)devMenuConfigurationDecorator
{
  return [self initWithBridge:nil
                        bridgeProxy:bridgeProxy
              bridgeModuleDecorator:bridgeModuleDecorator
                           delegate:delegate
                          jsInvoker:jsInvoker
      devMenuConfigurationDecorator:devMenuConfigurationDecorator];
}



- (instancetype)initWithBridge:(RCTBridge *)bridge
                      bridgeProxy:(RCTBridgeProxy *)bridgeProxy
            bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator
                         delegate:(id<RCTTurboModuleManagerDelegate>)delegate
                        jsInvoker:(std::shared_ptr<CallInvoker>)jsInvoker
    devMenuConfigurationDecorator:(RCTDevMenuConfigurationDecorator *)devMenuConfigurationDecorator
{
  if (self = [super init]) {
    _jsInvoker = std::move(jsInvoker);
    _delegate = delegate;
    _bridge = bridge;
    _bridgeProxy = bridgeProxy;
    _bridgeModuleDecorator = bridgeModuleDecorator;
    _invalidating = false;
    // 创建串行队列 _sharedModuleQueue 
    _sharedModuleQueue = dispatch_queue_create("com.meta.react.turbomodulemanager.queue", DISPATCH_QUEUE_SERIAL);
    _devMenuConfigurationDecorator = devMenuConfigurationDecorator;

    if (RCTTurboModuleInteropEnabled()) {
      // 省略旧架构兼容处理......
    }

    // 监听 RCTBridgeWillInvalidateModulesNotification 和 RCTBridgeDidInvalidateModulesNotification 通知,用于处理模块失效场景
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(bridgeWillInvalidateModules:)
                                                 name:RCTBridgeWillInvalidateModulesNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(bridgeDidInvalidateModules:)
                                                 name:RCTBridgeDidInvalidateModulesNotification
                                               object:nil];
  }
  return self;
}

RCTTurboModuleManager初始化逻辑非常简单,我们继续查看[strongSelf->_turboModuleManager installJSBindings:runtime]方法调用,它是在JS Runtime初始化完成之后才回调的,现在跟踪一下具体实现,源码react-native/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm

- (void)installJSBindings:(facebook::jsi::Runtime &)runtime
{
  /**
   * 我们会一直保留 TurboModuleManager 实例,直到 JavaScript 虚拟机被销毁。
   * 仅从 JavaScript 端使用/创建 TurboModule 是完全有效的。
   * 在这种情况下,如果 Objective-C 代码中没有对 TurboModuleManager 的强引用,我们就不应该释放它。
   * 因此,我们让 __turboModuleProxy 持有对 TurboModuleManager 的强引用。
   */
  auto turboModuleProvider = [self,
                              runtime = &runtime](const std::string &name) -> std::shared_ptr<react::TurboModule> {
    auto moduleName = name.c_str();

    TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName);

    // 检查模块是否已初始化
    auto moduleWasNotInitialized = ![self moduleIsInitialized:moduleName];
    if (moduleWasNotInitialized) {
      [self->_bridge.performanceLogger markStartForTag:RCTPLTurboModuleSetup];
    }

    /**
     * 默认情况下,所有 TurboModule 都是长期存在的。 
     * 此外,如果找不到名称为 `name` 的 TurboModule,则会触发断言失败。 
     */
    auto turboModule = [self provideTurboModule:moduleName runtime:runtime];

    if (moduleWasNotInitialized && [self moduleIsInitialized:moduleName]) {
      [self->_bridge.performanceLogger markStopForTag:RCTPLTurboModuleSetup];
    }

    if (turboModule) {
      TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName);
    } else {
      TurboModulePerfLogger::moduleJSRequireEndingFail(moduleName);
    }
    return turboModule;
  };

  if (RCTTurboModuleInteropEnabled()) {
     // 省略旧架构兼容代码......
  } else {
    TurboModuleBinding::install(runtime, std::move(turboModuleProvider));
  }
}

该方法是负责将 TurboModule 系统绑定到 JavaScript 运行时,建立 JS 与原生模块之间的桥梁。

这里的TurboModuleBinding::install方法我们在Android端时已经分析过了,不再赘述。TurboModulePerfLogger是性能日志相关的东西,直接忽略。

所以整个iOS的初始化流程,相比Android端简单太多了。

调用流程

《ReactNative新架构之Android端TurboModule机制完全解析》一文中,我们已经详细分析过了从JS层到C++层的查找逻辑。现在回顾一下关键地方:

// TurboModuleBinding.cpp

jsi::Value TurboModuleBinding::getModule(
    jsi::Runtime& runtime,
    const std::string& moduleName) const {
  std::shared_ptr<TurboModule> module;
  {
    TraceSection s("TurboModuleBinding::moduleProvider", "module", moduleName);
    module = moduleProvider_(moduleName);  // ← 调用moduleProvider获取模块
  }
  if (module) {
    TurboModuleWithJSIBindings::installJSIBindings(module, runtime);
    // 省略......

    // 状态:在 TurboModule 上未找到 jsRepresentation
    // 创建一个全新的 jsRepresentation,并将其附加到 TurboModule
    jsi::Object jsRepresentation(runtime);
    weakJsRepresentation =
        std::make_unique<jsi::WeakObject>(runtime, jsRepresentation);

    // 在属性访问时延迟填充 jsRepresentation。
    //
    // 这是如何工作的?
    //   1. 最初 jsRepresentation 是空的:{}
    //   2. 如果在 jsRepresentation 上的属性查找失败,JS 运行时将
    //   搜索 jsRepresentation 的原型:jsi::Object(TurboModule)。
    //   3. TurboModule::get(runtime, propKey) 执行。这会创建
    //   属性,将其缓存在 jsRepresentation 上,然后将其返回给JavaScript
    auto hostObject =
        jsi::Object::createFromHostObject(runtime, std::move(module));
    jsRepresentation.setProperty(runtime, "__proto__", std::move(hostObject));

    return jsRepresentation; 
  } else {
    return jsi::Value::null();
  }
}

这里是通过moduleProvider_方法返回TurboModule实例。而moduleProvider_实际上就是TurboModuleBinding::install方法调用时设置的那个provider闭包。也就是上面分析的installJSBindings中的turboModuleProvider。回顾一下其实现:

auto turboModule = [self provideTurboModule:moduleName runtime:runtime];

闭包中是调用provideTurboModule返回turboModule,跟踪一下实现:

// RCTTurboModuleManager.mm


/**
 * 给定一个 TurboModule 的名称,返回一个 C++ 对象,该对象是
 * TurboModule C++ 类的实例。这个类包装了 TurboModule 的 ObjC 实例。
 * 如果不存在提供名称的 TurboModule ObjC 类,程序将中止。
 *
 * 注意:所有 TurboModule 实例都被缓存,这意味着它们都是长生命周期的(目前如此)
 */
- (std::shared_ptr<TurboModule>)provideTurboModule:(const char *)moduleName runtime:(jsi::Runtime *)runtime
{
  // 缓存查找
  auto turboModuleLookup = _turboModuleCache.find(moduleName);
  if (turboModuleLookup != _turboModuleCache.end()) {
    TurboModulePerfLogger::moduleJSRequireBeginningCacheHit(moduleName);
    TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);
    return turboModuleLookup->second;
  }

  TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName);

 /**
  * 步骤 1:查找纯 C++ 模块。
  * 纯 C++ 模块具有优先级。
  */
  if ([_delegate respondsToSelector:@selector(getTurboModule:jsInvoker:)]) {
    int32_t moduleId = getUniqueId();
    TurboModulePerfLogger::moduleCreateStart(moduleName, moduleId);
    // 尝试从代理获取纯 C++ TurboModule
    auto turboModule = [_delegate getTurboModule:moduleName jsInvoker:_jsInvoker];
    if (turboModule != nullptr) {
      _turboModuleCache.insert({moduleName, turboModule});
      TurboModulePerfLogger::moduleCreateEnd(moduleName, moduleId);
      return turboModule;
    }

    TurboModulePerfLogger::moduleCreateFail(moduleName, moduleId);
  }

  // 在全局导出的 C++ TurboModule 映射中查找
  auto &cxxTurboModuleMapProvider = globalExportedCxxTurboModuleMap();
  auto it = cxxTurboModuleMapProvider.find(moduleName);
  if (it != cxxTurboModuleMapProvider.end()) {
    auto turboModule = it->second(_jsInvoker);
    _turboModuleCache.insert({moduleName, turboModule});
    return turboModule;
  }

  /**
   * 步骤 2:查找平台特定模块
   */
  id<RCTModuleProvider> module = [self _moduleProviderForName:moduleName];

  TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName);

  // 如果我们请求创建一个 TurboModule,其相应的 ObjC 类必须存在
  // 如果类不存在,那么 _provideObjCModule 返回 nil
  if (!module) {
    return nullptr;
  }

  std::shared_ptr<NativeMethodCallInvoker> nativeMethodCallInvoker = nullptr;
  dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(module, &kAssociatedMethodQueueKey);
  if (methodQueue) {
    /**
     * 步骤 2c:从 TurboModule 的方法队列创建原生 CallInvoker。
     */
    nativeMethodCallInvoker = std::make_shared<ModuleNativeMethodCallInvoker>(methodQueue);

    /**
     * 让 RCTCxxBridge 装饰原生 CallInvoker,使其能够感知 TurboModule 异步方法调用。
     * 这有助于桥接器及时触发 onBatchComplete。
     */
    if ([_bridge respondsToSelector:@selector(decorateNativeMethodCallInvoker:)]) {
      nativeMethodCallInvoker = [_bridge decorateNativeMethodCallInvoker:nativeMethodCallInvoker];
    }
  }

该方法是多级查找策略。查找优先级:

  1. 缓存查找 (最高优先级)
  2. 代理提供的纯 C++ 模块
  3. 全局 C++ 模块映射
  4. 平台特定 ObjC 模块 (最低优先级)

我们先跟踪一下纯C++ TurboModule的查找流程,这里的代理查找,其实最终就是调用DefaultTurboModules::getTurboModule(name, jsInvoker),这是前面已经得到的结论。来看一下其实现,源码react-native/packages/react-native/ReactCommon/react/nativemodule/defaults/DefaultTurboModules.cpp

/* static */ std::shared_ptr<TurboModule> DefaultTurboModules::getTurboModule(
    const std::string& name,
    const std::shared_ptr<CallInvoker>& jsInvoker) {
  if (name == NativeReactNativeFeatureFlags::kModuleName) {
    return std::make_shared<NativeReactNativeFeatureFlags>(jsInvoker);
  }

  if (name == NativeMicrotasks::kModuleName) {
    return std::make_shared<NativeMicrotasks>(jsInvoker);
  }

  if (name == NativeIdleCallbacks::kModuleName) {
    return std::make_shared<NativeIdleCallbacks>(jsInvoker);
  }

  if (name == NativeDOM::kModuleName) {
    return std::make_shared<NativeDOM>(jsInvoker);
  }

  if (ReactNativeFeatureFlags::enableWebPerformanceAPIsByDefault()) {
    if (name == NativePerformance::kModuleName) {
      return std::make_shared<NativePerformance>(jsInvoker);
    }
  }

  if (ReactNativeFeatureFlags::enableIntersectionObserverByDefault()) {
    if (name == NativeIntersectionObserver::kModuleName) {
      return std::make_shared<NativeIntersectionObserver>(jsInvoker);
    }
  }

#ifdef REACT_NATIVE_DEBUGGER_ENABLED_DEVONLY
  if (name == DevToolsRuntimeSettingsModule::kModuleName) {
    return std::make_shared<DevToolsRuntimeSettingsModule>(jsInvoker);
  }
#endif

  return nullptr;
}

可见,此处都是RN内置的一些模块。第三方要实现纯C++ TurboModule,那么注册集成就得考虑从globalExportedCxxTurboModuleMap中下手了。

现在继续查看_moduleProviderForName方法实现:

- (id<RCTModuleProvider>)_moduleProviderForName:(const char *)moduleName
{
  id<RCTModuleProvider> moduleProvider = nil;
  // 从代理获取模块提供者
  if ([_delegate respondsToSelector:@selector(getModuleProvider:)]) {
    moduleProvider = [_delegate getModuleProvider:moduleName];
  }

  if (RCTTurboModuleInteropEnabled() && ![self _isTurboModule:moduleName] && !moduleProvider) {
    return nil;
  }

  if (moduleProvider) {
    if ([moduleProvider conformsToProtocol:@protocol(RCTTurboModule)]) {
      // 如果模块提供者符合 RCTTurboModule 协议,需要初始化 ObjC 属性(如调度队列)
      return (id<RCTModuleProvider>)[self _provideObjCModule:moduleName moduleProvider:moduleProvider];
    }
    // 否则直接返回(C++ 模块)
    return moduleProvider;
  }

  // 如果没有找到模块提供者,尝试创建通过传统方式注册的 ObjC 模块
  return (id<RCTModuleProvider>)[self _provideObjCModule:moduleName moduleProvider:nil];
}


/**
 * 给定一个 NativeModule 的名称,返回该 NativeModule Objective-C 类的实例对象。如果不存在名称与给定名称匹配的 NativeModule,则返回 nil。
 *
 * 注意:所有 NativeModule 实例都会被缓存,这意味着它们都是长期存在的(目前是这样)。
 */
- (id<RCTBridgeModule>)_provideObjCModule:(const char *)moduleName moduleProvider:(id<RCTModuleProvider>)moduleProvider
{
  if (strncmp("RCT", moduleName, 3) == 0) {
    moduleName = [[[NSString stringWithUTF8String:moduleName] substringFromIndex:3] UTF8String];
  }

  ModuleHolder *moduleHolder = [self _getOrCreateModuleHolder:moduleName];

  if (!moduleHolder) {
    return nil;
  }

  TurboModulePerfLogger::moduleCreateStart(moduleName, moduleHolder->getModuleId());
  id<RCTBridgeModule> module = [self _provideObjCModule:moduleName
                                           moduleHolder:moduleHolder
                                          shouldPerfLog:YES
                                         moduleProvider:moduleProvider];

  if (module) {
    TurboModulePerfLogger::moduleCreateEnd(moduleName, moduleHolder->getModuleId());
  } else {
    TurboModulePerfLogger::moduleCreateFail(moduleName, moduleHolder->getModuleId());
  }

  return module;
}

// 获取或者创建一个ModuleHolder
- (ModuleHolder *)_getOrCreateModuleHolder:(const char *)moduleName
{
  std::lock_guard<std::mutex> guard(_moduleHoldersMutex);
  if (_invalidating) {
    return nullptr;
  }

  return &_moduleHolders[moduleName];
}

// 创建和管理 Objective-C 原生模块的实例,确保线程安全和单例模式
- (id<RCTBridgeModule>)_provideObjCModule:(const char *)moduleName
                             moduleHolder:(ModuleHolder *)moduleHolder
                            shouldPerfLog:(BOOL)shouldPerfLog
                           moduleProvider:(id<RCTModuleProvider>)moduleProvider
{
  bool shouldCreateModule = false;

  {
    std::lock_guard<std::mutex> guard(moduleHolder->mutex());

    // 如果模块已创建完成,直接返回缓存的实例
    if (moduleHolder->isDoneCreatingModule()) {
      if (shouldPerfLog) {
        TurboModulePerfLogger::moduleCreateCacheHit(moduleName, moduleHolder->getModuleId());
      }
      return moduleHolder->getModule();
    }

    if (!moduleHolder->isCreatingModule()) {
      shouldCreateModule = true;
      moduleHolder->startCreatingModule();
    }
  }

  if (shouldCreateModule) {
    /**
     * 步骤 2a:解析特定于平台的类
     */
    Class moduleClass = moduleProvider ? [moduleProvider class] : [self _getModuleClassFromName:moduleName];

    __block id<RCTBridgeModule> module = nil;

    // 判断是否应该创建模块(检查是否实现了RCTTurboModule协议)
    if ([self _shouldCreateObjCModule:moduleClass]) {
      __weak __typeof(self) weakSelf = self;
      dispatch_block_t work = ^{
        auto strongSelf = weakSelf;
        if (!strongSelf) {
          return;
        }
        module = [strongSelf _createAndSetUpObjCModule:moduleClass moduleName:moduleName moduleId:moduleHolder
                      ->getModuleId()];
      };

      // 某些模块需要在主线程初始化
      if ([self _requiresMainQueueSetup:moduleClass]) {
        RCTUnsafeExecuteOnMainQueueSync(work);
      } else {
        work();
      }
    }

    {
      std::lock_guard<std::mutex> guard(moduleHolder->mutex());

      moduleHolder->setModule(module);
      moduleHolder->endCreatingModule();
    }
    moduleHolder->cv().notify_all();

    return module;
  }

  std::unique_lock<std::mutex> guard(moduleHolder->mutex());

  // 如果其他线程正在创建模块,当前线程等待
  while (moduleHolder->isCreatingModule()) {
    /**
     * TODO(T65905574):
     * 如果负责创建和初始化 NativeModule 的线程发生阻塞,我们将在此处无限期等待
     * 这是传统 NativeModule 的行为。现在更改此行为可能会导致 TurboModule 中出现比 NativeModule 更多的崩溃/问题
     * 从而使 TurboModule 基础设施的测试更加困难。因此,我们应该考虑在 TurboModule 全面推广之后再进行更改
     */
    moduleHolder->cv().wait(guard);
  }

  return moduleHolder->getModule();
}

接下继续跟踪_createAndSetUpObjCModule实现:

/**
 * 给定一个 NativeModule 类及其名称,同步创建并初始化它。
 *
 * 此方法可以从两个不同的上下文中同步调用:
 *  - 调用 _provideObjCModule: 的线程
 *  - 主线程(如果 NativeModule 需要主队列初始化),阻塞调用 _provideObjCModule: 的线程。
 */
- (id<RCTBridgeModule>)_createAndSetUpObjCModule:(Class)moduleClass
                                      moduleName:(const char *)moduleName
                                        moduleId:(int32_t)moduleId
{
  id<RCTBridgeModule> module = nil;

  /**
   * 步骤 2b:请求宿主应用/代理来实例化此类
   */

  module = [self _getModuleInstanceFromClass:moduleClass];

  TurboModulePerfLogger::moduleCreateSetUpStart(moduleName, moduleId);

 /**
  * NativeModules 不需要/不想要 bridge 是合理的。
  * 在这种情况下,它们的实现中不会有 `@synthesize bridge = _bridge`,
  * ObjC 运行时也不会生成 `- (RCTBridge *) bridge { ... }` 方法。
  * 该属性也不会有 ivar 支持,这使得写入它变得不安全。
  * 因此,我们检查此方法是否存在,以确定是否可以安全地将 bridge 设置到 NativeModule。
  */
  if ([module respondsToSelector:@selector(bridge)] && (_bridge || _bridgeProxy)) {
    /**
      * 仅仅因为 NativeModule 有 `bridge` 方法,并不意味着
      * 它在其实现中合成了 bridge。因此,
      * 我们需要将设置 bridge 到 NativeModule 的代码
      * 包裹在 try/catch 中。这可以捕获 NativeModule
      * 作者手动指定 `bridge` 方法的情况。
      */
    @try {
        /**
         * RCTBridgeModule 将 bridge 属性声明为只读。
         * 因此,当 NativeModules 的作者通过 @synthesize bridge = bridge; 合成 bridge 时,
         * ObjC 运行时只生成 - (RCTBridge *) bridge: { ... } 方法。
         * 没有生成 setter,所以我们必须依赖 ObjC 的 KVC API
         * 来设置这些 NativeModules 的 bridge 属性。
         */
      if (_bridge) {
        [(id)module setValue:_bridge forKey:@"bridge"];
      } else if (_bridgeProxy) {
        [(id)module setValue:_bridgeProxy forKey:@"bridge"];
      }
    } @catch (NSException *exception) {
      RCTLogError(
          @"%@ has no setter or ivar for its bridge, which is not "
           "permitted. You must either @synthesize the bridge property, "
           "or provide your own setter method.",
          RCTBridgeModuleNameForClass([module class]));
    }
  }

  // 这是 conformsToProtocol:@protocol(RCTCallInvokerModule) 的更高性能替代方案
  if ([module respondsToSelector:@selector(setCallInvoker:)]) {
    RCTCallInvoker *callInvoker = [[RCTCallInvoker alloc] initWithCallInvoker:_jsInvoker];
    [(id<RCTCallInvokerModule>)module setCallInvoker:callInvoker];
  }

 /**
  * 一些模块需要自己的队列,但没有提供,所以我们需要为它们创建。
  * 这些模块通常有以下内容:
  *   `@synthesize methodQueue = _methodQueue`
  */

  dispatch_queue_t methodQueue = nil;
  BOOL moduleHasMethodQueueGetter = [module respondsToSelector:@selector(methodQueue)];

  if (moduleHasMethodQueueGetter) {
    methodQueue = [(id<RCTBridgeModule>)module methodQueue];
  }

  /**
   * 注意:RCTJSThread 是一个有效的方法队列,定义为 (id)kCFNull。
   * 它应该正确地不进入以下 if 条件块。
   */
  if (!methodQueue) {
    methodQueue = _sharedModuleQueue;

    if (moduleHasMethodQueueGetter) {
        /**
         * 如果模块有方法队列 getter,有两种情况:
         *  - 我们 @synthesize 了方法队列。在这种情况下,getter 最初会返回 nil。
         *  - 我们在 NativeModule 上有自定义的 methodQueue 函数。如果我们走到这里,
         *    那么该 getter 返回了 nil。
         *
         * 因此,我们使用 try/catch 和 ObjC 的 KVC API 尝试将方法队列分配给 NativeModule。
         * 在情况 1 中,我们会成功。在情况 2 中,会抛出异常,我们将忽略它。
         */

      @try {
        [(id)module setValue:methodQueue forKey:@"methodQueue"];
      } @catch (NSException *exception) {
        RCTLogError(
            @"%@ has no setter or ivar for its methodQueue, which is not "
             "permitted. You must either @synthesize the methodQueue property, "
             "or provide your own setter method.",
            RCTBridgeModuleNameForClass([module class]));
      }
    }
  }

  /**
   * 用 bridgeless 兼容的 API 装饰 NativeModules,这些 API 会调用 bridge。
   */
  if (_bridgeModuleDecorator) {
    [_bridgeModuleDecorator attachInteropAPIsToModule:module];
  }

  /**
   * 如果 NativeModule 符合 RCTInitializing 协议,调用其 initialize 方法
   */
  if ([module respondsToSelector:@selector(initialize)]) {
    [(id<RCTInitializing>)module initialize];
  }

#if RCT_DEV_MENU

  [_devMenuConfigurationDecorator decorate:module];

#endif

  /**
   * 将方法队列附加到 id<RCTBridgeModule> 对象。
   * 这是必要的,因为 id<RCTBridgeModule> 对象可以在需要方法队列之前被急切地创建/初始化。
   * 方法队列用于 id<RCTBridgeModule> 的 JS -> Native 调用。
   * 因此,我们需要在 provideTurboModule:runtime: 中创建 id<RCTBridgeModule> 的 
   * TurboModule jsi::HostObject 之前拥有它。
   */
  objc_setAssociatedObject(module, &kAssociatedMethodQueueKey, methodQueue, OBJC_ASSOCIATION_RETAIN);

  /**
   * 广播此 NativeModule 已创建
   *
   * TODO(T41180176): Investigate whether we can delete this after TM
   * rollout.
   */
  [[NSNotificationCenter defaultCenter]
      postNotificationName:RCTDidInitializeModuleNotification
                    object:_bridge
                  userInfo:@{@"module" : module, @"bridge" : RCTNullIfNil([_bridge parentBridge])}];

  TurboModulePerfLogger::moduleCreateSetUpEnd(moduleName, moduleId);

  return module;
}

这个方法实现很长,但其实就是创建 ObjC 模块实例并注入所有必要的依赖。具体说,就是:

  • 注入 BridgeProxy,让模块能调用 JS

  • 注入 CallInvoker,新架构的 JSI 调用机制

  • 配置方法队列,模块的异步方法在此队列执行

继续查看_getModuleInstanceFromClass方法的实现:

- (id<RCTBridgeModule>)_getModuleInstanceFromClass:(Class)moduleClass
{
  NSString *moduleNameStr = RCTBridgeModuleNameForClass(moduleClass);
  // 省略旧架构代码......

  id<RCTBridgeModule> module = (id<RCTBridgeModule>)[_delegate getModuleInstanceFromClass:moduleClass];

  if (!module) {
    module = [moduleClass new];
  }

  return module;
}

可见,最终还是通过代理的getModuleInstanceFromClass方法查找模块。前面已经分析过了,此方法最终也是调用RCTAppSetupDefaultModuleFromClass(moduleClass, self.delegate.dependencyProvider)方法查找,继续跟踪实现:

id<RCTTurboModule> RCTAppSetupDefaultModuleFromClass(Class moduleClass, id<RCTDependencyProvider> dependencyProvider)
{
  // private block used to filter out modules depending on protocol conformance
  NSArray * (^extractModuleConformingToProtocol)(RCTModuleRegistry *, Protocol *) =
      ^NSArray *(RCTModuleRegistry *moduleRegistry, Protocol *protocol) {
        NSArray<NSString *> *classNames = @[];

        if (protocol == @protocol(RCTImageURLLoader)) {
          classNames = (dependencyProvider != nullptr) ? dependencyProvider.imageURLLoaderClassNames : @[];
        } else if (protocol == @protocol(RCTImageDataDecoder)) {
          classNames = (dependencyProvider != nullptr) ? dependencyProvider.imageDataDecoderClassNames : @[];
        } else if (protocol == @protocol(RCTURLRequestHandler)) {
          classNames = (dependencyProvider != nullptr) ? dependencyProvider.URLRequestHandlerClassNames : @[];
        }

        NSMutableArray *modules = [NSMutableArray new];

        for (NSString *className in classNames) {
          const char *cModuleName = [className cStringUsingEncoding:NSUTF8StringEncoding];
          id moduleFromLibrary = [moduleRegistry moduleForName:cModuleName];
          if (![moduleFromLibrary conformsToProtocol:protocol]) {
            continue;
          }
          [modules addObject:moduleFromLibrary];
        }
        return modules;
      };
  // 省略......

  // No custom initializer here.
  return [moduleClass new];
}

三方TurboModule显然不符合上面的if判断逻辑,那么最终其实就是通过[moduleClass new]创建了RCTTurboModule的实例对象。

TurboModule注册

自动链接

首先打开react-native工程中提供的helloworld工程,配置文件react-native/private/helloworld/ios/Podfile

target 'HelloWorld' do
  config = use_native_modules!(['sh', '../scripts/config.sh'])

  use_react_native!(
    :path => "../../../packages/react-native",
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."
  )

  target 'HelloWorldTests' do
    inherit! :complete
    # Pods for testing
  end

  post_install do |installer|
    # scripts/react_native_pods.rb
    react_native_post_install(
      installer,
      config[:reactNativePath],
      :mac_catalyst_enabled => false,
      # :ccache_enabled => true
    )
  end
end

在以上的CocoaPod配置脚本中,use_native_modules! 函数是iOS端自动链接 (Autolinking) 的核心,它负责发现并链接所有第三方原生库,源码react-native/packages/react-native/scripts/cocoapods/autolinking.rb

# 自动链接原生模块
#
# Parameters:
# - config_command: 用于获取应用程序当前配置的命令,例如 ['npx', '@react-native-community/cli', 'config'],
# 您可以根据需要覆盖此命令,以避免依赖项。例如 ['cat', 'your_config.json']
def use_native_modules!(config_command = $default_command)
  return link_native_modules!(list_native_modules!(config_command))
end

继续查看list_native_modules!函数:

# 如果您使用这种方法来列出 React Native 模块,您的项目必须依赖于 @react-native-community/cli
#
# Parameters:
# - config_command: 用于获取应用程序当前配置的命令,例如 ['npx', '@react-native-community/cli', 'config']
def list_native_modules!(config_command)

  if !(config_command.is_a? Array and config_command.size > 0)
    # 省略日志打印......
    exit(1)
  end

  # 1. 执行 CLI 命令获取项目配置
  # 忽略标准错误输出,我们只关心标准输出和返回码。库可能会向标准错误输出警告信息,这会给 JSON 反序列化带来问题
  json, _, status = Pod::Executable.capture_command(config_command[0], config_command[1..], capture: :both)

  if not status.success?
    # 省略日志输出......
    exit(status.exitstatus)
  end

  config = JSON.parse(json)
  # 2. 获取所有依赖包
  packages = config["dependencies"]
  ios_project_root = Pathname.new(config["project"]["ios"]["sourceDir"])
  react_native_path = Pathname.new(config["reactNativePath"])
  codegen_output_path = ios_project_root.join("build/generated/autolinking/autolinking.json")

  # 3. 将配置写入 autolinking.json (供 Codegen 使用)
  FileUtils.mkdir_p(File.dirname(codegen_output_path))
  File.write(codegen_output_path, json)

  found_pods = []

  # 4. 遍历所有包,检查是否有 iOS 配置
  packages.each do |package_name, package|
    next unless package_config = package["platforms"]["ios"]

    name = package["name"]
    podspec_path = package_config["podspecPath"]
    script_phases = package_config["scriptPhases"]
    configurations = package_config["configurations"]

    # 跳过没有 podspec 的包
    if podspec_path.nil? || podspec_path.empty?
      # 省略......
      next
    end

    spec = Pod::Specification.from_file(podspec_path)

    # 跳过不支持当前目标平台的环境。
    next unless AutolinkingUtils.is_platform_supported?(current_target_definition, spec)

    podspec_dir_path = Pathname.new(File.dirname(podspec_path))

    relative_path = podspec_dir_path.relative_path_from ios_project_root

    found_pods.push({
      "configurations": configurations,
      "name": name,
      "root": package["root"],
      "path": relative_path.to_path,
      "podspec_path": podspec_path,
      "script_phases": script_phases
    })
  end

  if found_pods.size > 0
    pods = found_pods.map { |p| p[:name] }.sort.to_sentence
    Pod::UI.puts "Found #{found_pods.size} #{"module".pluralize(found_pods.size)} for target `#{current_target_definition.name}`"
  end

  return {
    "ios_packages": found_pods,
    "ios_project_root_path": ios_project_root.to_s,
    "react_native_path": react_native_path.relative_path_from(ios_project_root).to_s
  }
end

该函数主要是发现第三方库并返回扫描到的数据,生成autolinking.json。接下来查看link_native_modules!函数实现:

# Parameters:
# - config:
#   - :ios_packages - React Native iOS 包数组, e.g. [{ package_name: "Foo", package: { .. }}, ...]
#   - :ios_project_root_path - React Native 项目的 iOS 文件夹的绝对路径, e.g. /Users/foobar/project/rn_project/ios
#   - :react_native_path - React Native 相对于项目根目录的相对路径, e.g. ./node_modules/react-native
def link_native_modules!(config)
  Pod::UI.puts "link_native_modules! #{config}"

  if !(
    config[:ios_packages].is_a? Array and
    config[:ios_project_root_path].is_a? String and
    config[:react_native_path].is_a? String
  )
    # 省略日志打印......
    exit(1)
  end
  # 提取项目根路径
  ios_project_root = config[:ios_project_root_path]
  # 提取包列表
  packages = config[:ios_packages]
  found_pods = []

  # 遍历包并自动链接
  packages.each do |package|
    podspec_path = package[:podspec_path]
    configurations = package[:configurations]

    # 如果 podspec_path 为 nil 或空,添加警告到队列并继续处理下一个依赖
    if podspec_path.nil? || podspec_path.empty?
      # 省略日志打印......
      next
    end
    # 读取并解析 .podspec 文件,获取包的完整规范信息。
    spec = Pod::Specification.from_file(podspec_path)

    # 检查该包是否支持当前平台(iOS),如果不支持,跳过该包
    next unless AutolinkingUtils.is_platform_supported?(current_target_definition, spec)

    # 我们想在当前 CocoaPods target 内部进行查找
    # 以查看它是否已包含,这样做可以:
    #   1. 给你一个机会预先定义它
    #   2. 确保 CocoaPods 不会因为包含两次而崩溃
    #
    this_target = current_target_definition
    existing_deps = current_target_definition.dependencies

    # 跳过用户已经自己激活的依赖
    next if existing_deps.find do |existing_dep|
      existing_dep.name.split('/').first == spec.name
    end

    podspec_dir_path = Pathname.new(File.dirname(podspec_path))

    relative_path = podspec_dir_path.relative_path_from ios_project_root

    # 将找到的 React Native 模块注册到我们的 Pods 集合中(调用 CocoaPods 的 pod DSL 方法,添加依赖)
    pod spec.name, :path => relative_path.to_path, :configurations => configurations

    if package[:script_phases] && !this_target.abstract?
      # 可以是一个对象,或对象数组
      Array(package[:script_phases]).each do |phase|
        # see https://www.rubydoc.info/gems/cocoapods-core/Pod/Podfile/DSL#script_phase-instance_method
        # 获取完整的对象键
        Pod::UI.puts "Adding a custom script phase for Pod #{spec.name}: #{phase["name"] || 'No name specified.'}"

        # 支持传入相对于包根目录的路径
        if phase["path"]
          phase["script"] = File.read(File.expand_path(phase["path"], package[:root]))
          phase.delete("path")
        end

        # 支持将执行位置转换为 symbol
        phase["execution_position"] = phase["execution_position"]&.to_sym

        phase = Hash[phase.map { |k, v| [k.to_sym, v] }]
        script_phase phase
      end
    end

    found_pods.push spec
  end

  if found_pods.size > 0
    pods = found_pods.map { |p| p.name }.sort.to_sentence
    # 省略打印......
  end

  return {
    :reactNativePath => config[:react_native_path]
  }
end

此函数就是在处理node_module中下载的依赖,最核心的其实就是pod spec.name, :path => relative_path.to_path, :configurations => configurations这行代码,它等价于我们习惯的类似配置:

pod 'react-native-camera', :path => '../node_modules/react-native-camera', :configurations => ['Debug', 'Release']

环境配置

弄清楚了use_native_modules!自动链接,接下来再看看use_react_native! 做了什么,源码react-native/packages/react-native/scripts/react_native_pods.rb

# 设置所有 React Native 依赖的函数
# 
# 参数
# - path: React Native 安装路径。
# - fabric_enabled: 是否应启用 Fabric。
# - new_arch_enabled: [已弃用] 是否应启用新架构。
# - :production [已弃用] 依赖是否必须安装到 Debug 或 Release 构建目标。
# - hermes_enabled: 是否应启用 Hermes。
# - app_path: React Native 应用的路径。新架构需要。
# - config_file_dir: `package.json` 文件的目录,新架构需要。
def use_react_native! (
  path: "../node_modules/react-native",
  fabric_enabled: false,
  new_arch_enabled: NewArchitectureHelper.new_arch_enabled,
  production: false, # 已弃用
  hermes_enabled: true, # 已弃用。Hermes 是默认引擎,JSC 已移至社区支持
  app_path: '..',
  config_file_dir: '',
  privacy_file_aggregation_enabled: true
)
  # 1.初始化和环境变量设置

  error_if_try_to_use_jsc_from_core()
  warn_if_new_arch_disabled()

  hermes_enabled= true
  # 将 app_path 设置为环境变量,以便 podspecs 可以访问它
  ENV['APP_PATH'] = app_path
  ENV['REACT_NATIVE_PATH'] = path

  # 如果用户想跳过从 CocoaPods 运行 Codegen 步骤,我们将 RCT_SKIP_CODEGEN 设置为 true。
  # 这是我们从 CocoaPods 迁移过程中所需的
  ENV['RCT_SKIP_CODEGEN'] = ENV['RCT_SKIP_CODEGEN'] == '1' || ENV['RCT_IGNORE_PODS_DEPRECATION'] == '1' ? '1' : '0'

  ReactNativePodsUtils.check_minimum_required_xcode()

  # 当前目标定义由 CocoaPods 提供,它指的是
  # 调用了 `use_react_native!` 函数的目标。
  ReactNativePodsUtils.detect_use_frameworks(current_target_definition)

  # 2.清理和版本检测

  CodegenUtils.clean_up_build_folder(path, $CODEGEN_OUTPUT_DIR)

  # 我们也在第三方库中依赖此标志来正确安装依赖。
  # 如果使用标志启用新架构,最好依赖并启用此环境标志。
  relative_path_from_current = Pod::Config.instance.installation_root.relative_path_from(Pathname.pwd)
  react_native_version = NewArchitectureHelper.extract_react_native_version(File.join(relative_path_from_current, path))
  fabric_enabled = true

  ENV['RCT_FABRIC_ENABLED'] = "1"
  ENV['RCT_AGGREGATE_PRIVACY_FILES'] = privacy_file_aggregation_enabled ? "1" : "0"
  ENV["RCT_NEW_ARCH_ENABLED"] = "1"

  # 3.依赖配置(配置 React Native 核心库)

  prefix = path

  ReactNativePodsUtils.warn_if_not_on_arm64()

  # 更新 ReactNativeDependencies,以便我们可以轻松地在源码和预构建之间切换
  ReactNativeDependenciesUtils.setup_react_native_dependencies(prefix, react_native_version)

  # 更新 ReactNativeCoreUtils,以便我们可以轻松地在源码和预构建之间切换
  ReactNativeCoreUtils.setup_rncore(prefix, react_native_version)

  Pod::UI.puts "Configuring the target with the New Architecture\n"

  # 应包含在所有项目中的 Pods
  pod 'FBLazyVector', :path => "#{prefix}/Libraries/FBLazyVector"
  pod 'RCTRequired', :path => "#{prefix}/Libraries/Required"
  pod 'RCTTypeSafety', :path => "#{prefix}/Libraries/TypeSafety", :modular_headers => true
  pod 'React', :path => "#{prefix}/"
  if !ReactNativeCoreUtils.build_rncore_from_source()
    pod 'React-Core-prebuilt', :podspec => "#{prefix}/React-Core-prebuilt.podspec", :modular_headers => true
  end
  pod 'React-Core', :path => "#{prefix}/"
  pod 'React-CoreModules', :path => "#{prefix}/React/CoreModules"
  pod 'React-RCTRuntime', :path => "#{prefix}/React/Runtime"
  # 省略部分......
  pod 'React-jsi', :path => "#{prefix}/ReactCommon/jsi"
  pod 'RCTSwiftUI', :path => "#{prefix}/ReactApple/RCTSwiftUI"
  pod 'RCTSwiftUIWrapper', :path => "#{prefix}/ReactApple/RCTSwiftUIWrapper"

  if hermes_enabled
    setup_hermes!(:react_native_path => prefix)
  end

  pod 'React-jsiexecutor', :path => "#{prefix}/ReactCommon/jsiexecutor"
  # 省略部分......
  pod 'ReactCommon/turbomodule/core', :path => "#{prefix}/ReactCommon", :modular_headers => true
  pod 'React-NativeModulesApple', :path => "#{prefix}/ReactCommon/react/nativemodule/core/platform/ios", :modular_headers => true
  pod 'Yoga', :path => "#{prefix}/ReactCommon/yoga", :modular_headers => true
  setup_fabric!(:react_native_path => prefix)
  setup_bridgeless!(:react_native_path => prefix, :use_hermes => hermes_enabled)

  if ReactNativeDependenciesUtils.build_react_native_deps_from_source()
    pod 'DoubleConversion', :podspec => "#{prefix}/third-party-podspecs/DoubleConversion.podspec"
    pod 'glog', :podspec => "#{prefix}/third-party-podspecs/glog.podspec"
    pod 'boost', :podspec => "#{prefix}/third-party-podspecs/boost.podspec"
    pod 'fast_float', :podspec => "#{prefix}/third-party-podspecs/fast_float.podspec"
    pod 'fmt', :podspec => "#{prefix}/third-party-podspecs/fmt.podspec", :modular_headers => true
    pod 'RCT-Folly', :podspec => "#{prefix}/third-party-podspecs/RCT-Folly.podspec", :modular_headers => true
    pod 'SocketRocket', "~> #{Helpers::Constants::socket_rocket_config[:version]}", :modular_headers => true
  else
    # Install prebuilt React Native Core and React Native Dependencies
    ReactNativeCoreUtils.rncore_log("Using React Native Core and React Native Dependencies prebuilt versions.")
    pod 'ReactNativeDependencies', :podspec => "#{prefix}/third-party-podspecs/ReactNativeDependencies.podspec", :modular_headers => true

    if !ReactNativeCoreUtils.build_rncore_from_source()
      pod 'React-Core-prebuilt', :podspec => "#{prefix}/React-Core-prebuilt.podspec", :modular_headers => true
    end
  end

  pod 'ReactCodegen', :path => $CODEGEN_OUTPUT_DIR, :modular_headers => true
  pod 'ReactAppDependencyProvider', :path => $APP_DEPENDENCY_PROVIDER_OUTPUT_DIR, :modular_headers => true
  # 不需要,但 run_codegen 期望设置此值。
  folly_config = get_folly_config()
  # 运行 Codegen:扫描所有 TurboModule 和 Fabric 组件规范,生成 C++ 绑定代码
  run_codegen!(
    app_path,
    config_file_dir,
    :new_arch_enabled => NewArchitectureHelper.new_arch_enabled,
    :disable_codegen => ENV['DISABLE_CODEGEN'] == '1',
    :react_native_path => prefix,
    :fabric_enabled => fabric_enabled,
    :hermes_enabled => hermes_enabled,
    :codegen_output_dir => $CODEGEN_OUTPUT_DIR,
    :package_json_file => File.join(__dir__, "..", "package.json"),
    :folly_version => folly_config[:version]
  )

  pods_to_update = LocalPodspecPatch.pods_to_update(:react_native_path => prefix)
  if !pods_to_update.empty?
    if Pod::Lockfile.public_instance_methods.include?(:detect_changes_with_podfile)
      Pod::Lockfile.prepend(LocalPodspecPatch)
    else
      # 省略打印......
    end
  end
end

此函数太长,这里省略了大部分的依赖配置项。这里总结一下该函数做的事:

  1. 配置环境:设置环境变量、检查 Xcode 版本

  2. 添加 60+ 个 Pod 依赖:涵盖从核心到工具的所有模块

  3. 设置新架构:Fabric、TurboModule、Bridgeless

  4. 配置 Hermes 引擎

  5. 运行 Codegen:自动生成 C++ 绑定代码

  6. 处理预构建二进制:加速编译

总的说,这是 React Native iOS 项目的"一键配置"函数,它设置了新架构所需的所有依赖和工具链。

代码生成

继续分析run_codegen!函数,探索iOS端,是如何自动生成代码的。源码react-native/packages/react-native/scripts/cocoapods/codegen.rb

def run_codegen!(
  app_path,                                              # 应用根目录路径(必需)
  config_file_dir,                                       # 配置文件目录(必需)
  new_arch_enabled: true,                                # 是否启用新架构(默认 true,但当前未使用)
  disable_codegen: false,                                # 是否禁用 Codegen(默认 false)
  react_native_path: "../node_modules/react-native",    # React Native 源码路径
  fabric_enabled: false,                                 # 是否启用 Fabric(默认 false)
  hermes_enabled: true,                                  # 是否启用 Hermes(默认 true)
  codegen_output_dir: 'build/generated/ios',            # Codegen 输出目录
  config_key: 'codegenConfig',                          # package.json 中配置键名
  package_json_file: '~/app/package.json',              # package.json 文件路径(当前未使用)
  folly_version: Helpers::Constants.folly_config()[:version], # Folly 库版本
  codegen_utils: CodegenUtils.new()                     # CodegenUtils 实例(可注入用于测试)
  )

  if ENV["RCT_SKIP_CODEGEN"] == "1"
    return
  end

  codegen_utils.use_react_native_codegen_discovery!(
    disable_codegen,
    app_path,
    :react_native_path => react_native_path,
    :fabric_enabled => fabric_enabled,
    :hermes_enabled => hermes_enabled,
    :config_file_dir => config_file_dir,
    :codegen_output_dir => codegen_output_dir,
    :config_key => config_key,
    :folly_version => folly_version
  )
end

可以看到,实际上是委托给 CodegenUtils 执行代码生成。继续查看源码react-native/packages/react-native/scripts/cocoapods/codegen_utils.rb

   def use_react_native_codegen_discovery!(
      codegen_disabled,
      app_path,
      react_native_path: "../node_modules/react-native",
      fabric_enabled: false,
      hermes_enabled: true,
      config_file_dir: '',
      codegen_output_dir: 'build/generated/ios',
      config_key: 'codegenConfig',
      folly_version: Helpers::Constants.folly_config[:version],
      codegen_utils: CodegenUtils.new(),
      file_manager: File,
      logger: CodegenUtils::UI
      )
      return if codegen_disabled

      if CodegenUtils.react_codegen_discovery_done()
        logger.puts("Skipping use_react_native_codegen_discovery.")
        return
      end

      if !app_path
        logger.warn("Error: app_path is required for use_react_native_codegen_discovery.")
        logger.warn("If you are calling use_react_native_codegen_discovery! in your Podfile, please remove the call and pass `app_path` and/or `config_file_dir` to `use_react_native!`.")
        abort
      end

      relative_installation_root = Pod::Config.instance.installation_root.relative_path_from(Pathname.pwd)

      out = Pod::Executable.execute_command(
        'node',
        [
          "#{relative_installation_root}/#{react_native_path}/scripts/generate-codegen-artifacts.js",
          "-p", "#{app_path}",
          "-o", Pod::Config.instance.installation_root,
          "-t", "ios",
        ])
      Pod::UI.puts out;
      #  标记完成,防止重复执行
      CodegenUtils.set_react_codegen_discovery_done(true)
    end

此处实际上仍然是调用JS工具来生成代码,这里执行的命令,相当于:

node node_modules/react-native/scripts/generate-codegen-artifacts.js \
  -p /narwal/Your Project/helloworld \
  -o /narwal/Your Project/helloworld/ios \
  -t ios

继续跟踪react-native/packages/react-native/scripts/generate-codegen-artifacts.js

const executor = require('./codegen/generate-artifacts-executor');
// 省略......
executor.execute(argv.path, argv.targetPlatform, argv.outputPath, argv.source);

查看execute实现,源码react-native/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js

/**
 * 此函数是代码生成器的入口点。它执行以下操作:
 * - 读取 package.json 文件
 * - 提取库信息
 * - 设置命令行界面以生成代码
 * - 生成代码
 *
 * @param projectRoot:包含应用程序源代码的目录,package.json 文件位于该目录下。 
 * @param baseOutputPath:代码生成器的基本输出路径。 
 * @param targetPlatform:目标平台。支持的值包括:“android”、“ios”、“all”。 
 * @param source:调用代码生成器的来源。支持的值包括:“app”、“library”。 
 * @throws 如果找不到 React Native 的配置文件。 
 * @throws 如果在文件中找不到代码生成器配置。 
 * @throws 如果找不到代码生成器的命令行工具。
 */
function execute(
  projectRoot /*: string */,
  targetPlatform /*: string */,
  optionalBaseOutputPath /*: ?string */,
  source /*: string */,
  runReactNativeCodegen /*: boolean */ = true,
) {
  try {
    codegenLog(`Analyzing ${path.join(projectRoot, 'package.json')}`);

    const supportedPlatforms = ['android', 'ios'];
    if (
      targetPlatform !== 'all' &&
      !supportedPlatforms.includes(targetPlatform)
    ) {
      throw new Error(
        `Invalid target platform: ${targetPlatform}. Supported values are: ${supportedPlatforms.join(
          ', ',
        )}, all`,
      );
    }
    // 1.读取应用的 package.json
    const pkgJson = readPkgJsonInDirectory(projectRoot);

    if (runReactNativeCodegen) {
      // 2.如果 Codegen CLI 未构建,先执行 yarn build
      buildCodegenIfNeeded();
    }

    const platforms =
      targetPlatform === 'all' ? supportedPlatforms : [targetPlatform];

    // NOTE: We cache the external libraries search (which may not run) across platforms to not change previous behaviour
    const externalLibrariesCache /*: { current?: ?Array<$FlowFixMe> } */ = {};

    for (const platform of platforms) {
      // NOTE: This needs to be computed per-platform since `platform` can alter the path via a `package.json:codegenConfig.outputDir[platform]` override
      const baseOutputPath = computeBaseOutputPath(
        projectRoot,
        optionalBaseOutputPath,
        pkgJson,
        platform,
      );
      const reactNativeConfig = readReactNativeConfig(
        projectRoot,
        baseOutputPath,
      );
      // 3.扫描所有依赖库,找出包含 codegenConfig 的库
      const codegenEnabledLibraries = findCodegenEnabledLibraries(
        pkgJson,
        projectRoot,
        baseOutputPath,
        reactNativeConfig,
        externalLibrariesCache,
      );
      if (codegenEnabledLibraries.length === 0) {
        codegenLog('No codegen-enabled libraries found.', true);
      }

      const disabledLibraries = findDisabledLibrariesByPlatform(
        reactNativeConfig,
        platform,
      );
      const libraries = codegenEnabledLibraries.filter(
        ({name}) => !disabledLibraries.includes(name),
      );

      const outputPath = computeOutputPath(
        projectRoot,
        baseOutputPath,
        pkgJson,
        platform,
      );

      const reactCodegenOutputPath =
        platform === 'android'
          ? outputPath
          : path.join(outputPath, 'ReactCodegen');

      if (runReactNativeCodegen) {
        // 4.为每个库生成 Schema JSON(描述 TurboModule/Fabric 接口)
        const schemaInfos = generateSchemaInfos(libraries);
        // 5.根据 Schema 生成 C++/ObjC++ 代码
        generateNativeCode(
          reactCodegenOutputPath,
          schemaInfos.filter(schemaInfo =>
            mustGenerateNativeCode(projectRoot, schemaInfo),
          ),
          pkgJsonIncludesGeneratedCode(pkgJson),
          platform,
        );
      }

      if (source === 'app' && platform !== 'android') {
        // 6.生成 Fabric 第三方组件注册代码
        // These components are only required by apps, not by libraries and are Apple specific.
        generateRCTThirdPartyComponents(libraries, reactCodegenOutputPath);
        // 7.生成 TurboModule 提供者代码
        generateRCTModuleProviders(
          projectRoot,
          pkgJson,
          libraries,
          reactCodegenOutputPath,
        );
        // 8.生成自定义 URL Scheme 处理器
        generateCustomURLHandlers(libraries, reactCodegenOutputPath);
        generateUnstableModulesRequiringMainQueueSetupProvider(
          libraries,
          reactCodegenOutputPath,
        );
        // 9.生成应用级依赖提供者
        generateAppDependencyProvider(
          path.join(outputPath, 'ReactAppDependencyProvider'),
        );
        // 10.生成 ReactCodegen.podspec
        generateReactCodegenPodspec(
          projectRoot,
          pkgJson,
          reactCodegenOutputPath,
          baseOutputPath,
        );
        generatePackageSwift(
          projectRoot,
          outputPath,
          findReactNativeRootPath(projectRoot),
        );
      }

      cleanupEmptyFilesAndFolders(outputPath);
    }
  } catch (err) {
    codegenLog(err);
    process.exitCode = 1;
  }

  codegenLog('Done.', true);
  return;
}

我已经为上面10个比较关键的步骤增加了注释,流程还是很清晰的。由于这篇文章主要是研究iOS端TurboModule,那么需要重点关注一下generateRCTModuleProvidersgenerateAppDependencyProvider两个函数的实现。

源码react-native/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTModuleProviders.js

const MODULE_PROVIDERS_H_TEMPLATE_PATH = path.join(
  TEMPLATES_FOLDER_PATH,
  'RCTModuleProvidersH.template',
);

const MODULE_PROVIDERS_MM_TEMPLATE_PATH = path.join(
  TEMPLATES_FOLDER_PATH,
  'RCTModuleProvidersMM.template',
);

function generateRCTModuleProviders(
  projectRoot /*: string */,
  pkgJson /*: $FlowFixMe */,
  libraries /*: $ReadOnlyArray<$FlowFixMe> */,
  outputDir /*: string */,
) {
  // 1.创建输出目录
  fs.mkdirSync(outputDir, {recursive: true});
  // 2.生成头文件 RCTModuleProviders.h
  codegenLog('Generating RCTModulesProvider.h');
  const templateH = fs.readFileSync(MODULE_PROVIDERS_H_TEMPLATE_PATH, 'utf8');
  const finalPathH = path.join(outputDir, 'RCTModuleProviders.h');
  fs.writeFileSync(finalPathH, templateH);
  codegenLog(`Generated artifact: ${finalPathH}`);

  codegenLog('Generating RCTModuleProviders.mm');
  // 3.初始化一个模块收集容器
  let modulesInLibraries /*: {[string]: Array<$FlowFixMe>} */ = {};

  let app = pkgJson.codegenConfig
    ? {config: pkgJson.codegenConfig, libraryPath: projectRoot}
    : null;

  // 4.将应用自身的 TurboModule 加入处理列表
  const moduleLibraries = libraries
    .concat(app)
    .filter(Boolean)
    .filter(({config, libraryPath}) => {
      // 5.过滤出需要处理的模块库
      if (isReactNativeCoreLibrary(config.name) || config.type === 'components') {
        return false;
      }
      return true;
    });

  // 6.收集旧 API 配置(modulesProvider 字段)
  moduleLibraries.forEach(({config, libraryPath}) => {
    const libraryName = JSON.parse(
      fs.readFileSync(path.join(libraryPath, 'package.json'), 'utf8'),
    ).name;

    if (config.ios?.modulesProvider) {
      modulesInLibraries[libraryName] = Object.keys(
        config.ios?.modulesProvider,
      ).map(moduleName => {
        return {
          moduleName,
          className: config.ios?.modulesProvider[moduleName],
        };
      });
    }
  });

  // 7.收集新 API 配置(modules 字段)
  const iosAnnotations = parseiOSAnnotations(moduleLibraries);
  for (const [libraryName, {modules: moduleAnnotationMap}] of Object.entries(
    iosAnnotations,
  )) {
    for (const [moduleName, annotation] of Object.entries(
      moduleAnnotationMap,
    )) {
      if (annotation.className) {
        modulesInLibraries[libraryName] = modulesInLibraries[libraryName] || [];
        modulesInLibraries[libraryName].push({
          moduleName,
          className: annotation.className,
        });
      }
    }
  }

  // 8.生成 Objective-C 映射代码
  const modulesMapping = Object.keys(modulesInLibraries)
    .flatMap(library => {
      const modules = modulesInLibraries[library];
      return modules.map(({moduleName, className}) => {
        return `\t\t@"${moduleName}": @"${className}", // ${library}`;
      });
    })
    .join('\n');

  // 9.将映射代码注入模板并写入文件
  const templateMM = fs
    .readFileSync(MODULE_PROVIDERS_MM_TEMPLATE_PATH, 'utf8')
    .replace(/{moduleMapping}/, modulesMapping);
  const finalPathMM = path.join(outputDir, 'RCTModuleProviders.mm');
  fs.writeFileSync(finalPathMM, templateMM);
  codegenLog(`Generated artifact: ${finalPathMM}`);
}

现在我们来看一个生成的RCTModuleProviders实例,源码Your Project/ios/build/generated/ios/ReactCodegen/RCTModuleProviders.mm

@implementation RCTModuleProviders

+ (NSDictionary<NSString *, id<RCTModuleProvider>> *)moduleProviders
{
  static NSDictionary<NSString *, id<RCTModuleProvider>> *providers = nil;
  static dispatch_once_t onceToken;

  dispatch_once(&onceToken, ^{
    NSDictionary<NSString *, NSString *> * moduleMapping = @{
              @"RNCWebViewModule": @"RNCWebViewModule", // react-native-webview
    };

    NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:moduleMapping.count];

    for (NSString *key in moduleMapping) {
      NSString * moduleProviderName = moduleMapping[key];
      Class klass = NSClassFromString(moduleProviderName);
      if (!klass) {
        RCTLogError(@"Module provider %@ cannot be found in the runtime", moduleProviderName);
        continue;
      }

      id instance = [klass new];
      if (![instance respondsToSelector:@selector(getTurboModule:)]) {
        RCTLogError(@"Module provider %@ does not conform to RCTModuleProvider", moduleProviderName);
        continue;
      }

      [dict setObject:instance forKey:key];
    }

    providers = dict;
  });

  return providers;
}

这是一个懒加载 + 单例模式的实现。使用NSClassFromString通过字符串反射查找类,然后[klass new]实例化 Provider 并验证协议。

最后再来看generateAppDependencyProvider实现,源码react-native/packages/react-native/scripts/codegen/generate-artifacts-executor/generateAppDependencyProvider.js

function generateAppDependencyProvider(outputDir /*: string */) {
  fs.mkdirSync(outputDir, {recursive: true});
  codegenLog('Generating RCTAppDependencyProvider');

  const templateH = fs.readFileSync(
    APP_DEPENDENCY_PROVIDER_H_TEMPLATE_PATH,
    'utf8',
  );
  const finalPathH = path.join(outputDir, 'RCTAppDependencyProvider.h');
  fs.writeFileSync(finalPathH, templateH);
  codegenLog(`Generated artifact: ${finalPathH}`);

  const templateMM = fs.readFileSync(
    APP_DEPENDENCY_PROVIDER_MM_TEMPLATE_PATH,
    'utf8',
  );
  const finalPathMM = path.join(outputDir, 'RCTAppDependencyProvider.mm');
  fs.writeFileSync(finalPathMM, templateMM);
  codegenLog(`Generated artifact: ${finalPathMM}`);

  // Generate the podspec file
  const templatePodspec = fs
    .readFileSync(APP_DEPENDENCY_PROVIDER_PODSPEC_TEMPLATE_PATH, 'utf8')
    .replace(/{react-native-version}/, packageJson.version)
    .replace(/{react-native-licence}/, packageJson.license);
  const finalPathPodspec = path.join(
    outputDir,
    'ReactAppDependencyProvider.podspec',
  );
  fs.writeFileSync(finalPathPodspec, templatePodspec);
  codegenLog(`Generated podspec: ${finalPathPodspec}`);
}

此方法生成一个 统一的依赖提供者类(RCTAppDependencyProvider),它将之前生成的所有 Codegen 产物(TurboModule、Fabric 组件、URL 处理器等)聚合到一个统一接口中,供 React Native 运行时使用。

我们直接看生成之后的文件,源码Your Project/ios/build/generated/ios/ReactAppDependencyProvider/RCTAppDependencyProvider.mm

@implementation RCTAppDependencyProvider

- (nonnull NSArray<NSString *> *)URLRequestHandlerClassNames {
  return RCTModulesConformingToProtocolsProvider.URLRequestHandlerClassNames;
}

- (nonnull NSArray<NSString *> *)imageDataDecoderClassNames {
  return RCTModulesConformingToProtocolsProvider.imageDataDecoderClassNames;
}

- (nonnull NSArray<NSString *> *)imageURLLoaderClassNames {
  return RCTModulesConformingToProtocolsProvider.imageURLLoaderClassNames;
}

- (nonnull NSArray<NSString *> *)unstableModulesRequiringMainQueueSetup {
  return RCTUnstableModulesRequiringMainQueueSetupProvider.modules;
}

- (nonnull NSDictionary<NSString *,Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents {
  return RCTThirdPartyComponentsProvider.thirdPartyFabricComponents;
}

- (nonnull NSDictionary<NSString *, id<RCTModuleProvider>> *)moduleProviders {
  return RCTModuleProviders.moduleProviders;
}

@end

到此,基本上就闭环了。在本文开头,我们就分析了delegate.dependencyProvider = RCTAppDependencyProvider(),而在getModuleProvider方法实现中,self.dependencyProvider != nullptr) ? self.dependencyProvider.moduleProviders[providerName] : nullptr一行,正是从RCTModuleProviders.moduleProviders中查找模块。

九识智能再获3亿美元融资,估值超百亿|36氪首发

2026年2月24日 20:28

图为九识无人车

36氪获悉,九识智能近日完成新一轮超3亿美元融资,估值突破百亿人民币。这也意味着,就已有公开信息来看,RoboVan(L4级自动驾驶物流车)领域首家估值超百亿的独角兽诞生。

截止目前,九识智能已经获得了6次融资。

九识智能成立于2021年,专注于人工智能以及L4级自动驾驶技术的研发,核心产品为L4级自动驾驶城市配送车,业务聚焦于城市支线运输及B2B无人物流领域。

截至2025年年底,其无人配送车业务已覆盖全国300余个城市,并拓展至新加坡、阿联酋、日本、韩国、马来西亚等市场。2025年10月,九识智能和中国邮政签订了7000台无人车订单。

今年一月,九识与菜鸟无人车业务完成战略合并,整合后的九识智能同时运营九识与菜鸟无人车双品牌,RoboVan车队总规模超2万台,覆盖全球10余个国家超300座城市。

整合完成后,九识智能将进行双品牌运营。“菜鸟无人车”品牌将由菜鸟授权给九识使用,菜鸟自身不再生产和直接销售无人车,依托其物流科技基因,聚焦特定场景和KA客户,提供基于RoboVan的智能供应链深度服务,而九识智能将持续深耕自动驾驶全栈自研技术,提供全场景的智慧运力方案。

据36氪汽车了解,九识智能无人车能帮助客户降低约50%的末端配送成本。引入无人车后,快递加盟商每月花在一辆物流无人车的成本,大概在2000~3000元,在车辆成本、折旧费用、电费的综合计量下,其快递单票配送成本已从0.2元降到0.1元。

在不断达成合作的背后,九识的无人车部署规模每年基本以10倍速度增长。2023年,九识的无人车部署数量为200辆,2024 年增至 2000 辆,2026年初,通过与菜鸟整合后迅速达到20000辆。

九识智能联创周清曾表示,如果不算研发成本,九识智能已经实现盈亏平衡。如果算上研发成本的话,当车辆达到大概5万台的时候基本可以实现盈亏平衡。

目前九识智能卖得最好的产品是其首款L4级量产车型Z5,是九识在业内首推的货箱容积在5立方米以上的无人物流车。

在九识智能的产品布局中,Z系列是九识的标准款产品,车型从2立方到10立方不等。轻量版车型E系列则聚焦社区零售、轻抛货等细分场景,提供更低成本的选择,填补了轻量化场景的空白;而更高载重、充电效率更高的L系列则在于满足客户的重载需求场景。

近年来,九识智能瞄准的无人物流领域,正处在规模化落地的爆发节点上。过去一年,包括轻舟智航、德赛西威等车企供应商也开始入局无人配送市场,竞争不断升级。

周清在采访中曾表示:“行业已经从‘春秋时代’进化到了‘战国时代’的末期,头部效应会进一步明显,最终可能由‘秦汉’来统一了。”

深入理解 reduce:从面试题到设计思维

作者 yuki_uix
2026年2月24日 18:24

在准备前端面试的过程中,我发现一个有趣的现象:刷题时遇到的很多问题都能用 reduce 优雅地解决,但回想自己的实际项目经历,却几乎没有直接使用过它。这种"面试高频、项目冷门"的反差让我开始重新审视这个数组方法——它究竟只是个语法糖,还是代表着某种更深层的编程思维?

这篇文章记录了我重新学习 reduce 的过程。我想探讨的不只是"怎么用",更是"为什么用"以及"什么时候该想到它"。

为什么要重新认识 reduce

面试高频 vs 项目冷门的现象

翻看 LeetCode 和各种面试题库,reduce 的身影无处不在:

  • 数组求和、求积
  • 数组扁平化
  • 实现 mapfilter
  • 函数组合 (compose/pipe)
  • 对象转换、分组

但在实际项目中,我更习惯用 for 循环、mapfilter,甚至是 forEach。这是为什么?

我的反思是:可能并不是 reduce 不好用,而是我还没有建立起使用它的心智模型。就像刚学编程时,明明知道函数很重要,却还是习惯把所有代码写在一个文件里一样。

reduce 真正的价值

经过一段时间的研究,我逐渐意识到:reduce 不只是众多数组方法中的一个,它更像是一种数据转换的思维范式

当我们使用 map 时,我们在说:"把数组中的每个元素转换一下"。
当我们使用 filter 时,我们在说:"筛选出符合条件的元素"。
当我们使用 reduce 时,我们在说:

"把整个数组归约成另一种形态"。

这种"形态转换"的视角,让我看到了更多可能性。

本文目标

这篇文章希望达到三个目标:

  1. 理解原理reduce 到底在做什么?它的执行流程是怎样的?
  2. 建立思维: 什么样的问题适合用 reduce 解决?如何培养这种直觉?
  3. 实战应用: 从面试题到实际场景,如何灵活运用?

reduce 的工作原理

核心概念:累加器的演变

reduce 方法的核心在于累加器 (accumulator) 的概念。想象一个累加过程:

// 环境: 浏览器 / Node.js
// 场景: 理解 reduce 的基本执行流程

const numbers = [12345];

// 传统方式: 用 for 循环累加
let sum = 0; // 初始累加器
for (let i = 0; i < numbers.length; i++) {
  sum = sum + numbers[i]; // 更新累加器
}
console.log(sum); // 15

// reduce 方式
const sum2 = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum2); // 15

这两种方式在逻辑上是等价的。reduce 做的事情就是:

  1. 提供一个初始值 (累加器的起点)
  2. 对数组中的每个元素,执行一个函数来更新累加器
  3. 返回最终的累加器值

reduce 的优势在于:它是声明式的。我们描述了"做什么"(把所有元素加起来),而不是"怎么做"(逐个遍历、累加)。

参数拆解: reducer 函数的四个参数

reduce 的完整签名是:

array.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue)

让我们逐个理解这些参数:

// 环境: 浏览器 / Node.js
// 场景: 完整的 reduce 参数演示

const fruits = ['apple''banana''cherry'];

const result = fruits.reduce(
  (acc, curr, index, arr) => {
    console.log({
      iteration: index + 1,
      accumulator: acc,
      currentValue: curr,
      currentIndex: index,
      originalArray: arr
    });
    
    // 返回新的累加器值
    return acc + curr.length;
  },
  0 // 初始值
);

console.log('Final result:', result); // 18

/*
输出:
{ iteration: 1, accumulator: 0, currentValue: 'apple', currentIndex: 0, ... }
{ iteration: 2, accumulator: 5, currentValue: 'banana', currentIndex: 1, ... }
{ iteration: 3, accumulator: 11, currentValue: 'cherry', currentIndex: 2, ... }
Final result: 18
*/

参数说明

  • accumulator (acc): 累加器,保存每次迭代的中间结果
  • currentValue (curr): 当前正在处理的元素
  • currentIndex (index): 当前元素的索引 (可选,不常用)
  • array (arr): 原始数组 (可选,几乎不用)

大多数情况下,我们只需要前两个参数。

执行流程可视化

让我用一个更直观的例子来展示 reduce 的执行流程:

// 环境: 浏览器 / Node.js
// 场景: 购物车总价计算

const cart = [
  { name: 'book', price: 30 },
  { name: 'pen', price: 5 },
  { name: 'bag', price: 80 }
];

const total = cart.reduce((acc, item) => {
  console.log(`Current total: ${acc}, adding ${item.name} (${item.price})`);
  return acc + item.price;
}, 0);

console.log('Total:', total); // 115

/*
执行流程:
初始状态: acc = 0

第 1 次迭代:
  - 当前商品: { name: 'book', price: 30 }
  - acc = 0 + 30 = 30

第 2 次迭代:
  - 当前商品: { name: 'pen', price: 5 }
  - acc = 30 + 5 = 35

第 3 次迭代:
  - 当前商品: { name: 'bag', price: 80 }
  - acc = 35 + 80 = 115

返回最终的 acc: 115
*/

可以看到,reduce 其实是在不断"滚雪球":从一个初始值开始,每次迭代都基于上次的结果继续累积。

初始值的重要性

这是一个容易被忽视但很重要的点:初始值可以不提供

// 环境: 浏览器 / Node.js
// 场景: 有无初始值的区别

const numbers = [1234];

// 有初始值 (推荐)
const sum1 = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum1); // 10

// 无初始值: 第一个元素作为初始值,从第二个元素开始迭代
const sum2 = numbers.reduce((acc, curr) => acc + curr);
console.log(sum2); // 10

// 看似结果相同,但有个陷阱:
const emptyArray = [];

// 有初始值: 正常返回 0
const safeSum = emptyArray.reduce((acc, curr) => acc + curr, 0);
console.log(safeSum); // 0

// 无初始值: 抛出错误!
try {
  const unsafeSum = emptyArray.reduce((acc, curr) => acc + curr);
} catch (error) {
  console.error('Error:', error.message); 
  // TypeError: Reduce of empty array with no initial value
}

关键点

  • 不提供初始值时,reduce 会用数组的第一个元素作为初始值
  • 这在处理空数组时会报错
  • 建议总是提供初始值,让代码更健壮

另一个微妙之处:初始值的类型决定了最终结果的类型。

// 环境: 浏览器 / Node.js
// 场景: 初始值类型影响最终结果

const numbers = [123];

// 初始值是数字
const sum = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // 6 (number)

// 初始值是字符串
const str = numbers.reduce((acc, curr) => acc + curr, '');
console.log(str); // '123' (string)

// 初始值是数组
const doubled = numbers.reduce((acc, curr) => {
  acc.push(curr * 2);
  return acc;
}, []);
console.log(doubled); // [2, 4, 6]

// 初始值是对象
const stats = numbers.reduce((acc, curr) => {
  acc.sum += curr;
  acc.count += 1;
  return acc;
}, { sum: 0, count: 0 });
console.log(stats); // { sum: 6, count: 3 }

这就引出了 reduce 的一个强大特性:它可以把数组转换成任何数据结构——数字、字符串、对象、甚至另一个数组。

reduce 的设计哲学

声明式编程:描述"做什么"而非"怎么做"

当我刚开始学编程时,我的思维是"命令式"的:

// 命令式思维: 告诉计算机每一步怎么做
function getAdults(users) {
  const result = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].age >= 18) {
      result.push(users[i].name);
    }
  }
  return result;
}

reduce (以及其他函数式方法) 鼓励我们用"声明式"思维:

// 声明式思维: 描述我想要什么
function getAdults(users) {
  return users
    .filter(user => user.age >= 18)
    .map(user => user.name);
}

两者的区别在于:抽象层次。声明式代码更接近"我想要成年用户的名字",而命令式代码更像"先创建空数组,然后遍历,如果年龄大于等于 18..."。

reduce 把这种声明式思维推向了极致:我们只需要描述"如何从一个值变成下一个值",剩下的交给方法本身。

数据转换思维:输入形态 → 输出形态

使用 reduce 的关键在于:清晰地定义输入和输出的形态

让我举个例子:

// 环境: 浏览器 / Node.js
// 场景: 将用户数组转换为按年龄分组的对象

const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 25 },
  { name: 'David', age: 30 }
];

// 思考过程:
// 输入: Array<User>
// 输出: { [age]: Array<User> }
// 初始值: {} (空对象)

const grouped = users.reduce((acc, user) => {
  const age = user.age;
  
  // 如果这个年龄还没有对应的数组,创建一个
  if (!acc[age]) {
    acc[age] = [];
  }
  
  // 把用户添加到对应年龄的数组中
  acc[age].push(user);
  
  return acc;
}, {});

console.log(grouped);
/*
{
  25: [
    { name: 'Alice', age: 25 },
    { name: 'Charlie', age: 25 }
  ],
  30: [
    { name: 'Bob', age: 30 },
    { name: 'David', age: 30 }
  ]
}
*/

这个例子展示了典型的 reduce 思维:

  1. 明确输入形态:数组
  2. 明确输出形态:对象
  3. 选择合适的初始值:空对象
  4. 定义转换规则:根据年龄分组

当我开始用这种方式思考问题时,很多复杂的数据处理突然变得清晰了。

为什么说 reduce 是最底层的抽象

这是一个很有趣的发现:我们可以用 reduce 来实现 mapfilter 等其他数组方法。

// 环境: 浏览器 / Node.js
// 场景: 用 reduce 实现其他数组方法

// 1. 实现 map
Array.prototype.myMap = function(callback) {
  return this.reduce((acc, curr, index) => {
    acc.push(callback(curr, index));
    return acc;
  }, []);
};

const doubled = [123].myMap(x => x * 2);
console.log(doubled); // [2, 4, 6]

// 2. 实现 filter
Array.prototype.myFilter = function(callback) {
  return this.reduce((acc, curr, index) => {
    if (callback(curr, index)) {
      acc.push(curr);
    }
    return acc;
  }, []);
};

const evens = [1234].myFilter(x => x % 2 === 0);
console.log(evens); // [2, 4]

// 3. 实现 find
Array.prototype.myFind = function(callback) {
  return this.reduce((acc, curr) => {
    // 如果已经找到,直接返回
    if (acc !== undefined) return acc;
    // 否则检查当前元素
    return callback(curr) ? curr : undefined;
  }, undefined);
};

const firstEven = [1234].myFind(x => x % 2 === 0);
console.log(firstEven); // 2

这说明什么?

reduce 是一种更通用的抽象mapfilter 都是它的特例:

  • map: 把数组转换成另一个等长的数组
  • filter: 把数组转换成长度可能更小的数组
  • reduce: 把数组转换成任何东西

从这个角度看,reduce 代表的是"归约"这个更本质的概念。

与 map/filter 的关系

那是不是说我们应该用 reduce 替代所有其他方法?并不是。

我的理解是:

  • mapfilter 表达的是特定意图,代码可读性更好
  • reduce 更加通用,但也更抽象,可能降低可读性
  • 选择合适的工具取决于具体场景
// 环境: 浏览器 / Node.js
// 场景: 可读性对比

const numbers = [12345];

// 方案 A: 链式调用 (推荐,意图清晰)
const result1 = numbers
  .filter(x => x % 2 === 0)  // 我想要偶数
  .map(x => x * 2);          // 我想要它们的两倍

// 方案 B: 单一 reduce (更高效,但意图不够清晰)
const result2 = numbers.reduce((acc, x) => {
  if (x % 2 === 0) {
    acc.push(x * 2);
  }
  return acc;
}, []);

console.log(result1); // [4, 8]
console.log(result2); // [4, 8]

在大多数情况下,我会选择方案 A,因为可读性 > 微小的性能差异。但当链式调用导致多次遍历,且性能成为瓶颈时,单一的 reduce 可能是更好的选择。

典型应用场景

理解了原理和哲学,让我们看看 reduce 在实际场景中如何应用。

场景 1: 数据聚合

这是 reduce 最常见的用途:把一组数据聚合成单个值。

// 环境: 浏览器 / Node.js
// 场景: 订单统计

const orders = [
  { id: 1, amount: 100, status: 'completed' },
  { id: 2, amount: 200, status: 'pending' },
  { id: 3, amount: 150, status: 'completed' },
  { id: 4, amount: 300, status: 'completed' }
];

// 1. 求总金额
const total = orders.reduce((sum, order) => sum + order.amount0);
console.log('Total:', total); // 750

// 2. 求已完成订单的金额
const completedTotal = orders.reduce((sum, order) => {
  return order.status === 'completed' ? sum + order.amount : sum;
}, 0);
console.log('Completed:', completedTotal); // 550

// 3. 求最大金额订单
const maxOrder = orders.reduce((max, order) => {
  return order.amount > max.amount ? order : max;
});
console.log('Max order:', maxOrder); // { id: 4, amount: 300, ... }

// 4. 一次遍历获取多个统计信息
const stats = orders.reduce((acc, order) => {
  acc.total += order.amount;
  acc.count += 1;
  if (order.status === 'completed') {
    acc.completed += 1;
  }
  return acc;
}, { total: 0, count: 0, completed: 0 });

console.log('Stats:', stats);
// { total: 750, count: 4, completed: 3 }

第 4 个例子展示了 reduce 的一个优势:一次遍历完成多项统计。如果分开计算,就需要多次遍历数组。

场景 2: 数据重组

reduce 可以把数组转换成对象,这在很多场景下非常有用。

// 环境: 浏览器 / Node.js
// 场景: 构建查找表 (lookup table)

const products = [
  { id: 'p1', name: 'Laptop', price: 1000 },
  { id: 'p2', name: 'Mouse', price: 50 },
  { id: 'p3', name: 'Keyboard', price: 80 }
];

// 1. 按 id 索引 (常用于快速查找)
const productsById = products.reduce((acc, product) => {
  acc[product.id] = product;
  return acc;
}, {});

console.log(productsById['p2']);
// { id: 'p2', name: 'Mouse', price: 50 }

// 2. 按价格区间分组
const priceRanges = products.reduce((acc, product) => {
  const range = product.price < 100'cheap''expensive';
  if (!acc[range]) {
    acc[range] = [];
  }
  acc[range].push(product);
  return acc;
}, {});

console.log(priceRanges);
/*
{
  expensive: [{ id: 'p1', name: 'Laptop', price: 1000 }],
  cheap: [
    { id: 'p2', name: 'Mouse', price: 50 },
    { id: 'p3', name: 'Keyboard', price: 80 }
  ]
}
*/

// 3. 数组去重 (利用对象的 key 唯一性)
const numbers = [1223334];
const unique = Object.keys(
  numbers.reduce((acc, num) => {
    acc[num] = true;
    return acc;
  }, {})
).map(Number);

console.log(unique); // [1, 2, 3, 4]

这些转换在实际开发中非常常见,比如:

  • 从 API 获取数组数据,转换成对象以便快速查找
  • 对数据进行分组、分类
  • 去重、去除无效数据

场景 3: 数据扁平化

扁平化是面试题的常客,用 reduce 实现很自然。

// 环境: 浏览器 / Node.js
// 场景: 多维数组扁平化

// 1. 二维数组扁平化
const nested2D = [[12], [34], [5]];

const flat2D = nested2D.reduce((acc, arr) => {
  return acc.concat(arr);
}, []);

console.log(flat2D); // [1, 2, 3, 4, 5]

// 2. 多维数组扁平化 (递归)
function flattenDeep(arr) {
  return arr.reduce((acc, item) => {
    // 如果是数组,递归扁平化
    if (Array.isArray(item)) {
      return acc.concat(flattenDeep(item));
    }
    // 否则直接添加
    return acc.concat(item);
  }, []);
}

const nested = [1, [2, [3, [4]], 5]];
console.log(flattenDeep(nested)); // [1, 2, 3, 4, 5]

// 3. 对象数组中的嵌套数组扁平化
const data = [
  { id: 1, tags: ['js''react'] },
  { id: 2, tags: ['css''html'] },
  { id: 3, tags: ['js''vue'] }
];

const allTags = data.reduce((acc, item) => {
  return acc.concat(item.tags);
}, []);

console.log(allTags);
// ['js', 'react', 'css', 'html', 'js', 'vue']

// 去重后的所有标签
const uniqueTags = [...new Set(allTags)];
console.log(uniqueTags);
// ['js', 'react', 'css', 'html', 'vue']

值得一提的是,现代 JavaScript 提供了原生的 flat() 方法,但理解如何用 reduce 实现它,有助于加深对 reduce 的理解。

场景 4: 函数组合 (compose/pipe)

这是一个更高级的场景,但在函数式编程中非常重要。

// 环境: 浏览器 / Node.js
// 场景: 实现函数组合工具

// 1. compose: 从右到左执行函数
// compose(f, g, h)(x) === f(g(h(x)))
const compose = (...fns) => {
  return (initialValue) => {
    return fns.reduceRight((acc, fn) => fn(acc), initialValue);
  };
};

// 2. pipe: 从左到右执行函数  
// pipe(f, g, h)(x) === h(g(f(x)))
const pipe = (...fns) => {
  return (initialValue) => {
    return fns.reduce((acc, fn) => fn(acc), initialValue);
  };
};

// 示例:数据处理管道
const double = x => x * 2;
const addTen = x => x + 10;
const square = x => x * x;

// 使用 pipe (更符合阅读习惯)
const transform = pipe(double, addTen, square);
console.log(transform(5)); // ((5 * 2) + 10) ^ 2 = 400

// 使用 compose (数学函数的传统写法)
const transform2 = compose(square, addTen, double);
console.log(transform2(5)); // 同样是 400

// 实际场景:用户数据处理
const users = [
  { name: 'alice', age: 17, active: true },
  { name: 'bob', age: 25, active: false },
  { name: 'charlie', age: 30, active: true }
];

const processUsers = pipe(
  users => users.filter(u => u.active),      // 只要活跃用户
  users => users.filter(u => u.age >= 18),   // 只要成年用户
  users => users.map(u => u.name),           // 只要名字
  names => names.map(n => n.toUpperCase())   // 转大写
);

console.log(processUsers(users)); // ['CHARLIE']

虽然在日常开发中我们可能不会频繁使用 compose/pipe,但这个例子展示了 reduce 作为一种抽象工具的强大之处。

场景 5: 异步场景中的 reduce

这是一个比较进阶但很实用的技巧:用 reduce 串行执行异步操作。

// 环境: Node.js / 浏览器
// 场景: 串行执行 Promise

// 假设我们有一组需要顺序执行的异步任务
const tasks = [
  () => new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 1 done');
      resolve(1);
    }, 1000);
  }),
  () => new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 2 done');
      resolve(2);
    }, 500);
  }),
  () => new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 3 done');
      resolve(3);
    }, 800);
  })
];

// 使用 reduce 串行执行
async function runSequentially(tasks) {
  return tasks.reduce(async (previousPromise, currentTask) => {
    // 等待上一个任务完成
    const results = await previousPromise;
    // 执行当前任务
    const result = await currentTask();
    // 累积结果
    return [...results, result];
  }, Promise.resolve([]));
}

// 执行
runSequentially(tasks).then(results => {
  console.log('All tasks done:', results);
  // 输出顺序: Task 1 done, Task 2 done, Task 3 done
  // All tasks done: [1, 2, 3]
});

// 对比:如果用 Promise.all (并行执行)
// Promise.all(tasks.map(task => task())).then(results => {
//   console.log('All tasks done:', results);
//   // 输出顺序可能是: Task 2 done, Task 3 done, Task 1 done
// });

这个技巧在需要按顺序处理一系列异步操作时非常有用,比如:

  • 按顺序上传多个文件
  • 按顺序执行多个 API 请求 (每个请求依赖前一个的结果)
  • 数据库的顺序迁移操作

进阶技巧

处理异步:串行执行 Promise

在上面的场景 5 中我们已经看到了一个例子,让我再展开一些变体:

// 环境: Node.js / 浏览器
// 场景: 更复杂的异步串行处理

// 1. 每个任务依赖前一个任务的结果
const steps = [
  async (prev) => {
    console.log('Step 1, prev:', prev);
    return prev + 1;
  },
  async (prev) => {
    console.log('Step 2, prev:', prev);
    return prev * 2;
  },
  async (prev) => {
    console.log('Step 3, prev:', prev);
    return prev + 10;
  }
];

async function pipeline(steps, initialValue) {
  return steps.reduce(async (prevPromise, step) => {
    const prevValue = await prevPromise;
    return step(prevValue);
  }, Promise.resolve(initialValue));
}

pipeline(steps, 0).then(result => {
  console.log('Final result:', result);
  // Step 1, prev: 0 => 1
  // Step 2, prev: 1 => 2
  // Step 3, prev: 2 => 12
  // Final result: 12
});

// 2. 带错误处理的版本
async function pipelineWithErrorHandling(steps, initialValue) {
  return steps.reduce(async (prevPromise, step, index) => {
    try {
      const prevValue = await prevPromise;
      return await step(prevValue);
    } catch (error) {
      console.error(`Error at step ${index}:`, error.message);
      throw error; // 或者根据需求决定是否继续
    }
  }, Promise.resolve(initialValue));
}

性能考量:什么时候不该用 reduce

虽然 reduce 很强大,但并非万能。在某些情况下,使用它可能不是最佳选择:

// 环境: 浏览器 / Node.js
// 场景: 性能对比

const largeArray = Array.from({ length: 100000 }, (_, i) => i);

// 场景 1: 简单的求和
console.time('for loop');
let sum1 = 0;
for (let i = 0; i < largeArray.length; i++) {
  sum1 += largeArray[i];
}
console.timeEnd('for loop'); // 通常最快

console.time('reduce');
const sum2 = largeArray.reduce((acc, num) => acc + num, 0);
console.timeEnd('reduce'); // 稍慢,但差异不大

// 场景 2: 需要提前退出的情况
console.time('for with break');
let found1 = null;
for (let i = 0; i < largeArray.length; i++) {
  if (largeArray[i] === 50000) {
    found1 = largeArray[i];
    break; // 可以提前退出
  }
}
console.timeEnd('for with break');

console.time('reduce no early exit');
const found2 = largeArray.reduce((acc, num) => {
  if (acc !== null) return acc; // 模拟提前退出,但仍会遍历所有元素
  return num === 50000 ? num : null;
}, null);
console.timeEnd('reduce no early exit'); // 无法真正提前退出,性能较差

// 场景 3: find 比 reduce 更合适
console.time('find');
const found3 = largeArray.find(num => num === 50000);
console.timeEnd('find'); // 可以提前退出,性能好

我的建议

  1. 对于简单的求和、求积,性能差异可以忽略,优先考虑可读性
  2. 需要提前退出的场景,不要用 reduce,用 for 循环或 find/some 等方法
  3. 不要为了用 reduce 而用 reduce,选择最适合表达意图的方法

可读性平衡:复杂场景下的取舍

reduce 的逻辑变得复杂时,可读性可能成为问题:

// 环境: 浏览器 / Node.js
// 场景: 复杂的 reduce vs 多步骤处理

const transactions = [
  { type: 'income', amount: 1000, category: 'salary' },
  { type: 'expense', amount: 200, category: 'food' },
  { type: 'expense', amount: 300, category: 'transport' },
  { type: 'income', amount: 500, category: 'bonus' }
];

// 方案 A: 单一复杂的 reduce (不推荐)
const summary1 = transactions.reduce((acc, tx) => {
  if (tx.type === 'income') {
    acc.income += tx.amount;
    if (!acc.incomeByCategory[tx.category]) {
      acc.incomeByCategory[tx.category] = 0;
    }
    acc.incomeByCategory[tx.category] += tx.amount;
  } else {
    acc.expense += tx.amount;
    if (!acc.expenseByCategory[tx.category]) {
      acc.expenseByCategory[tx.category] = 0;
    }
    acc.expenseByCategory[tx.category] += tx.amount;
  }
  acc.balance = acc.income - acc.expense;
  return acc;
}, { 
  income: 0, 
  expense: 0, 
  balance: 0,
  incomeByCategory: {},
  expenseByCategory: {}
});

// 方案 B: 分步处理 (推荐)
const income = transactions
  .filter(tx => tx.type === 'income')
  .reduce((sum, tx) => sum + tx.amount0);

const expense = transactions
  .filter(tx => tx.type === 'expense')
  .reduce((sum, tx) => sum + tx.amount0);

const summary2 = {
  income,
  expense,
  balance: income - expense
};

console.log(summary2); // 更清晰

我的权衡原则:

  • 如果 reduce 的回调函数超过 5-7 行,考虑拆分或用其他方法
  • 如果需要嵌套的条件判断,可能不适合用 reduce
  • 优先考虑代码的可维护性,而非炫技

常见陷阱与调试技巧

在使用 reduce 时,我遇到过一些容易犯的错误:

// 环境: 浏览器 / Node.js
// 场景: 常见错误示例

// 陷阱 1: 忘记返回 accumulator
const wrong1 = [123].reduce((acc, num) => {
  acc.push(num * 2);
  // 忘记 return acc!
}, []);
console.log(wrong1); // undefined

// 正确做法
const correct1 = [123].reduce((acc, num) => {
  acc.push(num * 2);
  return acc; // 必须返回
}, []);

// 陷阱 2: 意外修改了原始对象
const data = { count: 0 };
const result = [123].reduce((acc, num) => {
  acc.count += num;
  return acc;
}, data); // 使用了外部对象作为初始值

console.log(data.count); // 6 - 原始对象被修改了!

// 正确做法:使用新对象
const correct2 = [123].reduce((acc, num) => {
  acc.count += num;
  return acc;
}, { count: 0 }); // 使用新对象

// 陷阱 3: 在 reduce 中使用 push 但期望得到新数组
const original = [123];
const result3 = original.reduce((acc, num) => {
  acc.push(num * 2);
  return acc;
}, []); // 虽然初始值是新数组,但每次都在修改同一个数组

// 如果需要不可变性,使用 concat
const immutable = original.reduce((acc, num) => {
  return acc.concat(num * 2);
}, []);

调试技巧

// 在 reducer 函数中添加日志
const debugReduce = [123].reduce((acc, num, index) => {
  console.log({
    iteration: index,
    current: num,
    accumulator: acc,
    returned: acc + num
  });
  return acc + num;
}, 0);

建立自己的 reduce 思维

识别模式:什么问题适合用 reduce

经过一段时间的学习和实践,我总结了一些"信号",提示我可能需要用 reduce

强信号 (很可能适合):

  1. 需要把数组"聚合"成单个值 (求和、求积、最值)
  2. 需要把数组转换成对象 (索引、分组)
  3. 需要累积一个复杂的状态 (计数器、统计信息)
  4. 需要扁平化嵌套结构
  5. 需要函数组合或管道处理

弱信号 (可能适合,但有其他选择):

  1. 需要转换数组 → 考虑 map 是否更清晰
  2. 需要过滤数组 → 考虑 filter 是否更清晰
  3. 需要查找元素 → 考虑 findsomeevery

反向信号 (可能不适合):

  1. 需要提前退出循环
  2. 逻辑非常复杂,嵌套层级深
  3. 团队成员对函数式编程不熟悉 (可读性第一)

思考框架:如何设计 reducer 函数

当我确定要用 reduce 后,我通常按这个步骤思考:

Step 1: 明确输入和输出

输入: [1, 2, 3, 4]
输出: 10

Step 2: 选择初始值

初始值: 0 (因为我要求和,0 是加法的单位元)

Step 3: 定义转换规则

每次迭代: 累加器 + 当前元素 = 新累加器

Step 4: 写成代码

[1234].reduce((acc, curr) => acc + curr, 0)

让我用一个更复杂的例子演示这个思考过程:

// 环境: 浏览器 / Node.js
// 场景: 统计单词出现次数

const text = 'hello world hello javascript world';
const words = text.split(' ');
// ['hello', 'world', 'hello', 'javascript', 'world']

// Step 1: 明确输入输出
// 输入: Array<string>
// 输出: { [word]: count }

// Step 2: 选择初始值
// 初始值: {} (空对象,用于存储单词和计数)

// Step 3: 定义转换规则
// 每次迭代:
//   - 如果单词已存在,计数 +1
//   - 如果单词不存在,设置为 1

// Step 4: 实现
const wordCount = words.reduce((acc, word) => {
  acc[word] = (acc[word] || 0) + 1;
  return acc;
}, {});

console.log(wordCount);
// { hello: 2, world: 2, javascript: 1 }

从面试题到实际项目的迁移

在刷题过程中,我发现很多 reduce 的技巧可以直接应用到实际项目中:

面试题场景 → 实际项目场景

面试题 实际场景
数组求和 购物车总价计算
数组转对象 API 数据索引优化
数组扁平化 处理嵌套的评论/回复数据
函数组合 (compose) 数据处理管道、中间件链
异步串行执行 文件上传、数据库迁移
按条件分组 数据可视化、报表生成
// 环境: React 项目
// 场景: 购物车总价计算 (实际项目例子)

// 购物车数据结构
const cartItems = [
  { id: 1, name: 'Book', price: 30, quantity: 2 },
  { id: 2, name: 'Pen', price: 5, quantity: 10 },
  { id: 3, name: 'Bag', price: 80, quantity: 1 }
];

// 计算总价 (考虑数量和折扣)
const calculateTotal = (items, discountRate = 0) => {
  const subtotal = items.reduce((sum, item) => {
    return sum + (item.price * item.quantity);
  }, 0);
  
  return subtotal * (1 - discountRate);
};

console.log(calculateTotal(cartItems)); // 190
console.log(calculateTotal(cartItems, 0.1)); // 171 (打9折)

// 在 React 组件中使用
function ShoppingCart({ items }) {
  const total = items.reduce((sum, item) => 
    sum + item.price * item.quantity0
  );
  
  return (
    <div>
      <h2>Total: ${total}</h2>
    </div>
  );
}

持续练习的建议

我的学习方法:

  1. 刷题时有意识地练习 :每次遇到可以用 reduce 解决的问题,先用 reduce 实现一遍,即使有更简单的方法

  2. 重构已有代码 :回顾项目中的循环逻辑,看看哪些可以用 reduce 改写

  3. 阅读优秀代码 :看看 Redux、Lodash 等库中 reduce 的使用方式

  4. 写博客总结 :就像我现在做的,把学到的东西写出来,加深理解

  5. 小项目实践 :试着用 reduce 实现一些工具函数:

    • 深拷贝
    • 对象 merge
    • 路径取值 (get、set)
    • 简单的状态管理

延伸与发散

在研究 reduce 的过程中,我产生了一些新的思考:

reduce 与函数式编程

reduce 其实来自函数式编程中的 fold 操作。在 Haskell、OCaml 等语言中,fold 是一个核心概念。这让我意识到:学习 reduce 不只是学一个数组方法,更是在学习一种编程范式

函数式编程的一些核心思想:

  • 不可变性 :每次返回新值,而不是修改旧值
  • 纯函数 :相同输入总是产生相同输出,无副作用
  • 声明式 :描述"做什么",而非"怎么做"

这些思想在现代前端开发中越来越重要,特别是在使用 React、Redux 等框架时。

reduce 在状态管理中的应用

Redux 的核心概念正是基于 reduce

// Redux 的 reducer 本质上就是一个 reduce 操作
function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO'return [...state, action.payload];
    case 'REMOVE_TODO'return state.filter(todo => todo.id !== action.payload);
    defaultreturn state;
  }
}

// 实际上就是:
const finalState = actions.reduce(todosReducer, initialState);

理解 reduce 有助于理解 Redux 的设计哲学:状态是不可变的,每次操作都产生新状态。

相关技术的对比

在学习 reduce 时,我也了解了一些相关的概念:

  • Array.prototype.reduceRight :从右往左 reduce,用于 compose 函数
  • Observable.reduce (RxJS):在响应式编程中的应用
  • Stream.reduce (Node.js):在流处理中的应用

这些概念虽然语法不同,但核心思想是一致的:把一系列值归约成单个值。

未来可能的演进

JavaScript 还在不断演进,可能未来会有更多与 reduce 相关的特性:

  • Pipeline Operator (|>):让函数组合更自然
  • Pattern Matching:让条件分支更简洁
  • Records & Tuples:不可变数据结构的原生支持

这些提案都与 reduce 的思想相关,值得持续关注。

我的困惑与疑问

在学习过程中,我还有一些未解的疑问:

  1. 性能优化的临界点:在什么规模的数据下,reduce 的性能劣势会明显?

  2. 可读性的度量:如何量化"可读性"?如何在团队中达成共识?

  3. 初学者友好性reduce 对新手来说确实比较抽象,如何更好地教学?

  4. 最佳实践的边界:什么情况下"过度使用 reduce"?如何把握这个度?

这些问题可能没有标准答案,但思考它们本身就很有价值。

小结

写完这篇文章,我对 reduce 有了更深的理解。它不仅仅是一个数组方法,更是一种归约思维的体现。

这个学习过程让我意识到:

  • 工具的价值不在于它有多强大,而在于我们是否真正理解并掌握了它
  • 很多时候"不会用"不是因为方法不好,而是缺少合适的心智模型
  • 从面试题到实际应用,需要的是迁移能力识别模式的直觉

我现在还不能说自己完全掌握了 reduce,但至少建立了一个思考框架。接下来的计划是:

  1. 在项目中有意识地寻找 reduce 的应用场景
  2. 尝试用 reduce 重构一些旧代码,观察效果
  3. 继续研究函数式编程的其他概念

如果你也在学习 reduce,或者有不同的理解和经验,欢迎交流。学习是一个持续迭代的过程,这篇文章只是我的一个阶段性总结。

最后,引用一句话:

"Simplicity is the ultimate sophistication." — Leonardo da Vinci

reduce 的美,或许就在于它用一个简单的概念,表达了复杂的转换过程。

参考资料

gsap -滚动插件 ScrollTrigger 简单demo

作者 大时光
2026年2月24日 18:06
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GSAP Scroll Demo</title>
    <style>
      body {
        margin: 0;
        font-family: "Segoe UI", sans-serif;
        background: #0b1020;
        color: #e2e8f0;
      }
      section {
        height: 100vh;
        position: relative;
        overflow: hidden;
      }
      .center-stack {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        width: 80vw;
        height: 60vh;
      }
      .stack-item {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        border-radius: 18px;
        opacity: 0.95;
      }
      .stack-1 { width: 140px; height: 140px; background: #0ea5e9; }
      .stack-2 { width: 200px; height: 200px; background: #22c55e; }
      .stack-3 { width: 280px; height: 280px; background: #f59e0b; }
      .stack-4 { width: 200px; height: 200px; background: #a855f7; }
      .stack-5 { width: 140px; height: 140px; background: #ef4444; }

      .quad {
        position: absolute;
        padding: 12px 16px;
        border-radius: 10px;
        font-size: 20px;
        font-weight: 600;
        background: #111827;
      }
      .quad-up { left: 50%; top: 20%; transform: translate(-50%, 0); }
      .quad-down { left: 50%; bottom: 20%; transform: translate(-50%, 0); }
      .quad-left { left: 12%; top: 50%; transform: translate(0, -50%); }
      .quad-right { right: 12%; top: 50%; transform: translate(0, -50%); }

      .list {
        position: absolute;
        left: 10%;
        top: 50%;
        transform: translateY(-50%);
        display: flex;
        flex-direction: column;
        gap: 16px;
      }
      .list-item {
        width: 140px;
        padding: 12px 16px;
        border-radius: 10px;
        background: #111827;
        font-size: 16px;
        font-weight: 600;
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/ScrollTrigger.min.js"></script>
</head>
<body>
    <section id="view-one">
      <div class="center-stack">
        <div class="stack-item stack-1" id="stack-1"></div>
        <div class="stack-item stack-2" id="stack-2"></div>
        <div class="stack-item stack-3" id="stack-3"></div>
        <div class="stack-item stack-4" id="stack-4"></div>
        <div class="stack-item stack-5" id="stack-5"></div>
      </div>
    </section>

    <section id="view-two">
      <div class="quad quad-up" id="quad-up">向上</div>
      <div class="quad quad-down" id="quad-down">向下</div>
      <div class="quad quad-left" id="quad-left">向左</div>
      <div class="quad quad-right" id="quad-right">向右</div>
    </section>

    <section id="view-three">
      <div class="list">
        <div class="list-item" id="list-1">第一个</div>
        <div class="list-item" id="list-2">第二个</div>
        <div class="list-item" id="list-3">第三个</div>
      </div>
    </section>

    <script>
      gsap.registerPlugin(ScrollTrigger);
       // 第一视图
      gsap.set(["#stack-1", "#stack-5"], { x: 0 });
      gsap.set(["#stack-2", "#stack-4"], { x: 0 });
      gsap.set("#stack-3", { x: 0 });

      gsap.timeline({
        scrollTrigger: {
          trigger: "#view-one",
          start: "top top",
          end: "+=400%",
          scrub: true,
          pin: true
        }
      })
      .to(["#stack-1", "#stack-5"],{ x:(i) => i === 0 ? -220 : 220, duration: 0.4 })
      .to(["#stack-2", "#stack-4"], { x:(i) => i === 0 ? -160 : 160, duration: 0.4 })

     // 第二视图
      gsap.set(["#quad-up", "#quad-down", "#quad-left", "#quad-right"], { opacity: 0 });
      gsap.set("#quad-up", { y: -40 });
      gsap.set("#quad-down", { y: 40 });
      gsap.set("#quad-left", { x: -40 });
      gsap.set("#quad-right", { x: 40 });

      gsap.timeline({
        scrollTrigger: {
          trigger: "#view-two",
          start: "top top",
          end: "+=400%",
          scrub: true,
          pin: true
        }
      })
      .to("#quad-up", { y: 0, opacity: 1, duration: 0.25 })
      .to("#quad-down", { y: 0, opacity: 1, duration: 0.25 })
      .to("#quad-left", { x: 0, opacity: 1, duration: 0.25 })
      .to("#quad-right", { x: 0, opacity: 1, duration: 0.25 });

     // 第三视图
      gsap.set(["#list-1", "#list-2", "#list-3"], { x: 0 });

      gsap.timeline({
        scrollTrigger: {
          trigger: "#view-three",
          start: "top top",
          end: "+=400%",
          scrub: true,
          pin: true
        }
      })
      .to("#list-1", { x: 100, duration: 0.33 })
      .to("#list-2", { x: 200, duration: 0.33 })
      .to("#list-3", { x: 300, duration: 0.33 });
    </script>
</body>
</html>

这个cdn是用官网的 如果报错 请使用魔法

scrollTrigger: { 
trigger: "#element-id", // 触发动画的元素
start: "top top", // 起始位置(元素顶部, 视口顶部)
end: "+=400%", // 结束位置(起始位置+400%视口高度)
scrub: true, // 动画进度与滚动同步
pin: true // 在动画期间固定元素 
}

效果就是

image.png

image.png

依次出现上下左右 文字

image.png

然后依次 向右移动

image.png

iOS Swift:蓝牙 BLE 连接外设CoreBluetooth

作者 tangbin583085
2026年2月24日 17:59

在 iOS 与智能硬件(手环、传感器、控制模块等)交互中,BLE(Bluetooth Low Energy)是最常用的通信方式。本文将基于 CoreBluetooth + Swift,给出一套工程可用的连接外设代码,并总结开发中最常遇到的注意事项。

适用场景:BLE 设备连接、读写特征、订阅通知、接收回包、断线重连。

一、准备工作

1)Info.plist 权限配置(必须)

iOS 13+ 起必须给出蓝牙使用说明,否则扫描/连接会失败或系统拒绝。

<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要使用蓝牙连接外设进行数据通讯</string>

如果你还需要后台持续蓝牙通信:需要 capability + plist 配置(后文注意事项会讲)。

2)导入框架

import CoreBluetooth

二、BLE 连接的标准流程

  1. 初始化 CBCentralManager
  2. 蓝牙开启后开始扫描
  3. 找到目标外设并连接
  4. 发现 Service
  5. 发现 Characteristic
  6. 找到写特征(Write)与通知特征(Notify)
  7. 开启通知并开始收发数据

三、代码:BluetoothManager(可直接用)

下面给出一个轻量但工程化的 BLE 管理器,支持:

  • 按名称/服务 UUID 过滤
  • 扫描超时
  • 连接成功后自动发现服务/特征
  • 自动开启 Notify
  • 写入数据(支持写入响应/无响应)
  • 断开回调(可扩展重连)
  • 简单的写入节流(避免写太快导致丢包) ``

你只需要把 UUID 替换成你设备的即可。

import Foundation
import CoreBluetooth

final class BluetoothManager: NSObject {
    
    static let shared = BluetoothManager()
    
    // MARK: - Public callbacks (按需扩展)
    var onStateChanged: ((CBManagerState) -> Void)?
    var onDiscovered: ((CBPeripheral, NSNumber) -> Void)?
    var onConnected: ((CBPeripheral) -> Void)?
    var onDisconnected: ((CBPeripheral, Error?) -> Void)?
    var onReceiveData: ((Data, CBCharacteristic) -> Void)?
    
    // MARK: - CoreBluetooth
    private var central: CBCentralManager!
    private(set) var peripheral: CBPeripheral?
    
    private var writeChar: CBCharacteristic?
    private var notifyChar: CBCharacteristic?
    
    // MARK: - Config (替换为你的设备 UUID)
    /// 推荐:用 Service UUID 过滤扫描,效率更高、结果更准
    private let targetServiceUUID = CBUUID(string: "FFF0")
    private let writeCharUUID      = CBUUID(string: "FFF1")
    private let notifyCharUUID     = CBUUID(string: "FFF2")
    
    /// 可选:按名称过滤(如果设备名称稳定)
    private let targetNamePrefix = "MyBLE"
    
    // MARK: - Scan control
    private var scanTimer: Timer?
    private let scanTimeout: TimeInterval = 10
    
    // MARK: - Write throttle
    private var writeQueue: [Data] = []
    private var isWriting = false
    
    private override init() {
        super.init()
        // queue 建议用串行队列,避免回调并发导致状态错乱
        let queue = DispatchQueue(label: "com.tangbin.ble.queue")
        central = CBCentralManager(delegate: self, queue: queue)
    }
    
    // MARK: - Public APIs
    
    /// 开始扫描
    func startScan() {
        guard central.state == .poweredOn else { return }
        stopScan()
        
        // 只扫目标 Service:更省电更精准(强烈推荐)
        central.scanForPeripherals(withServices: [targetServiceUUID], options: [
            CBCentralManagerScanOptionAllowDuplicatesKey: false
        ])
        
        startScanTimeoutTimer()
    }
    
    /// 停止扫描
    func stopScan() {
        if central.isScanning {
            central.stopScan()
        }
        scanTimer?.invalidate()
        scanTimer = nil
    }
    
    /// 连接外设
    func connect(_ p: CBPeripheral) {
        stopScan()
        peripheral = p
        peripheral?.delegate = self
        
        central.connect(p, options: [
            CBConnectPeripheralOptionNotifyOnDisconnectionKey: true
        ])
    }
    
    /// 主动断开
    func disconnect() {
        guard let p = peripheral else { return }
        central.cancelPeripheralConnection(p)
    }
    
    /// 发送数据(写入队列节流)
    func send(_ data: Data, withResponse: Bool = false) {
        guard let p = peripheral, let w = writeChar else { return }
        let type: CBCharacteristicWriteType = withResponse ? .withResponse : .withoutResponse
        
        // 如果写入无响应,也建议做节流,避免外设来不及处理
        writeQueue.append(data)
        pumpWriteQueue(peripheral: p, characteristic: w, type: type)
    }
    
    // MARK: - Private
    
    private func startScanTimeoutTimer() {
        scanTimer?.invalidate()
        scanTimer = Timer.scheduledTimer(withTimeInterval: scanTimeout, repeats: false) { [weak self] _ in
            self?.stopScan()
        }
        RunLoop.main.add(scanTimer!, forMode: .common)
    }
    
    private func pumpWriteQueue(peripheral p: CBPeripheral,
                                characteristic c: CBCharacteristic,
                                type: CBCharacteristicWriteType) {
        guard !isWriting else { return }
        guard !writeQueue.isEmpty else { return }
        
        isWriting = true
        let packet = writeQueue.removeFirst()
        
        // 注意:此处写入发生在 central 的队列上
        p.writeValue(packet, for: c, type: type)
        
        // withoutResponse 的情况下不会走 didWriteValueFor,所以用延迟释放
        if type == .withoutResponse {
            DispatchQueue.global().asyncAfter(deadline: .now() + 0.02) { [weak self] in
                self?.isWriting = false
                self?.pumpWriteQueue(peripheral: p, characteristic: c, type: type)
            }
        }
    }
}

// MARK: - CBCentralManagerDelegate
extension BluetoothManager: CBCentralManagerDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        onStateChanged?(central.state)
        
        if central.state == .poweredOn {
            // 可按需自动扫描
            // startScan()
        } else {
            // 蓝牙关闭/不可用时要清空状态
            stopScan()
            peripheral = nil
            writeChar = nil
            notifyChar = nil
        }
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {
        
        // 如果还想按名称做二次过滤
        if let name = peripheral.name, name.hasPrefix(targetNamePrefix) {
            onDiscovered?(peripheral, RSSI)
            connect(peripheral)
            return
        }
        
        // 不按名称过滤也行:只要服务 UUID 已经过滤,通常就很准
        onDiscovered?(peripheral, RSSI)
        connect(peripheral)
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        onConnected?(peripheral)
        
        // 连接后发现服务
        peripheral.discoverServices([targetServiceUUID])
    }
    
    func centralManager(_ central: CBCentralManager,
                        didFailToConnect peripheral: CBPeripheral,
                        error: Error?) {
        onDisconnected?(peripheral, error)
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDisconnectPeripheral peripheral: CBPeripheral,
                        error: Error?) {
        onDisconnected?(peripheral, error)
        
        // 可选:重连策略(示例:延迟重连)
        // DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        //     central.connect(peripheral, options: nil)
        // }
    }
}

// MARK: - CBPeripheralDelegate
extension BluetoothManager: CBPeripheralDelegate {
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard error == nil else { return }
        guard let services = peripheral.services else { return }
        
        for s in services {
            if s.uuid == targetServiceUUID {
                peripheral.discoverCharacteristics([writeCharUUID, notifyCharUUID], for: s)
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverCharacteristicsFor service: CBService,
                    error: Error?) {
        guard error == nil else { return }
        guard let chars = service.characteristics else { return }
        
        for c in chars {
            if c.uuid == writeCharUUID { writeChar = c }
            if c.uuid == notifyCharUUID { notifyChar = c }
        }
        
        // 开启通知(Notify)
        if let n = notifyChar {
            peripheral.setNotifyValue(true, for: n)
        }
        
        // 可选:读取一次初始值
        // if let n = notifyChar { peripheral.readValue(for: n) }
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didUpdateNotificationStateFor characteristic: CBCharacteristic,
                    error: Error?) {
        // 通知开关状态回调
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didUpdateValueFor characteristic: CBCharacteristic,
                    error: Error?) {
        guard error == nil else { return }
        let data = characteristic.value ?? Data()
        onReceiveData?(data, characteristic)
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didWriteValueFor characteristic: CBCharacteristic,
                    error: Error?) {
        // 只有 withResponse 才会进这个回调
        isWriting = false
        if let w = writeChar {
            pumpWriteQueue(peripheral: peripheral, characteristic: w, type: .withResponse)
        }
    }
}

四、调用示例(在 ViewController 里)

final class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let ble = BluetoothManager.shared
        
        ble.onStateChanged = { state in
            print("BLE state:", state.rawValue)
            if state == .poweredOn {
                ble.startScan()
            }
        }
        
        ble.onConnected = { p in
            print("已连接:", p.name ?? "unknown")
        }
        
        ble.onReceiveData = { data, ch in
            let hex = data.map { String(format: "%02x", $0) }.joined()
            print("收到数据((ch.uuid)):", hex)
        }
    }
    
    func sendCommand() {
        // 举例:发送一段 hex 指令
        let hex = "aabbccdd"
        let data = Data(hexString: hex) // 见下方扩展
        BluetoothManager.shared.send(data, withResponse: false)
    }
}

也可以复用我之前写的 ParseDataTool(Swift版)来做 Hex/Data 转换。


五、附:Data 十六进制扩展(可选)

extension Data {
    init(hexString: String) {
        let clean = hexString.replacingOccurrences(of: " ", with: "")
        var data = Data()
        var idx = clean.startIndex
        while idx < clean.endIndex {
            let next = clean.index(idx, offsetBy: 2)
            let byteStr = clean[idx..<next]
            if let b = UInt8(byteStr, radix: 16) {
                data.append(b)
            }
            idx = next
        }
        self = data
    }
}

六、开发注意事项(非常关键)

1)强烈建议:扫描时指定 Service UUID

更快、更省电、更准确
避免扫描全部 nil 会扫到大量无关设备,影响体验

central.scanForPeripherals(withServices: [targetServiceUUID], options: nil)

2)不要用 peripheral.name 当唯一标识

name 可能为空、可能变化。更靠谱的是:

  • 过滤 Service UUID
  • 使用 peripheral.identifier(UUID)做缓存识别(重连)

3)写数据别太快,否则丢包/外设卡死

BLE 写入速度过快常见问题:

  • 外设缓冲区溢出
  • 回包延迟或丢失
  • iOS 侧 write 被吞

建议做写入节流(本文示例已经做了队列 + 20ms 间隔)


4)区分 withResponse / withoutResponse

  • .withResponse:可靠,有回调 didWriteValueFor
  • .withoutResponse:速度快,但无写入确认,建议配合队列节流

实战建议:

  • 协议关键指令用 .withResponse
  • 大数据(如 OTA)用 .withoutResponse + 节流 + 外设 ACK

5)Notify 要记得开启(很多人漏掉)

外设回包多数走 Notify(通知),不打开你永远收不到数据:

peripheral.setNotifyValue(true, for: notifyChar)

6)断线是常态:要做重连策略

断线原因很多:

  • 超距
  • 外设省电休眠
  • 手机锁屏/系统资源回收

建议:

  • didDisconnectPeripheral 里做延迟重连
  • 或 UI 提示用户手动重连
  • 结合 peripheral.identifier 记住上次设备

7)后台蓝牙通信(可选)

如果你需要锁屏/后台持续通信:

  • Xcode → Signing & Capabilities → Background Modes → 勾选 Uses Bluetooth LE accessories
  • 并合理控制扫描/连接行为(后台会更耗电)

8)MTU 与分包问题

  • BLE 默认有效载荷常见为 20 字节(不同设备协商后可能变大)
  • 大数据(日志、图片、OTA)一定要做分包 + 协议确认

最后

本文给出了一套 Swift BLE 连接外设的我开发成熟项目过程中的代码,可直接运用在项目中,覆盖了:

  • 初始化、扫描、连接
  • 服务/特征发现
  • Notify 开启、收包回调
  • 写入(带队列节流)
  • 断开处理与重连扩展

如有写错的地方,敬请指正,相互学习进步,谢谢~

02 登录功能实现

作者 WWWWW先生
2026年2月24日 17:59

1. 登录界面基础校验

1.1 基础数据的双向绑定

登录界面的element-UI组件由三层组成,最外层是el-form,中间层是el-form-item,最内层是el-form表单元素组件。

三个组件各施其职共同完成表单校验功能,具体的实现方式:在el-input组件上使用v-model指令实现双向绑定,比如:v-model="form.username"

1.2 表单校验配置

  1. 表单对象的字段命名需与接口字段保持一致,便于后续表单提交。比如,这里的username和password。

  2. 规则对象是采用对象嵌套数组的形式,比如:

rules: {
        username: [
          {
            required: true,
            message: '请输入账号',
            trigger: 'blur'
          }
        ]
      }

校验规则:required设置必填项;message校验失败提示信息;trigger触发校验的事件,常用blur失焦事件。

  1. 组件绑定 el-form绑定:需要同时绑定:model="form":rules="rules"两个属性。el-form-item配置:通过prop属性指定对应的校验规则。

1.3 总结

表单校验是通过el-formel-form-itemel-input组件联合实现表单检验。其中,el-form绑定表单对象和规则对象,el-form-item指定校验规则,el-input双向绑定数据。

完整代码:

<template>
  <div class="login_body">
    <div class="bg" />
    <div class="box">
      <div class="title">智慧园区-登录</div>
      <!-- el-form :model="表单对象" :rules="规则对象"
        el-form-item prop属性指定一下要使用哪条规则
        el-input v-model双向绑定
      -->
      <el-form ref="form" :model="form" :rules="rules">
        <el-form-item
          label="账号"
          prop="username"
        >
          <el-input v-model="form.username"/>
        </el-form-item>

        <el-form-item
          label="密码"
          prop="password"
        >
          <el-input v-model="form.password"/>
        </el-form-item>

        <el-form-item prop="remember">
          <el-checkbox>记住我</el-checkbox>
        </el-form-item>

        <el-form-item>
          <el-button type="primary" class="login_btn">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script>

export default {
  name: 'Login',
  data() {
    return {
      // 表单对象
      form: {
        username: '',
        password: ''
      },
      // 规则对象
      rules: {
        username: [
          {
            required: true,
            message: '请输入账号',
            trigger: 'blur'
          }
        ],
        password: [
          {
            required: true,
            message: '请输入密码',
            trigger: 'blur'
          }
        ]
      }
    }
  }
}

</script>

2. 登录界面统一校验

实际上,登录时表单的校验分为两部分,第一部分是用户输入框失焦时触发的校验,另一根时点击登录按钮时的统一校验。

因为用户如果没进行任何输入,直接点击登录按钮,第一部分的校验将没法触发,因此,需要第二部分的统一校验来兜底。

实现方式: 通过给el-form组件添加ref="form"属性,然后使用this.$refs.form获取表单实例对象。

每个表单项需要通过prop属性指定对应的校验规则,如prop="username"会关联rules对象中定义的username的校验规则。

核心方法:调用表单实例的validate方法会对所有带prop属性的表单项进行统一校验。

回调参数:validate方法接收回调函数,参数valid为布尔值,当所有校验通过时为true,否则为false。

逻辑处理:通常在回调函数中判断valid值,为true时才执行后续登录逻辑。

事件绑定:给登录按钮添加@click="loginHandler"事件,在methods中定义loginHandler方法。

方法实现:在loginHandler中调用this.$refs.form.validate方法进行统一校验。

<template>
  <div class="login_body">
    <div class="bg" />
    <div class="box">
      <div class="title">智慧园区-登录</div>
      <!-- 基础校验:
        el-form :model="表单对象" :rules="规则对象"
        el-form-item prop属性指定一下要使用哪条规则
        el-input v-model双向绑定
        统一校验:
        获取表单的实例对象
        调用validate方法
      -->
      <el-form ref="form" :model="form" :rules="rules">
        <el-form-item
          label="账号"
          prop="username"
        >
          <el-input v-model="form.username"/>
        </el-form-item>

        <el-form-item
          label="密码"
          prop="password"
        >
          <el-input v-model="form.password"/>
        </el-form-item>

        <el-form-item prop="remember">
          <el-checkbox>记住我</el-checkbox>
        </el-form-item>

        <el-form-item>
          <el-button type="primary" class="login_btn" @click="loginHandler">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script>

export default {
  name: 'Login',
  data() {
    return {
      // 表单对象
      form: {
        username: '',
        password: ''
      },
      // 规则对象
      rules: {
        username: [
          {
            required: true,
            message: '请输入账号',
            trigger: 'blur'
          }
        ],
        password: [
          {
            required: true,
            message: '请输入密码',
            trigger: 'blur'
          }
        ]
      }
    }
  },
  methods: {
    loginHandler() {
      this.$refs.form.validate(valid => {
        // 所有的表单项都通过校验,valid变量才为true,否则就是false
        console.log(valid)
        if (valid) {
          console.log(111111)
        }
      })
    }
  }
}

</script>

<style scoped lang="scss">
  .login_body {
    display: flex;
  }
  .bg {
    width: 60vw;
    height: 100vh;
    background: url('~@/assets/login-bg.svg') no-repeat;
    background-position: right top;
    background-size: cover;
  }
  .box {
    margin: 200px 10% 0;
    flex: 1;
    .title {
      padding-bottom: 76px;
      font-size: 26px;
      font-weight: 500;
      color: #1e2023;
    }
    ::v-deep() {
      .ant-form-item {
        display: flex;
        margin-bottom: 62px;
        flex-direction: column;
      }
      .ant-form-item-label label {
        font-size: 16px;
        color: #8b929d;
      }
      .ant-input,
      .ant-input-password {
        border-radius: 8px;
      }
    }
  }
  .login_btn{
    width: 100%;
  }
</style>

  1. 测试场景1:不输入任何内容直接点击登录,valid值为false,阻止登录流程。
  2. 测试场景2:输入符合规则的账号密码后点击登录,valid值为true,允许继续登录操作。
  3. 调试技巧:可以在回调函数中添加console.log(valid)来验证校验结果是否符合预期。

3. 使用vuex管理用户的token

登录的目的是获取token用于多组件共享,token是字符串类型。

e8ec8a07-8bf2-47d3-b750-043fc0c39ae5.png 三大模块:

  1. state:存储token数据状态
  2. mutation: 同步修改token的唯一途径
  3. action: 包含接口调用和提交mutation

组件只需触发action,完成接口调用后提交mutation更新state。 store/modules/user.js中代码:

import { loginAPI } from '@/api/user'

export default {
  namespaced: true,
  // 数据状态 响应式
  state: {
    token: ''
  },
  // 同步修改 Vuex架构中,有且只有一种提交mutation
  mutations: {
    setToken(state, newToken) {
      state.token = newToken
    }
  },
  // 异步,接口请求 + 提交mutation
  actions: {
    async asyncLogin(ctx, { username, password }) {
      // 1. 调用登录接口
      const res = await loginAPI({ username, password })
      // 2. 提交mutation
      ctx.commit('setToken', res.data.token)
    }
  }
}

代码解读:

首先,token初始化为'',与后端返回的类型一致。namespaced:true确保模块的独立性。

mutations为规范写法,第一个参数为state对象,第二个参数newToken为荷载(payload),通过state.token=newToken直接赋值。

异步请求接口为规范写法,第一个参数ctx为上下文对象,第二个参数使用解构赋值明确参数要求。接口分为两步:第一步调用loginAPI接口获取token;第二步通过ctx.commit提交mutation并更新state。

api/user.js中代码:

import request from '@/utils/request'

// 登录函数
/**
 * @description: 登录函数
 * @param {*} data { username,password}
 * @return {*} promise
 */

// 函数:参数 + 逻辑 + 返回值
export function loginAPI(data) {
  return request({
    url: '/park/login', // baseURL + url
    method: 'POST', // GET/POST/PUT/DELETE
    data // 请求体参数
  })
  // 返回的是一个promise
}

这里面定义了loginAPI接口,接口参数:

url: '/park/login'
method: 'POST'
参数对象包含username和password字段
返回值:Promise对象,data中包含token字段

views/Login/index.vue组件中执行代码this.$store.dispatch('user/asyncLogin', this.form)即可触发调用。

4. 登录后跳转到首页

Token存储机制:登录接口调用成功后,将获取的token数据存储在Vuex状态管理中

路由跳转时机:登录成功后需要进行首页跳转,但要注意异步操作的顺序问题

职责分离原则:

  • Vuex只负责处理用户数据相关逻辑(如token存储)
  • 业务代码(路由跳转、提示消息)应放在业务组件中实现
  • 避免将业务逻辑混入状态管理模块

核心代码:

<script>

export default {
  name: 'Login',
  data() {
    return {
      // 表单对象
      form: {
        username: '',
        password: ''
      },
      // 规则对象
      rules: {
        username: [
          {
            required: true,
            message: '请输入账号',
            trigger: 'blur'
          }
        ],
        password: [
          {
            required: true,
            message: '请输入密码',
            trigger: 'blur'
          }
        ]
      }
    }
  },
  methods: {
    loginHandler() {
      this.$refs.form.validate(async valid => {
        // 所有的表单项都通过校验,valid变量才为true,否则就是false
        console.log(valid)
        if (valid) {
          // 确保token返回之后再跳转到首页,防止首页有一些需要依赖token的逻辑
          await this.$store.dispatch('user/asyncLogin', this.form)
          // 跳转到首页
          this.$router.push('/')
          // 提示用户登录成功
          this.$message({
            type: 'success',
            message: '登录成功'
          })
        }
      })
    }
  }
}

</script>

5. token持久化操作

Token的有效期会持续一定时间,在这段时间内没有必要重复请求token,但是vuex本身是基于内存的管理方式,刷新浏览器Token会丢失,为了避免丢失需要配置持久化进行缓存。

解决思路:1. 存储Token数据时,一份存入vuex,一份存入cookie;2.vuex中初始化Token时,优先从本地cookie获取,本地获取不到再初始化为空字符串。

  1. utils/auth.js中封装cookie操作的方法
// 专门用来操作cookie的方法包
// 内部封装了繁琐的操作方法 参数处理 暴露三个函数 get,set,remove
import Cookies from 'js-cookie'
import { TOKEN_KEY } from '@/constants/KEY'
// 获取token的方法
export function getToken() {
  return Cookies.get(TOKEN_KEY)
}

// 设置方法
export function setToken(token) {
  return Cookies.set(TOKEN_KEY, token)
}

// 删除方法
export function removeToken() {
  return Cookies.remove(TOKEN_KEY)
}
  1. store/modules/user.js中导入操作cookie的方法并新增存入cookie的操作,并在token初始化时优先从cookie获取
import { loginAPI } from '@/api/user'
import { setToken, getToken } from '@/utils/auth'

export default {
  namespaced: true,
  // 数据状态 响应式
  state: {
    // 2. vuex中初始化Token时,优先从本地cookie取,取不到再初始化为空字符串。
    token: getToken() || ''
  },
  // 同步修改 Vuex架构中,有且只有一种提交mutation
  mutations: {
    // 1. 存Token数据时,一份存入vuex,一份存入cookie
    setToken(state, newToken) {
      // 存入vuex
      state.token = newToken
      // 存入cookie
      setToken(newToken)
    }
  },
  // 异步,接口请求 + 提交mutation
  actions: {
    async asyncLogin(ctx, { username, password }) {
      // 1. 调用登录接口
      const res = await loginAPI({ username, password })
      // 2. 提交mutation
      ctx.commit('setToken', res.data.token)
    }
  }
}

6. token存取方式对比

  1. 为什么要使用vuex+cookies存储的方式

内存存储优势:Vuex基于内存管理,存取速度特别快(毫秒级),且封装了便捷的方法调用方式,适合高频操作场景

持久化需求:Cookies/localStorage基于磁盘存储,虽然存取速度稍慢(约慢一个量级),但具有刷新不丢失的特性

组合方案价值:同时利用Vuex的速度优势(运行时状态管理)和Cookies的持久化特性(长期存储),典型场景如用户登录态保持

  1. cookie vs localStorage

存储容量:

localStorage约5MB(不同浏览器有差异);
cookie仅几KB(个位数级别),大容量数据存储首选localStorage

操作权限:

localStorage纯前端操作;
cookie前后端均可操作(实际开发中后端操作为主),通过Set-Cookie头实现

请求携带:

cookie自动跟随接口发送(无需手动设置)
localStorage需手动添加到请求头(如Authorization头)

7. 添加token到请求头

前端请求接口后,后端需要对接口做鉴权,只有token有效,接口才能正常响应,返回正常的数据,token就是后端接口判断的标识。项目中,前端页面会请求非常多的接口,axios请求拦截器可以统一控制,一次添加,多个接口都生效。

utils/request.js中写入如下代码:

import axios from 'axios'
import { getToken } from './auth'

const service = axios.create({
  baseURL: 'https://api-hmzs.itheima.net/v1',
  timeout: 5000 // request timeout
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 添加token
    const token = getToken()
    if (token) {
      // 前面是固定写法,后面token的拼接模式是由后端来决定。
      config.headers.Authorization = token
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    return Promise.reject(error)
  }
)

export default service

8. 记住我功能的实现

交互表现

1.选中状态:当用户选中"记住我"复选框并登录后,再次返回登录页时,系统会自动回填之前输入的用户名和密码
2.未选中状态:当用户未选中复选框时登录,再次返回登录页会清除本地存储的账号密码数据

实现逻辑:

  1. 登录时处理: 当remember为true时,将账号密码存入localStorage;当remember为false时,清除localStorage中的账号密码
  2. 初始化处理:组件初始化时从localStorage取值并回填到表单

views/Login/index.vue中代码:

<template>
  <div class="login_body">
    <div class="bg" />
    <div class="box">
      <div class="title">智慧园区-登录</div>
      <!-- 基础校验:
        el-form :model="表单对象" :rules="规则对象"
        el-form-item prop属性指定一下要使用哪条规则
        el-input v-model双向绑定
        统一校验:
        获取表单的实例对象
        调用validate方法
      -->
      <el-form ref="form" :model="form" :rules="rules">
        <el-form-item
          label="账号"
          prop="username"
        >
          <el-input v-model="form.username"/>
        </el-form-item>

        <el-form-item
          label="密码"
          prop="password"
        >
          <el-input v-model="form.password"/>
        </el-form-item>
        <!-- 1.完成选择框的双向绑定,得到一个true或者false的选中状态 -->
        <!-- 2. 如果当前为true,点击登陆时,表示要己住,把当前的用户名和密码存入本地 -->
        <!-- 3. 组件初始化的时候,从本地取账号和密码,把账号密码存入用来双向绑定的form身上。 -->
         <!-- 4. 如果当前用户没有记住,状态为false,点击登录的时候要把之前的数据清空 -->
        <el-form-item prop="remember">
          <el-checkbox v-model="remember">记住我</el-checkbox>
        </el-form-item>

        <el-form-item>
          <el-button type="primary" class="login_btn" @click="loginHandler">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script>
const REMEMBER_KEY = 'remember_key'
export default {
  name: 'Login',
  data() {
    return {
      // 表单对象
      form: {
        username: '',
        password: ''
      },
      remember: false,
      // 规则对象
      rules: {
        username: [
          {
            required: true,
            message: '请输入账号',
            trigger: 'blur'
          }
        ],
        password: [
          {
            required: true,
            message: '请输入密码',
            trigger: 'blur'
          }
        ]
      }
    }
  },
  created() {
    // 去本地取一下之前存入的账号和密码,如果取到了就进行赋值操作
    const formStr = localStorage.getItem(REMEMBER_KEY)
    if (formStr) {
      const formObj = JSON.parse(formStr)
      this.form = formObj
    }
  },
  methods: {
    loginHandler() {
      this.$refs.form.validate(async valid => {
        // 所有的表单项都通过校验,valid变量才为true,否则就是false
        console.log(valid)
        if (valid) {
          // 添加记住我逻辑
          if (this.remember) {
            localStorage.setItem(REMEMBER_KEY, JSON.stringify(this.form))
          } else {
            localStorage.removeItem(REMEMBER_KEY)
          }
          // 确保token返回之后再跳转到首页,防止首页有一些需要依赖token的逻辑
          await this.$store.dispatch('user/asyncLogin', this.form)
          // 跳转到首页
          this.$router.push('/')
          // 提示用户登录成功
          this.$message({
            type: 'success',
            message: '登录成功'
          })
        }
      })
    }
  }
}

</script>

<style scoped lang="scss">
  .login_body {
    display: flex;
  }
  .bg {
    width: 60vw;
    height: 100vh;
    background: url('~@/assets/login-bg.svg') no-repeat;
    background-position: right top;
    background-size: cover;
  }
  .box {
    margin: 200px 10% 0;
    flex: 1;
    .title {
      padding-bottom: 76px;
      font-size: 26px;
      font-weight: 500;
      color: #1e2023;
    }
    ::v-deep() {
      .ant-form-item {
        display: flex;
        margin-bottom: 62px;
        flex-direction: column;
      }
      .ant-form-item-label label {
        font-size: 16px;
        color: #8b929d;
      }
      .ant-input,
      .ant-input-password {
        border-radius: 8px;
      }
    }
  }
  .login_btn{
    width: 100%;
  }
</style>

代码关键点分析:

1)选择框的双向绑定
绑定方式:使用v-model绑定remember变量
数据位置:remember作为独立变量而非表单对象属性,因为它不需要提交给后端
默认值:初始设置为false表示未选中状态
2)记住我逻辑 
存储机制:
使用localStorage存储账号密码
需要将表单对象转为JSON字符串存储
条件判断:仅在remember为true时执行存储操作,remember为false时,清除操作
常量定义:使用REMEMBER_KEY常量避免硬编码字符串重复
3)组件初始化回填
生命周期:在created或mounted钩子中执行回填逻辑
取值流程:从localStorage获取存储的字符串
使用JSON.parse转为对象
赋值给表单对象完成回填
健壮性处理:添加非空判断避免未存储数据时的错误
4)代码优化
常量提取:将'remember_key'提取为常量REMEMBER_KEY

9. 退出登录实现

退出登录功能位于导航栏右上角的用户下拉菜单中,对应组件文件为src/layout/components/Navbar.vue,方法绑定:已预置logout方法绑定在退出登录按钮的点击事件上。

<template>
  <div class="navbar">
    <div class="right-menu">
      <el-dropdown class="avatar-container" trigger="click">
        <div class="avatar-wrapper">
          <!-- 用户名称 -->
          <span class="name">黑马管理员</span>
        </div>
        <el-dropdown-menu slot="dropdown" class="user-dropdown">
          <router-link to="/">
            <el-dropdown-item> 首页 </el-dropdown-item>
          </router-link>
          <a target="_blank">
            <el-dropdown-item> 项目地址 </el-dropdown-item>
          </a>
          <!-- 实现思路:1.询问用户是否真的要退出登录;2. 用户同意之后,清空当前的用户数据并跳转到登录页 -->
          <el-dropdown-item divided @click.native="logout">
            <span style="display: block">退出登录</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    // 退出登录
    logout() {
      // 1. 询问用户
      this.$confirm('确认要退出登录吗,是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        // 确认回调
        // 1. 清空数据
        this.$store.commit('user/clearUserInfo')
        // 2. 跳转到登录界面
        this.$router.push(`/login?redirect=${this.$route.fullPath}`)
      }).catch(() => {
        // 取消或者.then中有错误
      })
    }
  }
}
</script>

使用Element UI的$confirm方法实现二次确认,文案修改为"确认要退出登录吗,是否继续?"。

确认回调:.then()中执行数据清除和跳转逻辑
取消回调:.catch()中显示"已取消删除"提示信息
  • 数据清除

Vuex清除:通过state.token = ''清空状态管理中的token

Cookie清除:调用removeToken()方法清除本地存储的token

mutation编写:在user模块中添加clearUserInfo方法,同时处理Vuex和Cookie的清理

  • 跳转登录页

路由跳转:使用this.$router.push('/login')实现页面跳转

完整调用:通过this.$store.commit('user/clearUserInfo')提交mutation

store/modules/user.js中具体代码如下:

import { loginAPI } from '@/api/user'
import { setToken, getToken, removeToken } from '@/utils/auth'

export default {
  namespaced: true,
  // 数据状态 响应式
  state: {
    // 2. vuex中初始化Token时,优先从本地cookie取,取不到再初始化为空字符串。
    token: getToken() || ''
  },
  // 同步修改 Vuex架构中,有且只有一种提交mutation
  mutations: {
    // 1. 存Token数据时,一份存入vuex,一份存入cookie
    setToken(state, newToken) {
      // 存入vuex
      state.token = newToken
      // 存入cookie
      setToken(newToken)
    },
    clearUserInfo(state) {
      // 清除vuex中的
      state.token = ''
      // 清除本地cookie中的
      removeToken()
    }
  },
  // 异步,接口请求 + 提交mutation
  actions: {
    async asyncLogin(ctx, { username, password }) {
      // 1. 调用登录接口
      const res = await loginAPI({ username, password })
      // 2. 提交mutation
      ctx.commit('setToken', res.data.token)
    }
  }
}

10. Token控制路由跳转

如果用户没有登录,即没有token,则不让用户进入某些页面。因此,需要通过token的有无来控制路由的跳转。

54168ac7-5dfa-4156-9fe3-c434ecc83f69.png

  1. 新建src/permission.js文件,并在main.js中通过import './permission'方式引入。
permission.js文件代码

// 所有和权限控制相关的代码
import router from "./router"
import { getToken } from "./utils/auth"

console.log('权限控制生效了')

const WHITE_LIST = ['/login', '/404']

// 1. 路由前置守卫
router.beforeEach((to, from, next) => {
     // to: 目标路由对象 到哪里去
    // from:路由对象 从哪里来的那个对象
    // next: 放行函数
    const token = getToken()
    if (token) {
        // 有token
        next()
    } else {
        // 没有token
        // 1. 是否在白名单内,即在白名单数组中是否存在
        if (WHITE_LIST.includes(to.path)) {
            next()
        } else {
            next('/login')
        }

    }
})

11. 接口错误统一处理

系统中会调用很多接口,每个接口都可能会出错,为了交互体验,需要对所有的接口进行错误处理,当错误发生时,告诉用户。

实现方式:利用axios的响应拦截器捕获所有接口错误。位置:utils/request.js

import axios from 'axios'
import { getToken } from './auth'
import { Message } from 'element-ui'

const service = axios.create({
  baseURL: 'https://api-hmzs.itheima.net/v1',
  timeout: 5000 // request timeout
})

// 请求拦截器
......

// 响应拦截器
service.interceptors.response.use(
  response => {
    return response.data
  },
  // 接口出错时,自动执行这个回调
  error => {
    // console.dir(error.response.data.msg)
    // 错误类型有可能有很多种,根据不同的错误码做不同的用户提示,写的位置都在这里
    Message({
      type: 'warning',
      message: error.response.data.msg
    })
    return Promise.reject(error)
  }
)

export default service

非组件调用:通过import { Message } from 'element-ui'引入独立消息组件

类型配置:使用type: 'warning'设置警告类型提示框

参数传递:所有的错误在error中,本次的封装在将error.response.data.msg中的,取出来作为message参数传递

天猫38大促阿里妈妈加码全链路AI经营,单商家至高可享50万补贴

2026年2月24日 17:51
2026年天猫38开门红&38焕新周今日正式启动,本次大促分为预售(2月24日-3月3日)与现货(2月24日-3月9日)两大关键波段。为助力商家捕捉春季趋势红利,阿里妈妈宣布以“AI启新,精种增长”为核心推出多重激励,实现确收增长。在营销工具上,阿里妈妈全链路AI产品矩阵和打法全线出击。在资金扶持上,38大促返利补贴重磅加码,以多重返点叠加助冲刺,单商家至高可享50万返点支持。

chown Cheatsheet

Basic Syntax

Use these core command forms for chown.

Command Description
chown USER FILE Change file owner
chown USER:GROUP FILE Change owner and group
chown :GROUP FILE Change group only
chown USER: FILE Change owner and set group to user’s login group
chown --reference=REF FILE Copy owner and group from another file

Common Examples

Common ownership changes for files and directories.

Command Description
chown root file.txt Set owner to root
chown www-data:www-data /var/www/index.html Set owner and group for a web file
sudo chown $USER:$USER file.txt Return ownership to current user
chown :developers app.log Change group only
chown --reference=source.txt target.txt Match ownership of another file

Recursive Changes

Apply ownership updates to full directory trees.

Command Description
chown -R USER:GROUP /path Recursively change owner and group
chown -R USER /path Recursively change owner only
chown -R :GROUP /path Recursively change group only
chown -R -h USER:GROUP /path Change symlink ownership itself during recursion
chown -R --from=OLDUSER:OLDGROUP NEWUSER:NEWGROUP /path Change only matching current ownership

Symlinks and Traversal

Control how chown treats symbolic links.

Command Description
chown USER:GROUP symlink Change target by default
chown -h USER:GROUP symlink Change symlink itself (not target)
chown -R -H USER:GROUP /path Follow symlink command-line args to directories
chown -R -L USER:GROUP /path Follow all directory symlinks
chown -R -P USER:GROUP /path Never follow symlinks (default)

Safe Patterns

Use these patterns to avoid ownership mistakes.

Command Description
chown --from=root root:root /path/file Change only if current owner matches
find /path -user olduser -exec chown newuser {} + Target only files owned by one user
find /path -group oldgroup -exec chown :newgroup {} + Target only one group
ls -l /path/file Verify ownership before and after changes
id username Confirm user and group names exist

Common Errors

Quick checks when ownership changes fail.

Issue Check
Operation not permitted You need root privileges; run with sudo
invalid user Verify user exists with getent passwd username
invalid group Verify group exists with getent group groupname
Changes did not apply recursively Confirm -R was used
Access still denied after chown Check permission bits with ls -l and ACLs

Related Guides

Use these guides for full ownership and permissions workflows.

Guide Description
Chown Command in Linux Full chown guide with examples
chgrp Command in Linux Change file group ownership
How to Change File Permissions in Linux (chmod command) Update permission bits
Understanding Linux File Permissions Ownership and permission model
How to List Groups in Linux Check group membership and IDs

继TailWindCss和UnoCss后的CSS-in-JS vs Utility-First 深度对比

作者 ElevenSylvia
2026年2月24日 17:17

CSS-in-JS vs Utility-First 深度对比

完整技术指南:从架构设计到开发实践的全面对比分析 配合AI硬肝

⚠️ 注意:本文讨论的是整个 CSS 方案生态,包括传统 CSS-in-JS(Styled Components、Emotion)和 Utility-First(Tailwind CSS、UnoCSS),为你选择最适合的样式方案提供全面参考。


目录

  1. 背景与趋势
  2. 核心概念解析
  3. Styled Components 深度指南
  4. Emotion 深度指南
  5. 架构设计深度对比
  6. 性能基准测试
  7. 开发体验详解
  8. 实战案例
  9. 迁移与混用策略
  10. 常见问题与解决方案
  11. 总结与选型建议

1. 背景与趋势

1.1 CSS 方案的演进历程

┌─────────────────────────────────────────────────────────────────────────────┐
                           CSS 方案演进时间线                                  
└─────────────────────────────────────────────────────────────────────────────┘

2013        2015        2017        2019        2021        2024        2026
                                                                    
                                                                    
CSS Modules  CSS-in-JS   Tailwind    Emotion     UnoCSS      Tailwind    混合方案
  (eBay)     (V1)       CSS V1      11          (Anthony    CSS 4.0    成为主流
                                      Fu)                      (Rust)
                                      
                                      
                              React 官方移除
                              CSS-in-JS 推荐

1.2 为什么 CSS-in-JS 引发争议?

1.2.1 React 团队的立场变化
// 2020 年:React 官方博客
// "We recommend CSS-in-JS libraries" - React 官方博客

// 2023 年:React 团队的变化
// React Server Components (RSC) 的出现
// - 运行时样式注入与 SSR 不兼容
// - 需要额外处理 hydration
// - 首屏性能影响显著

// React 团队的建议变化
const reactTeamRecommendation = {
  before: "CSS-in-JS is a great solution",
  now: "Consider CSS Modules or utility-first CSS",
  reason: "RSC compatibility + performance"
};
1.2.2 CSS-in-JS 的核心问题
// 问题 1:运行时开销
// 每次渲染都需要生成样式
const Button = styled.button`
  background: ${props => props.primary ? 'blue' : 'gray'};
`;

// 编译后的伪代码
function Button(props) {
  // 每次渲染都会执行
  const className = generateHash('background: blue'); // 运行时计算
  return <button className={className} />;
}

// 问题 2:SSR 不兼容
// 服务端渲染时样式未注入
// 需要使用 extractCritical 等工具
const ssrProblem = {
  server: "HTML without styles",
  client: "Flash of unstyled content (FOUC)",
  solution: "Additional SSR setup required"
};

// 问题 3:Bundle 体积
// 运行时库增加 JS Bundle
const bundleImpact = {
  'styled-components': '~32KB',
  'emotion': '~24KB',
  'total-react-app': '~400KB',
  percentage: '~8% of bundle'
};

1.3 Utility-First 的崛起

1.3.1 核心理念回归
/* 传统 CSS */
.button {
  padding: 10px 20px;
  background: blue;
  color: white;
  border-radius: 4px;
}

/* Utility-First */
<button class="px-5 py-2 bg-blue-500 text-white rounded">
  Button
</button>

/* 理念: */
/* 1. 单一职责:每个类只做一件事 */
/* 2. 组合优于继承:类名组合构建复杂样式 */
/* 3. 约束设计:预定义设计系统 */
1.3.2 为什么 2024-2026 年 Utility-First 主导?
┌─────────────────────────────────────────────────────────────────────────────┐
│                        Utility-First 主导的原因                              │
└─────────────────────────────────────────────────────────────────────────────┘

1. 性能优势
   ├── 构建时生成,无运行时开销
   ├── 原子化 CSS,Bundle 更小
   └── 首屏加载更快

2. 开发效率
   ├── 无需切换文件(样式在 HTML 中)
   ├── 响应式设计原生支持
   └── 重构友好(配置变更全局生效)

3. 生态成熟
   ├── Tailwind CSS 4.0 (Rust 引擎)
   ├── UnoCSS (即时生成,更快)
   └── 完善的设计系统集成

4. 框架无关
   ├── React、Vue、Svelte 都支持
   └── React Native (NativeWind)

2. 核心概念解析

2.1 什么是 CSS-in-JS?

CSS-in-JS 是一种将 CSS 样式作为 JavaScript 对象或字符串来编写,并最终注入到 DOM 的技术方案。

2.1.1 核心特征
// 特征 1:样式定义为 JavaScript
const styles = {
  button: {
    padding: '10px 20px',
    backgroundColor: 'blue',
    color: 'white'
  }
};

// 特征 2:组件与样式绑定
const Button = styled.button`
  background: blue;
  color: white;
`;

// 特征 3:动态样式基于 props
const DynamicButton = styled.button`
  background: ${props => props.variant === 'primary' ? 'blue' : 'gray'};
`;

// 特征 4:主题系统
const ThemedButton = styled.button`
  background: ${props => props.theme.colors.primary};
`;
2.1.2 解决的问题
问题 传统 CSS CSS-in-JS
全局污染 需要 BEM 命名 自动作用域
样式冲突 难以追踪 唯一哈希
动态样式 需要模板字符串 原生支持
死代码 难以移除 摇树优化

2.2 什么是 Utility-First?

Utility-First 是一种使用大量单一功能类(Utility Classes)组合构建界面的方法。

2.2.1 核心特征
// 特征 1:原子化类名
// flex = display: flex
// p-4 = padding: 1rem
// text-center = text-align: center
// rounded-lg = border-radius: 0.5rem

// 特征 2:约束设计系统
const designSystem = {
  colors: {
    primary: '#3b82f6',
    secondary: '#6b7280'
  },
  spacing: {
    1: '0.25rem',
    2: '0.5rem',
    4: '1rem'
  }
};

// 特征 3:响应式变体
// md:flex = @media (min-width: 768px) { .flex { display: flex; } }

// 特征 4:状态变体
// hover:bg-blue-500 = :hover { background: #3b82f6; }
2.2.2 解决的问题
问题 传统 CSS Utility-First
类名命名 需要思考名称 类名即样式
响应式 手动写 media query 前缀变体
设计一致性 需要规范文档 内置设计系统
样式复用 需要 CSS 组合 类名组合

3. Styled Components 深度指南

3.1 基础入门

3.1.1 安装与配置
# 安装
npm install styled-components
# 或
yarn add styled-components

# TypeScript 类型
npm install -D @types/styled-components
3.1.2 第一个组件
import styled from 'styled-components';

// 方法 1:模板字符串(推荐)
const Button = styled.button`
  padding: 10px 20px;
  background-color: blue;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  
  &:hover {
    background-color: darkblue;
  }
`;

// 方法 2:对象语法
const Button2 = styled.button({
  padding: '10px 20px',
  backgroundColor: 'blue',
  color: 'white',
  border: 'none',
  borderRadius: '4px',
  cursor: 'pointer'
});

// 使用
function App() {
  return <Button>Click me</Button>;
}

3.2 进阶用法

3.2.1 扩展样式(Extending Styles)
// 基础按钮
const BaseButton = styled.button`
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
`;

// 扩展:主要按钮
const PrimaryButton = styled(BaseButton)`
  background-color: #3b82f6;
  color: white;
  
  &:hover {
    background-color: #2563eb;
  }
`;

// 扩展:大按钮
const LargeButton = styled(BaseButton)`
  padding: 15px 30px;
  font-size: 18px;
`;

// 使用
function ButtonExamples() {
  return (
    <div>
      <BaseButton>基础</BaseButton>
      <PrimaryButton>主要</PrimaryButton>
      <LargeButton>大号</LargeButton>
    </div>
  );
}
3.2.2 动态属性(Passed Props)
// 接收 props 控制样式
const Button = styled.button<{ $variant?: 'primary' | 'secondary' | 'danger' }>`
  padding: 10px 20px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  
  /* 基于 props 条件渲染 */
  background-color: ${props => {
    switch (props.$variant) {
      case 'primary': return '#3b82f6';
      case 'danger': return '#ef4444';
      default: return '#6b7280';
    }
  }};
  
  color: ${props => props.$variant === 'secondary' ? '#1f2937' : 'white'};
  
  /* 基于 props 控制显示 */
  opacity: ${props => props.disabled ? 0.5 : 1};
  cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
`;

// 使用
function App() {
  return (
    <div>
      <Button>Default</Button>
      <Button $variant="primary">Primary</Button>
      <Button $variant="danger">Danger</Button>
      <Button disabled>Disabled</Button>
    </div>
  );
}
3.2.3 附加 Props(Attrs)
// 使用 attrs 添加静态属性
const Input = styled.input.attrs({
  type: 'text',
  placeholder: 'Enter text...'
})`
  padding: 10px;
  border: 1px solid #ddd;
`;

// 使用 attrs 添加动态属性
const EmailInput = styled.input.attrs(props => ({
  type: 'email',
  'data-testid': props.$testId,
  ariaLabel: props.$label
}))`
  padding: 10px;
  border: 1px solid #ddd;
`;

// 使用
function App() {
  return (
    <div>
      <Input />
      <EmailInput $testId="email" $label="Email Address" />
    </div>
  );
}

3.3 主题系统(Theming)

3.3.1 ThemeProvider 配置
import { ThemeProvider } from 'styled-components';

// 定义主题类型
interface Theme {
  colors: {
    primary: string;
    secondary: string;
    background: string;
    text: string;
    border: string;
  };
  spacing: {
    sm: string;
    md: string;
    lg: string;
  };
  borderRadius: {
    sm: string;
    md: string;
    lg: string;
  };
}

// 定义主题
const lightTheme: Theme = {
  colors: {
    primary: '#3b82f6',
    secondary: '#6b7280',
    background: '#ffffff',
    text: '#1f2937',
    border: '#e5e7eb'
  },
  spacing: {
    sm: '0.5rem',
    md: '1rem',
    lg: '1.5rem'
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px'
  }
};

const darkTheme: Theme = {
  ...lightTheme,
  colors: {
    primary: '#60a5fa',
    secondary: '#9ca3af',
    background: '#1f2937',
    text: '#f9fafb',
    border: '#374151'
  }
};

// 使用主题
const ThemedButton = styled.button`
  background: ${props => props.theme.colors.primary};
  color: ${props => props.theme.colors.background};
  padding: ${props => props.theme.spacing.md};
  border-radius: ${props => props.theme.borderRadius.md};
`;

// App 包装
function App() {
  const [isDark, setIsDark] = useState(false);
  
  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <ThemedButton>Theme Button</ThThemeProvider>
    </ThemeProvider>
  );
}
3.3.2 使用 useTheme
import { useTheme } from 'styled-components';

function ThemedComponent() {
  const theme = useTheme();
  
  return (
    <div style={{ 
      color: theme.colors.text,
      padding: theme.spacing.lg 
    }}>
      Current theme: {theme.colors.primary}
    </div>
  );
}

3.4 全局样式

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  /* 重置样式 */
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }
  
  /* 全局样式 */
  html {
    font-size: 16px;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
  
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: ${props => props.theme.colors.background};
    color: ${props => props.theme.colors.text};
    line-height: 1.5;
  }
  
  /* 全局链接样式 */
  a {
    color: ${props => props.theme.colors.primary};
    text-decoration: none;
    
    &:hover {
      text-decoration: underline;
    }
  }
  
  /* 全局按钮样式 */
  button {
    font-family: inherit;
  }
`;

// 使用
function App() {
  return (
    <>
      <GlobalStyle />
      <YourApp />
    </>
  );
}

3.5 样式组合与嵌套

3.5.1 伪类与伪元素
const InteractiveBox = styled.div`
  /* 伪类 */
  &:hover {
    background: blue;
    color: white;
  }
  
  &:focus {
    outline: 2px solid blue;
    outline-offset: 2px;
  }
  
  &:active {
    transform: scale(0.98);
  }
  
  /* 伪元素 */
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 2px;
    background: blue;
  }
  
  &::after {
    content: ' →';
  }
  
  /* 状态组合 */
  &:hover::after {
    content: ' ←';
  }
`;
3.5.2 嵌套选择器
const Card = styled.article`
  padding: 20px;
  border: 1px solid #ddd;
  
  /* 直接子元素 */
  & > h2 {
    font-size: 1.5rem;
    margin-bottom: 10px;
  }
  
  /* 后代元素 */
  & p {
    color: #666;
    line-height: 1.6;
    
    /* 嵌套伪类 */
    &:first-of-type {
      font-weight: bold;
    }
  }
  
  /* 同一父级下的其他元素 */
  & + & {
    margin-top: 20px;
  }
  
  /* 引用父级 */
  &:hover & {
    border-color: blue;
  }
`;

3.6 动画与关键帧

import { keyframes, css } from 'styled-components';

// 定义动画
const fadeIn = keyframes`
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;

const pulse = keyframes`
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
`;

// 使用动画
const AnimatedBox = styled.div`
  animation: ${fadeIn} 0.3s ease-out;
  
  /* 条件动画 */
  ${props => props.$isLoading && css`
    animation: ${pulse} 1.5s ease-in-out infinite;
  `}
`;

// 组合多个动画
const ComplexAnimation = styled.div`
  animation: 
    ${fadeIn} 0.3s ease-out,
    ${pulse} 2s ease-in-out 0.3s;
`;

3.7 CSS 媒体查询

const ResponsiveBox = styled.div`
  /* 默认样式(移动端) */
  width: 100%;
  padding: 10px;
  
  /* 平板 */
  @media (min-width: 768px) {
    width: 50%;
    padding: 20px;
  }
  
  /* 桌面 */
  @media (min-width: 1024px) {
    width: 33.333%;
    padding: 30px;
  }
  
  /* 更大屏幕 */
  @media (min-width: 1440px) {
    max-width: 1200px;
    margin: 0 auto;
  }
`;

3.8 与 React 深度集成

3.8.1 继承 HTML 元素
// 支持所有 HTML 元素
const StyledDiv = styled.div``;
const StyledSpan = styled.span``;
const StyledA = styled.a``;
const StyledInput = styled.input``;
const StyledSelect = styled.select``;
const StyledTextarea = styled.textarea``;

// 自定义元素
const StyledSvg = styled.svg`
  width: 24px;
  height: 24px;
  fill: currentColor;
`;
3.8.2 传递 className
// styled-components 会自动传递 className
// 但如果有其他 className 来源,需要合并

const StyledButton = styled.button`
  padding: 10px 20px;
  
  /* 接收外部 className */
  ${props => props.className && css`
    /* 外部样式会应用 */
  `}
`;

function App() {
  // 外部传入的 className 会自动合并
  return <StyledButton className="external-class">Button</StyledButton>;
}
3.8.3 Ref 转发
import { forwardRef } from 'react';

// styled-components 默认支持 ref
const Input = styled.input`
  padding: 10px;
  border: 1px solid #ddd;
`;

function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  return (
    <Input 
      ref={inputRef} 
      placeholder="Focus me"
    />
  );
}

// 自定义 ref 转发
const CustomInput = forwardRef<HTMLInputElement, Props>(
  ({ placeholder }, ref) => (
    <StyledInput ref={ref} placeholder={placeholder} />
  )
);

3.9 SSR 支持

3.9.1 Next.js 中的使用
// pages/_document.js (Pages Router)
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }

  render() {
    return (
      <Html lang="en">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}
3.9.2 App Router (Next.js 13+)
// 需要使用 Registry 组件
// app/components/Registry.tsx
'use client';

import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== 'undefined') {
    return <>{children}</>;
  }

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}

// app/layout.tsx
import StyledComponentsRegistry from './components/Registry';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}

4. Emotion 深度指南

4.1 基础入门

4.1.1 安装
# 完整安装(推荐)
npm install @emotion/react @emotion/styled

# 仅核心
npm install @emotion/react

# 僅 styled API
npm install @emotion/styled
4.1.2 三种使用方式
// 方式 1:css prop(最常用)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

function App() {
  return (
    <div
      css={css`
        padding: 20px;
        background: blue;
      `}
    >
      Hello
    </div>
  );
}

// 方式 2:styled 组件
import styled from '@emotion/styled';

const Button = styled.button`
  background: blue;
  color: white;
`;

function App() {
  return <Button>Click</Button>;
}

// 方式 3:jsx 函数
import { jsx } from '@emotion/react';

const styles = {
  container: {
    padding: 20,
    background: 'blue'
  }
};

function App() {
  return <div css={styles.container}>Hello</div>;
}

4.2 css prop 详解

4.2.1 基础使用
/** @jsxImportSource @emotion/react */

function BasicExample() {
  return (
    <div
      css={{
        padding: '20px',
        backgroundColor: '#f0f0f0',
        borderRadius: '8px'
      }}
    >
      Content
    </div>
  );
}
4.2.2 嵌套与选择器
function NestedExample() {
  return (
    <div
      css={{
        padding: '20px',
        
        // 嵌套选择器
        '& .title': {
          fontSize: '24px',
          fontWeight: 'bold'
        },
        
        // 伪类
        '&:hover': {
          backgroundColor: 'blue'
        },
        
        // 伪元素
        '&::before': {
          content: '"→"',
          marginRight: '8px'
        },
        
        // 媒体查询
        '@media (min-width: 768px)': {
          padding: '40px'
        }
      }}
    >
      <div className="title">Title</div>
    </div>
  );
}
4.2.3 动画
import { keyframes } from '@emotion/react';

const fadeIn = keyframes`
  from { opacity: 0; }
  to { opacity: 1; }
`;

function AnimatedExample() {
  return (
    <div
      css={{
        animation: `${fadeIn} 0.5s ease-out`,
        
        // 动态值
        animationDuration: '0.3s',
        animationDelay: '0.1s'
      }}
    >
      Animated Content
    </div>
  );
}

4.3 Styled Components 详解

4.3.1 基础语法
import styled from '@emotion/styled';

// 模板字符串语法
const Button = styled.button`
  padding: 10px 20px;
  background: blue;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
`;

// 对象语法
const Button2 = styled.button({
  padding: '10px 20px',
  background: 'blue',
  color: 'white',
  border: 'none',
  borderRadius: '4px',
  cursor: 'pointer'
});
4.3.2 动态 Props
interface ButtonProps {
  $variant?: 'primary' | 'secondary' | 'danger';
  $size?: 'sm' | 'md' | 'lg';
}

const StyledButton = styled.button<ButtonProps>`
  padding: ${props => {
    switch (props.$size) {
      case 'sm': return '5px 10px';
      case 'lg': return '15px 30px';
      default: return '10px 20px';
    }
  }};
  
  background: ${props => {
    switch (props.$variant) {
      case 'danger': return '#ef4444';
      case 'secondary': return '#6b7280';
      default: return '#3b82f6';
    }
  }};
  
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  
  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`;

function App() {
  return (
    <>
      <StyledButton>Default</StyledButton>
      <StyledButton $variant="primary">Primary</StyledButton>
      <StyledButton $size="lg">Large</StyledButton>
      <StyledButton disabled>Disabled</StyledButton>
    </>
  );
}
4.3.3 继承与扩展
// 基础样式
const BaseButton = styled.button`
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
`;

// 扩展样式
const PrimaryButton = styled(BaseButton)`
  background: #3b82f6;
  color: white;
  
  &:hover {
    background: #2563eb;
  }
`;

// 使用 as 切换基础元素
const LinkButton = styled(BaseButton)`
  background: transparent;
  color: #3b82f6;
  
  &:hover {
    text-decoration: underline;
  }
`;

function App() {
  return (
    <>
      <PrimaryButton as="a" href="/submit">As Link</PrimaryButton>
      <LinkButton>Actual Link</LinkButton>
    </>
  );
}

4.4 样式组合

4.4.1 css 标签
import { css } from '@emotion/react';

const baseStyles = css`
  padding: 10px 20px;
  border-radius: 4px;
`;

const primaryStyles = css`
  background: blue;
  color: white;
`;

function Component() {
  return (
    <div css={[baseStyles, primaryStyles]}>
      Combined Styles
    </div>
  );
}
4.4.2 条件样式
function ConditionalStyles({ isActive, isPrimary }) {
  return (
    <div
      css={[
        css`
          padding: 10px 20px;
          border-radius: 4px;
        `,
        isPrimary && css`
          background: blue;
          color: white;
        `,
        isActive && css`
          border: 2px solid blue;
        `
      ]}
    >
      Content
    </div>
  );
}

4.5 主题系统

4.5.1 ThemeProvider
import { ThemeProvider } from '@emotion/react';

interface Theme {
  colors: {
    primary: string;
    secondary: string;
    background: string;
  };
}

const theme: Theme = {
  colors: {
    primary: '#3b82f6',
    secondary: '#6b7280',
    background: '#ffffff'
  }
};

function App() {
  return (
    <ThemeProvider theme={theme}>
      <ThemedComponent />
    </ThemeProvider>
  );
}
4.5.2 使用主题
import { useTheme } from '@emotion/react';

function ThemedComponent() {
  const theme = useTheme();
  
  return (
    <div
      css={{
        background: theme.colors.background,
        color: theme.colors.primary
      }}
    >
      Themed Content
    </div>
  );
}

// 在 styled 中使用
const ThemedButton = styled.button`
  background: ${props => props.theme.colors.primary};
  color: ${props => props.theme.colors.background};
`;

4.6 全局样式

import { Global, css } from '@emotion/react';

function GlobalStyles() {
  return (
    <Global
      styles={css`
        * {
          box-sizing: border-box;
          margin: 0;
          padding: 0;
        }
        
        body {
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          line-height: 1.5;
        }
        
        a {
          color: inherit;
          text-decoration: none;
        }
        
        button {
          font-family: inherit;
          cursor: pointer;
        }
      `}
    />
  );
}

function App() {
  return (
    <>
      <GlobalStyles />
      <YourApp />
    </>
  );
}

4.7 关键优化技巧

4.7.1 静态样式提取
// ❌ 性能问题:每次渲染都创建新对象
function BadExample({ isPrimary }) {
  return (
    <div
      css={{
        padding: '10px',
        background: isPrimary ? 'blue' : 'gray', // 动态部分
        color: 'white' // 静态部分
      }}
    />
  );
}

// ✅ 优化:分离静态和动态样式
const staticStyles = css`
  padding: 10px;
  color: white;
`;

function GoodExample({ isPrimary }) {
  return (
    <div
      css={[
        staticStyles,
        isPrimary ? css`background: blue;` : css`background: gray;`
      ]}
    />
  );
}
4.7.2 useMemo 缓存
import { useMemo } from 'react';

function OptimizedComponent({ variant, size }) {
  const styles = useMemo(() => css`
    padding: ${size === 'lg' ? '20px' : '10px'};
    background: ${variant === 'primary' ? 'blue' : 'gray'};
  `, [variant, size]);
  
  return <div css={styles}>Content</div>;
}

4.8 SSR 支持

4.8.1 extractCritical
import { renderToString } from 'react-dom/server';
import { extractCritical } from '@emotion/server';

function renderToHTML(element) {
  const html = renderToString(element);
  const { ids, css } = extractCritical(html);
  
  return {
    html,
    css,
    ids // 用于 hydration
  };
}
4.8.2 Next.js App Router
// lib/EmotionCache.tsx
'use client';

import createCache from '@emotion/cache';
import { useServerInsertedHTML } from 'next/navigation';
import { CacheProvider } from '@emotion/react';
import React, { useState } from 'react';

export default function EmotionCacheProvider({ children }) {
  const [cache] = useState(() => createCache({ key: 'css' }));

  useServerInsertedHTML(() => {
    const names = Object.keys(cache.inserted);
    let i = names.length;
    let css = '';
    while (i--) {
      const name = names[i];
      css += cache.inserted[name];
    }
    return (
      <style
        key={cache.key}
        data-emotion={`${cache.key} ${names.join(' ')}`}
        dangerouslySetInnerHTML={{
          __html: css,
        }}
      />
    );
  });

  return <CacheProvider value={cache}>{children}</CacheProvider>;
}

4.9 Styled Components vs Emotion 对比

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Styled Components vs Emotion                             │
├───────────────────────────────────────┬─────────────────────────────────────┤
│            Styled Components          │              Emotion                │
├───────────────────────────────────────┼─────────────────────────────────────┤
│ 模板字符串为主要 API                   │ 三种方式:css prop、styled、jsx      │
│ 自动生成类名                          │ 需要手动处理类名                     │
│ React Native 支持                     │ 更轻量,性能更好                    │
│ 更成熟的 SSR 支持                      │ 更灵活的样式组合                    │
│ API 简洁直观                          │ 学习曲线稍陡,但更灵活               │
│                                      │                                     │
│ 适用:喜欢模板字符串语法                │ 适用:需要极致性能                   │
│      React Native 项目                │      需要 css prop 便利性            │
└───────────────────────────────────────┴─────────────────────────────────────┘

5. 架构设计深度对比

5.1 渲染流程对比

┌─────────────────────────────────────────────────────────────────────────────┐
│                          CSS-in-JS 渲染流程                                  │
└─────────────────────────────────────────────────────────────────────────────┘

Styled Components:

[组件定义]
     │
     ▼
[模板解析] ──→ 生成哈希类名
     │
     ▼
[React 渲染] ──→ createElement()
     │
     ▼
[生成 <style>] ──→ 注入到 DOM
     │
     ▼
[浏览器解析] ──→ 样式应用


输出示例:
<style>
  .Button-sc-1a2b3c { background: blue; }
</style>
<button class="Button-sc-1a2b3c">Click</button>


─────────────────────────────────────────────────────────────────────────────

Emotion (@emotion/react):

[组件渲染]
     │
     ▼
[css prop 处理]
     │
     ▼
[样式序列化]
     │
     ▼
[生成 <style>] ──→ 带缓存
     │
     ▼
[浏览器解析]


─────────────────────────────────────────────────────────────────────────────

Utility-First (Tailwind CSS):

[源代码编写]
     │
     ▼
[构建阶段扫描] ──→ 提取 class 属性
     │
     ▼
[JIT 编译] ──→ 匹配工具类
     │
     ▼
[生成 CSS] ──→ 原子化输出
     │
     ▼
[打包到 CSS 文件]


输出示例:
.bg-blue-500 { --tw-bg-opacity: 1; background-color: rgb(59 130 246); }
<button class="bg-blue-500">Click</button>

5.2 样式隔离机制对比

机制 Styled Components Emotion Tailwind CSS
隔离方式 哈希类名 哈希类名 原子类名
全局污染
动态样式 运行时生成 运行时生成 类名组合
清理机制 组件卸载时 组件卸载时 无需清理
SSR 兼容 需要配置 需要配置 原生支持

5.3 动态样式能力对比

场景 Styled Components Emotion Tailwind CSS
props 驱动 ✅ 原生支持 ✅ 原生支持 ⚠️ 需条件类名
主题系统 ✅ 完整 ✅ 完整 ✅ 完整
CSS 变量 ✅ 支持 ✅ 支持 ✅ 支持
计算样式 ✅ 支持 ✅ 支持 ⚠️ 受限

6. 性能基准测试

6.1 测试场景

测试项目:
  组件数量: 500
  样式规则: 平均 15 条/组件
  动态样式: 30% 组件有 props 样式
  测试框架: React 18 + Vite 5
  渲染次数: 1000 次状态更新

6.2 开发环境性能

首次加载时间
┌─────────────────────────────────────────────────────────────────────────────┐
│                         首次加载时间 (ms)                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  @emotion/react                                                           │
│  ████████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░  165ms  │
│                                                                            │
│  @emotion/styled                                                         │
│  ██████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░  155ms  │
│                                                                            │
│  styled-components                                                        │
│  ██████████████████████████████████████████████░░░░░░░░░░░░░░░░░░  185ms  │
│                                                                            │
│  Tailwind CSS                                                            │
│  ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  85ms   │
│                                                                            │
│  UnoCSS                                                                  │
│  ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  65ms   │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘
内存占用
┌─────────────────────────────────────────────────────────────────────────────┐
│                         运行时内存占用 (MB)                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ████████████████████████████████████████████████████████░░░░░  52MB     │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████░░░░░░░░░░░░░░░  42MB     │
│                                                                            │
│  @emotion/styled                                                          │
│  ████████████████████████████████████████████░░░░░░░░░░░░░░░░░░  38MB    │
│                                                                            │
│  Tailwind CSS                                                             │
│  ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  18MB     │
│                                                                            │
│  UnoCSS                                                                  │
│  ███████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  14MB    │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘

6.3 状态更新性能

1000 次渲染耗时
┌─────────────────────────────────────────────────────────────────────────────┐
│                         渲染耗时 (ms)                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ██████████████████████████████████████████████████████████████  380ms    │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████████████████░░░░░░░  345ms   │
│                                                                            │
│  @emotion/styled                                                          │
│  ████████████████████████████████████████████████████████░░░░░░░░  320ms    │
│                                                                            │
│  Tailwind CSS                                                             │
│  ████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░  145ms    │
│                                                                            │
│  UnoCSS                                                                  │
│  ██████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  120ms    │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘

6.4 生产构建对比

Bundle 大小
┌─────────────────────────────────────────────────────────────────────────────┐
│                         JS Bundle 增加 (KB)                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ████████████████████████████████████████████████████████░░░░  +32KB     │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████░░░░░░░░░░░░░░░░  +18KB    │
│                                                                            │
│  @emotion/styled                                                          │
│  █████████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░  +14KB    │
│                                                                            │
│  Tailwind CSS                                                             │
│  ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   +2KB     │
│                                                                            │
│  UnoCSS                                                                  │
│  ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   +1KB     │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘
CSS 输出大小
┌─────────────────────────────────────────────────────────────────────────────┐
│                         CSS 输出大小 (KB, 500 组件)                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ██████████████████████████████████████████████████████████████  156KB   │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████████████████░░░░░░░░  138KB  │
│                                                                            │
│  @emotion/styled                                                          │
│  ████████████████████████████████████████████████████████░░░░░░░░░░  128KB  │
│                                                                            │
│  Tailwind CSS (JIT)                                                       │
│  ████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░   85KB   │
│                                                                            │
│  UnoCSS                                                                   │
│  ██████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   52KB   │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘

7. 开发体验详解

7.1 代码组织对比

CSS-in-JS 模式
// 优点:样式与组件共存,易于查找
// 缺点:组件文件可能变长

// Button.tsx
const Button = styled.button`
  padding: 10px 20px;
  background: blue;
  color: white;
`;

const IconButton = styled(Button)`
  padding: 8px;
  border-radius: 50%;
`;

export function ButtonGroup() {
  return (
    <div>
      <Button>Save</Button>
      <IconButton>🔔</IconButton>
    </div>
  );
}
Utility-First 模式
// 优点:HTML 即视图,样式自解释
// 缺点:类名可能很长

// ButtonGroup.tsx
function ButtonGroup() {
  return (
    <div className="flex gap-2">
      <button className="px-5 py-2 bg-blue-500 text-white rounded">
        Save
      </button>
      <button className="p-2 bg-blue-500 text-white rounded-full">
        🔔
      </button>
    </div>
  );
}

7.2 类型安全对比

CSS-in-JS(完整类型推断)
// Styled Components
import styled, { CSSProperties, Theme } from 'styled-components';

interface Props {
  $variant: 'primary' | 'secondary';
  $size: 'sm' | 'md' | 'lg';
}

// 完整类型推断
const Button = styled.button<Props>`
  padding: ${p => p.$size === 'lg' ? '16px 32px' : '8px 16px'};
  background: ${p => p.$variant === 'primary' ? 'blue' : 'gray'};
  
  &:hover {
    background: ${p => p.$variant === 'primary' ? 'darkblue' : 'darkgray'};
  }
`;

// 使用时自动推断
<Button $variant="primary" $size="md" /> // ✅
<Button $variant="invalid" $size="md" /> // ❌ TypeScript 报错
Utility-First(类型辅助)
// 需要使用辅助库
import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva(
  'rounded-md font-medium transition-colors',
  {
    variants: {
      variant: {
        primary: 'bg-blue-500 text-white hover:bg-blue-600',
        secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2',
        lg: 'px-6 py-3 text-lg',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

// 类型推断
type ButtonProps = VariantProps<typeof buttonVariants>;

function Button({ variant, size, className, ...props }: ButtonProps) {
  return (
    <button 
      className={buttonVariants({ variant, size, className })} 
      {...props} 
    />
  );
}

7.3 重构体验对比

样式变更场景
场景:将所有主要按钮从蓝色改为绿色

CSS-in-JS:
├── 需要逐个文件修改
├── 搜索: styled.button`background: blue
├── 替换: background: green
└── 风险: 可能误改其他样式

Utility-First:
├── 修改 tailwind.config.js
├── theme: { colors: { primary: 'green-500' } }
└── 自动全局生效

7.4 调试体验对比

浏览器 DevTools
CSS-in-JS:
├── .sc-Button-abc123 { background: blue; }
├── React DevTools 显示组件树
├── styled-components 插件显示样式
└── 缺点: 哈希类名不易理解

Utility-First:
├── .bg-blue-500 { background: #3b82f6; }
├── 类名即样式含义
├── Tailwind DevTools 插件
└── 优点: 自解释类名

8. 实战案例

8.1 案例:带表单验证的登录页

需求
  • 邮箱/密码输入框
  • 实时验证
  • 错误提示
  • 加载状态
  • 暗色/亮色主题
Styled Components 实现
import styled, { css, keyframes } from 'styled-components';

const spin = keyframes`
  to { transform: rotate(360deg); }
`;

const Container = styled.div`
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: ${props => props.theme.colors.background};
  padding: 20px;
`;

const Form = styled.form`
  width: 100%;
  max-width: 400px;
  padding: 40px;
  background: ${props => props.theme.colors.card};
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
`;

const Title = styled.h1`
  font-size: 24px;
  font-weight: 600;
  color: ${props => props.theme.colors.text};
  margin-bottom: 24px;
  text-align: center;
`;

const InputGroup = styled.div`
  margin-bottom: 16px;
`;

const Label = styled.label`
  display: block;
  font-size: 14px;
  font-weight: 500;
  color: ${props => props.theme.colors.text};
  margin-bottom: 6px;
`;

const Input = styled.input<{ $hasError?: boolean }>`
  width: 100%;
  padding: 12px 16px;
  font-size: 16px;
  border: 2px solid ${props => 
    props.$hasError ? '#ef4444' : props.theme.colors.border
  };
  border-radius: 8px;
  outline: none;
  transition: border-color 0.2s;
  
  &:focus {
    border-color: ${props => 
      props.$hasError ? '#ef4444' : props.theme.colors.primary
    };
  }
  
  &::placeholder {
    color: ${props => props.theme.colors.placeholder};
  }
`;

const ErrorMessage = styled.span`
  display: block;
  font-size: 12px;
  color: #ef4444;
  margin-top: 4px;
`;

const SubmitButton = styled.button<{ $isLoading?: boolean }>`
  width: 100%;
  padding: 14px;
  font-size: 16px;
  font-weight: 600;
  color: white;
  background: ${props => props.theme.colors.primary};
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;
  position: relative;
  
  &:hover:not(:disabled) {
    background: ${props => props.theme.colors.primaryHover};
    transform: translateY(-1px);
  }
  
  &:disabled {
    opacity: 0.7;
    cursor: not-allowed;
  }
  
  ${props => props.$isLoading && css`
    color: transparent;
    
    &::after {
      content: '';
      position: absolute;
      width: 20px;
      height: 20px;
      top: 50%;
      left: 50%;
      margin-left: -10px;
      margin-top: -10px;
      border: 2px solid white;
      border-right-color: transparent;
      border-radius: 50%;
      animation: ${spin} 0.8s linear infinite;
    }
  `}
`;

// 主题
const theme = {
  colors: {
    background: '#f5f5f5',
    card: '#ffffff',
    text: '#1f2937',
    primary: '#3b82f6',
    primaryHover: '#2563eb',
    border: '#d1d5db',
    placeholder: '#9ca3af'
  }
};

// 组件
function LoginFormStyled() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<{email?: string; password?: string}>({});
  const [isLoading, setIsLoading] = useState(false);
  
  const validate = () => {
    const newErrors: typeof errors = {};
    if (!email.includes('@')) {
      newErrors.email = '请输入有效的邮箱地址';
    }
    if (password.length < 6) {
      newErrors.password = '密码至少需要 6 位';
    }
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!validate()) return;
    
    setIsLoading(true);
    // 模拟 API 调用
    await new Promise(resolve => setTimeout(resolve, 1500));
    setIsLoading(false);
  };
  
  return (
    <ThemeProvider theme={theme}>
      <Container>
        <Form onSubmit={handleSubmit}>
          <Title>登录</Title>
          
          <InputGroup>
            <Label>邮箱</Label>
            <Input
              type="email"
              placeholder="your@email.com"
              value={email}
              onChange={e => setEmail(e.target.value)}
              $hasError={!!errors.email}
            />
            {errors.email && <ErrorMessage>{errors.email}</ErrorMessage>}
          </InputGroup>
          
          <InputGroup>
            <Label>密码</Label>
            <Input
              type="password"
              placeholder="••••••••"
              value={password}
              onChange={e => setPassword(e.target.value)}
              $hasError={!!errors.password}
            />
            {errors.password && <ErrorMessage>{errors.password}</ErrorMessage>}
          </InputGroup>
          
          <SubmitButton type="submit" $isLoading={isLoading}>
            {isLoading ? '' : '登录'}
          </SubmitButton>
        </Form>
      </Container>
    </ThemeProvider>
  );
}
Tailwind CSS + clsx 实现
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

function cn(...inputs: (string | undefined | null | false)[]) {
  return twMerge(clsx(inputs));
}

function LoginFormTailwind() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<{email?: string; password?: string}>({});
  const [isLoading, setIsLoading] = useState(false);
  
  const validate = () => {
    const newErrors: typeof errors = {};
    if (!email.includes('@')) {
      newErrors.email = '请输入有效的邮箱地址';
    }
    if (password.length < 6) {
      newErrors.password = '密码至少需要 6 位';
    }
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!validate()) return;
    
    setIsLoading(true);
    await new Promise(resolve => setTimeout(resolve, 1500));
    setIsLoading(false);
  };
  
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50 p-5">
      <form 
        onSubmit={handleSubmit}
        className="w-full max-w-md p-10 bg-white rounded-xl shadow-lg"
      >
        <h1 className="text-2xl font-semibold text-gray-900 mb-6 text-center">
          登录
        </h1>
        
        <div className="mb-4">
          <label className="block text-sm font-medium text-gray-700 mb-1.5">
            邮箱
          </label>
          <input
            type="email"
            placeholder="your@email.com"
            value={email}
            onChange={e => setEmail(e.target.value)}
            className={clsx(
              "w-full px-4 py-3 text-base border-2 rounded-lg outline-none transition-colors",
              "placeholder:text-gray-400",
              errors.email 
                ? "border-red-500 focus:border-red-500" 
                : "border-gray-300 focus:border-blue-500"
            )}
          />
          {errors.email && (
            <span className="block text-xs text-red-500 mt-1">
              {errors.email}
            </span>
          )}
        </div>
        
        <div className="mb-6">
          <label className="block text-sm font-medium text-gray-700 mb-1.5">
            密码
          </label>
          <input
            type="password"
            placeholder="••••••••"
            value={password}
            onChange={e => setPassword(e.target.value)}
            className={clsx(
              "w-full px-4 py-3 text-base border-2 rounded-lg outline-none transition-colors",
              "placeholder:text-gray-400",
              errors.password 
                ? "border-red-500 focus:border-red-500" 
                : "border-gray-300 focus:border-blue-500"
            )}
          />
          {errors.password && (
            <span className="block text-xs text-red-500 mt-1">
              {errors.password}
            </span>
          )}
        </div>
        
        <button
          type="submit"
          disabled={isLoading}
          className={clsx(
            "w-full py-3.5 text-base font-semibold text-white rounded-lg",
            "transition-all duration-200",
            "hover:-translate-y-0.5 hover:shadow-lg",
            "disabled:opacity-70 disabled:cursor-not-allowed",
            isLoading && "relative text-transparent"
          )}
          style={{ background: '#3b82f6' }}
        >
          {isLoading ? '' : '登录'}
          {isLoading && (
            <span className="absolute inset-0 flex items-center justify-center">
              <span className="w-5 h-5 border-2 border-white border-r-transparent rounded-full animate-spin" />
            </span>
          )}
        </button>
      </form>
    </div>
  );
}

8.2 代码量对比

指标 Styled Components Tailwind CSS
总行数 142 行 78 行
组件文件 1 个 1 个
样式定义 内联 类名
可读性 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
可维护性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐

9. 迁移与混用策略

9.1 从 CSS-in-JS 迁移到 Utility-First

渐进式迁移策略
// 阶段 1:新组件使用 Utility-First,旧组件保持不变
function NewComponent() {
  return <div className="p-4 bg-white">新组件</div>;
}

const OldComponent = styled.div`
  padding: 1rem;
  background: white;
`;

// 阶段 2:共存
function Page() {
  return (
    <OldComponent>
      <NewComponent />
    </OldComponent>
  );
}

// 阶段 3:逐步重写旧组件
样式映射表
┌─────────────────────────────────────────────────────────────────────────────┐
                         样式转换对照表                                       
├─────────────────────────────────────┬─────────────────────────────────────┤
 styled-components                    Tailwind CSS                        
├─────────────────────────────────────┼─────────────────────────────────────┤
 display: flex                        flex                                
 display: grid                        grid                                
 flex-direction: column              flex-col                            
 align-items: center                 items-center                        
 justify-content: space-between      justify-between                    
 padding: 16px                       p-4                                 
 padding-top: 16px                   pt-4                                
 margin: 16px                        m-4                                 
 margin-bottom: 16px                 mb-4                                
 color: #fff                        │ text-white                          │
 background: #000                   │ bg-black                            │
 border-radius: 4px                 rounded                             
 font-size: 16px                    text-base (1rem)                   
 font-weight: 700                   font-bold                           
 width: 100%                         w-full                              
 height: 100%                        h-full                              
 box-shadow: 0 1px 3px rgba(0,0,0,0.1)  shadow-sm                          
 &:hover { ... }                    hover:...                           
 @media (min-width: 768px) { ... }  md:...                              
└─────────────────────────────────────┴─────────────────────────────────────┘

9.2 混合使用模式

场景:组件库 + 项目样式
// 使用 UI 库(可能使用 styled-components)
import { Button as AntButton } from 'antd';

// 项目样式使用 Tailwind
function MyPage() {
  return (
    <div className="p-4">
      <AntButton type="primary">库组件</AntButton>
      <button className="ml-4 px-4 py-2 bg-blue-500">
        项目按钮
      </button>
    </div>
  );
}
场景:Tailwind + CSS-in-JS 动画
import styled from 'styled-components';
import { keyframes } from 'styled-components';

// 使用 styled-components 处理复杂动画
const fadeIn = keyframes`
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: scale(1); }
`;

const AnimatedContainer = styled.div`
  animation: ${fadeIn} 0.3s ease-out;
`;

// Tailwind 处理布局
function Modal() {
  return (
    <AnimatedContainer className="fixed inset-0 flex items-center justify-center bg-black/50">
      <div className="bg-white rounded-lg p-6 max-w-md">
        Modal Content
      </div>
    </AnimatedContainer>
  );
}

10. 常见问题与解决方案

10.1 CSS-in-JS 常见问题

Q1: 样式闪烁(FOUC)
// 问题:SSR 时页面闪烁
// 解决:使用 extractCritical 或 styled-components 的 SSR 支持

// Next.js App Router
import StyledComponentsRegistry from './lib/registry';

export default function Layout({ children }) {
  return (
    <html>
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}
Q2: 样式不生效
// 问题:props 样式未应用
// 解决:确保使用正确的 prop 名称

// ❌ 错误
const Button = styled.button`
  background: ${props.variant}; // 缺少 theme 或正确引用
`;

// ✅ 正确
const Button = styled.button`
  background: ${props => props.$variant}; // 使用 transient props
  background: ${props => props.theme.colors.primary}; // 使用主题
`;
Q3: 性能问题
// 问题:大量动态样式导致性能下降
// 解决:提取静态样式

// ❌ 每次渲染都创建对象
const Bad = styled.div`
  padding: 10px;
  background: ${props => props.$color}; // 动态
`;

// ✅ 分离静态和动态
const StaticStyles = styled.css`
  padding: 10px;
`;

const Good = styled.div`
  ${StaticStyles}
  background: ${props => props.$color};
`;

10.2 Utility-First 常见问题

Q1: 类名过长
// 问题:复杂组件类名太多
// 解决:使用 shortcuts 或组件封装

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      shortcuts: {
        'card': 'bg-white rounded-lg shadow-sm p-6',
        'btn-primary': 'px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600',
        'input-base': 'w-full px-4 py-2 border border-gray-300 rounded focus:ring-2',
      }
    }
  }
};

// 使用
function Component() {
  return (
    <div className="card">
      <button className="btn-primary">Click</button>
    </div>
  );
}
Q2: 任意值写法
// 问题:需要使用非预设值
// 解决:使用方括号语法

// 任意颜色
<div className="bg-[#123456]">任意颜色</div>

// 任意数值
<div className="w-[123px] h-[calc(100vh-2rem)]">任意值</div>

// 任意属性
<div className="[--color:red]">CSS 变量</div>
Q3: 深层选择器
// 问题:需要复杂选择器
// 解决:使用 [&] 任意选择器

// & = 当前元素
<div className="[&]:p-4">当前元素</div>

// 嵌套
<div className="[&_p]:font-bold [&_p+&_p]:mt-2">
  <p>子元素</p>
</div>

// 伪类组合
<div className="[&:hover>span]:opacity-100">
  <span>悬停显示</span>
</div>

11. 总结与选型建议

11.1 方案对比矩阵

维度 Styled Components Emotion Tailwind CSS UnoCSS
性能 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
易用性 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
灵活性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
类型安全 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
SSR ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
学习曲线 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
生态 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
未来趋势 ⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

11.2 选型决策

┌─────────────────────────────────────────────────────────────────────────────┐
│                            选型决策树                                        │
└─────────────────────────────────────────────────────────────────────────────┘

开始
  │
  ├─► 项目类型?
  │    │
  │    ├─► React Native → styled-components ✅
  │    │
  │    ├─► 新 Web 项目 → Tailwind CSS / UnoCSS ✅
  │    │
  │    └─► Vue 项目 → Tailwind CSS / UnoCSS ✅
  │
  ├─► 团队背景?
  │    │
  │    ├─► 熟悉 JS/React → CSS-in-JS 或 Utility-First 都可
  │    │
  │    └─► 熟悉 CSS → Utility-First 更容易上手
  │
  ├─► 性能要求?
  │    │
  │    ├─► 极致性能 → Utility-First
  │    │
  │    └─► 一般性能 → 都可以
  │
  └─► 项目规模?
       │
       ├─► 小型 → 任意方案
       │
       ├─► 中大型 → Utility-First + 组件库
       │
       └─► 遗留项目 → 渐进式迁移

11.3 2026 年建议

┌─────────────────────────────────────────────────────────────────────────────┐
│                          2026 年技术建议                                     │
└─────────────────────────────────────────────────────────────────────────────┘

✅ 推荐选择:
│
├── 新 Web 项目 → Tailwind CSS 4.0
│   ├── 理由:性能最佳、生态成熟、Rust 引擎
│   └── 适用:ReactVueSvelte
│
├── 需要 React Nativestyled-components
│   ├── 理由:唯一全面支持 RN 的方案
│   └── 注意:考虑逐步迁移
│
├── 遗留项目 → 渐进式迁移
│   ├── 新组件:Utility-First
│   └── 旧组件:保持不变,逐步重写
│
└── 追求极致性能 → UnoCSS
    ├── 理由:即时生成、最小 Bundle
    └── 适用:大型项目、高性能需求

⚠️ 谨慎选择:
│
├── 新项目使用 CSS-in-JS
│   ├── React 官方不推荐
│   └── 维护风险增加
│
└── 纯 CSS-in-JS 方案
    └── 考虑混合方案

🔄 趋势观察:
│
├── Tailwind CSS 4.0 (Rust) 将成为主流
├── UnoCSS 生态快速发展
├── CSS 原生特性 (@layer, CSS 变量) 减少框架依赖
└── 混合方案(Utility-First + 组件库)成为常态

11.4 行动清单

## 开始使用 Utility-First

1. [ ] 安装 Tailwind CSS 或 UnoCSS
2. [ ] 配置设计系统(颜色、间距、字体)
3. [ ] 团队学习基础工具类
4. [ ] 使用 cva/class-variance-authority 管理变体
5. [ ] 使用 tailwind-merge 处理类名合并

## 从 CSS-in-JS 迁移

1. [ ] 评估当前样式复杂度
2. [ ] 新组件使用 Utility-First
3. [ ] 旧组件逐步重写
4. [ ] 移除不必要的 CSS-in-JS 依赖
5. [ ] 统一代码规范

📚 延伸阅读


CSS、Tailwind CSS、UnoCSS篇结束

彻底讲透医院移动端手持设备PDA离线同步架构:从"记账本"到"分布式共识",吊打面试官

2026年2月24日 17:12

一套解决"手术室铅门屏蔽导致WiFi掉线"的工业级方案,如何从生活常识进化成分布式系统理论?


第一层:幼儿园版 —— 为什么要有这个算法?

想象一下,你是一个在手术室工作的护士。

场景还原

  • 你拿着一个PDA(像一个大手机)给病人做登记
  • 手术室的铅门像一个大铁盖子,WiFi信号根本穿不进来
  • 电梯里、地下室、病区走廊,网络时有时无

问题来了
如果你每次点“保存”都要等网络响应,那在信号差的地方,APP就会一直转圈圈,甚至闪退。病人等着做手术,你却在和机器怄气。

最朴素的想法
能不能不管有没有网,我先记下来?等有网的时候,手机自己悄悄传上去,别让我操心。

这就是算法的原点本地优先(Offline-First) ——网络只是用来同步的工具,不是工作的前提。


第二层:小学生版 —— 用“草稿本”和“作业本”理解

我们把整个过程简化成小学生写作业的场景。

传统模式(在线模式)

  • 老师(服务器)说:“写作业必须在我眼皮底下写”
  • 你(客户端)只能对着老师写,老师一转身(断网),你就写不了
  • 这就是“在线API”的困境

本地优先模式

第一步:准备草稿本(本地数据库)
你随身带一个草稿本(手机里的SQLite数据库)。不管老师在不在,你先在草稿本上写。

第二步:给作业打标签
你在每道题旁边画个小标记:

  • 已写完(已保存到本地)
  • 老师还没看(待同步)
  • 这是修改过的(操作类型)

第三步:抄作业机制(同步逻辑)
网络好了,你开始往老师的正式作业本上抄:

  • 先抄新写的(增量同步)
  • 抄到一半断网了,记住抄到哪了(断点续传)
  • 下次联网接着抄

第四步:两人同时改作业怎么办(冲突解决)
如果两个同学同时改了同一道题:

  • 简单处理:谁最后改的听谁的(时间戳优先)
  • 高级处理:A改了第一问,B改了第二问,合并起来(字段级合并)

核心口诀先写草稿,有空再抄,抄不完的记位置,打架了看情况合并。


第三层:初中生版 —— 数据结构的雏形

现在我们要把草稿本设计得更科学一些。

3.1 普通笔记本的局限

如果只是简单存数据,会碰到几个问题:

  1. 我怎么知道哪些数据已经同步过了?
  2. 数据被改了好几次,只记最后的结果够吗?
  3. 每次同步要把整个本子都给老师看吗?太费劲了。

3.2 给数据加“贴纸”

我们在数据库的每一行数据后面,贴上几个隐藏标签:

字段名 含义 取值
sync_status 同步状态 0-未同步,1-同步中,2-已同步
op_type 操作类型 INSERT/UPDATE/DELETE
version 版本号 时间戳或自增数字

这样设计的好处

  • 一眼就能看出哪些数据还没上传
  • 知道这条数据是新增的、修改的还是删除的
  • 版本号可以用来比对谁更新

3.3 增量同步的雏形

不用每次都把所有数据传给服务器。客户端记住自己最后一次同步的版本号(last_sync_version),下次只问服务器:

“上次同步到版本100了,你这有版本101之后的新数据吗?”

这就是增量步进机制的雏形。


第四层:高中生版 —— 引入“流水账”思维

到了高中,我们要解决一个更复杂的问题:操作日志(Op-Log)

4.1 只记结果的问题

假设你修改了一条数据3次:

  1. 体温36.5 → 37.0
  2. 体温37.0 → 37.5
  3. 体温37.5 → 36.8

如果只存最后的结果(36.8),服务器永远不知道中间发生了什么。这在某些场景下是不行的(比如医疗审计需要完整轨迹)。

4.2 引入“流水账”

我们不再只关心数据长什么样,而是关心数据是怎么变的。

新建一个操作日志表,记录:

时间 操作人 对象 字段 旧值 新值
10:01 护士A 患者X 体温 36.5 37.0
10:05 护士A 患者X 体温 37.0 37.5
10:10 护士B 患者X 血压 120 130

这个设计的神奇之处

  • 网络断了也不怕,流水账存在本地
  • 恢复联网后,按顺序重放(Replay)这些操作
  • 即使服务器数据乱了,也能通过重放恢复到正确状态
  • 可以追溯每一个操作的源头

4.3 触发器自动记账

手动记录太麻烦。我们让数据库自己记:

-- 创建触发器:当体温表被修改时,自动往日志表插一条记录
CREATE TRIGGER log_temperature_changes
AFTER UPDATE ON patient_vitals
FOR EACH ROW
BEGIN
    INSERT INTO sync_log (record_id, field_name, old_value, new_value, op_time)
    VALUES (NEW.id, 'temperature', OLD.temperature, NEW.temperature, NOW());
END;

这就是数据操作溯源的核心思想。


第五层:大学本科版 —— 完整同步协议设计

现在我们要设计一套完整的同步协议,包含握手、传输、确认、重试、冲突解决。

5.1 网络状态检测

APP需要知道网络什么时候好、什么时候坏。

基础版:监听浏览器的online/offline事件

window.addEventListener('online', () => {
    console.log('网络恢复了,开始同步');
    startSync();
});

进阶版:自适应心跳检测

  • 正常时:每30秒发一次心跳(省电)
  • 弱网时:每5秒发一次心跳(快速感知恢复)
  • 断网时:停止心跳(省流量)

5.2 同步的四个阶段

当检测到网络恢复,启动以下流程:

第一阶段:数据预校验

客户端先发个“打招呼”包,告诉服务器:

  • 我有多少条待同步数据
  • 这些数据的MD5摘要

服务器快速比对,如果有冲突,提前告诉客户端:“你有一条数据和服务器版本不一致,准备打架。”

第二阶段:双向增量同步

向上推(Push)

  • 把本地sync_status=0的数据打包
  • 每20条一个包(分片上传),避免一次性数据太大
  • 每个包带一个唯一ID(client_request_id

幂等设计:如果网络波动导致同一个包发了两次,服务器看到重复的ID,直接返回“已收到”,不重复入库。这保证了数据不重复

向下拉(Pull)

  • 客户端告诉服务器自己最新的版本号
  • 服务器返回更新的数据

第三阶段:事务确认(ACK机制)

原子提交:只有当收到服务器的成功确认(ACK)后,客户端才把本地sync_status从0改成2。

重试策略:如果失败,不能疯狂重试。采用指数避退

  • 第1次失败:等1秒重试
  • 第2次失败:等2秒
  • 第3次:等4秒
  • 第4次:等8秒
  • 最大不超过1分钟

这防止了网络刚恢复又断开时的“雪崩效应”。

第四阶段:冲突裁决

这是最复杂的部分。两个护士同时改同一个病人怎么办?

策略一:时间戳优先(Last Write Wins)

  • 谁最后改的听谁的
  • 适用于体征数据这种“只取最新值”的场景

策略二:字段级合并

  • A护士改了体温,B护士改了血压
  • 服务器把两个修改合并成一条新数据
  • 适用于病历文书这种多字段独立的场景

策略三:版本向量(Vector Clock)

  • 分布式系统的高级解法
  • 记录每个节点的修改历史
  • 复杂但精确

第六层:硕士阶段 —— 极端场景下的专项优化

现在我们要把系统做到99.9%的可用性,必须处理各种极端情况。

6.1 弱网下的分片传输

如果同步的数据里有照片(比如手术签字单),文件可能好几兆。

问题:一次性传一个大文件,传一半断网了,下次要从头传。

解法:二进制分片 + 断点续传

// 把文件切成1MB的片
const CHUNK_SIZE = 1024 * 1024; // 1MB

function uploadFile(file, fileId) {
    const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
    
    for (let i = 0; i < totalChunks; i++) {
        const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
        uploadChunk(chunk, fileId, i);
    }
}

// 上传每个片
function uploadChunk(chunk, fileId, index) {
    // 检查这个片是否已经上传过(断点续传)
    if (isChunkUploaded(fileId, index)) {
        return; // 已上传,跳过
    }
    
    // 上传逻辑...
}

效果:医生走出手术室WiFi覆盖区,回到办公室后能从上次断开的字节位继续传,不用重头传。

6.2 乐观UI解决卡顿问题

痛点:护士点保存,如果网络不好,界面转圈圈,护士以为卡了,会再点一次,导致重复提交。

解法:乐观UI

function saveVitalSign(data) {
    // 1. 立即显示"已保存"(乐观更新)
    showSuccessMessage('已保存(本地)');
    
    // 2. 角落里显示黄色小图标"同步中"
    showSyncStatus('syncing', 'yellow');
    
    // 3. 真正去同步
    syncToServer(data).then(() => {
        // 4. 同步成功,黄变绿
        showSyncStatus('synced', 'green');
    }).catch(() => {
        // 5. 同步失败,黄变红
        showSyncStatus('failed', 'red');
    });
}

用户体验:护士不用盯着进度条发呆,可以继续做下一件事。真正实现了无感覆盖

6.3 写前日志(WAL)解决并发卡顿

问题:后台正在同步大量数据(写数据库),前台护士想查患者列表(读数据库),会不会卡?

解法:SQLite的WAL模式

默认情况下,SQLite是读写互斥的:写的时候不能读,读的时候不能写。

开启WAL(Write-Ahead Logging)模式后:

  • 写操作:写在日志文件里
  • 读操作:读原数据库文件
  • 两者可以同时进行
PRAGMA journal_mode=WAL; -- 开启WAL模式

效果:同步任务在后台疯狂写数据,前台查询患者列表依然丝滑流畅。

6.4 智能带宽管控

如果同时有很多数据要同步,不能一股脑全发出去,会把正常业务带宽占满。

策略

  • 核心数据(如危急值):高优先级,立即发
  • 普通数据(如常规体征):中优先级,排队发
  • 非关键数据(如操作日志):低优先级,空闲时发

实现:维护三个优先级的队列

class SyncQueue {
    constructor() {
        this.highPriority = []; // 立即发
        this.mediumPriority = []; // 普通
        this.lowPriority = []; // 空闲时发
    }
    
    add(data, priority) {
        this[priority + 'Priority'].push(data);
        this.scheduleSync();
    }
    
    scheduleSync() {
        // 先发高优先级
        if (this.highPriority.length > 0) {
            this.sendBatch(this.highPriority);
        } 
        // 如果网络空闲,发中优先级
        else if (this.isNetworkIdle()) {
            this.sendBatch(this.mediumPriority);
        }
        // 极空闲时发低优先级
        // ...
    }
}

第七层:博士阶段 —— 理论的升华与范式总结

站在更高的维度,我们可以总结出这套算法的数学本质哲学意义

7.1 从CAP定理看本地优先

分布式系统有个著名的CAP定理:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),三者只能取其二。

传统在线API选择了:

  • 放弃分区容错性(P):网络断了你就用不了
  • 保持一致性(C)和可用性(A)

本地优先架构的选择

  • 接受分区是常态(P)
  • 保证可用性(A):断网也能用
  • 通过异步同步实现最终一致性(Eventually Consistent)

哲学转变:从“强一致性”到“最终一致性”,从“网络必须可靠”到“网络不可靠是默认前提”。

7.2 数据结构的数学本质

这套算法的核心数据结构可以抽象为:

本地影子库 = 业务数据 + 元数据(状态+版本+操作类型)

操作日志 = 时间序列上的状态转移函数

同步协议 = 分布式状态机中的状态复制

用数学语言描述:

  • 每个客户端是一个独立的状态机
  • 操作日志是状态转移的输入序列
  • 同步过程是两个状态机之间的状态对齐
  • 冲突解决是状态合并函数

7.3 CRDT的引入(最前沿的方向)

CRDT(Conflict-free Replicated Data Types,无冲突复制数据类型)是一种更高级的解决方案。

传统冲突解决:先发生冲突,再解决(打架了再拉架)

CRDT的思路:设计数据结构,使其天生不会打架

比如一个计数器:

  • A护士加1
  • B护士加2
  • 无论以什么顺序同步,最终结果都是3

这就是数学上可证明的最终一致性

CRDT在医疗场景的应用

  • 计数器类数据(如输液滴数):天然适用
  • 集合类数据(如用药清单):可以设计成“添加永不冲突”的结构
  • 文本类数据(如病历):可以使用类似于Git的合并算法

7.4 算法复杂度分析

空间复杂度

  • 本地影子库:O(n),n是业务数据量
  • 操作日志:O(m),m是操作次数,可能远大于n

时间复杂度

  • 增量同步:O(k),k是变更的数据量,不是全量
  • 冲突检测:O(1) 通过版本号
  • 字段级合并:O(f),f是字段数量

网络开销

  • 相比全量同步,减少90%以上的流量
  • 相比在线API,增加约20%的握手开销

7.5 理论的落地:一个完整的数学定义

我们可以给出这个同步算法的形式化定义:

设客户端状态为 C,服务器状态为 S,同步协议 P 是一个四元组:

P = (D, L, V, M)

其中:

  • D 是本地影子库,D = {(key, value, status, version)}
  • L 是操作日志,L = [(op, timestamp, vector_clock)]
  • V 是版本向量,V = [v1, v2, ..., vn]
  • M 是合并函数,M: (C_state, S_state) → new_state

同步的目标是:经过有限次同步后,C 和 S 达到最终一致,即:
lim_{t→∞} distance(C_t, S_t) = 0


第八层:简历/面试话术 —— 如何包装成亮点

现在你已经完全理解了这套算法,关键是怎么在面试中说出来。

8.1 初级话术(说得清)

“我在做医院移动护理项目时,解决了手术室WiFi信号差的问题。我采用了本地优先的设计,数据先存SQLite,网络好了再同步。通过给数据加同步状态字段,实现了增量同步。还用了操作日志记录变更历史,保证数据不丢。”

8.2 中级话术(有深度)

“针对手术室铅门屏蔽导致的频繁断网场景,我设计了一套本地优先的增量同步架构。核心是本地影子库+操作日志+增量步进的三位一体模型。

我在业务表中扩展了sync_status、version等元数据,用于状态追踪。同时通过数据库触发器记录操作日志,确保操作可追溯。同步时采用版本比对,只传增量数据,减少90%的流量。

为了解决并发冲突,我实现了字段级合并策略,两个护士同时修改不同字段时能自动合并。针对大文件传输,我做了二进制分片和断点续传,保证照片等数据能可靠上传。”

8.3 高级话术(有体系,有数据)

“在处理手术室移动端业务时,针对铅门屏蔽导致的频繁掉线难题,我放弃了传统的在线API模式,实现了一套本地优先的增量同步架构

架构设计
我基于SQLite构建了本地影子库,在业务表基础上扩展了sync_status、version等元数据,实现数据状态的本地持久化。同时引入操作日志表,通过数据库触发器自动记录每一次字段级变更,形成可追溯的变更流水线。

同步协议
设计了四阶段同步流程:预校验(MD5摘要比对)→双向增量(分片上传+幂等处理)→事务确认(原子提交+指数避退)→冲突裁决(时间戳优先+字段级合并)。

专项优化

  • 针对弱网环境,实现二进制分片传输和断点续传,大文件传输成功率从72%提升到99.5%
  • 采用自适应心跳检测,网络恢复后500ms内启动同步
  • 引入乐观UI,护士点击保存后即时反馈,后台静默同步,用户无感知
  • 开启SQLite WAL模式,实现读写并发,同步时不阻塞前台查询

成果
这套架构把数据同步的失败率从原始的15%降低到了0.1%以下。最关键的是实现了业务上的无感覆盖:医生在盲区录入的数据,走出病区的瞬间就能在几百毫秒内完成静默同步。医生根本不知道网络断过,业务照常进行。

理论升华
这套方案的实质是从CAP理论中选择了AP(可用性+分区容忍性),通过最终一致性保证数据准确。从数学上看,它是分布式状态机之间的状态复制协议,操作日志是状态转移函数的输入序列。”

8.4 应对追问:你可能被问到的点

Q1:如果本地数据量很大,同步会不会很慢?

A:我们做了三级优化。第一,增量同步,只传变更数据。第二,分片并发,20条一批同时上传。第三,优先级调度,核心数据优先传。实测1万条数据能在30秒内完成同步。

Q2:怎么保证数据不丢?

A:四重保障。第一,本地持久化,写入成功才返回用户。第二,事务确认,收到服务端ACK才标记已同步。第三,重试机制,失败后指数避退重试。第四,操作日志溯源,即使极端情况也能通过日志恢复。

Q3:多个端同时改同一份数据怎么办?

A:我们实现了字段级合并。通过版本向量记录每个字段的最后修改时间和节点,同步时对比向量,不同字段自动合并,同一字段以时间戳为准。这比简单的“最后写入胜出”更精细。

Q4:你们的方案和现有的框架(如CouchDB、PouchDB)有什么区别?

A:现有框架解决的是通用同步问题,但我们针对医疗场景做了深度定制。比如字段级合并策略符合医疗文书的多作者协作场景,优先级调度保证危急值优先上传,分片传输针对医疗影像优化。我们是业务驱动的技术选型和定制。


第九层:上帝视角 —— 与其他技术的对比

9.1 与CouchDB/PouchDB对比

CouchDB是成熟的Offline-First数据库,自带同步协议。

我们的方案 vs CouchDB

  • 相同点:都采用MVCC(多版本并发控制)、增量同步、冲突检测
  • 不同点:我们更轻量,直接基于SQLite,不需要部署CouchDB服务端
  • 优势:医疗系统常有现有关系数据库,我们的方案更容易集成

9.2 与GraphQL订阅对比

GraphQL订阅通过WebSocket实现实时推送。

适用场景不同:

  • GraphQL订阅:适合在线实时协作(如在线文档)
  • 我们的方案:适合网络不稳定、需要离线工作的场景(如移动护理)

9.3 与WebSocket/长连接对比

WebSocket假设网络持续可用。

我们的方案假设网络不可靠是常态。

哲学差异:WebSocket是在线优先,我们是离线优先

9.4 与Git版本控制类比

有趣的是,我们的方案和Git惊人地相似:

Git 我们的方案
本地仓库 本地影子库
commit 操作日志
push/pull 双向同步
merge 冲突解决
branch 多客户端分支
rebase 版本对齐

这个类比可以帮助面试官快速理解。


第十层:总结与核心记忆点

如果面试紧张,只要记住这4个关键词,就能串联起整个知识体系:

核心四词记忆法

1. 本地优先(Offline-First)

  • 哲学:网络是同步工具,不是工作前提
  • 实现:数据先写本地SQLite

2. 操作日志(Op-Log)

  • 哲学:记流水账比记结果更有价值
  • 实现:触发器自动记录变更历史

3. 增量同步(Incremental Sync)

  • 哲学:只传变化的部分
  • 实现:版本号+MD5摘要+分片传输

4. 最终一致性(Eventual Consistency)

  • 哲学:允许暂时不一致,但最终会一致
  • 实现:冲突解决+字段级合并

🎯 一句话概括

这是一套把“网络不可靠”作为默认前提,通过“本地存储+操作日志+增量同步+冲突解决”实现业务无感覆盖的分布式数据同步方案。

🔥 终极必杀技

如果面试官问:“你觉得自己最牛的技术方案是什么?”

你可以这样回答(配合自信的眼神):

“我最引以为豪的是一个解决手术室断网同步的方案。在那个场景里,网络不是偶尔断,是物理层面被铅门屏蔽。我设计了一套本地优先的增量同步架构,把数据同步的失败率从15%降到0.1%以下。

最让我得意的是,这个方案不仅仅是写代码,而是从哲学层面重新思考了网络和业务的关系——我们不再依赖网络,而是让网络服务于业务。医生在盲区录入的数据,走出手术室的瞬间就完成静默同步,他完全感知不到网络的存在。

我觉得,最好的技术就是让用户感受不到技术的存在。这套方案做到了。”

CSS属性 - 边距属性

作者 GinoWi
2026年2月24日 17:11

CSS属性 - 边距属性

内边距

边框和内容之间的距离就是内边距。

  • 格式:
/* 非连写 */
padding-top: ${padding-top};
padding-right: ${padding-right};
padding-bottom: ${padding-bottom};
padding-left: ${padding-left};

/* 连写 */
padding: ${padding-top} ${padding-right} ${padding-bottom} ${padding-left};
  • 渲染样式:

    未设置padding属性的渲染效果:

    可以通过开发者工具中的Computed面板观察外面红色盒子的样式:

    nopadding-computed

    Computed面板当中可以看到这个时候的padding属性为0。当我们设置padding属性后观察一下变化:

    image-20260214102417792

    再通过Computed面板观察一下外面红色盒子当前的样式:

    image-20260214102741517

    这个时候可以看到盒子里面的padding属性为30px,此时元素整体的宽高也会变成560px。

    示例代码:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8">
            <title>边距属性练习</title>
            <style>
                .outBox {
                    background-color: red;
                    width: 500px;
                    height: 500px;
                    /*这里使用了连写的方式同时设置了,上右下左四个方向的内边距*/
                    padding: 30px;
                }
                .inBox {
                    background-color: green;
                    width: 200px;
                    height: 200px;
                }
            </style>
        </head>
        <body>
            <div class="outBox">
                <div class="inBox"></div>
            </div>
        </body>
    </html>
    

    补充内容:

    Computed 面板展示的是“计算后的值” ‌:是将影响元素尺寸的所有 CSS 属性(如 widthheightpaddingbordermargin)都拆解并显示为最终的、浏览器实际使用的像素值(或其它单位)。

    盒模型图是核心可视化工具‌:在 Computed 面板的底部,通常会有一个‌可交互的盒模型示意图‌。这个图从内到外清晰地展示了四个层级:

    • 内容区 (content) ‌:显示 widthheight 的最终计算值。如果这两个值在 CSS 中被设置为 auto,这里就会显示 auto × auto,表示其尺寸由内容或父容器决定。
    • 内边距 (padding) ‌:显示四个方向的内边距数值。
    • 边框 (border) ‌:显示四个方向的边框宽度。
    • 外边距 (margin) ‌:显示四个方向的外边距数值。

    元素宽高‌:可以通过将 width + padding-left + padding-right + border-left + border-right 来计算出元素的‌总宽度‌(即“布局宽度”或 offsetWidth)。同理,可以通过将height + padding-top + padding-bottom + border-top + border-bottom来计算出‌总高度‌。这个“整体大小”就是元素在页面上实际占据的空间。

  • 注意点:

    • 通过上述示例我们可以观察到,给标签设置了内边距,标签的宽度和高度会发生变化(增加内边距的距离)。

    • 连写格式这三个属性的取值省略规律:

      • ${padding-top} ${padding-right} ${padding-bottom} ${padding-left} ---> ${padding-top} ${padding-right} ${padding-bottom} :省略左内边距的设置,取值和右内边距一样。
      • ${padding-top} ${padding-right} ${padding-bottom} ${padding-left} ---> ${padding-top} ${padding-right} : 省略下内边距、左内边距的设置,下内边距取值和上内边距取值一样,左内边距取值和右内边距取值一样。
      • ${padding-top} ${padding-right} ${padding-bottom} ${padding-left} ---> ${padding-top}:省略右内边距、下内边距、左内边距,则被省略的内边距的取值和上内边距的取值一样。
    • 通过上述示例可以发现,内边距也会有背景颜色,同父元素颜色。

外边距

标签和标签之间的距离就是外边距。

  • 格式:
/* 非连写 */
margin-top: ${margin-top};
margin-right: ${margin-right};
margin-bottom: ${margin-bottom};
margin-left: ${margin-left};

/* 连写 */
margin: ${margin-top} ${margin-right} ${margin-bottom} ${margin-left};
  • 渲染样式:

    • 两元素父子关系:

      先设置一个父元素div和一个子元素div的初始样式:

      nomargin

      然后给子元素设置margin-top属性:

      margin-1

      可以看到父元素的margin属性仍为0

      margin-computed

      可以观察到子元素的margin属性为30px

      margin-computed

      可以观察到,如果设置了margin-top: 30px;属性,父元素会一同被顶下来。

      我们可以通过给父元素设置border属性,来先观察当下需要了解的margin属性:

      margin-2

      <!DOCTYPE html>
      <html>
          <head>
              <meta charset="utf-8">
              <title>边距属性练习</title>
              <style>
                  .outBox {
                      background-color: red;
                      width: 500px;
                      height: 500px;
                      border: 1px solid #000;
                  }
                  .inBox {
                      background-color: green;
                      width: 200px;
                      height: 200px;
                      margin: 30px;
                  }
              </style>
          </head>
          <body>
              <div class="outBox">
                  <div class="inBox"></div>
              </div>
          </body>
      </html>
      
    • 两元素为兄弟关系:

      这里就不再做对比,直接展示设置margin属性后的效果:

      margin-brother

      <!DOCTYPE html>
      <html>
          <head>
              <meta charset="utf-8">
              <title>边距属性练习</title>
              <style>
                  .box1 {
                      background-color: red;
                      width: 100px;
                      height: 100px;
                  }
                  .box2 {
                      background-color: green;
                      width: 100px;
                      height: 100px;
                      margin: 30px;
                  }
              </style>
          </head>
          <body>
              <div class="box1"></div>
              <div class="box2"></div>
          </body>
      </html>
      
  • 注意点:

    • 连写格式取值省略时的规律:

      • ${margin-top} ${margin-right} ${margin-bottom} ${margin-left} ---> ${margin-top} ${margin-right} ${margin-bottom}:省略对左边距的指定,左边距的取值和右边距一样。
      • ${margin-top} ${margin-right} ${margin-bottom} ${margin-left} ---> ${margin-top} ${margin-right}: 省略下边距、左边距的指定,下边距取值同上边句一样,左边距取值同右边距一样。
      • ${margin-top} ${margin-right} ${margin-bottom} ${margin-left} ---> ${margin-top}:省略右边距、下边距、左边距的指定,被省略的边距的取值同上边距一样。
  • 外边距的那一部分是没有背景颜色的。
  • 在默认布局的垂直方向上,默认情况下外边距是不会叠加的,会出现合并显现,原则上是谁设定的外边距比较大就听谁的。
  • 在默认布局的水平方向上,默认情况下外边距是会叠加的,不会进行合并。

外边距合并

外边距合并的现象简单来说就是当两个垂直方向的外边距相遇,会形成一个外边距,此外边距大小为两个边距中的最大值(如果两者相等则取其中一个)。

外边距合并的三种情况:

  • 两元素为相邻的兄弟关系:

    image-20260224150323247

  • 两元素为父子关系:

    这里引用MDN文档的解释:

    如果没有设定边框(border)、内边距(padding)、行级(inline)内容,也没有创建区块格式化上下文或间隙来分隔块级元素的上边距(margin-top)与其内一个或多个子代块级元素的上边距(margin-top);或者没有设定边框、内边距、行级内容、高度(height)或最小高度(min-height)来分隔块级元素的下边距(margin-bottom)与其内部的一个或多个后代后代块元素的下边距(margin-bottom),则会出现这些外边距的折叠,重叠部分最终会溢出到父代元素的外面。

    也就是说合并后的外边距大小是取父元素及其内首个/最后一个子代块级元素的上边距/下边距的较大值,但是最终的渲染结果都会是“溢出”到父元素面的外边距。

    image-20260224155323022

  • 空元素的外边距合并:

    如果块级元素没有设定边框、内边距、行级内容、高度来分隔块级元素的上边距(margin-top)及其下边距(margin-bottom),则会出现其上下外边距的折叠。

    image-20260224160810735

注意点:

  • 上述情况的组合会产生更复杂的(超过两个外边距的)外边距折叠。
  • 即使某一外边距为 0,这些规则仍然适用。因此就算父元素的外边距是 0,第一个或最后一个子元素的外边距仍然会(根据上述规则)“溢出”到父元素的外面。
  • 如果包含负边距,折叠后的外边距的值为最大的正边距与最小(绝对值最大)的负边距的和。
  • 如果所有的外边距都为负值,折叠后的外边距的值为最小(绝对值最大)的负边距的值。这一规则适用于相邻元素和嵌套元素。
  • 外边距折叠仅与垂直方向有关。

解决外边距折叠的方法:

  • 可以通过触发BFC来解决外边距合并的问题
  • 可以通过设置padding、border属性来解决

参考资料:

W3School官方文档:www.w3school.com.cn

MDN官方文档:developer.mozilla.org

Vibe coding(AI编程一网打尽)

作者 AKclown
2026年2月24日 16:59

前言

近一年来vibe coding席卷全球,现在很多公司都开始并且要求使用AI编程工具,我本人使用过类似Cursor这种IDE形式工具,也是使用codexClaude code这种ClI工具。接下来我就使用Open code这款开源的代码agent工具来从头过一遍rulescommandagent抓取agent请求分析MCP服务skills这些基础概念以及使用,也方便后续我自己能够快速查找对应的内容。主要先用起后续在深入agent开发。

opencode的使用

1. 安装

npm install -g opencode-ai

2. 创建next.js项目(用于演示)

pnpm create next-app@latest ai-coding --yes

3. 进入到ai-coding目录,执行opencode,出现如下界面表示成功

image.png

4. 配置第三方模型

opencode也提供一些免费的模型,例如Kimi K2.5GLM-4.7等等,可以通过命令/modles来查看,后面有FREE表示免费标识。 image.png 为了达到演示效果,接下来我将配置Googlegemini服务。当然你也可以选择其他模型提供商,例如OPEN AIGITHUB Copilot

  • 配置opencode-antigravity-auth插件,该插件用于在opencode启用OAuth 对 Antigravity(Google 的 IDE)进行身份验证,这样就可以通过谷歌凭证访问Antigravity的模型。例如 gemini-3-pro 和 claude-opus-4-5-thinking

    • 方式一: 让AI agent自动帮我们完成opencode-antigravity-auth安装 (让LLM处理)
    Install the opencode-antigravity-auth plugin and add the Antigravity model definitions to ~/.config/opencode/opencode.json by following: https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/dev/README.md
    
    • 方式二:
      编辑~/.config/opencode/opencode.json文件,追加如下内容
    {
      "plugin": ["opencode-antigravity-auth@latest"]
    }
    
    • 登录认证
    opencode auth login
    

image.png

在进行认证的时候,无法完成Goole谷歌大概率是VPN墙的问题。导致了如下两个链接无法访问

https://oauth2.googleapis.com/token
https://www.googleapis.com/oauth2/v1/userinfo

可以通过postman主动去获取这两个数据,并且修改它的插件代码既可以绕过请求。在之后遇到授权问题时,可以通过debugger这个插件来排查是哪一步存在问题
<主机目录>/.cache\opencode\node_modules\opencode-antigravity-auth

image.png

关于opencode中Oauth认证的流程如下图: image.png

尴尬的是这样子绕过了登录流程的,虽然能够获取到Gemini模型列表了,但是已经模型请求依旧显示网络异常,所以还是要解决CLI不能使用系统代理问题。

提示: 通过解决上面的授权,我们能够更加清晰的知道整个授权流程是怎么样的,以及可以确定插件内部无法发送oauth2.googleapis.com导致,而不是其他问题导致的

我的翻墙工具存在终端代理的命令,只需要将这个命令输入到你需要执行opencode的窗口,然后就可以正常使用模型了。如果你的Clash应该存在TUN模式打开即可

export https_proxy=http://127.0.0.1:50750 http_proxy=http://127.0.0.1:50750 all_proxy=socks5://127.0.0.1:50751

image.png

5. oh-my-opencode (多代理协作插件)

用 Claude 做编排,用 GPT 做推理,用 Kimi 提速度,用 Gemini 处理视觉。模型正在变得越来越便宜,越来越聪明。没有一个提供商能够垄断。我们正在为那个开放的市场而构建。Anthropic 的牢笼很漂亮。但我们不住那。

核心理念

很多人使用 AI 编程工具会经历从”惊艳”到”冷静”的过程:写小功能很快,但一到重构、迁移、补测试、清 ESLint 警告等大型任务就容易卡壳。Claude-code中这个问题也是存在的, 在Cladue上面有Ralph插件来做这个事儿, 但OpenCode则有更强的 Oh-My-OpenCode 插件:

  • 不是简单叠加 UI,而是将”单模型”升级为多代理协作系统
  • 通过主控代理 Sisyphus 负责任务拆分、委派、推进
  • 把不同类型的工作分派给不同角色的代理,模拟真实开发团队的协作方式
代理角色
角色 职责
Sisyphus(主控) Tech Lead + 项目经理组合,负责拆解 TODO、分配任务、推动进度
Oracle 架构设计、深度调试、复杂问题分析
Librarian 文档检索、API 查阅、资料收集
Explore 代码库探索、依赖分析、边界定位
Frontend Engineer UI/UX 设计、前端组件开发
工作模式

ultrawork(或简写 ulw)是核心的工作模式开关:

小任务:正常使用即可
大任务(跨文件/跨模块/需要查资料/需要持续推进):开启 ultrawork 更稳

当开启 ultrawork 时,系统会:

  • 并行探索代码库
  • 启动后台任务
  • 强力推进直到完成

到目前我们完成opencode的安装以及关联第三方提供商,接下来就来了解一些关于AI agent的一些常见概念以及使用

常用的command

接下来学习一下opencode中常用且好用快捷操作命令有哪些? 帮你提效AI工具

快捷键 功能 说明
Ctrl+X -> N 新建会话 ctrl+x同时按,然后松开再按N
Ctrl+X -> L 会话列表 ctrl+x同时按,然后松开再按N
shift+Enter 换行(不发送) 用于多行提示词
⬆/⬇ 切换历史记录 用于多行提示词
ESC 中断这次会话 中断这次会话
Ctrl+X -> M 打开模型列表 切换模型列表
Ctrl+X -> U 撤销(undo) 回退到这次对话初始状态(觉得这次对话不好)
Ctrl+X -> R 重做(redo) 重回到这一次undo之前
F2 切换最近模型 快捷切换最近使用的模型
Ctrl+X -> y 复制AI消息 复制AI的完整回复
Ctrl+X -> B 切换侧边栏 查看会话饼图
PageUp/Down 滚动对话 快速浏览长对话

关于AGENTS.md文件

AGENTS.md是一个markdown文件,我们可以将其提交到Git仓库中,用于自定义AI编码代理在仓库中的行为。它位于对话记录的顶部,紧邻系统提示符下方。(该文件不是必须的)

1. 为什么需要写AGENTS.md?

AGENT非常擅长局部修改,但缺乏上下文时容易“跑偏”。AGENTS.md 就像一张地图:告诉它命令在哪里,用什么测试、PR 怎么写、哪些操作禁止。顺带一提,这也能帮助新同事快速融入团队。

2. AGENTS.md如何查找呢?(Monorepos

关于AGENTS.md文件查找跟node_modules的查找方式类似,以就近文件优先进行查找

repo/  
├─ AGENTS.md            # 全仓默认规则  
├─ apps/  
│  └─ web/  
│     ├─ AGENTS.md      # 仅对 /apps/web/** 生效  
│     └─ src/components/Button.tsx  
└─ packages/  
   └─ api/  
      ├─ AGENTS.md      # 仅对 /packages/api/** 生效  
      └─ src/routes/users.ts

3. 可选的AGENTS.override.md

有时可能需要一份临时、优先级更高的规则,例如发布封板、事故应急、合规冲刺等
• 该文件名只是约定,不是标准;只有部分工具/CLI 会自动识别。
• 如果工具不支持,可通过命令行额外指定策略文件实现同样效果。
• 建议“少量、明确、短期”:写清目的、约束内容、结束条件,窗口结束后立即删除。

repo/  
├─ AGENTS.md  
├─ AGENTS.override.md        # 全仓临时规则(若被支持)  
└─ packages/  
   └─ api/  
      ├─ AGENTS.md  
      └─ AGENTS.override.md  # 仅 API 范围的事故模式(若被支持)

4. 针对过大的agents.md文件可能来自于如下原因:

agents.md文件会在每次对话开始时自动加载,让Agent能立即了解的我们的项目。因此表明agent.md文件不宜过大才不会占用过多的上下文和消耗过多的token

It's automatically loaded at the start of every conversation, giving the agent immediate understanding of your project.
  • 不同的开发者添加了相互矛盾的建议,没有人能够完成完整风格,难以维护的混乱实际上损害了Agents的性能。
  • 另一个罪魁祸首:自动生成的 AGENTS.md 文件。 切勿使用初始化脚本自动生成 AGENTS.md。 他们在文件中充斥着“对大多数情况有用”但最好逐步披露的内容。 生成的文件优先考虑全面性而不是限制性

对于agents.md会在每一次会话中都会加载,无论这个agents.md内容是否相关,这会导致token浪费问题

设想 影响
轻量、专注的agents.md 有更多的token用于特定的任务
体积庞大、臃肿的agents.md 用于实际工作的tokens变少、agent会感到困惑
不相干的说明 tokens的浪费 + agent分心 = 浪费性能

意味编写的agents.md文件应该尽可能的小。 同时也需要注意要保持agent.md文件的更新,不要使用过时的agent.md文件

5. AGENTS.md文件内容

一般情况agents.md包含下面几大类既可以了,而且尽可能极简

  1. 整个项目的架构和模式(技术栈、项目结构)
  2. 遵循我们的编码风格和规范(代码规范)
  3. 声明正确的命令进行构建和测试(包管理工具、构建命令)
  4. 避免已知的陷阱和反模式(已知问题)
  5. 做出符合你的偏好的决策

推荐agents结构

# AGENTS.md

## Project overview
Brief description of what this project does and its primary purpose.

## Tech stack
- Framework: Next.js 14 with App Router
- Language: TypeScript (strict mode)
- Database: PostgreSQL with Prisma ORM
- Styling: Tailwind CSS

## Architecture
Explain the codebase structure and key patterns.

## Commands
```bash
bun install        # Install dependencies
bun run dev        # Start development server
bun run test       # Run tests
bun run build      # Production build
```

## Code style
- Prefer functional components
- Use early returns
- Name files in kebab-case
- Write tests for business logic

## Patterns to follow
Document established patterns in the codebase.

## Things to avoid
List antipatterns and common mistakes.

6. 使用渐进式披露

不要将所有的内容都塞进AGENTS.md中,而是使用渐进式披露。仅向Agent提供其当前所需要的内容,并在需要时将其指向其他资源。

模型启动时会加载所有 Skills 的基本信息(名称和用途),但不会读取具体内容。只有当模型真正需要某个 Skill 来指导工作时,才会去读取那份文档的详细内容。这种按需加载的方式,就是渐进式披露

特定语言的规则移至单独的文件

如下文件属于属于大包大揽行为在开启会话的时候会将所有agent.md内容都加载到会话中 image.png 针对上一个文件进行渐进式披露优化,agents.md只包含大纲内容,不会再初次开启会话时就将agent.md所有的内容都加载而是使用才加载对应的规则。例如当编写Typescript的时候才会去加载TypeScript Conventions,可以理解为按需加载 image.png

7. 如何重构AGENTS.md文件

如果我们对已有的agents.md感觉到困惑难以维护,使用如下提示词以渐进式的公式重构我们的提示词。

I want you to refactor my AGENTS.md file to follow progressive disclosure principles.

Follow these steps:

1. **Find contradictions**: Identify any instructions that conflict with each other. For each contradiction, ask me which version I want to keep.

2. **Identify the essentials**: Extract only what belongs in the root AGENTS.md:
   - One-sentence project description
   - Package manager (if not npm)
   - Non-standard build/typecheck commands
   - Anything truly relevant to every single task

3. **Group the rest**: Organize remaining instructions into logical categories (e.g., TypeScript conventions, testing patterns, API design, Git workflow). For each group, create a separate markdown file.

4. **Create the file structure**: Output:
   - A minimal root AGENTS.md with markdown links to the separate files
   - Each separate file with its relevant instructions
   - A suggested docs/ folder structure

5. **Flag for deletion**: Identify any instructions that are:
   - Redundant (the agent already knows this)
   - Too vague to be actionable
   - Overly obvious (like "write clean code")

image.png

rules (角色)

rules是给agents一个行为准则,当我们发现需要反复告诉AI同样的事情时就需要Rules了。例如问答时使用英/中回答、回复时需要精简回答、遇到不确定的事情需要告知用户、系统环境、用户偏好设置及其他无法通过代码编译器或检查器管理发现的实现细节

  • rules的特点

    1. 一次设置,长期生效
    2. 定义AI的人设行为模式
    3. 适合长期稳定的需求
  • rules的缺点

    1. 只能定义做什么不做什么无法定义怎么做
    2. 无法包含复杂的操作步骤
    3. 仍然比较抽象

如何写好rules可以根据cursor中的最佳实践来实现

示例

下面来看看加了rules和没有加rules的区别,如下是我的提示词,有规则和没有规则只有生成的文件名称导出变量不同

在当前目录下新建一个hooks-by-rules.ts,创建一个名称为useListByRules的hooks方法。请求url为请求地址参数,params为请求参数,返回对应的loading、pageSize、total等参数以及refresh、getList、changePage等方法
  • hooks.ts文件是还没有创建rules规则时创建的hooks,会有堆的loading、error的useState
  • hooks-by-rule.ts文件是具备rules规则时创建的hooks,采用了@tanstack/react-query的useQuery来实现 image.png 如下是我的hook.mdc规则编写,声明了需要使用useQuery方式来实现。 针对rules你可以自己写一部分然后让agent帮你完善或者改进
    • globs声明作用域文件
    • description标识当前rule的限定范围
    • alwaysApply 是否应用于每次会话 image.png

目前我在opencode.json中配置了,agent并没有自动读取我的rules。(暂时不知道为什么)

{
  "$schema": "https://opencode.ai/config.json",
  "instructions": [
    ".opencode/rules/*.md"
  ]
}

为了让opencode自动读取我的rules,我在agent.md文件追加了如下内容 image.png 是不是觉得rulesagent.md文件的能力差不多?
是的,你没有理解错。agent.md只是rules的更简单的一种方式,它是纯markdown文件,没有结构化配置元数据头部(就是头部不需要声明globs、description、alwaysApply定义)。它适用于项目规则比较简单的场景,Agent.md更加轻量、更易读的指导说明替代方案

MCP(模型上下文协议)

MCP是由Claude背后的公司Anthropic开放的标准。虽然听起来很有技术性,但是核心思想很简单。为AI Agent代理提供统一的方式来连接工具、服务和数据。

使用MCP即插即用。agent可以将结构化请求发送到任何MCP兼容工具,实时获取结果,甚至将多个工具链接在一起,无需提前了解具体细节

MCP Server的调用方法,当用户在AI Agent提问的时候,会将MCP作为system提示词发送给LLM,其中提示词包含了这个MCP的能力描述,以及对应的API能力(有点类似一个插件的API文档),然后LLM模型就知道了该MCP具备什么能力,从而能够判断是否需要调用MCP Server来实现你的需求。 后续在抓取请求会在分析具体传递的MCP数据会更加直观。 MCP.png

接下来实战一下MCP Server的使用,你也可以通过awesome-mcp-serverssmithery.ai来查找你需要的MCP服务

Chrome Devtools Mcp

在使用Agent工具调试前端项目的时候,是不是经常遇到这样一个痛点。Agent没办法获取到浏览器控制台的信息,导致你在调试的时候都需要手动的告知或复制对应信息Agent工具,它才能帮你修复bug问题。下面通过集成 chrome-devtools-mcp来解决这一痛点

Chrome Devtools Mcp的时候,让我们的code agent能够实时的检测和控制谷歌浏览器。它充当模型上下文协议(MCP)服务器,使我们的code agent能够访问Chrome Devtools的全部功能,实现可靠的自动化深度调试性能分析

在没有集成Chrome Devtools Mcp的时候,openCode编程工具并不能直接访问读取我们的谷歌浏览器的控制面板 image.png

接下来我们来实现一下在opencode集成Chrome Devtools Mcp,当我根据文档在~/.config/opencode/opencode.json中配置如下配置,然后通过/mcps查找发现Chrome Devtools Mcp属于disabled状态。但是并没有任何报错,因此我们可以通过向Agent提问来解决这个问题。

 "mcp": {
    "chrome-devtools": {
      "type": "local",
      "command": ["npx", "-y", "chrome-devtools-mcp@latest"],
      "enabled": true
    }
  }

通过在Agent中输入npx -y chrome-devtools-mcp@latest加上我在前面问为什么配置的MCP为disabled状态。那么模型会自动思考这个问题,发现我本地的Node版本低了一点。(在使用coding agent工具的时候,定位问题方式跟传统定位问题方式也有所改变,我们应该先让Agent帮我分析一下可能的问题,让Agent自己尝试解决问题image.png 可以通过Space空格键切换mcp的disabled或者enable状态

打开github找到chrome-devtools-mcp项目,给他点个star

在命令行输入如上prompt来看看是否Chrome Devtools Mcp是否配置成功,因为默认启动新的Chorm实例的沙盒环境因此没有登录信息,opencode会提示你登录用户信息,当你登录完成在叫opencode帮你继续执行即可。 image.png

默认chrome-devtools-mcp都会启动新 Chrome 实例的沙盒环境中运行 MCP 服务器。在沙盒环境中用户登录信息无法共享,那么如何将mcp链接到正在运行的Chrome实例呢?

  1. 在mcp配置文件中添加--browser-url选项,这个选项值是正常运行Chrom实例的URL地址。默认为http://127.0.0.1:9222地址
 "mcp": {
    "chrome-devtools": {
      "type": "local",
      "command": ["npx", "-y", "chrome-devtools-mcp@latest","--browser-url=http://127.0.0.1:9222"],
      "enabled": true
    }
  }
  1. 启动Chorme浏览器
    针对该文章更改了远程调试开关以提高安全性,从 Chrome 136 开始,我们将更改 --remote-debugging-port 和 --remote-debugging-pipe 的行为如果尝试调试默认的 Chrome 数据目录,系统将不再遵循这些开关。,这些开关必须与 --user-data-dir 开关搭配使用,才能指向非标准目录。非标准数据目录使用不同的加密密钥,这意味着 Chrome 的数据现在可以免受攻击者的侵害。
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-profile-stable"
  1. 测试一下
    输入如下的prompt提示词,就会在我使用上面命令启动的Chrome实例上运行内容,而不是又独自打开一个新的Chrome实例运行。只需要在第二步打开的Chrome实例登录保存用户即可
打开github找到chrome-devtools-mcp项目,给他点个star

image.png

让opencode结合chrome的面板帮我分析一下我的next.js代码哪里有错,如下是我的源代码 image.png 给opencode提供prompt提示词如下

http://localhost:3000/请结合chrome的面板,分析一下为什么我点击"测试一下"按钮,会报错,在控制台并没有打印出"我是测试按钮"

opencode会找到对应的错误,并且给出修复建议 image.png

我们给opencode配置了Chrome Devtools Mcp后,不再需要我们手动复制Chrome控制台信息给opencode了,我们也可以通过Chrome Devtools Mcp来实现网站性能/安全分析并且给出对应的修复建议。我们也了解了MCP的配置和用法,针对其他MCP用法也是大同小异就不一一演示了,感兴趣的小伙伴自行查阅对应MCP官网进行配置就可以了

注意事项

当我们使用MCP服务器时,会将该MCP的描述、API能力以系统提示词添加到我们的上下文中。如果我们有很多MCP服务,那么会把所有MCP服务都添加到我们的上下文中。因此我们应该只安装或者开启我们需要的MCP服务从而节省上下文,防止超出上下文限制。在opencode中可以/mcps选择对应的mcp服务器,再通过Space空格键来切换状态

skills

什么是的skills

Agent skills是一种轻量的开放格式,用于通过专业知识和工作流程扩展AI agent的功能。

skill是包含SKILL.md文件的文件夹。该文件包含元数据(name和description)以及告诉Agent如何执行特定任务的说明。SKILLS也可以包含scriptreferencesassets

my-skill/
├── SKILL.md          # Required: instructions + metadata
├── scripts/          # Optional: executable code
├── references/       # Optional: documentation
└── assets/           # Optional: templates, resources

为什么需要使用skills

skills是可重用的,基于文件系统资源为AI agent提供特定领域的专业知识。将通用AI agent变为专业的Agent在工作流程、上下文和最佳实践。与prompt不同(一次性任务对话说明),skills按需加载,无需在多个对话中重复提供相同的指导。

将通用Agent看作为一家饭店,skills在其中就充当菜谱的角色。当你的饭店拥有了菜谱那么这家饭店就具备了那些特色菜,也将菜系作为一个工作流的方式存储下。这样每次顾客点西红柿炒蛋就无需重新学一遍,做出来的口味也会更加统一

主要优点

  • 更专业的Agent(垂直agent): 根据特定领域任务定制功能
  • 减少重复: 一次创建,自动使用
  • 组合功能: 组合skills去构建复杂的工作流程

使用skills

skills在代码执行环境中运行,AI Agent拥有文件系统访问权限、bash命令和代码执行能力。 image.png

接下来演示一下find skillscreate skills,因为这两个skill比较常用和通用。我们可以在skills市场搜索开源skills使用,也可以自定义我们自己的skills。

find skills

这个skills可以帮助我们从开放Agent skills生态系统发现并且安装skill

skills.sh找到其使用方法和skills功能说明 image.png 在终端执行如下命令进行find-skills进行安装

npx skills add https://github.com/vercel-labs/skills --skill find-skills

image.png 重启opencode输入/skills即可查看已经安装skills image.png 接下来来看一下find-skillls的SKILL.md文件。一个skill文件一般包含了使用场景(description)触发条件具体的workflow可能的script\reference\assets具体实例错误处理等等。(对于使用者来说只了解该skills是解决什么问题以及如何通过prompt触发它即可)

  • description用于声明这个skills的职责是什么(清晰+简短)
  • When to Use This Skill声明了何时触发这个skill(用户询问如何完成某件事情,其中这件事情是一项常规任务且已掌握相关技能。询问可以完成某件事情的skill等等...) image.png

了解了find-skills的触发条件之。下面我们来测试一下,输入下面的prompt,opencode会自动使用find-skills提供的npx skills find去查找是否存在符合要求的skills

我想在想要创建创建一个skills,有没有对应的skills帮助我实现这个操作

image.png

skill creator

一般情况下每个公司每到年底都需要年终述职,那是不是也可以自定义一个skills来帮我们生成一个关于年终述职的PPT呢? 对于书写自定义skills我们可以通过skill-creator这个skills帮我创建。接下来通过skill-creator来创建一个关于年终述职PPT的skills

帮我生成一个提示词,用于实现年终总结PPT的skills实现的提示,我会把这个提示词喂给skill-creator来创建我的自定义skills

image.pngskill-creator也提供了一个python脚本用于打包分享给其他opencode用户 image.png 通过该skills帮我生成了一个ppt现在有点丑。因为只是演示关系我就不再优化skills提示词了,例如我们可以优化使用Office-PowerPoint-MCP-Server来使用ppt操作,而不是通过python脚本直接生成。追加更多与用户交互的提示词(让用户提供更多的信息、以及年终数据...)等等。(发挥你的想象力,去创建使用好玩的skills,分享在skills.sh也是ok的) image.png

让你的skills更加专业

skill文件一般包含了使用场景(description)触发条件具体的workflow可能的script\reference\assets具体实例错误处理等等,如何写好skills呢? 如下有三个建议:

1. 功能职责单一化
一个专业的skill首先要有清晰的职责范围,不要试图让一个skill做太多的事情,专注于解决一个具体的问题会更好。例如: 文件整理就文件整理,不要又涉及分析文件内容。这样让skill变得复杂且难以维护

2. 渐进式批漏
不要一次性把所有内容塞给AI agent,而是根据需要逐步获取信息。因为模型的上下文窗口是有限制的,而skill本质也是prompt提示词给到AI agent,全部加载进来的话会占用大量的上下文空间。渐进式纰漏就是让skill先加载目录(文件名称、description描述),只有确定需要使用某个skill才加载其的详细内容。我们一般将skill的详细文档示例代码参考资料放到单独的文件,然后再SKILL.md通过链接的形式进行关联。

就像维基百科一样: 从一个目录开始,仅在必要时才会引用越来越深的片段

3. description清晰度
如下SKILL.md可以使用,但是给出的信息太少,AI agent不知道这个skill是干什么的

name: file-rename  
description: 重命名文件  
# 文件重命名  
  
xxxxxxx

改进之后,description提供了更多信息,让AI agent能够更加准确理解这个SKILL的用途和方法

name: file-rename  
description: 批量重命名文件,支持按规则添加前缀、后缀、序号等。当用户需要批量修改文件名、统一文件命名格式时使用。  
# 文件批量重命名工具  
  
xxxxx

同时也可以参考一些优秀的开源skills写法,例如: 如何写出好的 Skill?拆解 skill-creator 背后的设计

skills如何工作

这里不再概述了claude code写的非常清晰了。这个Claude Code Skills & skills.sh - Crash Course来自于Youtube的视频讲skills也非常清晰

推荐skills

superpowers是一个完整的软件开发工作流程,适用于您的编码代理。它基于一组可组合的“skills”和一些初始指令构建而成,确保您的代理能够使用这些技能。让coding agent更加规范的编写代码,避免屎山代码

superpowers基本工作流(让AI agent执行更加规范、系统化)
image.png
安装完成superpowersskills集合后,其包含了brainstormin头脑风暴将你的粗略想法(想做一个需求,但是细节并没有想明白)输出为设计文档、writing-plans根据设计文档来生成执行计划、然后execute-plan来执行和实施计划等等工作流。 image.png 针对opencode可给定superpowers工作流需要执行哪些步骤,你可以通过/brainstormin/write-plan/execute-plan这个命令来执行。

https://github.com/obra/superpowers 学学 superpowers 怎么用,
当我提到"使用 superpowers 工作流"时,按这个顺序来:
1)先头脑风暴(brainstorm)
2)再写计划(write-plan)
3)最后按计划实现和测试(execute-plan)

上面提到的/brainstormin/write-plan/execute-plan这三步也是最核心步骤

  1. /brainstormin明确需求确定需要做什么
  2. /write-plan 写个执行计划
  3. /execute-plan按照执行计划一步一步执行

先通过brainstormin生成主题设计文档,然后通过write-plan拆分主题设计文档生成主题执行计划,然后再调用execute-plan执行任务。
image.png 最终效果如下
image.png

AGENTS.md、rules、mcp、skills的关系

3061f42318710801c85df8e1b946cc7e.png

总结

经过前面步骤,我们学会了opencode的基础操作,以及涉及了Agent.mdRulesMcps以及skills的使用,由于这篇幅比较长了。关于open routes多Agents协作、抓取Agent请求分析MCP\SKILLS交互原理、模型对比选择等放到之后有时间再整理

参考资料

agents
AGENTS.md Guide
A Complete Guide To AGENTS.md
AGENTS.md、层级规则与可选的 AGENTS.override.md
Rules & Guidelines
Agent Skills、Rules、Prompt、MCP,一文把它们理清楚了
Cursor Rules
opencode-rules
MCP Explained: The New Standard Connecting AI to Everything
chrome-devtools-mcp
Claude Code Skills 上手指南:从概念到真正用起来
Agent Skills Claude Code Skills & skills.sh - Crash Course
Introducing Agent Skills
Superpowers-入门快速指南 从“代码搬砖工”到“AI 团队主管”:OpenCode + Oh My OpenCode 开启多智能体协作新纪元

React项目白屏兜底神器?ErrorBounary你了解吗?

2026年2月24日 16:28

技术背景

JavaScript 的错误会破坏 React 的内部状态,进而导致整个页面崩溃。为了解决这个问题,React 16 引入了错误边界(ErrorBounary),错误边界可以捕获子组件的 JavaScript 错误,打印这些错误并展示降级 UI。

官方文档定义:

默认情况下,如果你的应用程序在渲染过程中抛出错误,React 将从屏幕上删除其 UI。为了防止这种情况,你可以将 UI 的一部分包装到 错误边界 中。错误边界是一个特殊的组件,可让你显示一些后备 UI,而不是显示例如错误消息这样崩溃的部分。

要实现错误边界组件,你需要提供 static getDerivedStateFromError,它允许你更新状态以响应错误并向用户显示错误消息。你还可以选择实现 componentDidCatch 来添加一些额外的逻辑,例如将错误添加到分析服务。

使用方式:

可以使用已有的 JS库 react-error-bounary 替代自己实现

# npm
npm install react-error-boundary

# pnpm
pnpm add react-error-boundary

# yarn
yarn add react-error-boundary




import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary fallback={<div>Something went wrong</div>}>
  <ExampleApplication />
</ErrorBoundary>

源码分析:

import * as React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    //构造函数初始化状态
    this.state = { hasError: false };
  }

  //这是一个静态生命周期方法,当子组件抛出错误时会被调用
  static getDerivedStateFromError(error) {
    // 更新状态,以便下一次渲染将显示后备 UI。
    return { hasError: true };
  }

  //在错误发生后调用,用于记录错误信息
  componentDidCatch(error, info) {
    logErrorToMyService(
      error,
      // 示例“组件堆栈”:
      // 在 ComponentThatThrows 中(由 App 创建)
      // 在 ErrorBoundary 中(由 APP 创建)
      // 在 div 中(由 APP 创建)
      // 在 App 中
      info.componentStack,
      // 警告:Owner Stack 在生产中不可用
      React.captureOwnerStack(),
    );
  }

  
  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义后备 UI
            return (
        <div>
          <p>当前页面出错了,请联系Bone值班同学</p>
        </div>
      );
    }

    return this.props.children;
  }
}
<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <Profile />
</ErrorBoundary>

官网地址: zh-hans.react.dev/reference/r…]

影响范围&边界

错误边界可捕获常见场景中的错误:

  1. 子组件内部错误
const ErrorUaughtComponent = () => {
  return (
    <div>
     //未定义变量CcConfig
      <h1>{CcConfig.length}</h1>
    </div>
  );
};
  1. 组件主动抛出错误
const ErrorComponent = () => {
  throw new Error('这是一个测试错误');
};
  1. 组件new一个错误(语法错误) ps:一个 React「函数组件」必须返回 ReactNode
const ErrorNewtComponent = () => {
  return new Error('这是一个测试错误');
};

错误边界无法捕获以下场景中出现的错误:

  1. 它自身抛出来的错误(并非它的子组件)
// 把错误边界自身写成会崩溃的组件
const BadBoundary = () => {
  throw new Error('BadBoundary 自己炸了');
};

const Demo1 = () => (
  <ErrorBoundary>
    <BadBoundary />
  </ErrorBoundary>
);
  1. 异步的错误(例如 setTimeoutrequestAnimationFrame 回调函数,接口报错,

form.validateFields()校验错误等)

const ErrorComponent = () => {
  setTimeout(() => {
    throw new Error('这是一个测试错误');
  }, 1000);
};
  1. 事件中的错误 (错误边界无法捕获事件处理器内部的错误)
const ErrorComponent = () => {
  const handleClick = () => {
    throw new Error('点击事件里的错误');
  };
  return <button onClick={handleClick}>点我报错</button>;
};

  1. 服务端渲染的错误(SSR渲染)
  2. console.error () (只是打印错误信息到控制台,不会终止程序)
const ErrorNewtComponent = () => {
 console.error('这只是一条日志,不会中断渲染');
};

目前已在在内部系统以页面维度、组件维度进行试用:

const ErrorBoundaryWrapper: React.FC<ErrorBoundaryProps> = (
  props: ErrorBoundaryProps,
) => {
  const location = useLocation();
  const shouldEnableErrorBoundary =
    location.pathname.startsWith('/feature/index') ||
    location.pathname.startsWith('/featureDetails/index');

  if (!shouldEnableErrorBoundary) {
    return props.children;
  }
  return <ErrorBoundary {...props} />;
};

总结

✅收益

  1. 兜底白屏:React 18 以后,生产环境任何未被捕获的错误都会把整棵组件树卸载成“白屏”;全局 ErrorBoundary 可以把白屏变成降级 UI(如“系统开小差”)。
  2. 统一埋点:一次 catch 所有渲染阶段错误,便于 Sentry、阿里 ARMS、灯塔等监控平台统计。
  3. 渐进式降级:可以配合 React.lazy、Suspense,对局部模块再包一层 ErrorBoundary,形成“全局兜底 + 局部细粒度”两级策略。

⚠️ 需要注意的 6 件事

场景 注意点 建议
事件处理器/异步代码 ErrorBoundary 只能捕获渲染阶段错误;onClicksetTimeoutPromise.reject 不在其捕获范围。 考虑事件里手动 try/catch
SSR 服务端渲染时,ErrorBoundary 抛出的错误如果不处理,Node 进程会 500。 在 SSR 入口也包一层 ErrorBoundary,并返回 500 页面。
重复渲染 一旦进入 Error 状态,React 会卸载整棵树并显示 fallback;如果 fallback 本身又抛错,会死循环。 fallback 组件必须足够简单(纯静态 UI),不要依赖 props/context。
性能 全局 ErrorBoundary 会阻止整页重渲染,对交互密集场景(如编辑器、画布)可能过度杀伤。 路由级业务模块级再包一层,做到“局部爆炸局部降级”。
错误信息泄露 生产环境不要把 error.stack 直接展示给用户,防止源码路径泄露。 fallback={error => <ErrorPage code={500} message="系统繁忙" />}
热更新 Vite/Webpack 热更新时抛错会被 ErrorBoundary 吞掉,导致看不到编译错误。 开发环境单独关掉全局 ErrorBoundary,或在 fallback 里加一个“刷新页面”按钮。

一句话总结

全局 ErrorBoundary 是“保险丝”,不是“万能药”。
只要记住“只兜底渲染错误 + 保持 fallback 简单 + 异步错误手动补”这三点,就可以放心用。

❌
❌