阅读视图

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

从零到一:在React前端中集成The Graph查询Uniswap V3池数据实战

从零到一:在React前端中集成The Graph查询Uniswap V3池数据实战

背景

上个月,我接手了一个DeFi收益聚合器项目的前端开发。产品经理提了一个需求:要在仪表盘首页展示用户可能感兴趣的几个热门Uniswap V3流动性池的实时数据,包括24小时交易量、总流动性和当前手续费率。

我的第一反应是:“简单,直接用 ethers.jsviem 去读合约的 public 变量和事件不就行了?” 于是,我吭哧吭哧写了段代码,通过 useEffect 轮询调用池子合约的 slot0 函数获取当前价格,再通过 provider.getLogs 拉取最近24小时的 Swap 事件来计算交易量。本地测试时,面对一个池子还好。一上线,用户钱包里要是多几个池子,页面直接卡死,RPC调用次数爆炸,速度慢得让人想砸键盘。我意识到,对于这种需要聚合和分析历史链上数据的场景,直接与节点交互是条死路。这时,我想起了那个听过很多次但一直没亲手用过的工具——The Graph。

问题分析

The Graph 的核心是一个去中心化的索引协议,它监听区块链事件,将数据按照定义好的模式(Subgraph)处理后存入可高效查询的数据库。对于前端来说,我们不用再关心如何从海量事件日志里筛选和计算,只需要像调用API一样,用GraphQL查询语句去获取已经处理好的结构化数据。

我的需求很明确:查询Uniswap V3在以太坊主网上特定池子的聚合数据。理论上,我不需要自己部署Subgraph,因为Uniswap官方已经维护了一个非常完善的 Uniswap V3 Subgraph。我的任务就是在前端React应用中,学会如何与这个已部署的Subgraph进行交互。

最初的尝试是直接用 fetchaxios 向Subgraph的GraphQL端点发送POST请求。这确实能跑通,但很快遇到了问题:1. 需要手动管理查询字符串和变量,容易出错;2. 缺乏类型安全,返回的数据结构全靠猜;3. 没有内置的请求状态(loading, error)管理,需要自己用useState和useEffect封装,很繁琐。我需要一个更“React”的、类型友好的解决方案。

核心实现

第一步:环境搭建与GraphQL客户端选择

首先,我创建了一个新的React + TypeScript项目(或者在你的现有项目中操作)。关键的依赖是 @apollo/clientgraphql。Apollo Client 是一个强大的GraphQL状态管理库,它提供了React Hook(如 useQuery)、缓存、错误处理等开箱即用的功能,能极大简化前端与The Graph的交互。

npm install @apollo/client graphql

接下来,我需要初始化Apollo Client,并配置其连接到Uniswap V3的Hosted Service端点。

这里有个坑:The Graph的Hosted Service端点URL结构是 https://api.thegraph.com/subgraphs/name/<用户名>/<子图名称>。对于Uniswap V3以太坊主网,用户名是 uniswap,子图名称是 uniswap-v3。千万别去官方文档里找“API Key”,Hosted Service在查询限额内是免费的,直接使用即可。

我创建了一个文件 lib/apolloClient.ts 来配置客户端:

// lib/apolloClient.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

// Uniswap V3 以太坊主网 Subgraph 端点
const UNISWAP_V3_GRAPH_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3';

const httpLink = new HttpLink({
  uri: UNISWAP_V3_GRAPH_ENDPOINT,
});

// 创建 Apollo Client 实例
// 注意:默认缓存策略可能不适合实时性极高的数据,对于交易量等数据可以考虑调整fetchPolicy
export const apolloClient = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network', // 优先返回缓存,同时在后台更新
    },
    query: {
      fetchPolicy: 'network-only', // 对于主动查询,总是从网络获取
    },
  },
});

第二步:编写GraphQL查询并生成类型

这是核心步骤。我需要去 The Graph Explorer 找到 uniswap/uniswap-v3 子图,研究其数据模式(Schema)。我需要的池子(Pool)数据,在Schema中对应 Pool 实体,里面包含了 id(合约地址)、totalValueLockedUSDvolumeUSDfeesUSDtoken0token1 等字段。

为了获取24小时数据,子图通常会有类似 poolDayData 的时间序列实体。经过探索,我发现查询最近24小时数据的最佳方式是:先查询 Pool 实体本身获取当前快照数据(如TVL),再关联查询其最新的 poolDayData(按日期排序取第一条)来获取过去24小时的交易量和手续费。

我创建了一个GraphQL查询文件 queries/poolData.graphql

# queries/poolData.graphql
query PoolData($poolId: String!) {
  # 查询池子基础信息
  pool(id: $poolId) {
    id
    totalValueLockedUSD
    feeTier
    token0 {
      id
      symbol
      decimals
    }
    token1 {
      id
      symbol
      decimals
    }
    # 关联查询最近的日数据(过去24小时)
    poolDayData(first: 1, orderBy: date, orderDirection: desc) {
      volumeUSD
      feesUSD
      date
    }
  }
}

注意这个细节$poolId 是池子的合约地址,但在The Graph中,id 字段通常是全小写的地址字符串。所以从链上获取的地址,在传入查询变量前最好先 .toLowerCase() 处理一下,避免查不到数据。

接下来,为了让TypeScript认识查询返回的数据结构,我使用GraphQL Code Generator来自动生成类型。这需要额外配置,但一劳永逸。简单起见,我也可以手动定义类型,但对于复杂查询,自动生成更可靠。这里我展示手动定义的方式,更贴近快速上手的场景:

// types/poolData.ts
export interface Token {
  id: string;
  symbol: string;
  decimals: string;
}

export interface PoolDayData {
  volumeUSD: string;
  feesUSD: string;
  date: number;
}

export interface PoolData {
  id: string;
  totalValueLockedUSD: string;
  feeTier: string;
  token0: Token;
  token1: Token;
  poolDayData: PoolDayData[];
}

export interface GraphQLPoolResponse {
  pool: PoolData | null;
}

第三步:创建自定义React Hook

为了让数据获取逻辑可以在组件中优雅复用,我决定将其封装成一个自定义Hook:usePoolData

// hooks/usePoolData.ts
import { useQuery, gql } from '@apollo/client';
import { GraphQLPoolResponse } from '../types/poolData';

// 直接在Hook中定义GraphQL查询,避免额外文件
// 注意:gql`...` 是Apollo Client的模板标签函数,用于解析GraphQL查询字符串
const POOL_DATA_QUERY = gql`
  query PoolData($poolId: String!) {
    pool(id: $poolId) {
      id
      totalValueLockedUSD
      feeTier
      token0 {
        id
        symbol
        decimals
      }
      token1 {
        id
        symbol
        decimals
      }
      poolDayData(first: 1, orderBy: date, orderDirection: desc) {
        volumeUSD
        feesUSD
        date
      }
    }
  }
`;

interface UsePoolDataProps {
  poolId: string | undefined; // 池子合约地址
  skip?: boolean; // 是否跳过查询
}

export const usePoolData = ({ poolId, skip = false }: UsePoolDataProps) => {
  // 使用 useQuery Hook
  // 它自动处理 loading, error 状态,并返回 data
  const { loading, error, data, refetch } = useQuery<GraphQLPoolResponse>(
    POOL_DATA_QUERY,
    {
      variables: {
        poolId: poolId?.toLowerCase(), // 关键:地址转小写
      },
      skip: !poolId || skip, // 如果没有poolId或主动跳过,则不执行查询
      // fetchPolicy: 'network-only' // 可以根据需要覆盖默认策略
    }
  );

  // 对返回的数据进行简单处理和类型断言
  const poolData = data?.pool;

  return {
    loading,
    error,
    poolData,
    refetch, // 用于手动刷新数据
  };
};

这个Hook的设计非常“React”:它接收依赖项(poolId),管理内部状态,并返回一个清晰的状态对象。在组件中使用时,我可以轻松地根据 loading 显示加载框,根据 error 显示错误信息,用 poolData 渲染UI。

第四步:在组件中集成并使用

最后,我在一个React组件中使用这个Hook。假设我要显示USDC/ETH 0.05%费率的池子(一个非常常见的池)。

// components/PoolCard.tsx
import React from 'react';
import { usePoolData } from '../hooks/usePoolData';

// 已知的 Uniswap V3 USDC/ETH 0.05% 池地址(以太坊主网)
const USDC_ETH_POOL_ADDRESS = '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640';

const PoolCard: React.FC = () => {
  const { loading, error, poolData } = usePoolData({
    poolId: USDC_ETH_POOL_ADDRESS,
  });

  if (loading) {
    return <div className="p-4 border rounded-lg">加载池数据中...</div>;
  }

  if (error) {
    return (
      <div className="p-4 border rounded-lg bg-red-50 text-red-700">
        查询失败: {error.message}
      </div>
    );
  }

  if (!poolData) {
    return <div className="p-4 border rounded-lg">未找到池子数据</div>;
  }

  const dailyVolume = poolData.poolDayData[0]?.volumeUSD || '0';
  const tvl = poolData.totalValueLockedUSD;

  return (
    <div className="p-4 border rounded-lg shadow-sm bg-white">
      <h3 className="font-bold text-lg">
        {poolData.token0.symbol} / {poolData.token1.symbol} Pool
      </h3>
      <p className="text-sm text-gray-500">费率: {Number(poolData.feeTier) / 10000}%</p>
      <div className="mt-3 space-y-2">
        <div>
          <span className="text-gray-600">总锁定价值 (TVL): </span>
          <span className="font-semibold">
            ${Number(tvl).toLocaleString(undefined, { maximumFractionDigits: 0 })}
          </span>
        </div>
        <div>
          <span className="text-gray-600">24小时交易量: </span>
          <span className="font-semibold">
            ${Number(dailyVolume).toLocaleString(undefined, { maximumFractionDigits: 0 })}
          </span>
        </div>
        <div className="text-xs text-gray-400">
          池地址: {poolData.id}
        </div>
      </div>
    </div>
  );
};

export default PoolCard;

至此,一个完整的、从The Graph获取Uniswap V3池数据并展示的前端功能就实现了。代码清晰、类型安全、且易于维护和扩展。

完整代码

以下是关键文件的完整代码汇总,你可以复制到一个新的React + TypeScript项目中运行测试:

1. 安装依赖:

npx create-react-app my-graph-demo --template typescript
cd my-graph-demo
npm install @apollo/client graphql

2. 配置 Apollo Client (src/lib/apolloClient.ts):

import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

const UNISWAP_V3_GRAPH_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3';

const httpLink = new HttpLink({
  uri: UNISWAP_V3_GRAPH_ENDPOINT,
});

export const apolloClient = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
    query: {
      fetchPolicy: 'network-only',
    },
  },
});

3. 定义类型 (src/types/poolData.ts):

export interface Token {
  id: string;
  symbol: string;
  decimals: string;
}

export interface PoolDayData {
  volumeUSD: string;
  feesUSD: string;
  date: number;
}

export interface PoolData {
  id: string;
  totalValueLockedUSD: string;
  feeTier: string;
  token0: Token;
  token1: Token;
  poolDayData: PoolDayData[];
}

export interface GraphQLPoolResponse {
  pool: PoolData | null;
}

4. 创建自定义Hook (src/hooks/usePoolData.ts):

import { useQuery, gql } from '@apollo/client';
import { GraphQLPoolResponse } from '../types/poolData';

const POOL_DATA_QUERY = gql`
  query PoolData($poolId: String!) {
    pool(id: $poolId) {
      id
      totalValueLockedUSD
      feeTier
      token0 {
        id
        symbol
        decimals
      }
      token1 {
        id
        symbol
        decimals
      }
      poolDayData(first: 1, orderBy: date, orderDirection: desc) {
        volumeUSD
        feesUSD
        date
      }
    }
  }
`;

interface UsePoolDataProps {
  poolId: string | undefined;
  skip?: boolean;
}

export const usePoolData = ({ poolId, skip = false }: UsePoolDataProps) => {
  const { loading, error, data, refetch } = useQuery<GraphQLPoolResponse>(
    POOL_DATA_QUERY,
    {
      variables: {
        poolId: poolId?.toLowerCase(),
      },
      skip: !poolId || skip,
    }
  );

  const poolData = data?.pool;

  return {
    loading,
    error,
    poolData,
    refetch,
  };
};

5. 创建展示组件 (src/components/PoolCard.tsx):

import React from 'react';
import { usePoolData } from '../hooks/usePoolData';

const USDC_ETH_POOL_ADDRESS = '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640';

const PoolCard: React.FC = () => {
  const { loading, error, poolData } = usePoolData({
    poolId: USDC_ETH_POOL_ADDRESS,
  });

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;
  if (!poolData) return <div>无数据</div>;

  const dailyVolume = poolData.poolDayData[0]?.volumeUSD || '0';
  const tvl = poolData.totalValueLockedUSD;

  return (
    <div style={{ border: '1px solid #ccc', padding: '1rem', borderRadius: '8px' }}>
      <h3>{poolData.token0.symbol} / {poolData.token1.symbol} Pool</h3>
      <p>费率: {Number(poolData.feeTier) / 10000}%</p>
      <div>
        <div>TVL: ${Number(tvl).toLocaleString()}</div>
        <div>24h Volume: ${Number(dailyVolume).toLocaleString()}</div>
      </div>
      <small>地址: {poolData.id}</small>
    </div>
  );
};

export default PoolCard;

6. 在应用入口集成 (src/App.tsx):

import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from './lib/apolloClient';
import PoolCard from './components/PoolCard';
import './App.css';

function App() {
  return (
    <ApolloProvider client={apolloClient}>
      <div className="App">
        <h1>Uniswap V3 池数据看板 (The Graph)</h1>
        <PoolCard />
        {/* 可以在这里添加更多 PoolCard,传入不同的 poolId */}
      </div>
    </ApolloProvider>
  );
}

export default App;

运行 npm start,你应该能看到一个显示USDC/ETH池数据的卡片。

踩坑记录

  1. “池子找不到 (Pool not found)”:这是我遇到的第一个也是最多人踩的坑。我确认地址没错,但查询返回 null。后来在The Graph的Discord社区提问才知道,Subgraph中存储的地址 id 字段全是小写。而我从链上或Etherscan复制的地址可能是大小写混合的校验和格式。解决方法:在将地址作为变量传入查询前,务必执行 .toLowerCase()

  2. 查询超时或响应慢:第一次查询一个不常被查询的冷门池子时,可能会遇到响应时间较长的情况。这是因为The Graph的索引器需要为这次查询执行索引工作。解决方法:对于用户体验要求高的场景,前端要做好加载状态提示。另外,可以检查Subgraph的健康状态,有时是公共端点负载问题。

  3. 数据类型不匹配:GraphQL查询返回的数字,即使是 BigInt 在Subgraph中,通过API返回时也是字符串格式。直接用于计算会出错。解决方法:在前端使用前,用 Number()parseFloat() 或更适合大数的库如 BigNumber.js (ethers.js自带) 进行转换。我的示例中用了 Number(),对于TVL和交易量这种可能很大的数,在生产环境中建议使用 ethers.BigNumberBigInt 来处理。

  4. “Cannot read property ‘symbol’ of null”:在测试时,我传了一个非Uniswap V3池的地址,查询返回的 pool 不为 null,但内部的 token0token1 可能为 null(如果子图索引不完整)。解决方法:在组件渲染中使用可选链操作符 ?. 或进行严格的空值检查,就像我在示例中处理 poolDayData[0] 一样。

小结

这次实战让我彻底把The Graph从“听说过”变成了“上手用过”。它的核心价值在于将复杂的链上数据索引、聚合工作从前端剥离,让开发者能像查询普通API一样高效获取结构化数据。对于构建数据驱动的DeFi、NFT应用前端,它几乎是必备工具。下一步,我可以探索更复杂的查询(如分页获取多个池子、历史时间序列分析),甚至尝试为自己项目的合约部署一个专属的Subgraph。

工业和信息化部将发布一批“人工智能+”高价值场景

日前在武汉举行的中国人工智能产业发展联盟第十七次全体会议上,工业和信息化部科技司副司长杜广达表示,工业和信息化部将以制造业为主战场、应用牵引为主线,发布一批“人工智能+”高价值场景,探索一批典型应用,建设一批特色智能体,提供一批新型智能终端,研制一批新标准,培育一批产业应用人才,打造一批优质企业,全面推动人工智能与制造业深度融合。(新华社)

为什么有钱人也不买GUCCI了?

曾经一只酒神包便能掀起排队热潮的GUCCI(古驰),如今似乎正通过关店、降价来重新找回市场。

3月底,GUCCI上海环贸iapm旗舰店关闭。此前,该品牌已撤掉上海芮欧百货和新世界大丸百货两家标志性门店——仅两年内一个城市内就关了三家店,关店数量占了总数的三分之一。

但另一边,开进奥莱的GUCCI热闹非凡,成了排队王中的“当红炸子鸡”。只要去奥莱,大部分消费者首奔的店都是GUCCI,因为在那里可以淘到5折甚至2.5折的货品。

GUCCI奥莱门店 | 图 王涵艺摄

正价店卖不动,奥莱店“抢疯”。曾经的顶奢品牌GUCCI,到底怎么了?

GUCCI为何卖不动?

开云集团2025年财报显示,GUCCI营收同比暴跌22%至59.92亿欧元,营业利润腰斩至9.66亿欧元。

这样的下滑势头,已持续近3年。2024年,GUCCI营收已经同比上一年下降了23%;2023年,GUCCI营收也同比单位数下跌。

可供对比的是,在2022年,GUCCI的年销售额曾突破100亿欧元大关。

有人说,这是奢侈品行业的寒流,所有品牌都不好过。

但事实是,同属顶奢阵营的爱马仕、LV、香奈儿虽有业绩波动,却未陷入连续下滑的泥潭,唯独GUCCI一路“跌跌不休”。在四大奢侈品牌中,GUCCI也是唯一开了奥莱折扣店的品牌,仅在国内就曾开过8家。

所以抛开大环境,为什么偏偏GUCCI“命最苦”?

奢侈品专家、要客研究院院长周婷博士指出,风格频繁变动,没有形成稳定、可传承的品牌美学是其一。“古驰在不同设计师带领下风格变化较大,从Alessandro Michele的‘极繁主义’到Sabato De Sarno的‘简约优雅’,缺乏稳定的品牌核心记忆点,导致消费者难以形成持续的品牌认同。”

GUCCI门店陈列的商品 | 图 王涵艺摄

其二是缺乏爆款吸引力。“过去依靠酒神包、老爹鞋等爆款带动增长,但近年新产品缺乏足够创新,难以激发消费者购买欲望,老客户流失,新客户吸引力不足。”周婷称。

时间倒回十年前,GUCCI在2015秋冬秀场上的Dionysus酒神包,2016春夏系列的Sylvie,2016年秋冬女装秀场上的GG Marmont,每一款包几乎都是超级爆款。

GUCCI 2015秋冬Dionysus酒神包 | 图源:GUCCI微信公众号

GUCCI 2016年秋冬GG Marmont | 图源:GUCCI微信公众号

GUCCI的典型消费者云姐就是酒神包的购买者之一,但在那之后,她未曾再购入过GUCCI的包,她认为: “近年来的GUCCI没有话题度,也没有爆品。”

“同时,爆款生命周期短,新品乏力,过度依赖logo与印花,缺乏低调高级的经典款,与顶奢客群‘去logo化’需求相悖也是GUCCI存在的问题。”周婷补充称。

去logo化、极简风的消费趋势,是否真实存在?GUCCI的竞争对手LV用“黑武士”给出了答案。LV于2025年夏季上市的“黑武士”就属于走低调奢华风格路线的单品,它是LV Carryall系列中的全黑款,虽不是官方限量款,却长期处于官网缺货、门店需预订的状态,足以说明它踩中了风口。

此外,服装行业资深顾问、前品牌操盘手顾川告诉有意思报告,GUCCI的定价混乱,也亲手摧毁了品牌溢价。“奥莱渠道的过度使用,导致产品以折扣价大量流通,削弱了专柜产品的稀缺性和高端感,消费者更倾向于在折扣渠道购买,影响品牌整体销售。”

在小红书平台,除了有网友惊叹GUCCI的2.5折高折扣,还有人求助:“专柜买回来的GUCCI外套,还没拆吊牌就进奥莱,损失的8500元该怎么办?”

图源:小红书

“这种渠道管控和定价策略,就是在告诉消费者专柜价是虚高的,帮助他们养成‘非折扣不消费’的习惯。”顾川认为,“频繁折扣又让品牌的高端形象一落千丈,陷入恶性循环。”

当品牌调性被稀释,消费者对GUCCI的“高端”属性就会产生怀疑。在一篇GUCCI打折吐槽帖下方,“买GUCCI只要跨出店门,就立马5折,你敢背一下,再对折”“GUCCI只适合在奥莱蹲”等等都是评论区的高赞留言。

图源:小红书

GUCCI不是没有意识到这些,为实现业绩“止血”,GUCCI采取了一系列自救措施。

GUCCI自救,能行吗?

首先是换人。2025年2月,开云集团宣布结束与创意总监Sabato De Sarno的合作。同年,任命前Balenciaga创意总监德姆纳(Demna Gvasalia)为新任艺术总监。

与此同时,开云集团还宣布了弗朗西丝卡·贝尔莱蒂尼(Francesca Bellettini)为新任GUCCI总裁兼CEO,标志着GUCCI新时代的开启。

其次,除了人事洗牌,GUCCI也在“断臂求生”。2025年,开云集团全球净关店75家,2026年计划再关100家。卢卡·德·梅奥透露:“在75家关闭的门店中,可能有40%都属于GUCCI,奥莱折扣店为主,但这并非全部。”

在业绩交流会上,开云集团CEO卢卡·德·梅奥还表示,亚洲地区GUCCI门店数量已趋于饱和,因此关店计划将重点集中在韩国、日本还有部分中国地区,“如果特别考虑到2026年,我预计40%的关店会发生在亚洲市场”。

截至2月10日,GUCCI的全球门店数是497家。其中,亚太地区(不含日本)共有166家,占比超3成,是GUCCI全球门店数最多的区域。

图源:开云集团官网

此外,降价也成为GUCCI提振业绩的另一重要举措。事实上,自2025年以来,开云集团管理层已多次表态,承认GUCCI此前的定价策略与市场需求存在偏差。“过去几年行业的‘涨价力’(inflationary power)已不复存在,‘奢侈品疲劳’(luxury fatigue)现象已成为行业共识,我们已正视这一市场现实。”

开云集团管理层在业绩交流会上明确表示,品牌部分产品此前的定价确实存在失控问题,经过对产品结构的重新梳理与优化,GUCCI新推出的“La Famiglia”系列已调整定价策略,使其更具市场竞争力,且该系列已获得市场层面的认可。

“La Famiglia”,意大利语意为“家庭”,该系列由新任艺术总监Demna操刀。在这个“家庭”里,除了一个印满logo的行李箱,还有37款造型分别对应37个有着不同个性和态度的角色。从GUCCI释出的2026春夏系列大片中能发现,这一系列更加强调GUCCI旧元素的运用。

GUCCI“La Famiglia”系列 | 图源:小红书@GUCCI

例如,马衔扣不只是点缀式地出现在一双乐福鞋上,更是以极高的出现频率,横跨成衣、裤装、包袋与鞋履多个品类。此外,该系列还重新审视了竹子在GUCCI设计遗产中的角色、聚焦GG字母组合等等。

对此,Demna上任后曾公开表示:“我们正在构思一个愿景来告诉公众,GUCCI的本质是什么?”

GUCCI的马衔扣出现在各种产品上 | 图源:小红书@GUCCI

也就是说,GUCCI正试图通过这些视觉特色来唤醒大家对于品牌的原始记忆。

但这样做,是否有用?

开云集团 CFO Armelle Poulou 透露称,中国客群(Chinese cluster)在第四季度的表现略有改善,同比跌幅收窄至“中双位数”(mid-teens)。 在第三季度业绩交流会上,Armelle Poulou也提到:“手袋品类表现出复苏迹象,在手袋领域,截至2025年9月底,超过60%的手袋销售额来自新品。”

但管理层也承认,尽管新系列备受赞誉,但新创意的商业转化需要时间,GUCCI正处于一个“过渡期”。

“客观来说,GUCCI的自救动作是有效果的,但长期来看,GUCCI要翻身还很难。”周婷指出,“GUCCI此前的一系列操作,影响了高净值人群对其品牌高端性的认知,这种消费偏见短期内已基本形成不可逆态势。”

顾川也认为降价是“饮鸩止渴”,“就像维多利亚的秘密、Alexander Wang、Charles & Keith一样,一旦靠降价走下神坛,就再也回不去了”。

作者:王涵艺

编辑:田纳西

值班编辑:贾诗卉

头图来源:小红书@GUCCI

本文来自微信公众号“有意思报告”,作者:王涵艺,36氪经授权发布。

Vue3 日历组件选型指南:五大主流方案深度解析

在 Vue3 项目开发中,日历组件是日程管理、预约系统、数据可视化等场景的核心组件。不同项目对日历的功能需求差异极大——有的只需基础日期选择,有的需要支持多日程展示、自定义节假日、拖拽调整等复杂功能。本文从「易用性、扩展性、性能」三个维度,深入分析 5 款主流 Vue3 日历组件,并提供选型建议,帮助开发者快速找到适配场景的最佳方案。

一、Vue3 Datepicker:轻量无依赖的基础款

Vue3 Datepicker 是一款纯 Vue3+TypeScript 开发的日历组件,主打轻量与无依赖特性。该组件体积仅约 5KB,却提供了日期范围选择、禁用日期、自定义格式等实用功能。其样式简洁,开发者可通过 CSS 轻松覆盖默认样式,同时完美适配移动端与 PC 端。得益于纯 Vue3 的实现方式,该组件对 Composition API 和 Options API 都有良好的兼容性。

安装命令:

npm install vue3-datepicker --save

使用示例:

<template>
  <div class="basic-calendar">
    <Datepicker
      v-model="selectedDate"
      :disabled-dates="disabledDates"
      format="YYYY-MM-DD"
      placeholder="选择日期"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import Datepicker from 'vue3-datepicker';
import 'vue3-datepicker/dist/index.css';

const selectedDate = ref<Date | null>(null);

const disabledDates = (date: Date) => {
  const day = date.getDay();
  return day === 0 || day === 6;
};
</script>

<style scoped>
.basic-calendar {
  width: 300px;
  margin: 20px;
}
</style>

适用场景:表单中的生日选择、订单日期筛选等轻量级日期选择需求,以及追求极小打包体积的项目。

二、Element Plus Calendar:生态集成的标准化选择

Element Plus Calendar 是饿了么团队出品的企业级日历组件,与 Element Plus 组件库深度集成,视觉风格统一。该组件支持月视图、周视图、日视图三种模式切换,提供日程数据绑定能力,开发者可自定义单元格内容展示。内置国际化功能、日期范围选择、禁用日期等基础能力,并提供完整的 TypeScript 类型定义,可与 Vue3+Vite 开发环境无缝配合。

安装命令:

npm install element-plus --save

使用示例:

<template>
  <div class="el-calendar-demo">
    <el-calendar v-model="currentDate">
      <template #date-cell="{ data }">
        <p :class="data.isSelected ? 'is-selected' : ''">
          {{ data.day.split('-').pop() }}
        </p>
        <span v-if="scheduleMap[data.day]" class="schedule-count">
          {{ scheduleMap[data.day] }}条日程
        </span>
      </template>
    </el-calendar>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { ElCalendar } from 'element-plus';
import 'element-plus/dist/index.css';

const currentDate = ref<Date>(new Date());

const scheduleMap = ref({
  '2026-02-06': 3,
  '2026-02-08': 1,
  '2026-02-10': 2,
});
</script>

<style scoped>
.is-selected {
  color: #409eff;
  font-weight: bold;
}
.schedule-count {
  font-size: 12px;
  color: #f56c6c;
}
</style>

适用场景:使用 Element Plus 组件库的中后台管理系统,需要快速实现标准化日历和日程功能的项目。

三、FullCalendar Vue3:复杂场景的全功能方案

FullCalendar Vue3 是基于业界知名的 FullCalendar 核心库封装的 Vue3 组件,专为复杂日程管理场景设计。该组件支持月视图、周视图、日视图、列表视图、时间轴视图等 10 余种视图类型,提供了日程拖拽、调整时长、重复日程设置、自定义事件渲染等丰富功能。组件兼容 Vue3 的组合式 API,可与 Pinia 或 Vuex 状态管理库无缝集成。此外,还支持 Google 日历和 iCal 导入,具备国际化与时区切换能力。

安装命令:

npm install @fullcalendar/vue3 @fullcalendar/core @fullcalendar/daygrid @fullcalendar/interaction

使用示例:

<template>
  <div class="full-calendar-demo">
    <FullCalendar
      :plugins="calendarPlugins"
      initialView="dayGridMonth"
      :events="calendarEvents"
      editable="true"
      selectable="true"
      @dateClick="handleDateClick"
      @eventClick="handleEventClick"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';

const calendarPlugins = ref([dayGridPlugin, interactionPlugin]);

const calendarEvents = ref([
  { title: '产品评审会', start: '2026-02-06', end: '2026-02-07', color: '#409eff' },
  { title: '版本发布', start: '2026-02-09', color: '#67c23a' },
]);

const handleDateClick = (info: any) => {
  alert(`选择了日期: ${info.dateStr}`);
};

const handleEventClick = (info: any) => {
  alert(`点击了日程: ${info.event.title}`);
};
</script>

<style scoped>
.full-calendar-demo {
  width: 90%;
  margin: 20px auto;
}
</style>

适用场景:企业 OA 系统、会议室预约、课程表管理等复杂日程管理场景,需支持拖拽操作、多视图切换、复杂事件配置的项目。

四、Vant4 Calendar:移动端友好的轻量选择

Vant4 Calendar 是有赞团队出品的移动端日历组件,专为移动端 H5 和小程序场景优化。该组件在交互设计上充分考虑移动端特性,支持滑动切换月份、手势操作等移动端常见交互方式。功能方面支持日期范围选择、快捷日期选择(如近 7 天、近 30 天)、自定义弹窗样式等实用能力。组件体积仅约 8KB,性能表现优异,支持按需引入,与 Vant4 组件库整体风格保持一致。

安装命令:

npm install vant --save

使用示例:

<template>
  <div class="vant-calendar-demo">
    <van-button @click="showCalendar = true">选择日期</van-button>
    <van-calendar
      v-model:show="showCalendar"
      v-model="selectedDate"
      type="range"
      :min-date="minDate"
      :max-date="maxDate"
      @confirm="handleConfirm"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { VanCalendar, VanButton } from 'vant';
import 'vant/lib/index.css';

const showCalendar = ref(false);
const selectedDate = ref<[Date, Date]>([new Date(), new Date()]);
const minDate = ref(new Date('2026-01-01'));
const maxDate = ref(new Date('2026-12-31'));

const handleConfirm = (dates: [Date, Date]) => {
  console.log('选择的日期范围:', dates);
  showCalendar.value = false;
};
</script>

适用场景:移动端 H5 页面、小程序项目,需轻量级、交互友好的日期选择功能。

五、Vue3 Simple Calendar:极简逻辑的定制基石

Vue3 Simple Calendar 是一款独特的日历组件,它不包含任何样式封装,仅提供核心日历逻辑。该组件基于 Vue3 Composition API 开发,体积仅 3KB,没有任何第三方依赖。开发者可以完全自定义 UI 和交互方式,组件只负责处理日历的基本逻辑,如月份切换、日期选中、日期渲染回调等。

安装命令:

npm install vue3-simple-calendar --save

使用示例:

<template>
  <div class="custom-calendar">
    <simple-calendar
      v-model="currentMonth"
      @date-click="handleDateClick"
    >
      <template #header="{ year, month, prevMonth, nextMonth }">
        <div class="calendar-header">
          <button @click="prevMonth">上一月</button>
          <h3>{{ year }}年{{ month }}月</h3>
          <button @click="nextMonth">下一月</button>
        </div>
      </template>
      <template #day="{ date, isToday, isWeekend }">
        <div
          class="day-cell"
          :class="{ today: isToday, weekend: isWeekend, selected: selectedDate === date }"
        >
          {{ date.getDate() }}
        </div>
      </template>
    </simple-calendar>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import SimpleCalendar from 'vue3-simple-calendar';

const currentMonth = ref<Date>(new Date());
const selectedDate = ref<Date | null>(null);

const handleDateClick = (date: Date) => {
  selectedDate.value = date;
  console.log('选中日期:', date);
};
</script>

<style scoped>
.custom-calendar {
  width: 350px;
  margin: 20px;
}
.calendar-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.day-cell {
  width: 50px;
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.today {
  background-color: #409eff;
  color: white;
  border-radius: 50%;
}
.weekend {
  color: #f56c6c;
}
.selected {
  border: 2px solid #67c23a;
  border-radius: 50%;
}
</style>

适用场景:需要品牌化设计日历 UI、特殊交互效果的项目,或仅需复用核心日历逻辑的定制化开发场景。

六、选型指南与核心原则

面对多种日历组件选择,开发者需要根据项目实际情况做出判断。以下是各组件的核心对比:

组件类型 核心优势 适用场景 打包体积
Vue3 Datepicker 轻量无依赖、易定制 基础日期选择、表单场景 ~5KB
Element Plus Calendar 大厂背书、生态集成 中后台标准化日程功能 ~15KB
FullCalendar Vue3 全功能、复杂交互 企业 OA、预约系统 ~100KB
Vant4 Calendar 移动端适配、交互友好 移动端 H5、小程序 ~8KB
Vue3 Simple Calendar 完全自定义、极简逻辑 个性化 UI、定制化交互 ~3KB

美国土安全部召回员工,下笔薪资无着落

美国国土安全部10日要求所有因“停摆”而无薪休假的员工返岗。尽管美国总统特朗普已签署行政令,从10日开始为国土安全部超过3.5万名员工补发工资,但这可能是他们短期内最后一笔收入,下笔薪资尚无着落。(新华社)

多家快递公司宣布涨价

受地区局势和油价波动影响,不仅航空燃油费在涨,快递费也要涨了。日前,UPS、DHL、联邦快递、顺丰国际等多家国际快递物流企业调整了燃油附加费收取标准,部分企业进行了上调。值得注意的是,不仅是国际快递件,受国际油价飙升与行业政策调整双重影响,国内多地的快递企业也于近期集体增设或者上调了燃油附加费。 (新华日报)

千亿诉讼临近 OpenAI指控马斯克搞“突袭”

在美东时间周五晚间提交的法庭文件中,OpenAI表示,马斯克本周早些时候提出的新诉求,显然意在“刁难被告、扰乱庭审程序,同时试图重塑其就本案对外塑造的舆论形象”。马斯克于2024年起诉OpenAI及微软公司,指控这家ChatGPT开发者在从这家软件巨头获得数十亿美元投资、并计划重组为营利性企业后,背弃了其作为研究机构的创立初衷。该案定于4月27日开庭审理。OpenAI与微软均否认存在不当行为。(新浪财经)

连载05-Claude Skill 不是抄模板:真正管用的 Skill,都是从实战里提炼出来的

别再直接 Fork 别人的 Claude Skill:真正有用的 Skill,都是从项目里长出来的

AI Coding 系列第 05 篇 · 核心工具

封面

我第一次批量导入公开 Skill 模板的时候,是真的以为自己走了捷径。

GitHub 上一堆 star 很高的仓库,code review、需求分析、文档编写、调研、拆任务,看起来什么都有。我当时的想法很直接:既然别人已经把常见工作流整理好了,我直接 fork 一份,全量导入,不就能让 Claude 立刻更稳、更懂项目吗?

结果用了几天,我反而越来越不放心。

不是因为它“明显做错了什么”,而是因为它总在看起来没问题的地方出问题。格式完整,措辞专业,检查项也不少,可真正让我在项目里反复吃亏的那几件事,它一次都没替我盯住。异步链里是不是又漏了 await,这次 migration 有没有回滚方案,新同学是不是又顺手写了 throw new Error(),数据库 schema 改了之后 Prisma 类型是不是也一起更新了。

它会提醒一堆“大家普遍都应该注意”的东西,却不知道“我们团队到底最怕什么”。

后来我才明白,问题不是 Skill 机制不好,而是我导入的根本不是“自己的 Skill”,只是别人整理好的经验。

这些经验当然有价值,但它们解决的是共性问题,不会天然长成你项目里的“肌肉记忆”。

真正有用的 Skill,恰恰应该做一件事:

把那些你本来总要重复提醒、总会漏掉、总会在项目里反复踩坑的动作,固化成默认动作。

也就是一句话:

Skill 的本质,不是收藏经验,而是固化默认动作。

这篇文章会从最基本的边界讲起,一路走到 SKILL.md、源码机制、任务类型和可执行能力。内容不少,但我尽量只保留真正有助于你在项目里把 Skill 用起来的部分。


先说结论

如果你只记住这几条,这篇文章就已经值回时间:

  • 对所有任务都生效的规则,写进 CLAUDE.md;只对某类重复任务生效的,做成 Skill;只对这一次有效的,写进 Prompt
  • 只有“输入相对稳定、输出有模式、而且容易漏步骤”的任务,才值得沉淀为 Skill
  • 通用 Skill 模板只能当原材料,项目级 Skill 必须自己裁剪、自己维护
  • description 不是装饰字段,它承担了触发场景的职责,最好把 Use when... 直接写进去,关键词前置、长度克制
  • Claude 启动时主要只看 frontmatter,Skill 正文在真正触发时才按需载入
  • allowed-tools 是权限边界,不是行为建议;paths 是条件激活,不是说明文字
  • 第一个 Skill 不要挑最关键的任务,先拿中等风险任务练手

一、公开 Skill 模板为什么一开始很香,后来却越用越别扭

我现在反而会对“看起来很全”的公开 Skill 模板保持一点警惕。

不是因为它们没用,而是因为它们太容易制造一种错觉:好像什么都覆盖到了,但真正最重要的东西其实没进去。

公开模板最常见的问题,不是方向错,而是下面这三种。

1. 太宽泛

它什么都管一点,但什么都不够深。

它会告诉你“注意异常处理”“注意性能”“注意安全”,这些当然没错。但这些话本身不构成你项目里的工作流。它不知道你们统一用的是 AppError,不知道你们数据库变更必须检查回滚,也不知道你们哪几个目录历史包袱最重。

2. 太嘈杂

50 行模板里,真正有价值的可能只有 5 行。

剩下的 45 行不是完全没用,而是在和那 5 行争夺 Claude 的注意力。对于 agent 来说,规则不是越多越强。很多时候,8 行写透项目约束的 Skill,比 50 行“样样都提一点”的模板更有用。

3. 太不像你的项目

这点最致命。

公开模板知道“大家普遍应该注意什么”,但不知道“你们团队反复死在哪些地方”。而真正有价值的 Skill,恰恰应该把那些项目特有、团队高频踩坑的东西固化下来。

说得更直白一点:你把一个新同事扔进团队,给他一份行业通用培训材料,当然比什么都不给强;但如果你不告诉他“我们团队最容易出错的是哪三件事”,他依然干不好你最在意的活。

所以正确姿势不是“找一个最全的模板直接用”,而是:

先借鉴,再裁剪,最后只留下真正属于你项目的那几条。

公开模板到项目 Skill 的提炼路径


二、先把 Prompt、CLAUDE.md、Skill 这三件事彻底分清楚

很多人不是不会写 Skill,而是一开始就把这三件事混在一起了。

判断方法其实很简单,只问一个问题:

这个要求的作用范围到底有多大?

  • 这个要求对所有任务都成立吗?如果是,放 CLAUDE.md
  • 这个要求只对某一类任务成立吗?如果是,做成 Skill
  • 这个要求只对这一次成立吗?如果是,写进 Prompt

举几个特别典型的例子:

“所有 throw 必须是 AppError
这是全局规则。不管你是在写新功能、修 bug,还是做重构,都要遵守。它应该进 CLAUDE.md

“代码审查时按固定顺序检查数据库、异步和错误处理”
这只在 code review 这种任务里才触发,它不是全局规则,而是任务模板,所以应该做成 Skill

“这次先只分析原因,不要动代码”
这只对当前这次任务有效,应该写进 Prompt

最容易搞混的是 CLAUDE.mdSkill。它们都能约束 Claude 的行为,但本质完全不同:

  • CLAUDE.md 是永远生效的规则
  • Skill 是遇到对应任务才触发的模板

如果要打个比方:

  • CLAUDE.md 是交通规则
  • Skill 是导航路线
  • Prompt 是你这次上车前临时交代的一句话

这三层一旦分清楚,后面 80% 的混乱都会自动消失。

Prompt、CLAUDE.md、Skill 的边界图

一个常见误判:很多问题根本不需要写 Skill

我后来发现,很多人想写 Skill,并不是因为真的存在一个稳定、重复、值得沉淀的任务,而是因为这一次和 Claude 协作得不顺

比如目标没说清,边界没收紧,上下文没给够,或者你真正缺的是一条全局规则,却误以为自己需要一份任务模板。这个时候你如果急着把它沉淀成 Skill,本质上只是把一次性的混乱模板化。

几个很常见的误判场景是:

  • 这次需求本身还在摇摆,连你自己都没想清楚要什么
  • 这个问题只发生过一次,下次未必还会以同样的形状出现
  • 你真正缺的是全局约定,比如错误处理、目录规范、命名规则
  • 你只是想表达“这次先别改代码”“这次先只分析原因”这种一次性约束

写 Skill 之前,先问自己一句话:

这个问题下次还会以差不多的形状再来一次吗?如果不会,先别急着写 Skill。


三、什么时候一个任务真的值得被沉淀成 Skill

不是所有重复任务都值得沉淀。

我现在给自己的标准其实很克制,就一句话:

同一类任务做了三次以上,而且每次都要重新给 Claude 解释背景。

反过来说,如果某个任务每次背景和目的都完全不同,就不值得沉淀。比如“写文档”这个动作本身很常见,但公司文档、API 文档、用户手册的写法完全不同,它们应该是三个不同的 Skill,而不是一个叫“写文档”的通用模板。

在真正开始写之前,我会先做三个检查。

1. 输入是否稳定

“根据 Figma 设计稿生成 React 组件”这种任务,输入格式相对稳定,比较适合沉淀。

“根据 SQL 查询结果生成图表”这种任务,每次数据格式和图表类型都可能差很多,Skill 会很难写得稳。

2. 输出是否有共同模式

“写 Pull Request 描述”很适合,因为它天然就有固定框架:改了什么、为什么改、怎么测试。

但“和 AI 讨论技术方案”这种任务,每次深度、重点、结论都不同,就不太适合硬沉淀成一个模板。

3. 有没有容易漏掉的关键步骤

最值得沉淀成 Skill 的任务,通常不是“最复杂”的任务,而是那些不特别提醒就容易漏一步的任务。

Skill 最有价值的地方,不是让 Claude 变得更聪明,而是把你每次最容易忘的检查项,固化成默认动作。

所以一个任务如果同时满足下面三点:

  • 输入相对稳定
  • 输出有共同模式
  • 总有一两步容易漏

它就很值得沉淀成 Skill。

什么任务值得沉淀成 Skill


四、从一个真实痛点开始,走完 Skill 的提炼过程

光讲判断标准还是有点抽象,不如走一遍完整例子。

代码审查,几乎每个后端工程师每周都在做,也是最容易进入“重复解释”困境的任务。用它来走一遍完整的 Skill 提炼过程会很清楚。

你反复踩的坑

假设你们团队每周都做代码审查,而且总在重复盯这几件事:

  • 有人改一个功能,顺手动了三个不相关模块
  • 新同学不知道项目里统一用 AppError,直接 throw new Error()
  • Promise 链里漏了 await
  • 数据库查询没有索引,或者潜在 N+1 没被看出来

这就是非常典型的“该沉淀 Skill 的信号”。

先设计内容,再去想格式

一个好 Skill,先别急着写文件。先把内容层想清楚,只要回答四个问题:

1. 什么时候用

不是写“代码审查”四个字,而是写清楚触发场景。

❌ 代码审查
✅ 当我提交 PR 前,检查我的 TypeScript 后端代码是否符合项目约定

差别在于:模糊的描述会让 Claude 在不该用的时候乱触发,而具体的场景描述更容易精准命中。

2. 按什么顺序做

步骤尽量不要超过五步。

你从公开模板里借灵感,但通用模板有 50 行,而你真正关心的可能只有四件事:改动范围、错误处理、异步操作、数据库查询。

1. 读完整个改动的 diff,确认改动是否只涉及这个 PR 的范围
2. 检查错误处理:所有 throw 都必须是 throw new AppError()
3. 检查异步操作:Promise 链是否有遗漏的 await
4. 检查数据库查询:是否有 N+1 问题,关键查询是否 explain 过

3. 输出长什么样

不要写“请清晰输出”。这种话几乎没有约束力。直接给格式。

🔴 Critical: ...
🟡 Warning: ...
✅ Suggestion: ...
Summary: X critical issues to fix before merge.

4. 什么时候不适用

写清楚边界比写清楚功能更重要。

比如:

  • 不审查 UI 层代码
  • 不关注代码风格
  • 改动超过 500 行先拆 PR

这些“我不做什么”的声明,往往比“我会做什么”更能防止 Claude 越界。

到这里,你脑子里其实已经有一个能用的 Skill 了。下一步只是把它放进 Claude Code 认识的格式里。


五、真正落到 SKILL.md 文件层,哪些字段值得你认真写

一个完整的 SKILL.md,通常会长这样:

---
name: code-review
description: Review TypeScript backend code before merging. Use when asked to review code, check a PR, or verify implementation before committing.
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash(git diff *)
argument-hint: "[PR 分支名或文件路径]"
arguments:
  - target
---

# Code Review

## 步骤
1. 读取 ${target} 的改动 diff
2. 检查错误处理:所有 throw 必须是 throw new AppError()
3. 检查异步操作:Promise 链是否有遗漏的 await
4. 检查数据库查询:是否有 N+1 问题

## 输出格式
🔴 Critical: ...
🟡 Warning: ...
✅ 通过: ...

这里最值得你认真写的,其实是下面几个字段。

name
名字别太抽象。要让人一眼知道它是做什么的。像 helperutilstools 这种名字几乎没有路由价值,远不如 code-reviewpr-summaryapi-conventions 这种具体命名。

description
这是现在最关键的字段。它不只是“简介”,还承担了“触发场景”的职责。你最好直接把 Use when... 写进去,而不是写一句空话。更重要的是,别把它写成长段说明文。关键词尽量前置,长度最好控制在 250 字符左右,太长往往只会稀释命中信号。官方还特别提醒,description 最好用第三人称去写,像 “Analyzes pull requests...” 这种句式,比 “I can help...” 或 “You can use this...” 更稳。

allowed-tools
它决定这个 Skill 具备哪些能力。这个字段后面我会在源码部分展开讲,因为它比很多人想象的更“硬”。

arguments
让 Skill 接受参数,比如目标文件、目录、分支名。${target} 会在正文里被替换成你传进去的实际值;如果你喜欢按位置拿参数,也可以用 $0$1 这类方式。

还有几个很好用,但不是每次都要上的字段。

argument-hint
告诉调用者这个 Skill 期待什么参数。

model: haiku
简单任务可以指定更轻量的模型,直接省成本。像格式化、重命名、简单改写这类工作,很多时候没必要上更重的模型。

paths
让 Skill 只在某些路径下激活。适合模块边界明确的项目。

context: fork
高风险操作放进独立上下文,避免污染主会话。

disable-model-invocation: true
禁止 Claude 自动触发,只允许你手动 /skill-name 调用。部署、发版、发邮件这类有副作用的 Skill,应该优先考虑加上。

大多数 Skill 根本不需要把字段填满。真正实用的思路不是“功能全”,而是“正好够用”。

如果一个 Skill 只是做常规代码审查,namedescriptionallowed-toolsarguments 往往就够了。只有当你真的遇到参数化、模块隔离、上下文隔离这些需求时,再往上加。

SKILL.md 不是整个 Skill,它只是入口

很多人以为一个 Skill 就是一份 SKILL.md。其实不是。

更实用的做法通常是:把 SKILL.md 控制在足够短、足够清楚的范围里,让它承担“入口”和“调度”职责;真正长的规范、示例、脚本都拆出去。

一个 Skill 目录完全可以长这样:

my-skill/
├── SKILL.md
├── reference.md
├── examples/
│   └── sample.md
└── scripts/
    └── helper.py

这里的关键点不是“可以放很多文件”,而是:这些文件不会自动加载,必须在 SKILL.md 里显式引用。

比如:

## 参考资料
- 完整的 API 规范见 [reference.md](reference.md),需要查接口细节时读它
- 期望的输出格式见 [examples/sample.md](examples/sample.md)

这个设计和前面说的懒加载是同一套思路:不是 Skill 触发时把所有材料都灌进上下文,而是只在真正需要的时候再去读。

所以:

  • reference.md 适合放项目特有知识,比如内部 API 规范、禁用库、架构约定
  • examples/ 适合放期望输出样例,帮助 Claude 对齐格式
  • scripts/ 适合放真正可执行的辅助脚本,让 Skill 不只是“描述怎么做”,还能“先把上下文准备好”

这点很重要,因为它决定了 Skill 的上限不是“几行 prompt”,而是“一个有入口、有知识、有执行能力的局部工作流”。

Skill 放在哪,决定它是谁的能力

这点很容易被忽略,但工程上很重要。

同样是一个 Skill,放在不同位置,意义完全不一样:

  • ~/.claude/skills:你个人所有项目都能用,适合个人长期习惯
  • .claude/skills:只在当前项目生效,适合团队项目约定
  • <plugin>/skills:跟着插件走,适合做模块化分发

如果不同层级里恰好有同名 Skill,优先级也不是平均的。官方规则更接近:企业级配置优先于个人级,个人级优先于项目级;插件 Skill 因为带命名空间,通常不会和前面这些直接撞名。

官方文档里甚至把这件事讲得很直接:Skill 存放的位置,本身就是它的作用域设计。

这背后的工程含义非常大。

如果你把一个强项目耦合的 Skill 放进个人目录,它就会带着这个项目的假设跑到别的仓库里;反过来,如果你把一个本该跨项目复用的通用 Skill 只塞在项目目录里,它的复用价值又被锁死了。

在 monorepo 里,这件事更有意思。Claude Code 会自动发现子目录下的 .claude/skills/。也就是说,你完全可以让 packages/frontend/.claude/skills/ 只服务前端包,让 packages/backend/.claude/skills/ 只服务后端包,而不是把所有知识都堆在仓库根目录。

这时 Skill 就不只是“提示词文件”,而是团队知识的分发机制:

  • 个人层的 Skill,固化的是你的工作习惯
  • 项目层的 Skill,固化的是团队约定
  • 包级 Skill,固化的是模块边界里的局部知识

如果你能把这层想清楚,很多“这个规则到底该放哪”的问题,答案会比只看内容本身更清楚。


六、如果只停在经验层,这篇还差半口气:我后来去翻了源码

前面这些判断,靠经验其实也能总结出来。

但我后来还是不太满足。因为有几个问题如果不看实现,心里总会悬着:

  • description 到底是不是自动触发的关键?
  • allowed-tools 到底只是提示,还是硬限制?
  • paths 到底是真过滤,还是只是写给人看的说明?

我后来去翻了一遍源码,结论是:这些字段比我一开始以为的更“硬”。

1. 为什么触发逻辑主要看 frontmatter,而不是正文

loadSkillsDir.ts 里有一个函数 estimateSkillFrontmatterTokens,注释写得非常直接:

/**
 * Estimates token count for a skill based on frontmatter only
 * (name, description, whenToUse) since full content is only loaded on invocation.
 */
export function estimateSkillFrontmatterTokens(skill: Command): number {
  const frontmatterText = [skill.name, skill.description, skill.whenToUse]
    .filter(Boolean)
    .join(' ')
  return roughTokenCountEstimation(frontmatterText)
}

这段代码背后的意思非常重要。

Claude Code 启动时,主要只把每个 Skill 的 frontmatter 信息算进上下文。Skill 正文不是一开始就全量塞进去,而是在你真正触发它的时候才加载。

这直接解释了两件事。

第一,Claude 不是先把你整篇 Skill 读完再判断要不要触发,它先看的就是前面这几行。换句话说,触发效果主要取决于 frontmatter,不取决于正文写得多漂亮。

第二,Skill 多不等于上下文立刻爆炸,因为启动时压进去的不是全文,而是 frontmatter。

源码里保留了 whenToUse 这个概念,但从现在的文档实践看,推荐做法已经更偏向把触发描述直接写进 description。所以对大多数人来说,最稳的策略不是纠结“要不要额外写一个触发字段”,而是把 description 写得具体、可命中、带触发场景。

比如:

description: Review TypeScript backend code before merging. Use when asked to review code, check a PR, or verify implementation before committing.

比“代码审查 Skill”这种描述强太多了。

源码注释里其实还暗含了一个很实用的提醒:触发描述不是越长越稳。冗长的 whenToUsedescription 不会线性提高命中率,很多时候只是在白白消耗首轮缓存和注意力。所以对这个字段最好的优化,不是“多写一点”,而是“把真正会命中的词放到前面”。

2. 为什么 allowed-tools 不是建议,而是权限边界

这一点是我看源码之后感受最强的一处。

Skill 执行时,getPromptForCommand 会在返回内容之前把 allowedTools 写进工具权限上下文:

getAppState() {
  const appState = toolUseContext.getAppState()
  return {
    ...appState,
    toolPermissionContext: {
      ...appState.toolPermissionContext,
      alwaysAllowRules: {
        ...appState.toolPermissionContext.alwaysAllowRules,
        command: allowedTools,
      },
    },
  }
}

这说明 allowed-tools 不是“提醒 Claude 尽量这样做”,而是权限层的强制限制。

比如一个 code review Skill 只开放 ReadGrepGlobBash(git diff *),那它就不是“理论上不该写文件”,而是从架构上根本没有写文件的能力Bash(git diff *) 这种写法也不是装饰,它真的只允许 git diff 开头的命令,其他 Bash 调用会被挡住。

这让我对 allowed-tools 的理解完全变了。它不是“不信任模型”,而是最小权限设计。就像你给数据库只读账号只开 SELECT 权限,不是因为你怀疑这账号会作恶,而是因为这个任务本来就不该拥有写权限。

3. 为什么 paths 不是说明文字,而是条件激活机制

源码里,带 paths 的 Skill 在加载时会被单独分流到一个 conditionalSkills Map:

// Separate conditional skills (with paths frontmatter) from unconditional ones
for (const skill of deduplicatedSkills) {
  if (skill.type === 'prompt' && skill.paths && skill.paths.length > 0
      && !activatedConditionalSkillNames.has(skill.name)) {
    newConditionalSkills.push(skill)
  } else {
    unconditionalSkills.push(skill)
  }
}

// Store conditional skills for later activation when matching files are touched
for (const skill of newConditionalSkills) {
  conditionalSkills.set(skill.name, skill)
}

// 最后只返回无条件的 Skill
return unconditionalSkills

这段逻辑的含义是:带 paths 的 Skill,根本不会像普通 Skill 一样直接进入启动时上下文。它会先待在一个“条件激活区”里,只有当你在会话里碰到了匹配路径的文件,它才会被真正激活。

这点对复杂项目非常有价值。

比如你给支付模块写一个 paths: src/payment/** 的 Skill,在你处理用户系统、文章系统、管理后台时,这个 Skill 对 Claude 几乎是隐身的。只有当你真的进入 src/payment/ 相关文件,它才“出现”。

这也是我现在很认同的一种团队实践:不要在根目录堆一个什么都想管的大 Skill 集合,而是让复杂模块在自己的目录附近维护自己的 Skill。

4. 为什么大型项目不该只在根目录维护一套总 Skill

还有一个很容易被忽略,但工程上非常实用的机制:Claude Code 会从当前文件所在目录一路向上寻找 .claude/skills

源码大概是这样:

// Walk up to cwd but NOT including cwd itself
while (currentDir.startsWith(resolvedCwd + pathSep)) {
  const skillDir = join(currentDir, '.claude', 'skills')
  // ...check if exists, then load
  currentDir = dirname(currentDir)
}

// Sort by path depth (deepest first) so skills closer to the file take precedence
return newDirs.sort((a, b) => b.split(pathSep).length - a.split(pathSep).length)

这里最关键的是最后一行:deepest first。也就是说,越靠近当前文件的 Skill,优先级越高。

这意味着你放在 src/auth/.claude/skills/ 里的 Skill,可以自然覆盖根目录下更通用的同名 Skill。对 monorepo 或大仓库来说,这个机制非常好用:

  • packages/api/.claude/skills/ 可以放 API 专属 Skill
  • packages/web/.claude/skills/ 可以放前端专属 Skill
  • 根目录只保留真正的全局规则

如果把上面四点放在一起看,设计 Skill 的顺序其实会变得很清楚:

  • 先把 frontmatter 写准,再去打磨正文步骤
  • 先按最小权限收紧 allowed-tools,再考虑要不要给更多能力
  • 只有模块边界明确时再上 paths,不要为了“高级”硬加
  • 多目录项目优先做“离代码更近”的局部 Skill,而不是维护一个大而全的总模板

5. 为什么长对话里,Skill 不会轻易“失忆”

还有一个很多人会担心的问题:会话一长、上下文一压缩,前面调过的 Skill 会不会就悄悄失效了?

从实现思路看,Claude Code 不是简单把它们扔掉,而是会把最近调用过的 Skill 重新注入压缩后的上下文。工程上你可以把它理解成:Skill 不是“一次触发完就全靠模型自己记住”,而是一个可以被系统再次带回来的工作单元。

当然,这也不是说你可以无限制地把 Skill 写成超长文档。实现上会有保留预算,比如最近调用的 Skill 只会保留前一段核心内容,而不是把所有正文永久塞在上下文里。所以前面那条原则依然成立:把 frontmatter 写准,把正文写短,把真正长的材料拆到 reference 或脚本里。

Skill 运行机制与条件激活


七、不是所有 Skill 都应该让 Claude 自动触发

到这里,其实已经够你写出一个基础可用的 Skill 了。

但如果你真的准备在项目里长期用,接下来有一个问题迟早会遇到:

这个 Skill 到底应该让 Claude 自动触发,还是只能我手动触发?

这背后其实对应两种完全不同的 Skill。

1. 参考型 Skill:给 Claude 补充背景知识

这类 Skill 的作用不是“执行一个任务”,而是“把某个项目知识注入到当前工作里”。

比如 API 设计规范、错误处理约定、数据库命名规则。这些东西你希望 Claude 在写代码、改代码、review 代码时,只要场景合适就自动想起来。

这类 Skill 的特点是:

  • 倾向自动触发
  • 内容会留在主对话上下文里
  • 更像“局部规范”而不是“独立任务”

比如:

---
name: api-conventions
description: API design patterns and conventions for this codebase. Use when writing or reviewing API endpoints.
---

响应格式统一用 { success, data, timestamp }
禁止在 controller 层直接写 SQL,通过 service 层操作
所有异步函数必须有 try-catch,错误统一 throw new AppError()

2. 任务型 Skill:给 Claude 一个要完成的动作

这类 Skill 有明确边界,通常还可能带副作用。

比如 /deploy/send-release-email/prepare-release-notes/migrate-db。这类 Skill 更像一段可执行流程,而不是知识注入。

这类 Skill 的特点是:

  • 通常应该手动触发
  • 有副作用时最好加 disable-model-invocation: true
  • 有风险时再配 context: fork

比如:

---
name: deploy
description: Deploy the application to production. Manual trigger only.
context: fork
disable-model-invocation: true
allowed-tools:
  - Bash(npm run test)
  - Bash(npm run build)
  - Bash(git push *)
---

1. 跑完整测试:npm run test
2. 确认测试全绿后构建:npm run build
3. 推送到部署分支
4. 等待 CI 完成并检查健康状态

这里的关键不是字段多了,而是触发权变了。

你真正要想清楚的问题是:

这件事我愿不愿意让 Claude 自己判断“现在该触发了”?

如果答案是“不愿意”,那就不要让它自动触发。

3. disable-model-invocation: trueuser-invocable: false 不是一回事

这是一个很容易混淆,但又非常关键的区别。

默认情况下,你和 Claude 都可以调用一个 Skill。你可以手动 /skill-name,Claude 也可以在觉得合适时自动加载它。

但很多人会把下面两个字段混为一谈:

  • disable-model-invocation: true
  • user-invocable: false

它们看起来都像“限制调用”,其实限制的是两件完全不同的事。

disable-model-invocation: true 的意思是:Claude 不能自己触发,只有你能手动触发。 这类 Skill 适合部署、发版、发邮件、推送消息这类你必须自己掌握时机的动作。更重要的是,它还会把这个 Skill 的描述从 Claude 的常驻上下文里拿掉,平时的上下文成本直接归零,只有你手动调用时才完整加载。

user-invocable: false 的意思则是:你不能从 / 菜单里把它当命令来点,但 Claude 仍然可以在合适时自动用它。 这类 Skill 更适合背景知识,比如老系统架构、内部缩写、遗留约定。这些东西你希望 Claude 在相关任务里自动想起来,但你并不需要一个显眼的 /legacy-context 命令天天挂在菜单里。

所以更准确的判断应该是:

  • 有副作用、要你亲自控制时机:disable-model-invocation: true
  • 只是背景知识、不适合被人手动当命令点:user-invocable: false
  • 想限制 Claude 到底能不能调用某些 Skill:去配权限规则,而不是只盯菜单显示

这组区别值得写进脑子里,因为它直接决定了 Skill 的“触发权”到底属于谁。

参考型 Skill 与任务型 Skill


八、Skill 的天花板:从静态模板到可执行能力

前面几节讲的,主要还是“怎么把经验写成一个好模板”。但如果 Skill 只能放静态文字,它的上限其实并不高。

真实项目里,很多任务依赖的是实时信息:当前 PR 的 diff、评论区讨论、今天的测试结果、最新 schema、CI 状态。你当然可以在 Skill 里写“先去看这些东西”,但这样一来,最关键的一步又变回 Claude 自己先兜一圈去找。

Claude Code 里真正更有意思的地方是:Skill 不只是 prompt 模板,它还可以在触发瞬间先准备上下文。

1. 动态注入:先拿真实数据,再交给 Claude

比如你要做一个 PR 总结 Skill,不一定要让 Claude 先自己去猜该看哪些信息,你可以直接让 Skill 触发时先把它们准备好:

---
name: pr-summary
description: Summarize the current pull request. Use when asked to review or summarize a PR.
context: fork
allowed-tools:
  - Bash(gh *)
---

## 当前 PR 信息
- diff:!`gh pr diff`
- 评论区讨论:!`gh pr view --comments`
- 涉及文件:!`gh pr diff --name-only`

## 你的任务
基于以上真实数据,总结这个 PR 做了什么、为什么改、有哪些潜在风险。

这个 Skill 真正触发时,前面的命令会先执行,输出直接注入到 prompt 里。Claude 拿到的不是“请你去看看 PR”这种模糊要求,而是已经准备好的真实上下文。

这也是为什么前面说 scripts/ 不是装饰。如果一个 Skill 需要先查状态、先取数、先做一轮预处理,那它就不再只是“告诉 Claude 怎么做”,而是在执行前把材料也一起备好了。更复杂一点时,你完全可以把逻辑放进 scripts/,再通过 CLAUDE_SKILL_DIR 去调用目录里的脚本,让 Skill 触发时先跑一轮取数或整理。

2. 这件事为什么重要:它决定了 Skill 的上限

一旦你理解了动态注入这层能力,就会发现 Skill 的上限根本不只是“几行 prompt”。

它可以同时承担三件事:

  • 定义触发条件
  • 限制工具权限
  • 在执行前准备实时上下文

换句话说,Skill 不是只能做静态模板,它完全可以长成一个带入口、带约束、还能主动取数的局部能力单元。

不要只把 Skill 当成“写给模型的一段话”,而要把它当成“一个局部工作流的入口”。

3. 从架构位置看,Skill 不是 Tool,也不是 Agent

如果再往上抽一层,我现在对 Skill 的理解是:

  • Tool 是原子能力
  • Skill 是任务知识和操作规约
  • Agent 是执行与编排单元

这个分层不一定是官方唯一表述,但作为工程心智模型非常有用。因为一旦把 Skill 放到 Tool 和 Agent 中间去理解,很多判断都会顺下来:

  • 该写成脚本的,别硬写进 Skill 正文
  • 该放进 CLAUDE.md 的全局规则,别误沉淀成任务 Skill
  • 该拆给 SubAgent 的复杂协作,别硬让一个 Skill 扛完

Skill 真正做的事,不是替代 Tool,也不是替代 Agent,而是把“什么场景下、按什么步骤、调用哪些能力”组织成可复用的任务单元。


九、真正让 Skill 变靠谱的,不是写出来,而是验证出来

这是我觉得官方 best practices 里最容易被忽略、但最能拉开水平差距的一点。

很多人写 Skill 的方式是:先凭感觉写一版,再上项目里试试。这样当然也能跑,但它很容易陷进一种错觉里: 你以为自己在“优化 Skill”,其实只是一直在补想象中的问题。

官方更推荐的思路其实更工程化:

先准备评测样例,再写 Skill。

最轻量、也最实用的做法,是先找出三个真实场景:

  • 一个 Claude 原本就能做得不错的
  • 一个 Claude 容易漏步骤的
  • 一个 Claude 容易理解偏、触发错或者输出不稳的

先不用 Skill 跑一遍,记录基线。看它具体错在哪:

  • 是没想到要用这个 Skill
  • 是触发了,但没读对参考资料
  • 是步骤顺序不稳
  • 是输出格式飘了
  • 是该脚本处理的确定性工作,被它自己“猜”过去了

然后再写最小版本的 Skill,只补刚才暴露出来的那几个缺口,而不是一上来把所有可能性都写满。

把这个过程压缩成一句话,其实就是:

  1. 先找失败样例
  2. 再写最小 Skill
  3. 再看它是不是真的修掉了失败样例

如果三轮下来都没有明显提升,那大概率不是 Skill 写得不够多,而是这个问题根本不该沉淀成 Skill。

不要只看结果,还要看 Claude 是怎么“导航”这个 Skill 的

只看最终结果对不对还不够,更重要的是看 Claude 在过程中到底是怎么用这个 Skill 的。

官方专门建议观察 Claude 实际怎么使用 Skill,而不是只看最终结果对不对。比如:

  • 它是不是总在错误顺序里读文件
  • 它是不是老是忽略某个引用文件
  • 它是不是每次都反复读同一个 reference,那这部分也许该往 SKILL.md 主体里提
  • 某个 examples 文件是不是从来没被读过,那它可能根本没价值,或者信号太弱

这些观察特别重要,因为它能直接反推出信息架构是不是合理。

我现在会把一个 Skill 是否成熟,简单看成四个问题:

  • 会不会在该触发的时候触发
  • 触发后会不会按预期去读材料
  • 执行过程会不会漏掉关键步骤
  • 输出结果能不能稳定复现

如果你团队里会混用不同模型,官方还建议至少跨你计划使用的模型测一遍。不是因为所有模型都得兼容,而是因为有些 Skill 对提示强度和结构依赖更高,换模型后会暴露出你原来没看到的问题。

说到底,Skill 的成熟度,不是靠“我觉得写得挺全”来判断,而是靠一组真实任务能不能稳定跑通来判断。


十、写 Skill 时,最常见的四种设计模式,以及最容易踩的反模式

这里先说清楚:下面这四种名字,不是官方文档逐字给出的固定术语,而是我结合 Anthropic 官方 best practices 做的工程化归纳。

但它们确实能覆盖大多数项目里真正会遇到的 Skill 设计问题。

1. 模板驱动模式:解决“输出不稳定”

这类 Skill 的核心不是让 Claude 更会想,而是让它更稳定地按结构输出。

适合:

  • 周报
  • PR 描述
  • 事故复盘
  • 评审报告

它的关键不是“给一个模板”,而是把模板当成输出接口。模板负责格式,SKILL.md 负责路由和规则。模板里不要塞判断逻辑,也不要把一个模板写成一套小程序。

如果一个模板已经长到一百多行,而且开始出现大量条件分支,通常不是你模板写得认真,而是职责已经混了。

2. 脚本增强模式:解决“结果不稳定”

这类 Skill 的核心是:确定性的事交给脚本,不要交给模型猜。

适合:

  • 指标统计
  • CSV 解析
  • 正则匹配
  • Git / PR / CI 状态抓取
  • 需要预处理的上下文准备

官方对这件事的说法很直白:solve, don't punt。能脚本算出来的,就别只写一句“请 Claude 自行分析”。

这类 Skill 真正提高的,不是文风,而是可靠性。你把概率型推理替换成确定性执行,整个 Skill 的下限会明显抬高。

3. 知识分层模式:解决“上下文过载”

这是官方反复强调的 progressive disclosure 思路。

也就是:SKILL.md 只做入口和导航,把大块知识拆进 reference/examples/forms/ 这类文件里,按需读取,而不是一次灌满。

这类模式适合:

  • 领域知识很多的 Skill
  • 不同子领域差异很大的 Skill
  • 需要高级用法、边界情况、案例库的 Skill

它的关键不是“拆文件”,而是“拆得有层次”。官方明确不建议深层嵌套引用。最稳的做法是所有重要材料都从 SKILL.md 一层直达,别让 Claude 从 advanced.md 再跳 details.md 才看到真正关键信息。

4. 工具隔离模式:解决“能力边界失控”

这类模式最容易被低估。

很多人写 Skill 时只关注“让它能做事”,但真正稳定的 Skill 往往同样重视“让它不能乱做事”。

适合:

  • 部署
  • 发版
  • 写数据库迁移
  • 调外部系统
  • 会改文件、发消息、推远端的任务

这一类 Skill 的核心组合通常是:

  • allowed-tools 收到最小
  • 必要时配 context: fork
  • 不希望自动触发时加 disable-model-invocation: true

它不是在限制 Claude 的创造力,而是在设计这个能力包的安全边界。

最常见的反模式

如果把前面四种模式反过来看,最常见的坑基本也就集中在下面这些地方:

  • 一个 Skill 管太多事,什么都想覆盖,最后什么都不够准
  • description 写得很空,只写“处理文档”“帮助分析”这种谁都能套上的话
  • 一上来给一堆方案,不给默认路径,让 Claude 自己从五六种做法里摇摆
  • 模板里写判断逻辑,或者把本该脚本做的事丢给模型推理
  • reference 层级太深,真正关键信息藏在第二跳、第三跳文件里
  • 把会过期的信息硬写进 Skill,比如时间敏感规则、旧接口切换说明
  • 默认假设工具和依赖都已经装好,结果一跑就断
  • 在路径里写 Windows 反斜杠,跨环境直接出问题

你会发现,所谓反模式,本质上就是一句话:

该约束的地方没约束,该拆开的地方没拆开,该交给脚本的地方还在让模型猜。


十一、完整案例:把一个通用 code review 模板,提炼成你项目真正需要的 Skill

上面说了这么多抽象原则,不如走一遍完整例子。

假设你们团队每周都做后端代码审查,而且总在重复盯这几件事:

  • 有人改一个功能,顺手动了三个不相关模块
  • 新同学不知道项目里统一用 AppError,直接 throw new Error()
  • Promise 链里漏了 await
  • 数据库查询没有索引,或者潜在 N+1 没被看出来

这就是非常典型的“该沉淀 Skill 的信号”。

第一步:先确认痛点到底是什么

这一步别着急写模板,先把“你们到底在反复出什么问题”说清楚。

很多团队的问题不是“没有 code review”,而是每次 review 的注意力都被分散了。真正高频出错的点,永远是那几类项目特有的约束。

所以要沉淀的不是“代码审查”这四个字,而是你们团队在代码审查里最容易漏掉的那几类检查。

第二步:从公开模板里提取真正有用的部分

这时候公开模板就有用了,但它的用途不是直接上生产,而是当素材库。

假设你找到一个 50 行的通用 code review 模板。你真正该提取的,可能只有下面这几类东西:

  • 逻辑正确性,尤其是异步操作
  • 项目约定的遵守,比如 AppError、错误处理模式
  • 数据库相关的风险,比如 N+1、索引、查询范围
  • 改动范围是否聚焦,不要顺手改不相干文件

剩下那些跟你们项目关系不大的部分,就应该果断删掉。

第三步:把它压缩成一个真正能用的 Skill

最后落地出来的 Skill,应该更像这样:

---
name: code-review
description: Review TypeScript backend code before merging. Use when asked to review code, check a PR, or verify implementation before committing.
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash(git diff *)
argument-hint: "[PR 分支名或文件路径]"
---

# Code Review Skill

## Steps
1. 读 $ARGUMENTS 的改动 diff,确认改动是否只涉及这个 PR 的范围(不要顺手改无关文件)
2. 检查错误处理:所有 throw 都必须是 throw new AppError(),不能 throw new Error()
3. 检查异步操作:Promise 链是否有遗漏的 await,错误是否被正确 catch
4. 检查数据库查询:是否有 SELECT * 的懒惰写法,是否明显的 N+1 查询,关键查询是否 explain 过

## Output Format
Issues found (Critical → Warning → Info):

🔴 **Line 45**: Missing `.select()` in Prisma query - this will fetch unnecessary columns
🟡 **Line 67**: Potential N+1: loop inside `posts.map()` should use `Promise.all()`**No AppError violations** — all errors properly handled

Summary: 1 critical issue to fix before merge.

## Caveats
- 不审查 UI 层代码(只关心后端逻辑)
- 不关注代码风格(那是 prettier 的事)
- 如果一个改动涉及多个不相干功能,分别提交 PR 再 review

你会发现,到这一步之后,Skill 就不再是“通用模板的中文版”了,而是你们项目真正有用的一个局部工作流。

50 行公开模板,最后可能只剩下 4 个真正属于你项目的核心关注点。但恰恰是这 4 个点,才决定它到底值不值得用。

第四步:在真实使用里继续迭代

Skill 从来不是一次写完的。

比如你用了两周之后,又发现一个常见问题:改了数据库 schema,但忘记更新 Prisma 类型。那就把它加进去:

4.5. 检查 Prisma 类型:如果改了数据库,Prisma schema 和生成的 types 是否都已更新

这时候你会发现,Skill 真正的价值不是“第一次写出来”,而是在真实工作里被持续打磨。


十二、Skill 的维护节奏,比第一次写出来更重要

Skill 不是写好就扔。

如果你写完之后三个月不看,它很快就会从“项目经验”重新退化成“历史遗留文档”。

我更推荐一个更贴近实际的轻量节奏:

前一两周
高频使用,快速迭代。每次用完就问自己三个问题:步骤是不是太复杂?输出是不是太啰嗦?有没有漏掉今天刚踩到的新坑?

稳定之后每周一次
回顾一次。看看最近有没有经常被遗漏的步骤,有没有新的痛点需要加入。对 Skill 这种高频小迭代的东西来说,按周看通常比按月看更合适。

每个月做一次清理
把已经不再是问题的注意事项删掉,把那些已经变成全局共识的规则移进 CLAUDE.md。这一步的重点不是加内容,而是防止 Skill 越写越胖。

Skill 应该越用越精炼,而不是越写越臃肿。


十三、一个特别反直觉,但很重要的经验:第一个 Skill,故意别写最重要的任务

这条我非常想单独拿出来讲。

因为很多人第一次沉淀 Skill,会本能地想挑一个最关键的任务,比如“生产环境发布前检查”“数据库迁移前审查”“支付流程改动 review”。

但工程上更稳的做法,其实正好相反。

大多数人写的第一个 Skill,质量都不会太高。触发条件偏模糊,步骤偏啰嗦,输出格式也不够稳定。这很正常,因为你第一次做这件事时,对“什么是好的 Skill”还没有直觉。

如果你一开始就把它用在最关键的任务上,一旦写得不够好,伤害会非常直接:要么关键场合出问题,要么你从此对这套机制失去信心。

更好的策略是:先拿一个重要程度中等、容错率比较高的任务练手。

比如:

  • 写周报
  • 生成 PR 描述
  • 做一次常规 code review

先跑两周,迭代两三次,等你对 Skill 的节奏有感觉了,再去沉淀真正关键的流程。

第一个 Skill 的目的,不是直接解决最大的问题,而是让你学会怎么写 Skill。


十四、如果你今天就想开始,可以直接做这三个动作

别先想着搭一整套系统,直接从最小动作开始:

任务一: 列出你最近一个月里重复做过三次以上的任务。用第一节的判断法(对所有任务成立?→ CLAUDE.md;只对某类任务成立?→ Skill;只对这次成立?→ Prompt),确认你要处理的是 Skill 还是 CLAUDE.md 的事。

任务二: 为这个任务写一个 Skill。先想清楚四个问题(什么时候用、按什么顺序、输出长什么样、什么时候不适用),然后包装成 SKILL.md。目标是精炼——大多数好 Skill 的有效指令不超过 10 行。记住:第一次的目的是练手,不是写出完美的 Skill。

任务三: 选三个你最近真的遇到过的场景,先不用 Skill 跑一遍,再用 Skill 跑一遍,对比它到底有没有少漏步骤、少返工、少补充解释。然后在 SKILL.md 里补一行今天发现的问题(哪个步骤漏了,或者哪个注意事项需要加)。这就是 Skill 的维护节奏,也是最轻量的评测方法。

真正好的 Skill,几乎都不是第一次就写对的,而是在实际使用里慢慢长出来的。


下篇预告

第 06 篇:Sub-agents 实战——什么时候应该拆任务,怎么设计子任务边界

单个 Claude 实例有上下文上限,复杂任务拆成多个子任务让 Sub-agents 并行处理,理论上能大幅提速。但什么时候值得拆?拆错了会有什么代价?下一篇会拆开 Sub-agents 的真实适用场景,以及最常见的过度设计陷阱。


写在最后

公开 Skill 模板当然有用,但它的价值更像脚手架,而不是成品。

你不需要一个很复杂的 Skill 系统。你需要的,往往只是把团队最容易反复犯错的那几件事提前写下来,让 Claude 每次都替你盯住。

如果你现在想动手,就直接做这三步:列出最近一个月里重复做过三次以上的任务;用"三问法"确认它该放 CLAUDE.mdSkill 还是 Prompt;先写一个只有 5 到 10 行的版本,实际用一次,再立刻改第一轮。不要追求第一版完美——Skill 的价值,从来不是"写出来的那一刻",而是"它开始帮你减少重复错误的那一天"。

评论区想聊一个问题:你现在最卡的,到底是"写不出规则",还是"没分清哪些该放 CLAUDE.md、哪些该做成 Skill"? 这两个问题看起来很像,但解法完全不同。

觉得这篇有帮助,点个赞让更多工程师看到 👍


AI Coding 系列持续更新。用别人的 Skill 模板是起点,不是终点。真正管用的 Skill,只有你自己的项目才能提炼出来。

3月北京二手房成交近2万套,创近15个月新高

北京市住房和城乡建设委员会发布的最新数据显示,今年3月,北京二手房成交19886套,创下过去15个月以来单月最高纪录。业内人士表示,今年一季度多种因素叠加,楼市交易活跃,出现交易回暖行情。(央视新闻)

硅基同事埋的坑,我用2小时才填平:Nuxt 4 路由踩坑:可选参数 [[id]] 与 [id] 的区别

个人网站

在开发博客系统时,遇到了一个路由不生效的问题:/section 可以访问,但 /section/id 却始终无法匹配。折腾了一番后发现是 Nuxt 文件路由的可选参数语法理解有误。

问题背景

需求很简单:

  • /blog → 显示博客列表,默认选中第一篇文章
  • /blog/kubernetes-1-32-release → 显示博客列表,选中指定文章

最初创建了两个路由文件:

pages/
├── [section].vue        # 匹配 /blog
└── [section]/
    └── [id].vue         # 匹配 /blog/xxx

结果:/blog 正常,/blog/kubernetes-1-32-release 却始终匹配不上。

问题原因

最初采用了两个独立文件的方式:

pages/
├── [section].vue        # 匹配 /blog
└── [section]/
    └── [id].vue         # 匹配 /blog/xxx

这种结构看似合理,实则存在多个问题:

  1. 代码重复:两个文件 95% 代码相同,维护成本高
  2. 状态同步:需要额外处理跨页面状态共享
  3. 路由匹配:某些情况下 Nuxt 无法正确区分两个路由,导致 /blog/xxx 匹配失败

解决方案

Nuxt 提供了可选路由参数语法 —— 双括号 [[param]],用一个文件同时处理两种情况:

pages/
└── [section]/
    └── [[id]].vue    # 同时匹配 /blog 和 /blog/xxx

核心区别

语法 含义 匹配示例
[id] 必需参数 /blog/abc ✅ / /blog
[[id]] 可选参数 /blog/abc ✅ / /blog

[[id]] 是 Nuxt/Vue Router 的特殊语法,表示该参数可以存在也可以不存在

实现方案

合并后的 [[id]].vue

<script setup>
const route = useRoute()
const section = route.params.section
const articleId = route.params.id  // 可能为 undefined

// 有 id 用 id,没有则用 firstArticleId
const activeArticleId = ref(articleId || firstArticleId)

// 监听路由变化(SPA 导航时更新)
watch(() => route.params.id, (newId) => {
  if (newId) activeArticleId.value = newId
})

// 点击文章时更新 URL
const handleSelectArticle = (id) => {
  activeArticleId.value = id
  navigateTo(`/${section}/${id}`, { replace: true })
}
</script>

关键点

  1. route.params.id 可能为 undefined:需要提供默认值
  2. 添加路由监听:SPA 内导航时 URL 变化不会重新执行 setup,需要 watch
  3. navigateTo 更新 URL:选中文章时同步 URL,支持分享和书签

调试技巧

在排查过程中,发现 Nuxt 4 的 console.log 在 SSR 阶段可能被过滤。一个实用的做法是在 composable 中添加显眼前缀:

export function useContentArticles(section: string) {
  console.log('>>> [useContentArticles] section:', section)
  console.log('>>> [useContentArticles] cache keys:', Object.keys(sectionDataCache))
  // ...
}

终端输出:

>>> [useContentArticles] section: blog
>>> [useContentArticles] cache keys: [ 'blog', 'interview', 'nuxt4' ]

总结

场景 推荐方案
单一页面 + 可选子路径 [[id]].vue
完全不同的两个页面 分开两个文件
参数必需 [id].vue

可选参数 [[param]] 是 Nuxt 文件路由的利器,用好了可以大幅减少代码重复。但要注意处理 undefined 的情况和路由变化的监听。

延伸阅读
nuxt4完整系列,持续更新中...

内容有帮助?点赞、收藏、关注三连!评论区等你 💪

拿下豪华种子轮,一家明星AI公司宣布倒闭

作者/吴琼

报道/投资界PEdaily

唏嘘一幕。

近日,知名AI创业公司Yupp宣布:将停止服务,于4月15日正式关闭。要知道,这距离Yupp产品上线才不到一年时间。

曾经,Yupp的故事颇具前景:瞄准AI模型评测赛道——通过免费模型服务吸引用户评测,再将测评数据卖给模型厂商。犹记得2024年,公司拿下3300万美元(约合2.2亿元)的豪华种子轮,身后聚集a16z合伙人、Google首席科学家、Twitter联合创始人等超45位天使投资人。

只是,Yupp却以极为荒诞的方式失败:投资人的钱还没花完,AI技术的演进就让其市场不复存在。而这一切,就发生在短短几个月之间。

拿下豪华种子轮,一家明星AI公司宣布倒闭

从成立到关门,Yupp只存活了22个月。

故事始于一支精英团队;创始人兼CEO Pankaj Gupta职业生涯遍布Twitter、Google,Coinbase等硅谷最顶尖的科技公司,且此前就有成功创业经验;联合创始人兼AI负责人Gilad Mishne曾是GoogleX的机器学习负责人;首席科学家Jimmy Lin是麻省理工学院博士、滑铁卢大学教授。

早在2010年,三人于Twitter相识,共同构建和优化大规模推荐与搜索系统,成为此后共同创业的契机。

直至2024年6月,他们共同成立Yupp。创业的理由很简单:彼时AI竞争激烈,全球涌现的大模型层出不穷,Yupp最初的设想是帮助用户比较和选择最佳模型,以应对AI可能带来的幻觉问题。

而另一边,大模型表现不仅取决于算力与算法表现,同样依赖人类反馈,AI企业往往基于这些反馈数据来优化、改进模型。他们正是从这两边需求中找到机会,创建一家AI模型评估平台。2025年6月,Yupp正式上线。

说起来,Yupp的商业逻辑很简单:用户在平台输入问题后,平台将从数百个AI模型池中抽取两个,并展示两份答案,用户可以反馈哪些模型更好并给出理由;完成反馈后,平台会随机给予用户一定数量的积分,相应积分可用于继续调用AI模型,还可以兑换成现金。

这里聚集了市面上最炙手可热的大模型,从ChatGPT、Claude,到Gemini、DeepSeek,还有Grok、Llama……超500种模型,任意选择,全部免费。

对用户来说,只需要在一个平台上就能体验多种模型,不仅免费还能收获一些零花钱,于是Yupp迅速走红。官方显示,目前Yupp已经吸引超过130万用户注册。

精英团队加上风头正劲的大模型赛道,投资人迅速集结。成立不久,Yupp就完成由a16z合伙人Chris Dixon领投的种子轮融资,金额高达3300万美元(约合人民币2亿元)。即便放眼硅谷,这样的大额种子轮也并不多见。

更为豪华的还有跟投阵营——谷歌首席科学家Jeff Dean、Twitter联合创始人Biz Stone、Pinterest联合创始人Evan Sharp、Perplexity首席执行官Aravind Srinivas、Cred首席执行官Kunal Shah、斯坦福大学的四位教授(Dan Boneh、Chris Re、Nick McKeown、Balaji Prabhakar)……超45位天使投资人,几乎聚集了AI圈最权威的面孔。

“Yupp的设计将人类的判断转化为一种可再生经济资源。随着新的交互不断涌现,数据会‘过期’,从而形成一个良性循环:更多的使用带来更及时的评估;更及时的评估催生出更优秀的模型;更优秀的模型吸引更多用户。”对于这笔投资,彼时a16z言语之中充满��待。

但这一切在几个月后戛然而止:Pankaj Gupta和Gilad Mishne共同在社交平台宣布:停止Yupp的服务。如今再点开Yupp官网,基于文本、图像、编码等多个分类,赫然躺着多个大模型排行榜。只是,它们再也不会更新。

谁杀死了它?

“我们的产品与市场契合度不够高”,Yupp告别博客中写到。

很难想象,一家AI创业公司最后却因AI发展太快而倒闭。在Pankaj Gupta的描述中,“仅在过去一年时间内,AI模型的能力格局发生了巨大变化,而且还将继续快速变化。未来不仅仅是模型,而是Agent系统。”

2025年Yupp上线时,AI行业的主流交互方式还是Chatbot——用户输入提示词,模型返回文本。这一模式下,“哪个模型回答得更好”确实有其市场。但AI的进化速度显然超过他们的预期。

短短几个月,行业风向发生巨大变化,Agent成为行业主题。正如年初爆火的“龙虾”OpenClaw,用户不再只是获取信息,而是需要事情被完成。相对应的,底层模型需要连接数百种工具和子系统,最终协同完成任务。

还没到达顶峰,Yupp就落入用户、客户双双流失的境地。

一方面,Yupp吸引用户的方式简单粗暴——免费且有利可图。但当头部模型的回答质量差距缩小,用户通过Yupp进行多模型比较的需求自然下降。

另一方面,虽然人类反馈数据对AI模型的后训练至关重要,但是Yupp提供的仅仅是普通消费者在免费使用时随手点击的偏好。而模型厂商的主流选择是,与Scale AI、Mercor等头部玩家合作,用PhD级别的专家提供高质量的强化学习反馈。Yupp与对手们提供的数据质量,显然不在一个量级。

尤其当下,对AI的评判标准变为“哪个Agent能帮我更好地完成任务”,相反,对于Chatbot层面的模型评估就显得没那么重要了。但对于Agent的评估,并非过去简单对比两段文字就能完成,而是需要在真实工作场景中验证,已经不再是普通用户所能实现的。

如此,Yupp终究难以生存下去。

那场豪华种子轮,意外成了Yupp最后的高光时刻。唏嘘的是,Yupp甚至没来得及用完融资的钱。“剩余资金将返还给我们的投资人”,Pankaj Gupta写到。一段创业故事就此结束。

AI洗牌潮,99%创业项目将消亡

“人间一年,AI一天”,大家如此形容当下AI演进速度。

自ChatGPT横空出世以来,全球掀起一场AI创业浪潮。AI为许多行业带来全新的想象空间,也带来过往难以想象的创业机会。中国信通院数据显示,截至2025年9月,全球人工智能企业达到37664家。未来几年,这一数据仍将以指数级上升。

这也带来了AI时代最魔幻一幕。

一边,AI正以前所未有的速度,催生新一批高估值公司。2025年全年诞生的新晋独角兽中,有六成是AI独角兽。许多公司从成立到成为百亿估值独角兽,不过短短一两年时间,这在过往互联网时代是难以想象的。

但另一边,AI技术爆发式迭代,从文本生成到图像、视频创作,从被动问答到主动执行任务的智能体系统,行业每隔数月就迎来一次范式革新。高压之下,许多AI初创公司从高光登场到黯然落幕的周期也被极度压缩。

稍不注意,就被AI快车抛下了。

最近轰动的当属Sora,这是OpenAI推出的文生视频模型Sora。2024年初,Sora以一段长视频引爆全网,就连马斯克也发出一句意味深长的感慨:“人类愿赌服输”。但两年过去,等来的却是一纸关停信号。

仅仅存活25个月,Sora“猝死”的理由很残酷。据美国《福布斯》杂志估算,Sora项目每年的运行成本高达50多亿美元,单月算力成本突破千万美元级别;但自上线以来,Sora应用程序内的总收入仅约210万美元,高昂的运维成本与微薄的收入之间形成鲜明对比。

一边是种子选手黯然退场,一边是新技术仍在以近乎疯狂的速度迭代突破。新旧交替之间,只留下无尽唏嘘。AI赛道的残酷正在于此:竞争白热化到甚至来不及为倒下的玩家驻足默哀,下一轮技术浪潮便已汹涌而至。

第一批AI项目开始倒闭了。早些时候,曾被看作是欧洲AI行业希望之星的Robin AI,被挂上了破产网站。究其原因,虽其瞄准的“AI+法律”行业颇具前景,但Robin AI里的功能,完全可以通过Claude等完成;缓慢的技术迭代速度更是成为原罪,使其逐渐与竞争对手拉开差距。最后,Robin AI被资本集体抛弃了。

想起几个月前,硅谷一篇名为《99%的AI初创公司将在2026年消亡》的文章刷屏。作者Srinivas Rao直言:“当下的AI繁荣,不过是互联网泡沫的翻版。”

他以2000年互联网浪潮为例,当时互联网被认为会改变世界,后来证明确实如此。电子商务、搜索引擎、社交媒体、在线服务,都在之后二十多年里迅速发展。当时同样涌现互联网创业潮,但很多企业在技术真正成熟之前,就因为资金、商业模式、竞争等种种问题被淘汰。

这样一幕也会在AI重演。“企业的发展轨迹与技术的发展轨迹有着天壤之别”,桥水基金创始人瑞·达利欧此前判断,“这类周期的常态是,技术会一直向前,但初期绝大多数企业都会倒闭,只有极少数能够存活。”

保持敬畏,且行且珍惜。

本文来自微信公众号“投资界”,作者:吴琼,36氪经授权发布。

凯文沃什获特朗普提名出任下一任美联储主席,提名确认程序恐遭延误

凯文·沃什被唐纳德·特朗普提名担任下一任美联储主席,但其提名的确认程序面临延误风险,可能要等到杰伊·鲍威尔现任任期结束后才能完成,随着时间愈发紧迫,特朗普挑选的这位美联储掌门人候选人正面临日益吃紧的时间表。距离鲍威尔第二任主席任期届满仅剩一个月,颇具影响力的参议院银行委员会仍未为沃什安排确认听证会。(新浪财经)

CSS 技巧:CSS 单位使用指南

写完《现代 CSS 中的相对单位》这节课之后,我原本以为大家会带着一种“原来如此”的轻松感继续往下写代码。但很快,私下就有不同的同学问:为什么这里不用 px ?什么时候该用 remvw0% 到底有什么区别?移动端布局什么该用 rem 还是 vw ?这些问题不仅在一个地方出现,而是在不同平台、不同时间被反复提起。

于是,我决定单独用一节课,把这些零散的问题集中起来,好好聊一聊。我们不再孤立地看某一个单位,而是把 pxemrem% 、视窗单位(如 vwvh)、容器查询单位(如 cqwcqh)以及 exchlh 放在同一个语境下,重新理解它们的作用,以及更重要的——如何做出选择。

CSS 单位选择的本质:不是用什么,而是跟随谁变化

在实际开发中,很多人一开始都会本能地选择 px。它足够简单、直接,几乎不需要额外思考——设计稿量多少就写多少,浏览器呈现出来的结果也高度一致。这种“所见即所得”的确定性,在项目初期确实非常有吸引力。

但随着项目逐渐复杂,这种看似稳妥的选择,往往会开始暴露出问题。页面在不同设备上看起来似乎“差不多”,却总差那么一点协调感;字体系统变得难以统一,局部调整容易牵一发动全身;为了适配各种尺寸,不得不引入越来越多的媒体查询;而组件一旦脱离原有上下文,复用时也常常出现不可预期的表现。

很多人会因此怀疑,是不是一开始就不应该使用 px 。但从结果来看,问题并不在于某一个具体单位,而在于我们没有在一开始建立一套清晰的尺寸关系

这正是这节课真正要解决的核心问题。CSS 单位表面上是在描述“长度”,但它们更本质的作用,其实是在定义一种关系——元素的尺寸,应该跟随变化

当你写下一个值时,本质上是在做一次“绑定”。使用 px ,意味着它几乎不跟随外部环境变化;使用 % ,它会依赖父元素;使用 em ,它会响应当前元素的字体大小(font-size);使用 rem ,则是跟随根元素(html);视窗单位(如 vwvh)绑定的是浏览器的视窗,而 cqwcqh 这样的容器查询单位,则让元素直接跟随其所在的父容器。

也就是说,你并不是在选择“用哪种单位更好”,而是在决定这个元素应该“听谁的”。是跟随屏幕变化,还是跟随父级容器?是依赖全局排版系统,还是保持自身的稳定?一旦换一个角度去看,很多原本让人纠结的问题,其实会自然消失。

你会开始意识到,px 并不是错误,它只是选择了不响应变化;rem 也不是万能方案,它只是把变化集中在一个全局入口;vw 看起来很灵活,但有时会让组件失去边界感;而容器查询单位,则是在回答一个更现代的问题——组件是否可以只关心自己所在的空间,而不是整个页面。

接下来,我们不会从定义出发逐个讲解每个单位,而是先建立一套“尺寸关系模型”,再去分析每种单位适合绑定的对象,最后总结出一套可以直接应用在项目中的选择策略。目标不是让你记住所有单位的细节,而是让你在任何场景下,都能快速判断——这里的尺寸,究竟应该跟随谁变化

CSS 单位的尺寸关系模型

如果把前面所有零散的讨论收拢在一起,其实可以得到一个非常关键的结论:

CSS 单位并不是一堆彼此独立的“语法选项”,它们本质上构成了一整套尺寸依赖关系系统

一旦从这个角度去理解,就可以把所有单位放进一个统一的框架中来看待,也就是所谓的“CSS 单位的尺寸关系模型”。

这个模型的核心可以用一句话概括:每一个尺寸,本质上都是依附在某个参照物上的结果。 你写下的从来不是一个孤立的数值,而是一种“绑定关系”。也就是说,当你在写 CSS 时,你真正做的事情并不是定义一个长度,而是在决定这个长度应该“跟谁产生关系”。

基于这个视角,我们就可以把所有常见单位重新整理一遍。不再按语法分类,而是按照它们“依附谁”来划分。这样整理之后,会得到一个非常清晰的结构——一个由不同“关系层级”组成的尺寸模型

绝对关系:不跟随任何对象

在所有尺寸关系中,“绝对关系”是容易理解的一类,它几乎不依附于任何外部对象。典型代表就是 px 。当你使用 px 时,本质上传达的是一种明确的意图:这个尺寸应当保持稳定,不随着环境变化而改变。

这种特性让 px 在很多场景下非常有价值。比如边框、阴影、精细对齐等细节控制,都需要高度确定性和可预测性,这正是 px 的优势所在。它提供了一种接近“所见即所得”的体验,让你可以精确地掌握视觉结果。

但问题在于,一旦把这种“固定性”扩展到布局层面,比如用在容器尺寸、间距系统或整体排版上,就会开始削弱页面的弹性。页面难以自然适配不同设备,响应能力变差,后期往往需要额外的补丁来弥补。

所以,与其把 px 简单理解为“绝对单位”,不如换一个更本质的视角来看——它其实是一种主动拒绝建立关系的选择

全局关系:跟随根元素

在“在局关系”这一类中,所有尺寸都会统一依附于同一个源头——根元素(html)。最典型的代表就是 rem 。当你使用 rem 时,本质上传达的是一种明确的选择:这个尺寸不由局部环境决定,而是完全跟随全局系统

这种机制非常适合用于构建设计系统。无论是字体层级、间距体系,还是组件的基础尺寸,都可以基于 rem 来建立。一旦调整根元素的字号(font-size),整个页面就会按照既定比例整体缩放,从而实现一致且可控的响应效果。

不过,这种“全局绑定”也有一个前提:你必须建立清晰且合理的全局规则。如果没有这一步,rem 并不会减少复杂度,只是把原本分散在各处的问题,集中转移到了全局层面。换句话说,rem 并不是简化问题的工具,而是放大设计系统价值的工具。

局部关系:跟随当前上下文

“局部关系”这一类单位,强调的是尺寸与当前上下文之间的依赖关系,而不是与整个页面或全局系统的绑定。典型代表是 em% 。其中,em 相对于当前元素的字体大小(font-size),而 % 通常相对于父元素的尺寸计算。需要知道的是,% 是一个较为复杂的单位,它应用在不同属性值时,依附的参照物是不同的。

当你使用这类单位时,其实是在表达这样一种意图:这个元素的大小,不由全局决定,而是由它所处的环境来决定。换句话说,它就是“就地适应”的。

这种特性在组件内部尤其有用。例如按钮、卡片这类 UI 元素,其内边距、间距、子元素尺寸等,都可以随着组件自身的变化而自然缩放,从而形成更具弹性的结构。

不过,这种“局部依赖”也带来一个隐患:它是可以层层嵌套的。一旦组件结构变得复杂,多层 em% 叠加之后,最终的计算结果往往不再直观,甚至难以追踪。因此,在使用这类单位时,需要对层级关系保持足够的控制,避免无意中引入过深依赖链。

环境关系:跟随视口

“环境关系”这一类单位,直接把尺寸绑定到浏览器视口,也就是屏幕本身。典型代表是 vwvh ,以及 vminvmax 等。使用这些单位时,元素的尺寸应当随着屏幕大小按比例变化——屏幕多大,它就多大。

这种能力在很多场景中非常有用。比如全屏布局(如首屏视觉区域)、流式字号,以及一些强依赖视觉比例的设计,都可以通过视口单位获得非常自然的响应效果。

不过,一旦把这类单位用在组件内部,就容易引发问题。因为此时组件的尺寸不再由自身或其容器决定,而是直接被整个页面环境“接管”。结果就是,组件失去了应有的独立性,在不同布局或上下文中变得难以控制和复用。

容器关系:跟随父容器

“容器关系”代表的是一种更现代的尺寸绑定方式——元素不再依赖整个视口,而是直接依附于自身所在的父容器。典型单位是 cqwcqh 等容器查询单位。

当你使用这类单位时,这个元素的尺寸,不再由屏幕决定,而是由它当前所处的空间来决定。也就是说,它只关心“自己有多少可用空间”,而不是整个页面有多大。

这正好解决了传统响应式设计中一个核心限制——媒体查询只能基于视口进行判断,但在实际开发中,组件更关心的往往是自身容器的尺寸,而不是屏幕尺寸本身。

容器单位的出现,让响应式能力从“页面级”下沉到了“组件级”。组件不再依赖外部布局或全局断点,就可以根据自身环境自动调整,从而真正具备独立、自适应的能力。这也是现代 CSS 组件化设计中非常关键的一步。

内容关系:跟随文本与排版度量

“内容关系”这一类单位,关注的已不再是布局结构,而是文本本身的度量。典型代表包括 chexlhrlh 。与前几类不同,它们不依赖视口、容器或父元素,而是直接绑定到字体和排版系统之上。

当你使用这些单位时,其实是在表达一种更偏向内容驱动的设计思路。尺寸应当由文本本身来决定,而不是由外部布局强加。比如,ch 基于字符宽度,常用于控制理想的阅读行长;ex 反映字体的 x-heightlhrlh 则直接与行高(line-height)相关,用来建立稳定的垂直节奏。

这种方式的价值在于,它把“排版经验”转化成了“系统规则”。例如,用 60ch 控制段落宽度,可以自然得到更舒适的阅读长度;用 1lh 控制段落或模块之间的间距,可以让整体节奏始终保持 一致,而不需要反复微调具体数值。

从更高层来看,这类单位所代表的,是一种设计理念的转变:不是让内容去适配布局,而是让布局围绕内容生长

比例关系:无单位

这一类并不是在描述“跟随谁变化”,而是在定义变化本身。与前面所有单位不同,它不关心依附对象,而是直接规定变化的比例,因此,可以把它理解为一种“比例关系”。典型的例子包括:

line-height: 1.5;
flex: 1;
opacity: 0.8;

这些值有一个共同特点:它们表达的不是一个具体长度,而是一个倍数关系。换句话说,它们关注的不是“多大”,而是“多少倍”。以最常见的 line-height: 1.5 为例:

p {
    font-size: 16px;
    line-height: 1.5;
}

这里的 1.5 实际含义是:行高等于当前字体大小的 1.5 倍。如果字体大小从 16px 变成 20px,行高会自动变成 30px

关键在于,这种关系是“内嵌”的,而不是“引用”的。它不像 em 那样依赖字体作为单位,也不像 rem 依赖根元素,而是直接把比例规则写进属性本身。这种表达方式更直接,也更稳定。

正因为如此,在很多场景中,无单位数值反而是更推荐的写法,尤其是在排版中。相比固定的 24px,或者可能受嵌套影响的 1.5emline-height: 1.5 不依赖具体单位,不会产生层级累积问题,还能随着字体变化自动调整。同时,它的语义也更清晰——表达的是“阅读密度”,而不是一个具体尺寸。

这种“比例关系”其实在 CSS 中无处不在,并不仅仅局限于排版。在 Flex 布局中:

.item {
    flex: 1;
}

表达的是空间分配的比例,而不是具体宽度。

在 Grid 中,虽然 fr 看起来像单位,但本质上同样是一种比例系统:

grid-template-columns: 1fr 2fr;

表示的是 1:2 的空间分配关系。再比如透明度、变换等属性:

opacity: 0.5;
scale: 1.2;

同样是在描述相对变化,而不是绝对数值。

从这个角度来看,无单位数值其实承担的是另一种角色:它不负责“绑定关系”,而是负责在既定关系中,定义变化的幅度

在理解了前面的所有关系之后,其实还可以再往前走一步,把整个模型进一步抽象。换一个更简洁的视角来看,CSS 中的所有尺寸,本质上都在回答两个问题:它是否需要变化?如果需要变化,它应该跟随谁变化。

第一个问题是在区分“固定”和“响应”。有些尺寸需要保持稳定,比如边框或精细对齐,这时选择的是“不变化”;而有些尺寸则需要随着环境调整,这时就进入“响应”的范畴。第二个问题则是在确定依附对象——是跟随全局系统、局部上下文、视口环境、容器空间,还是内容本身。

一旦用这种方式思考,单位选择就不再是一个依赖经验或记忆的过程,而是一个可以被推导的决策过程。你不再需要记住“某种场景用某种单位”,而是先明确关系,再自然得出答案。

这也正是“尺寸关系模型”真正重要的地方。很多开发中的问题,并不是单纯因为“单位选错了”,而是关系选错了:在应该使用全局关系的地方使用了局部关系,在应该依赖容器的地方却绑定了视口,在应该围绕内容排版的地方却用了固定值。结果就是,系统逐渐失去一致性,适配变得困难,维护成本不断增加。

而这个模型的价值,在于提供了一个统一的判断框架:先决定关系,再选择单位。一旦顺序颠倒——先选单位,再试图去适配各种场景——问题往往就会不断出现。

最佳使用场景

从这里开始,CSS 单位就不再是一组零散的工具,而是一整套可以被设计、被推理、也可以被复用的系统。你不再是在零碎地选择某一个单位,而是在基于一套清晰的关系模型,去构建整个页面的尺寸逻辑。

接下来,更关键的一步,是把这套模型真正落到实际项目中。也就是说,在具体场景里,你到底该使用哪一类单位。与其去记忆“某种场景对应某种单位”,不如换一种更稳定的思考方式——从“关系”出发,反推出“选择”。一旦你明确了这个元素应该跟随谁变化,单位本身几乎是自然浮现的。

因此,接下来的内容,我们会基于这套“尺寸关系模型”,按顺序逐一拆解每一类单位在真实项目中的最佳使用场景。重点不在于记住规则,而在于理解背后的逻辑,并能够在不同情境中灵活应用。

当你理解到这一层时,CSS 单位这件事就不再是一个依赖经验的选择题,而是一个可以建模、可以推导,甚至可以系统化设计的能力。这一步,基本已经进入“设计系统思维”的范畴了。

px :用于“需在稳定、不参与响应”的细节

px 最适合的使用场景,其实并不在布局层面,而是在那些需要稳定、不参与响应变化的细节上。换句话说,当一个尺寸的变化不会带来收益,甚至可能破坏视觉一致性时,px 往往是最合理的选择。

典型的例子包括细边框(如 1px0.5px)、图标对齐的微调、阴影与描边、分割线,以及一些小范围的视觉校正。这些细节的共同点在于:它们依赖精确的像素控制,一旦随着环境变化而缩放,反而容易产生模糊、偏移或不协调的视觉效果

从这个角度来看,在这些场景中使用 px,反而是一种更“响应式”的选择——因为你在有意识地避免不必要的变化,让关键细节保持稳定。

但需要注意的是,这种优势并不适用于布局层面。如果把 px 用作主要的布局单位,比如容器宽度、模块间距或字体尺寸,一旦页面进入多端适配阶段,这些“固定值”就会迅速变成负担,限制整体的弹性和可扩展性。

rem:用于“全局一致”的系统级尺寸

rem 最适合的使用场景,是构建设计系统中的“全局一致”尺度。它的核心价值在于,把所有尺寸统一绑定到根元素,从而让整个页面在同一个规则下运作。

在实际项目中,rem 常用于定义全局字体体系、统一的间距系统,以及组件的基础尺寸(例如 paddingmargingap)。这些场景有一个共同特点:它们需要在整个页面中保持一致,并且能够随着整体策略一起缩放

例如,你可以基于 rem 定义一套间距系统:

:root {
    --space-1: 0.5rem;
    --space-2: 1rem;
    --space-3: 2rem;
}

当根元素的字号发生变化时,这些间距会自动按比例调整,而不需要逐个修改组件。这种方式可以极大地降低维护成本,同时保证视觉上的一致性。

不过需要注意的是,rem 更适合用于“系统层”,而不是“局部例外”。如果某个组件需要根据自身环境动态变化,比如根据容器或上下文自适应,那么继续使用 rem 反而会让它变得僵硬。换句话说,rem 擅长统一规则,但并不擅长处理局部差异。

em% :用于“组件内部的自适应关系”

em% 这一类单位,最适合用于构建组件内部的自适应关系。它们的特点是依赖当前上下文,而不是全局系统,因此非常适合用来描述组件内部各个元素之间的比例关系。

例如,一个按钮组件可以这样定义:

.button {
    font-size: 1rem;
    padding: 0.5em 1em;
}

这里的 em 实际上传达的是一种关系:按钮的内边距应该随着字体大小变化。也就是说,组件内部的空间是“绑定”在自身排版之上的,而不是写死为某个固定值。

类似的使用场景还有很多,比如图标与文字之间的比例关系(使用 em),子元素宽度占父元素的比例(使用 %),以及卡片内部结构中常见的 100% 宽度元素等。这些都属于在组件内部建立相对关系的典型用法。

这类单位的核心价值在于,让组件具备“自我缩放”的能力。当组件被放入不同的上下文中,比如更大的容器、不同的字号环境,或者不同的布局结构时,它可以自然适配,而不需要额外调整具体数值。

不过需要注意的是,这种依赖关系是可以层层叠加的。一旦嵌套层级过深,em 的计算路径就会变得复杂,最终结果也不再直观。因此,在复杂结构中使用时,需要有意识地控制层级,避免关系链失控。

视窗单位:用于“与屏幕强绑定”的布局或视觉

vwvh 这类视窗单位,最适合用于那些与屏幕强绑定的布局或视觉设计。它们直接以浏览器视口为参照,因此表达的是一种非常明确的关系:尺寸应该随着屏幕变化,而不是由容器或内容决定。

在实际项目中,这类单位常见于全屏布局、流式字号,以及一些依赖屏幕比例的视觉效果。例如:

.hero {
    height: 100vh;
}

或者:

h1 {
  font-size: clamp(2rem, 5vw, 4rem);
}

这些用法的共同点在于:它们让元素的尺寸直接响应视口变化,从而在不同设备上保持一种整体的视觉比例

不过需要注意的是,这种“直接绑定屏幕”的能力,并不适合用在组件内部。一旦在组件中使用 vwvh,组件的尺寸就会脱离自身上下文,被整个页面环境所控制。这会导致组件难以复用,也更难在不同布局中保持稳定表现。因此,视口单位更适合作为页面级或视觉级的工具,而不是组件级的默认选择。

窗器单位:用于“真正可复用的响应式组件”

cqwcqh 等容器查询单位,最核心的使用场景是构建真正可复用的响应式组件。与视口单位不同,它们并不依赖整个页面,而是直接绑定到组件所在的父容器。

例如一个卡片组件:

.card {
    width: 100%;
    padding: 5cqw;
}

这里表达的是一种非常清晰的关系:组件的尺寸不由屏幕决定,而是由它当前所处的容器空间决定。也就是说,组件只关心“自己有多少可用空间”,而不关心整个页面的大小。

这种能力在很多场景中尤为重要。比如同一个组件需要出现在不同布局中(侧边栏、主内容区或网格中),或者组件需要根据可用空间自动调整结构,又或者希望减少对媒体查询的依赖。这些问题,用传统的视口响应方式往往很难优雅解决,而容器单位则可以直接应对。

从更高层来看,容器查询单位的意义在于,让组件具备一种“环境感知能力”,但这个环境是局部的,而不是全局的。组件不再依赖页面断点,而是根据自身所处的上下文自我调整。这也可以说是现代 CSS 从页面级响应式迈向组件级响应式的关键一步。

chlh 等:用于“排版与阅读体验驱动”的设计

chlh 等这类单位,最适合用于那些以排版和阅读体验为核心的内容区域。与前面侧重布局的单位不同,它们直接绑定到文本本身的度量,因此更适合解决“读起来是否舒服”的问题。

典型的应用场景包括:控制文章正文的宽度(例如 max-width: 60ch)、设置段落之间的间距(如 margin-bottom: 1lh),以及建立整体的垂直节奏。这些场景有一个共同点:尺寸不应该由布局决定,而应该由内容本身来驱动

例如:

.article {
    max-width: 65ch;
    line-height: 1.6;
}

通过 ch 控制行长,可以直接得到更理想的阅读宽度,而不需要反复尝试具体数值。再比如:

p {
    margin-bottom: 1lh;
}

使用 lh 来定义间距,可以让段落之间始终与当前的文本节奏保持一致。

这类单位的真正价值,在于它们把原本依赖经验的排版决策,转化为可以复用的系统规则。你不再需要不断“微调数值”,而是可以通过单位本身,让设计自然成立。

无单位值

无单位数值最适合用于那些本质上是在表达“比例关系”,而不是具体尺寸的场景。与其他单位不同,它不依附于某个参考系,而是直接定义一种倍数规则,因此特别适合用来描述变化幅度、分配关系或节奏控制。

在实际开发中,这类写法广泛存在于排版、布局和视觉效果中。例如在排版中:

p {
    line-height: 1.6;
}

这里的 1.6 表达的是行高与字体大小之间的比例关系,而不是一个固定高度。这样做的好处是,无论字体如何变化,行高都会自动按比例调整,从而保持稳定的阅读节奏。这类用法的共同点在于:它们关注的不是尺寸本身,而是尺寸之间的关系。

无单位数值的核心价值在于,它把设计中的“比例”和“节奏”直接表达出来,而不会受到单位或上下文嵌套的干扰。相比使用固定值或依赖单位的写法,它通常更加稳定、可预测,也更贴近设计本意。

从更高层来看,这类写法实际上是在把“经验规则”转化为“数学关系”。你不再是在不断调整一个个具体数值,而是在定义一套可以自动生效的比例系统。这也是它在现代 CSS 中越来越重要的原因。

如果把前面所有使用场景再压缩成一条清晰的决策路径,其实可以用一个非常简单的思考顺序来判断:首先,这个尺寸是否需要变化?如果答案是不需要,那么直接使用 px,让它保持稳定即可。

如果这个尺寸需要变化,接下来要问的就是——它应该跟随谁变化。是跟随全局系统(rem),还是组件内部(em%),是跟随屏幕(vwvh),还是跟随容器(cqwcqh),又或者是跟随内容本身(chlh)。当这个问题有了明确答案,单位的选择几乎是自然得出的结果。

当你用这种方式做决策时,会慢慢意识到一件事:你不再是在“选单位”,而是在设计一套尺寸关系。一旦关系是清晰的,代码本身就会变得更加稳定、可预测,同时也更容易维护。这才是 CSS 单位真正的使用方式。

不过,到这里还只是第一步。如果你已经接受了“尺寸关系模型”的视角,那么接下来一个更贴近真实开发的问题就会浮现出来:在实际项目中,我们几乎不会只使用一种单位——那不同类型的单位应该如何组合使用?

这一步,才是真正体现 CSS 能力的地方。因为一旦进入“混合使用”,你所做的就不再只是选择某一种关系,而是在有意识地组合多种关系,让它们在同一个系统中协同工作。

混合使用的本质:叠加多个“跟随逻辑”

在讨论“混合使用”之前,需要先把一件事说清楚:混合单位并不是随意拼凑,而是在同一个元素上,有意识地叠加不同层级的“依附关系” 。每一种单位,都在负责一部分逻辑,而不是彼此冲突。

举个最简单的例子:

.card {
    padding: 1rem 2em;
}

这段代码里,其实同时存在两种关系。1rem 绑定的是全局系统,用来保证整体的一致性;而 2em 则跟随组件内部的字体大小,用来维持局部的协调性。

从结果来看,这并不是混乱,而是一种分工:全局负责统一规则,局部负责自适应细节。正是这种“关系的叠加”,让组件既能融入整体系统,又能在自身内部保持灵活性。

最常见的组合:全局 + 局部(rem + em

这是最推荐、也最稳定的一种组合方式:让不同层级的关系各司其职,而不是互相干扰。一个常见且有效的模式是——用 rem 定义基础尺寸,用 em 来做组件内部的比例调整。

例如一个按钮组件:

.button {
    font-size: 1rem;       /* 全局控制 */
    padding: 0.5em 1.2em;  /* 局部跟随 */
    border-radius: 0.5rem; /* 系统统一 */
}

这里的结构其实非常清晰。按钮的整体尺寸(如字体大小、圆角)是绑定在全局系统上的,因此可以保持一致性;而内部的间距(padding)则依赖自身字体,从而能够随着组件大小自然缩放。

这种组合方式的核心在于分层:全局负责统一规则,局部负责内部协调。最终的效果是,组件既能融入整体设计系统,又能在自身范围内保持灵活性——既统一,又不僵硬。

布局 + 内容:%fr + chlh

在内容型页面(比如文章或文档)中,一种非常高效且稳定的组合方式是:用布局单位控制结构,用内容单位控制阅读体验。也就是说,先解决“内容放在哪里”,再解决“内容读起来是否舒服”。

例如:

.layout {
    display: grid;
    grid-template-columns: 1fr min(65ch, 100%);
}

在这个结构中,1fr 负责的是整体布局的空间分配,让页面具备弹性;而 65ch 则用来限制文本的最大行长,从而保证阅读的舒适度。两者关注的层面完全不同,却可以自然协同。

再比如段落间距的处理:

.article p {
    margin-bottom: 1lh;
}

这里使用 lh 来控制间距,使其直接跟随文本节奏变化,而不是依赖固定数值。

从整体来看,这种组合的核心在于分工明确:布局单位决定“块在哪里、占多少空间”,而内容单位决定“读起来是否舒适” 。当两者各自负责自己的问题时,页面既能保持结构上的灵活性,又能在阅读体验上更加稳定自然。

响应式组合:rem + vw

这是现代响应式设计中非常常见、也非常实用的一种组合方式。它的核心思路可以概括为一句话:在“稳定”和“流动”之间找到平衡

例如:

h1 {
    font-size: clamp(2rem, 5vw, 4rem);
}

这段代码实际上融合了多层关系。2rem 作为最小值,提供一个基于全局系统的稳定下限;5vw 作为中间值,让字体能够随着视口变化而动态调整;而 4rem 则作为最大值,防止尺寸在大屏上无限增长而失控。

从本质上来看,这种写法并不是简单地混用单位,而是在构建一套“有边界的响应规则”:允许尺寸根据环境变化,但同时为这种变化设定清晰的上下限。这样既能获得流动性的优势,又能避免不可控带来的问题。

这种模式的价值在于,它让响应式不再是“要么固定,要么完全流动”的二选一,而是变成一种可控、可设计的连续变化过程。

组件级响应:容器单位 + rem

当你开始使用容器查询单位时,一种非常自然、也非常实用的组合方式就会出现:让容器决定结构,用 rem 维持系统一致性

例如:

.card {
    padding: 2rem;
    gap: 2cqw;
}

这里其实是两种关系在协同工作。rem 用来定义基础间距,确保组件始终遵循整体设计系统;而 cqw 则根据容器宽度进行微调,让组件在不同空间下都能做出合适的调整。

这种组合的关键在于分工清晰:全局系统负责“统一语言”,容器关系负责“适应环境” 。最终的结果是,组件既能够在不同容器中灵活变化,又不会偏离整体设计风格,从而同时具备一致性和适应性。

固定 + 弹性:px + 其他单位

在实际开发中,“完全响应式”并不总是目标。有些细节本身就需要保持稳定,比如 1px 的边框、精细的阴影、或者分隔线。这些元素一旦随着环境变化,反而容易破坏视觉的清晰度和一致性。

因此,一种非常常见且合理的做法是,把不同类型的尺寸分开处理。例如:

.card {
    padding: 1rem;
    border: 1px solid #ddd;
}

这里的 rem 用来控制整体结构,让组件能够随着全局系统调整;而 px 则用来锁定细节,确保边框始终保持清晰和稳定。

这种组合方式的核心,其实是一种取舍:让该变化的部分具备弹性,让不该变化的细节保持稳定。当这两者各自发挥作用时,页面既有响应能力,又不会失去精致度,这是一种非常健康且实用的平衡。

calc():显式组合关系

当你需要更精细地控制多种尺寸关系时,可以借助 calc() 把这些关系“写出来”。它的作用,不只是做简单计算,而是把原本隐含在设计中的逻辑,转化为清晰可读的表达。

例如:

.container {
    padding: calc(1rem + 2vw);
}

这里实际上是在组合两种关系:1rem 提供一个稳定的基础间距,而 2vw 让间距可以随着屏幕略微放大。也就是说,这个间距既有全局一致性,又具备一定的响应能力。

再比如:

.box {
    height: calc(100vh - 4rem);
}

这里表达的是另一种常见逻辑:用视口高度作为整体空间,再减去一个固定的导航高度,从而得到实际可用区域。

从本质上看,calc() 的价值在于,它让不同关系之间的组合变得明确且可控。你不再需要在脑中“估算”这些关系,而是可以直接用数学形式把它们表达出来,从而让代码本身就成为设计逻辑的一部分。

当你逐渐熟悉这些单位的组合方式之后,会开始看到一个更高层的模式——不同单位,其实在解决不同“层级”的问题。换句话说,它们并不是随意选择的工具,而是在各自的层面上承担不同职责。

例如,px 更偏向视觉细节层,用来处理那些不应该变化的精细部分;rem 属于设计系统层,负责全局的一致性;em% 则工作在组件内部,用于建立局部的自适应关系;vwvh 面向页面环境,与屏幕尺寸直接相关;cqw 这样的容器单位属于组件环境层,让组件根据自身空间变化;而 chlh 则作用于内容层,用来优化排版和阅读体验。

当这些层级被理清之后,一个非常重要的原则也会变得清晰:在混合使用单位时,最容易出问题的情况,是多个单位在“争夺同一个控制权”。比如,一个尺寸既想跟随视口(vw),又想跟随容器(cqw);或者字体既由 rem 控制,又在嵌套中被 em 不断放大。这种情况下,结果往往会变得不可预测,也更难维护。

为了避免这种问题,可以用一个非常简单但实用的判断方式:在写下任何一个尺寸之前,先问自己一句——这个值,主要由谁来决定? 如果答案是唯一的,那么直接选择对应的单位即可;如果答案涉及多个因素,那么就需要通过组合(例如 clamp()calc(),或者分层设计)来明确各自的作用边界。

当你开始有意识地“混合关系”,而不是简单地“混合单位”时,你会发现一个明显的变化:CSS 不再是一个依赖不断试错的过程,而是变成了一套可以被设计、被推导的系统。这一步,其实就是从“会写 CSS”,走向“会设计 CSS”的关键转变。

小结

当你把这些内容全部走完,再回头看最开始的问题——“到底该用 pxrem,还是 vw?”——其实已经不那么重要了。

因为你已经不再依赖某一个“正确答案”,而是拥有了一套可以自己推导答案的思考方式。

你会开始从“关系”出发,而不是从“单位”出发;先判断这个尺寸应该跟随谁,再去选择合适的表达方式。很多过去需要反复试错的问题,也会在这个过程中自然消失。

从更长远来看,这种思维方式的价值,并不仅仅局限在 CSS 单位上。它本质上是在训练你用一种更结构化的方式去理解界面:把页面拆成不同层级,把尺寸拆成不同关系,再通过组合让它们协同工作。

当你做到这一点时,CSS 就不再只是“写样式”,而是在构建一套有逻辑的系统。

而这,也正是从“能实现设计”,走向“能设计系统”的关键一步。

崔东树:3月的A00级电动车剧烈萎缩带来均价的上升,2026年销量增长压力较大

36氪获悉,乘联分会秘书长崔东树今日发文称,常规燃油车价格持续上升,市场萎缩在中低端很明显,而高端萎缩慢,因此从2021年的16.6万元上升到2024年的18.8万元。2025年随着高端燃油车的较快萎缩,2025年的均价降低到18.2万元,2026年3月均价18.1万元,燃油车购买群体买车逐步稳定。中国汽车普及仅有254台/千人,而汽车消费普及是未来最重要趋势,电动化的低成本带来车价持续下降是趋势。目前随着车购税免税政策退出后,新能源车免税技术指标的升级,部分短续航和高耗电车型面临改款压力。3月的A00级电动车剧烈萎缩带来均价的上升,2026年销量增长压力较大。

Node.js 如何判断入口文件:从 require.main 到 ES Module 实现

目标:判断当前文件是否被直接执行(而不是被 import)


一、CommonJS(对照)

if (require.main === module) {
  main();
}

二、ES Module 写法(核心)

import { fileURLToPath } from 'url';

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  main();
}

三、关键点(只记这 3 个)

  • import.meta.url → 当前文件(URL)
  • fileURLToPath() → 转为本地路径
  • process.argv[1] → 启动入口文件路径

👉 判断本质:
入口路径 === 当前文件路径

四、推荐封装

import { fileURLToPath } from 'url';

export const isMain = (meta) => process.argv[1] === fileURLToPath(meta.url);

// 使用:
if (isMain(import.meta)) {
  main();
}

五、关键技术点拆解(用于深入理解)

1️⃣ import.meta.url

返回当前模块的 URL,例如:

file:///Users/demo/index.js

2️⃣ fileURLToPath

file:// URL 转换为本地路径:

/Users/demo/index.js

3️⃣ process.argv[1]

Node 启动时的入口文件路径:

node index.js

# 得到:/Users/demo/index.js

中信证券:流动性冲击扰动,互联网板块关注业绩韧性与AI主线

36氪获悉,中信证券研报称,进入2026年以来,互联网板块受AI叙事变化、互联网公司重回投入期带来对短期盈利节奏的担忧,以及地缘政治事件带来的市场风偏下降和流动性收紧等因素影响,估值回落至历史低位。展望2026年,考虑到流动性扰动的不确定性,建议关注低估值、业绩确定性较高、现金流稳健及股东回报较好的公司;同时,AI产业趋势仍在加速演绎,建议关注模型能力迭代、Agent范式创新以及业绩兑现带来AI叙事下的催化。

怎么在VS Code 调试vue2 源码

总结一下怎么在VS Code 调试vue2 源码

  • 克隆vue2源码到本地
  • 在源码根目录安装依赖 把项目跑起来 生成/dist/vue.js文件
  • 在/examples/ 下随便找个文件(引入的文件要是我们生成的vue.js) 打上断点
  • 安装Live Server插件 把我们打上断点的文件在浏览器打开
  • 在.vscode文件夹下配置launch.json
  • 点击VS Code的Run and Debug图标 就可以进入了

我们开始吧~

克隆vue2源码到本地

去Github克隆源码,克隆后我们用VS Code打开。

git clone https://github.com/vuejs/vue

image.png

在源码根目录安装依赖 把项目跑起来
pnpm i

image.png

image.png

把项目跑起来

npm/pnpm run dev

image.png

bundles /Users/gongzemin/Documents/GitHub/vue/src/platforms/web/entry-runtime-with-compiler.ts → dist/vue.js...

entry-runtime-with-compiler.ts 这个入口文件打包生成 dist/vue.js 这个最终可用的 Vue 文件

生成了dist文件夹 里面有vue.js

image.png

在examples/ 下随便找个文件 打上断点

我们找examples/classic/commits/app.js 在如图位置打上断点

image.png

commits/index.html 这个文件引入了vue.min.js, 我们刚才构建出来的是vue.js文件,我们把引入的文件改成vue.js

<script src="../../../dist/vue.js"></script>
安装Live Server插件 把我们打上断点的文件在浏览器打开

image.png

安装好插件后,打开文件的上下文菜单 可以看到Open with Live Server

image.png

这样我们就可以打开我们的examples/classic/commits/index.html 文件了 是用服务器打开的

image.png

在.vscode文件夹下配置launch.json

注意这里的URL是我们的要调试URL路径

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Run parser",
      "runtimeExecutable": "parser",
      "cwd": "${workspaceFolder}/packages/reactivity-transform/node_modules/@vue/compiler-core",
      "args": []
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "调试Vue源码",
      "url": "http://localhost:5501/examples/classic/commits/index.html",
      "webRoot": "${workspaceFolder}",
      "sourceMaps": true,
      "sourceMapPathOverrides": {
        "webpack:///./*": "${webRoot}/*",
        "webpack:///packages/*": "${webRoot}/packages/*",
        "*": "${webRoot}/*"
      },
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}
点击VS Code的Run and Debug图标

点击Run and Debug图标, 选择调试Vue源码(就是我们配置launch.json里面配置的name)

image.png 看到app.js 进入我们打的断点了

image.png

我们点击Step Into

image.png

就进入Vue()构造函数了

image.png

调试vue3源码方法也一样 参考这篇笔记

❌