普通视图

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

深度实践:Spec-Coding,AI 独立完成复杂 UI 开发的可行性方案

2026年1月13日 11:51

核心协作流程图

graph TD
    %% 阶段 0: 启动与深度探测
    Start[输入原型 URL / 新页面需求] --> HookB_Probe[激活 Hook B: 视觉协议桥接]
    
    subgraph MCP_Layer [MCP 感知层: AI 的五感]
        MCP_Nav[navigate_page: 访问原型]
        MCP_Snap[take_snapshot: DOM 审计]
        MCP_Shot[take_screenshot: 视觉采样]
    end

    HookB_Probe --> MCP_Nav
    MCP_Nav --> MCP_Snap
    MCP_Snap --> MCP_Shot

    %% 阶段 1: Spec 生成与动态校准
    MCP_Shot --> Spec_Gen[生成结构化 Spec & Task.md]
    
    subgraph Dynamic_Alignment [动态校准闭环]
        Spec_Gen -->|循环采样探测| MCP_Snap
        MCP_Snap -->|特征反馈| Spec_Gen
    end

    %% 阶段 2: 规则注入
    Spec_Gen --> Rules_Layer[Rules 注入: AI 驱动 UI 实现严格规范]
    
    subgraph Core_Rules [核心底线约束]
        R1[组件映射: FuncTable/DyForm]
        R2[视觉规范: Dark Mode/禁绿令]
        R3[架构模式: MVC 分离/i18n]
    end
    
    Rules_Layer -.-> R1 & R2 & R3
    R1 & R2 & R3 -.->|微调任务细则| Spec_Gen

    %% 阶段 3: 增量任务执行
    Spec_Gen --> Task_Anchor[配置环境锚点: 双网址锁定]
    Task_Anchor --> Task_Exec[原子任务执行: 阶段 1-4]

    %% 阶段 4: 执行中的 Hook 校验
    subgraph Task_Hook [任务执行 UI Hook 闭环]
        T_Before[执行前: MCP 记录状态]
        T_Code[生成并应用代码]
        T_After[执行后: MCP 截图对比]
        
        T_Before --> T_Code --> T_After
    end

    Task_Exec --> T_Before
    T_After -->|存在偏差| T_Code
    T_After -->|校验一致| Delivery[高质量交付 & 代码合并]

    %% 样式定义
    style MCP_Layer fill:#e1f5fe,stroke:#01579b
    style Core_Rules fill:#fff3e0,stroke:#e65100
    style Task_Hook fill:#f3e5f5,stroke:#4a148c
    style Dynamic_Alignment stroke-dasharray: 5 5

一、 为什么 Spec 实践是 AI 开发质量的终极保证?

在 AI 开发的早期,我们往往追求“一句话生成页面”。但回到真实的业务场景,这种方式产出的代码几乎不可用。AI 会产生严重的幻觉,或者为了追求视觉一致性而进行无意义的过度设计

通过 Spec-Coding (规范驱动) 协议的实践,我们并不是在寻找一种“最聪明”的模型,而是构建一套质量与效率的确定性底线

  • 消除幻觉约束:Spec 为 AI 提供了清晰的操作边界,防止其随意引入非标库、手写原生 HTML 或滥用内联样式。
  • 解决逻辑断层:通过预定义的架构协议,确保 AI 生成的代码逻辑自洽,而非一堆无法运行的 UI 占位符。
  • 规模化交付的共性:当团队所有 AI 开发都遵循同一套 Spec,产出的代码在可维护性和一致性上能达到高度统一,避免了“千人千面”的代码污染。

二、 核心架构:Rules, Hook & MCP 的协同链路

该方案的可行性核心在于将“人眼识别的规范”转化为“机器执行的自动化指令”。我们建立了一份**《AI 驱动:UI 实现与任务执行严格规范》**作为核心指导文档。其底层逻辑如下:

1. 协作流程图

graph TD
    A[原型输入 URL/链接] --> B{是否有 Figma 设计稿?}
    B -- 无 Figma / 只有原型 --> C[激活 Hook B: 深度探测]
    B -- 有 Figma --> D[DLS 变量映射 & 截图还原]
    
    C --> E[MCP Chrome DevTools]
    E --> F[核心意图识别]
    
    F --> G[生成结构化 Spec & Task.md]
    G -.->|不断调用 MCP 采样校验| E
    
    G --> H[Rules 约束注入: UI 实现严格规范]
    H -.->|根据规范微调任务细则| G
    
    H --> I[原子任务执行]
    I --> J[Hook A: 合规性自检]
    J --> K[MCP 循环校验]
    
    K -- 视觉/代码偏差 --> I
    K -- 校验通过 --> L[高质量交付]

2. 三大支柱的职责分工

  • Rules (规则层) :以《AI 驱动:UI 实现与任务执行严格规范》为准则,定义了视觉底线、组件映射规则(如强制复用标准表格/表单组件)以及架构模式(如 MVC 分离),确保 AI 输出不偏离团队基建。
  • Hook (钩子层) :作为“拦截器”,在代码写入前校验组件复用、样式原子化及请求模式,并负责从视觉信号到开发任务的翻译。
  • MCP (能力层) :赋予 AI “五感”。通过 Chrome DevTools MCP,让 AI 能够实时感知浏览器真实的运行环境,解决“盲目编码”带来的视觉与逻辑偏差。

三、 深度实践中的难点与破解之道

在跑通这一链路的过程中,我们重点攻克了几个令开发者头疼的工程难题:

1. 解决 UI 还原与组件抽象的冲突

  • 难点:AI 倾向于通过堆砌 CSS 实现像素级还原,这往往导致嵌套极深、难以维护。
  • 解决:利用 MCP 视觉审计,将页面拆解为组件树。AI 必须先确认布局重心,再应用 Rules 进行组件映射。这意味着 AI 会优先思考“这里该用哪个公共组件”,而不是“这里该写哪个 CSS 属性”。

2. 规避代码不可用与逻辑空洞

  • 难点:生成的代码往往缺失 Loading、Error 处理或 API 链路不通。
  • 解决:实施 “环境锚点”强制化。在 Task 顶部锁定预览地址,AI 在生成每一阶段逻辑后,必须通过 MCP 回访页面验证交互是否闭环,而非一次性“盲写”到底。

3. 处理“只有原型,没有设计稿”的极端情况

这是最能体现方案可行性的场景。当手中只有一个运行中的 Web 原型 URL 时,通过 Hook B 驱动 MCP 探测,AI 能自动提取 DOM 结构和交互链路,生成高度还原的 Spec。这在老旧项目重构或竞品分析场景下极大地降低了 UI 逆向工程的成本。


四、 两种实践路径的体验总结

场景 A:无 Figma,只有 Web 原型 (对齐模式)

在这种场景下,我们通过 Chrome DevTools MCP 强行补齐了信息差。AI 通过“观察”原型的 Spacing 节奏和交互反馈,实现了高保真的代码重构。虽然过程涉及多轮 Hook 校验,但产出的代码健壮性远超人工手动临摹。

场景 B:基于 Figma (MCP 驱动模式)

当拥有 Figma 时,通过Figma MCPDLS (Design Language System) 变量与截图,还原度可直接达到 95% 以上。此时 AI 的工作重心不再是像素,而是转向 Rules 合规性,重点确保 API 请求模式、控制器逻辑完全符合团队的工程标准。


五、 总结:AI 开发的本质是协议的胜利

通过本次实践,我们发现:如果说大模型的能力决定了生成的上限,那么严密的协议则决定了交付的下限。协议的作用,是让 AI 在处理复杂业务时,从随机的灵感闪现转化为稳定的工程输出

折腾完这一整套体系后,最直观的感受是:AI 编程终于从‘抽卡撞运气’变成了‘照方抓药’。当模型能力(智商)足够强时,这套 Spec 就像是一条工业传送带,把 AI 那些天马行空的幻觉收拢进工程规范里。毕竟对我们业务开发来说,一个听话、稳定、不乱写代码的 AI,远比一个偶尔能写出‘神来之笔’但大多时候在挖坑的 AI 要靠谱得多

langchainjs&langgraphjs入门(二)格式化输出

作者 The_massif
2026年1月14日 08:49

格式化输出

zod

zod是一个ts的类型校验库,langchain官方推荐使用zod来定义ai输出的schema,例如:

import {z} from 'zod'
// 期望ai返回对象,其中包含name,age,skills
const schema = z.object({
    name:z.string().describe('姓名'),
    age: z.number().init().describe('年龄'),
    skills: z.array(z.string()).describe('技能')
})

安装:

npm i zod

速查表:

API 用途 示例
z.string() 字符串 z.string()
z.number() 数字 z.number().int()
z.boolean() 布尔值 z.boolean()
z.object() 对象 z.object({ name: z.string() })
z.array() 数组 z.array(z.string())
z.enum() 枚举 z.enum(["A", "B"])
z.union() 联合类型 z.string().or(z.number())
z.optional() 可选字段 z.string().optional()
z.nullable() 允许 null z.string().nullable()
z.default() 默认值 z.number().default(0)
z.literal() 字面量 z.literal("on")
z.record() 键值映射 z.record(z.string())
z.tuple() 元组 z.tuple([z.string(), z.number()])
.refine() 自定义校验 .refine(s => s.length > 3)
.transform() 转换输出 .transform(s => s.toUpperCase())
.describe() 描述 z.string().describe('姓名')

withStructuredOutput

上一章节的例子可以看到模型的输出只是普通的字符串,并没有格式化.无法直接使用.要想让模型输出格式化的内容可以使用官方推荐的zod,使用他来定义数据结构并验证数据结构是否正确,从而帮助langchain实现输出的格式化和验证.

  1. 首先使用zod定义类型
  2. 然后通过langchain提供的.withStructuredOutput接口使用类型,调用这个方法传入zod定义的类型.模型将添加所需的所有模型参数和输出解析器

示例

import model from './1调用模型.mjs'
import { z } from 'zod'

const schema = z.object({ isCool: z.boolean() }) // 定义输出类型
const structuredLlm = model.withStructuredOutput(schema)

const res = await structuredLlm.invoke('我帅不帅')
console.log("res:",res); // res: { isCool: true }输出了结构化的内容,可以看到模型也知道我很帅

实际上withStructuredOutput在背后会根据schema自动生成严格的提示词,并自动解析验证模型输出,然后将结果返回给开发者

withStructuredOutputlangchain封装后的便捷api,如果想深入理解背后做了什么可以查看这里,后面我们也会详细讲解

StringOutputParser

StringOutoutParser可以从LLM回复的消息中直接提取文本内容,使得我们获取的不再是AIMessage对象而是纯文本

用法

实例化的方式进行创建

import { StringOutputParser } from "@langchain/core/output_parsers";

// 创建实例(无需传递任何参数)
const outputParser = new StringOutputParser();
// 使用
const res = await llm.invoke('你好')
const str = outputParser.invoke(res)

// 简便使用
const chain = llm.pipe(outputParser)
const res = chain.invoke('你好')

对于格式化输出,为便于记忆暂时先了解这么多.知道langchain提供了这么个功能,当上述能力不满足实际开发场景时,再去翻阅官方文档即可.

关注我,该专栏会持续更新!

昨天 — 2026年1月13日首页

AI写代码记录

作者 _瑶瑶_
2026年1月13日 16:36

直接使用AI写代码目前应该算是挺火的,cursor感觉无所不能,Trae也在齐头并进,虽然说差点,但是满足日常开发绰绰有余。

但是在使用中我有一些自己的观点:

(1)使用AI协助开发是最优解,依赖AI开发有待商榷
(2)开发经验浅的人不适合直接使用AI编程
(3)非开发人员(比如美工)尽量不要直接用AI设计界面以及功能

在开发中,我一直秉承着老式开发原则,除了重复性功能(比如:登录防抖节流等)、数据处理(将接口返回的数据重构为其它的数据机构)之外,其实很少用到AI,更多的把AI当做一个知识库,忘记了的查询一下,未接触过的了解一下,仅此而已。

为什么突然想浅记一下,因为最近有两起使用过程不那么愉快的经历。

事件一:

公司让美工设计界面,美工指挥cursor直接生成了一套没有连接后端数据的一套完整的系统。在不了解公司使用的vue开发时,cursor先用nextjs构建了一套服务端渲染的系统,在想和公司之前做的项目融合的时候发现完全不一样。

在了解公司使用的技术栈之后,重新使用vue生成了一套一模一样的完整的系统,可能是不了解技术,描述的不够准确,生成的内容不是那么恰当。

下面举一个例子可能更明白一点。

总共有4个路由界面,4个路由的结构为:

  {
    path: "/homePage",
    name: "homePage",
    component: () => import("@/views/HomePage.vue"),
  },
  {
    path: "/userPage",
    name: "userPage",
    component: () => import("@/views/UserPage.vue"),
  },
  {
    path: "/messagePage",
    name: "messagePage",
    component: () => import("@/views/MessagePage.vue")
  },
  {
    path: "/articlePage",
    name: "articlePage",
    component: () => import("@/views/ArticlePage.vue")
  }

每一个页面vue文件的代码为

<template>
    <Header />
    ……
</template>

每一个页面的布局为 image.png

嗯……看到运行效果的时候其实感觉不出来一点不对,觉得设计的蛮好的,但是准备开发的时候发现,header组件是每一个页面都有的,而且在其中会需要项目的信息,所以就在Header中调用了项目信息。这些信息每一个页面都需要,但是这样写每个页面都获取不到项目的信息。而且切换页面的时候,header中会重新调接口,事情彷佛有些不对……

既然用了vue,vue也做了很多单页面应用,而且这个项目结构来看的话就是个单页面应用呀,但是美工的描述和AI的生成并没有意识到这个问题,个人认为正确的结构应该是下面这样的。

路由结构

 {
    path: "/homePage",
    component: Layout,
    children: [{
      path: "",
      name: 'homePage',
      component: () => import("@/views/secondVersion/HomePage.vue"),
    }]
  },
  {
    path: "/userPage",
    component: Layout,
    children: [{
      path: "",
      name: 'userPage',
      component: () => import("@/views/UserPage.vue"),
    }]
  },
  {
    path: "/messagePage",
    component: Layout,
    children: [{
      path: "",
      name: "messagePage",
      component: () => import("@/views/MessagePage.vue")
    }]
  },
  {
    path: "/articlePage",
    component: Layout,
    children: [{
      path: "",
      name: "articlePage",
      component: () => import("@/views/ArticlePage.vue")
    }]
  },

页面结构应该是这样的

image.png

这样每一次的路由切换只切换router-view的部分

所以不了解项目结构的美工不建议直接使用AI生成页面并直接让前端继续开发功能,因为可能会有很多地方不合理,有时候修改的时间甚至大于直接开发的时间。

事件二

公司同事很喜欢用AI直接写功能,但是可能自己的基础不是很牢固,平时做页面、写功能都没有问题,但是稍微一复杂的东西就使用AI实现。

有一个这个功能,类似于购物车数量功能,可加(一次加1)可减(一次减1)可输入,也可以长按加(一次加5)长按减(一次减5)。

因为数量有范围,范围是1-99,如果没有值的情况下,默认显示99,(不知道为什么会有这个设定)

我模拟的部分(类似)AI给的代码如下:

const max = 99;
const numValue = ref(0)
const defaultValue = ref(99)

const operate = (type) => {
    let tempValue = numValue.value || defaultValue;
    if (type == 1) {
        if (tempValue >= max) {
            // 已经是最大值不能设置
        }
        tempValue += 1;
    }
    ……
}

其它功能都好了,就是值为0的时候按加号会提示“已经是最大值不能设置”。

按说这个问题很好解决,虽然我贴的内容很少,但是同事就一直没有解决,可能一天工作也很累,看的眼睛都花了,所以才没有解决。但是我认为如果不是AI生成的,是自己写的是不是就不会出现这个问题。

毕竟自己写的话,在0是有效值的时候是不会用这个值为0的变量去进行||运算的。

总结

以上就是最近的使用过程,不能否认AI带来的优点是使工作更方便的更高效,但是不合理的使用也会带来很多麻烦。

昨天以前首页

【AI 编程实战】第 7 篇:登录流程设计 - 多场景、多步骤的优雅实现

作者 HashTang
2026年1月12日 21:13

登录是用户进入应用的第一道门,但设计一个体验好、可维护、多场景适用的登录流程并不简单。这篇文章以心动恋聊小程序为例,展示如何和 AI 对话,设计一套完整的登录系统——从微信授权到手机号绑定,从弹窗组件到全局状态管理。

系列专栏【AI 编程实战:TRAE SOLO 全栈开发指南】

本篇主题:登录流程设计 - 多场景、多步骤的优雅实现

实战项目:心动恋聊 - AI 恋爱聊天助手

一、开篇:登录流程的复杂性

1.1 看似简单,实则复杂

很多人觉得登录就是"点击按钮 → 调用接口 → 跳转首页",但实际业务中的登录流程要复杂得多:

用户点击登录
    ↓
微信静默登录(获取 openid)
    ↓
检查是否已注册?
    ├── 是 → 直接登录成功
    └── 否 → 需要手机号授权
              ↓
         用户授权手机号
              ↓
         是否新用户?
              ├── 是 → 完善资料(性别、年龄)
              └── 否 → 登录成功

还有更多场景

  • 任意页面触发登录(不只是登录页)
  • 登录成功后执行回调(如继续之前的操作)
  • 登录弹窗而非登录页(不打断用户浏览)
  • 登录状态全局同步

1.2 我需要什么

我需要一套完整的登录系统

  • 微信一键登录 + 手机号授权
  • 新用户引导完善资料
  • 全局登录弹窗(任意页面可触发)
  • 登录成功回调机制
  • 状态全局同步

二、整体架构设计

2.1 我的需求

我:需要设计一套登录系统,要求:
    1. 微信小程序环境
    2. 支持微信登录 + 手机号授权
    3. 新用户要引导填性别和年龄
    4. 任意页面都能触发登录弹窗
    5. 登录成功后能执行回调

2.2 AI 分析并给出架构

AI:这是一个比较复杂的登录系统,我来设计整体架构。

📐 架构设计:

┌─────────────────────────────────────────────────┐
│                    业务页面                       │
│   (首页、我的、详情页...任意需要登录的地方)         │
└───────────────────────┬─────────────────────────┘
                        │ 调用
                        ▼
┌─────────────────────────────────────────────────┐
│              loginModalStore                     │
│   (全局状态:控制弹窗显示、管理回调队列)            │
└───────────────────────┬─────────────────────────┘
                        │ 驱动
                        ▼
┌─────────────────────────────────────────────────┐
│              LoginModalRoot                      │
│   (全局组件:挂载在 App 层,监听 Store 状态)       │
└───────────────────────┬─────────────────────────┘
                        │ 包含
                        ▼
┌─────────────────────────────────────────────────┐
│              LoginModal                          │
│   (登录弹窗:多步骤流程 login→gender→age)         │
└───────────────────────┬─────────────────────────┘
                        │ 调用
                        ▼
┌─────────────────────────────────────────────────┐
│               userStore                          │
│   (用户状态:登录方法、用户信息、持久化)            │
└─────────────────────────────────────────────────┘

🗂️ 文件结构:

src/
├── store/
│   ├── user.ts           # 用户状态 + 登录方法
│   └── loginModal.ts     # 登录弹窗状态
├── components/
│   └── auth/
│       └── LoginModalRoot.vue  # 全局登录弹窗容器
└── pages/
    └── my/
        └── components/
            └── LoginModal.vue  # 登录弹窗主体

这样设计的好处:
1. 任意页面调用 loginModalStore.open() 即可触发登录
2. 登录逻辑集中在 LoginModal,不分散到各页面
3. 支持回调机制,登录成功后执行特定操作
我:这个架构看起来不错,开始实现吧

三、登录弹窗状态管理

3.1 设计 loginModalStore

AI:先实现登录弹窗的状态管理。

核心功能:
1. 控制弹窗显示/隐藏
2. 管理登录成功回调队列
3. 触发登录流程或资料完善流程
// src/store/loginModal.ts
import { defineStore } from 'pinia';
import type { UserInfo } from 'shared-types';

type LoginSuccessHandler = (userInfo?: UserInfo) => void | Promise<void>;

interface LoginModalState {
  visible: boolean;
  shouldStart: boolean; // 是否触发登录流程
  shouldStartProfileCompletion: boolean; // 是否触发资料完善流程
  queue: LoginSuccessHandler[]; // 回调队列
}

export const useLoginModalStore = defineStore('loginModal', {
  state: (): LoginModalState => ({
    visible: false,
    shouldStart: false,
    shouldStartProfileCompletion: false,
    queue: [],
  }),

  actions: {
    /**
     * 打开登录弹窗
     * @param options.onSuccess - 登录成功后的回调
     * @param options.startProfileCompletion - 是否直接进入资料完善
     */
    open(options?: { onSuccess?: LoginSuccessHandler; startProfileCompletion?: boolean }) {
      // 将回调加入队列
      if (options?.onSuccess) {
        this.queue.push(options.onSuccess);
      }

      this.visible = true;

      if (options?.startProfileCompletion) {
        this.shouldStartProfileCompletion = true;
      } else {
        this.shouldStart = true;
      }
    },

    /**
     * 登录流程已启动(由 LoginModalRoot 调用)
     */
    markStarted() {
      this.shouldStart = false;
    },

    /**
     * 资料完善流程已启动
     */
    markProfileCompletionStarted() {
      this.shouldStartProfileCompletion = false;
    },

    /**
     * 登录成功,执行所有回调
     */
    async resolveSuccess(userInfo?: UserInfo) {
      const callbacks = [...this.queue];
      this.queue = [];

      // 依次执行回调
      for (const callback of callbacks) {
        await callback(userInfo);
      }

      this.visible = false;
      this.shouldStart = false;
      this.shouldStartProfileCompletion = false;
    },

    /**
     * 关闭弹窗
     */
    close() {
      this.visible = false;
      this.shouldStart = false;
      this.shouldStartProfileCompletion = false;
      this.queue = [];
    },
  },
});
AI:使用方式:

// 任意页面触发登录
import { useLoginModalStore } from '@/store/loginModal'

const loginModalStore = useLoginModalStore()

// 场景1:简单触发登录
loginModalStore.open()

// 场景2:登录成功后执行操作
loginModalStore.open({
  onSuccess: (userInfo) => {
    console.log('登录成功:', userInfo)
    // 继续之前的操作,如发送消息、购买会员等
  }
})

// 场景3:直接进入资料完善(用户已登录但未填资料)
loginModalStore.open({ startProfileCompletion: true })

3.2 我追问回调队列的作用

我:为什么要用队列?直接存一个回调不行吗?
AI:队列是为了处理"连续触发"的场景。

🎯 场景示例:
用户在页面 A 点击"发送消息"(需要登录)
→ 弹出登录框
→ 用户没登录,又点击了"收藏"(也需要登录)
→ 这时有两个回调等待执行

如果只存一个回调,第二个会覆盖第一个。
用队列就能保证两个操作都能在登录后执行。

当然,如果你的业务不需要这种场景,用单个回调也可以。

四、全局登录弹窗容器

4.1 LoginModalRoot 设计

我:loginModalStore 有了,但弹窗组件在哪里渲染?
    每个页面都引入一遍吗?
AI:不用。创建一个全局容器组件,挂载在 App 层。

设计思路:
1. LoginModalRoot 放在 App.vue 或 Layout 中
2. 它监听 loginModalStore 的状态
3. 状态变化时,调用 LoginModal 的方法
<!-- src/components/auth/LoginModalRoot.vue -->
<template>
  <LoginModal ref="loginModalRef" v-model="modalVisible" @login-success="handleLoginSuccess" />
</template>

<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue';
import type { UserInfo } from 'shared-types';
import LoginModal from '@/pages/my/components/LoginModal.vue';
import { useLoginModalStore } from '@/store/loginModal';

const loginModalStore = useLoginModalStore();
const loginModalRef = ref<InstanceType<typeof LoginModal> | null>(null);

// 双向绑定弹窗显示状态
const modalVisible = computed({
  get: () => loginModalStore.visible,
  set: (value: boolean) => {
    loginModalStore.setVisible(value);
  },
});

// 登录成功处理
const handleLoginSuccess = async (userInfo?: UserInfo) => {
  await loginModalStore.resolveSuccess(userInfo);
};

// 监听"开始登录"信号
watch(
  () => loginModalStore.shouldStart,
  async (shouldStart) => {
    if (!shouldStart) return;
    await nextTick();
    loginModalRef.value?.startLogin();
    loginModalStore.markStarted();
  },
);

// 监听"开始资料完善"信号
watch(
  () => loginModalStore.shouldStartProfileCompletion,
  async (shouldStart) => {
    if (!shouldStart) return;
    await nextTick();
    loginModalRef.value?.startProfileCompletion();
    loginModalStore.markProfileCompletionStarted();
  },
);
</script>

4.2 挂载到全局

AI:把 LoginModalRoot 挂载到 Layout 或 App 层:
<!-- src/layouts/default.vue -->
<template>
  <slot />
  <!-- 全局登录弹窗 -->
  <LoginModalRoot />
</template>

<script setup lang="ts">
import LoginModalRoot from '@/components/auth/LoginModalRoot.vue';
</script>
AI:这样所有使用 default 布局的页面都能触发登录弹窗,
不需要每个页面单独引入。

五、多步骤登录弹窗

5.1 登录流程设计

我:现在要实现 LoginModal 本身了。
    流程是:登录 → 性别选择 → 年龄选择 → 完成
AI:我来设计多步骤的状态机:

📋 步骤状态:
- login: 登录界面(微信授权 + 协议勾选)
- gender: 性别选择(新用户)
- age: 年龄选择(新用户)

📋 流程逻辑:
1. 用户点击登录 → 调用微信登录
2. 检查返回结果:
   - 有 token + user_id → 老用户,直接成功
   - 无 token → 需要手机号授权
   - needsRegistration=true → 新用户,进入资料完善
<!-- src/pages/my/components/LoginModal.vue -->
<template>
  <u-popup
    v-model="isVisible"
    mode="bottom"
    border-radius="24"
    :safe-area-inset-bottom="true"
    :closeable="true"
    @close="handleClose"
  >
    <view class="login-modal-content">
      <!-- 步骤1:登录界面 -->
      <view v-if="currentStep === 'login'" class="login-step">
        <view class="pt-4.5 pb-10">
          <text class="block text-center text-lg font-bold">欢迎登录</text>
        </view>

        <!-- 登录按钮 -->
        <view class="px-9 pb-4">
          <XButton
            text="手机号快捷登录"
            :loading="isLoading"
            :open-type="needPhoneAuth ? 'getPhoneNumber' : undefined"
            @getphonenumber="handlePhoneNumber"
            @click="handleLoginClick"
          />
        </view>

        <!-- 协议勾选 -->
        <view class="px-9 pb-20">
          <view class="flex items-center justify-center" @click="toggleAgreement">
            <view
              class="w-5 h-5 rounded-full border flex items-center justify-center"
              :class="isAgreed ? 'bg-primary border-primary' : 'border-gray-400'"
            >
              <u-icon v-if="isAgreed" name="checkmark" size="20" color="#fff" />
            </view>
            <text class="ml-2 text-sm">
              勾选同意
              <text class="text-primary" @click.stop="openAgreement('user')">《用户协议》</text>
              和
              <text class="text-primary" @click.stop="openAgreement('privacy')">《隐私政策》</text>
            </text>
          </view>
        </view>
      </view>

      <!-- 步骤2:性别选择 -->
      <view v-else-if="currentStep === 'gender'" class="gender-step">
        <view class="pt-4 pb-10">
          <text class="block text-center text-lg font-bold">选择你的性别</text>
          <text class="block text-center text-sm text-gray-500 mt-2">更精准匹配回复话术</text>
        </view>

        <view class="flex justify-center gap-8 pb-20">
          <view
            v-for="gender in genderOptions"
            :key="gender.value"
            class="flex flex-col items-center"
            @click="selectGender(gender.value)"
          >
            <image :src="gender.icon" class="w-32 h-32" />
            <text class="mt-2">{{ gender.label }}</text>
            <view
              v-if="selectedGender === gender.value"
              class="w-5 h-5 rounded-full bg-primary mt-2"
            />
          </view>
        </view>
      </view>

      <!-- 步骤3:年龄选择 -->
      <view v-else class="age-step">
        <view class="pt-4 pb-10">
          <text class="block text-center text-lg font-bold">选择你的年龄段</text>
        </view>

        <view class="flex flex-wrap justify-center gap-4 pb-20">
          <view
            v-for="age in ageOptions"
            :key="age"
            class="px-6 py-3 rounded-full"
            :class="selectedAge === age ? 'bg-primary text-white' : 'bg-gray-100'"
            @click="selectAge(age)"
          >
            {{ age }}
          </view>
        </view>
      </view>
    </view>
  </u-popup>
</template>

5.2 登录逻辑实现

// LoginModal.vue <script setup>
import { ref, computed } from 'vue';
import { useUserStore } from '@/store/user';
import { toast } from '@/utils/toast';
import { GenderEnum, AgeGroupEnum } from 'shared-types';
import { requestWechatLoginCode } from '@/utils/wechat';

const userStore = useUserStore();

// 当前步骤
const currentStep = ref<'login' | 'gender' | 'age'>('login');

// 状态
const isAgreed = ref(false);
const isLoading = ref(false);
const needPhoneAuth = ref(false);
const selectedGender = ref('');
const selectedAge = ref('');

// 性别和年龄选项
const genderOptions = [
  { value: 'male', label: '男', icon: '/static/images/male.png' },
  { value: 'female', label: '女', icon: '/static/images/female.png' },
];
const ageOptions = ['00后', '05后', '90后', '80后', '70后'];

/**
 * 处理登录按钮点击
 */
const handleLoginClick = async () => {
  if (!isAgreed.value) {
    toast.warning('请勾选同意用户协议');
    return;
  }

  // 如果需要手机号授权,由 open-type 处理
  if (needPhoneAuth.value) return;

  await performWechatLogin();
};

/**
 * 执行微信登录
 */
const performWechatLogin = async () => {
  isLoading.value = true;
  try {
    // 1. 获取微信 code
    const loginCode = await requestWechatLoginCode();

    // 2. 调用 Store 登录方法
    const result = await userStore.wechatLogin({ code: loginCode });

    // 3. 判断结果
    if (result.token && result.user_id) {
      // 已有账号
      if (result.needsRegistration) {
        // 新用户,需要完善资料
        currentStep.value = 'gender';
      } else {
        // 老用户,直接成功
        completeLogin();
      }
    } else {
      // 需要手机号授权
      needPhoneAuth.value = true;
    }
  } catch (error) {
    console.error('微信登录失败:', error);
    toast.error('登录失败,请重试');
  } finally {
    isLoading.value = false;
  }
};

/**
 * 处理手机号授权
 */
const handlePhoneNumber = async (event: any) => {
  const { code, errMsg } = event.detail || {};

  if (!code) {
    if (errMsg?.includes('user deny')) {
      toast.info('已取消手机号授权');
    }
    return;
  }

  isLoading.value = true;
  try {
    const loginCode = await requestWechatLoginCode();
    const result = await userStore.phoneLogin({
      code,
      login_code: loginCode,
    });

    if (result.needsRegistration) {
      currentStep.value = 'gender';
    } else {
      completeLogin();
    }
  } catch (error) {
    toast.error('验证手机号失败');
  } finally {
    isLoading.value = false;
  }
};

/**
 * 选择性别
 */
const selectGender = (gender: string) => {
  selectedGender.value = gender;
  // 延迟跳转,让用户看到选择效果
  setTimeout(() => {
    currentStep.value = 'age';
  }, 500);
};

/**
 * 选择年龄
 */
const selectAge = async (age: string) => {
  selectedAge.value = age;

  // 提交资料
  setTimeout(async () => {
    await submitProfile();
  }, 300);
};

/**
 * 提交用户资料
 */
const submitProfile = async () => {
  const genderValue = selectedGender.value === 'male' ? GenderEnum.MALE : GenderEnum.FEMALE;

  const ageMapping: Record<string, number> = {
    '00后': AgeGroupEnum.POST_00,
    '05后': AgeGroupEnum.POST_05,
    '90后': AgeGroupEnum.POST_90,
    '80后': AgeGroupEnum.POST_80,
    '70后': AgeGroupEnum.POST_70,
  };

  await userStore.updateUserInfo(
    {
      gender: genderValue,
      age_group: ageMapping[selectedAge.value],
    },
    '登录成功',
  );

  completeLogin();
};

/**
 * 完成登录
 */
const completeLogin = () => {
  emit('login-success', userStore.userInfo);
  handleClose();
};

/**
 * 关闭弹窗
 */
const handleClose = () => {
  isVisible.value = false;
  // 重置状态
  setTimeout(() => {
    currentStep.value = 'login';
    isAgreed.value = false;
    selectedGender.value = '';
    selectedAge.value = '';
    needPhoneAuth.value = false;
  }, 300);
};

// 暴露方法给父组件
defineExpose({
  startLogin: () => {
    resetState();
    isVisible.value = true;
  },
  startProfileCompletion: () => {
    currentStep.value = 'gender';
    isVisible.value = true;
  },
});

六、业务页面集成

6.1 触发登录的场景

我:登录弹窗做好了,业务页面怎么用?
AI:业务页面只需要调用 loginModalStore.open():
// 场景1:需要登录才能操作的按钮
const handleSendMessage = () => {
  if (!userStore.isLoggedIn) {
    loginModalStore.open({
      onSuccess: () => {
        // 登录成功后,继续发送消息
        doSendMessage();
      },
    });
    return;
  }
  doSendMessage();
};

// 场景2:封装成通用的登录检查函数
const requireLogin = (callback: () => void) => {
  if (userStore.isLoggedIn) {
    callback();
    return;
  }

  loginModalStore.open({
    onSuccess: () => callback(),
  });
};

// 使用
const handleBuyVip = () => {
  requireLogin(() => {
    uni.navigateTo({ url: '/pages/vip/index' });
  });
};

const handleCollect = () => {
  requireLogin(() => {
    doCollect();
  });
};

6.2 我追问:能不能更简洁

我:每次都要写 if (!isLoggedIn) 很繁琐,能不能更简洁?
AI:可以封装成 Hook:
// src/hooks/useRequireLogin.ts
import { useUserStore } from '@/store/user';
import { useLoginModalStore } from '@/store/loginModal';

export function useRequireLogin() {
  const userStore = useUserStore();
  const loginModalStore = useLoginModalStore();

  /**
   * 需要登录才能执行的操作
   */
  const requireLogin = (callback: () => void | Promise<void>) => {
    if (userStore.isLoggedIn) {
      callback();
      return;
    }

    loginModalStore.open({
      onSuccess: async () => {
        await callback();
      },
    });
  };

  /**
   * 创建需要登录的处理函数
   */
  const withLogin = <T extends (...args: any[]) => any>(fn: T): T => {
    return ((...args: Parameters<T>) => {
      requireLogin(() => fn(...args));
    }) as T;
  };

  return {
    requireLogin,
    withLogin,
    isLoggedIn: computed(() => userStore.isLoggedIn),
  };
}
// 页面中使用
const { requireLogin, withLogin, isLoggedIn } = useRequireLogin();

// 方式1:包装函数
const handleSendMessage = () => {
  requireLogin(() => {
    doSendMessage();
  });
};

// 方式2:直接创建带登录检查的函数
const handleBuyVip = withLogin(() => {
  uni.navigateTo({ url: '/pages/vip/index' });
});

const handleCollect = withLogin(() => {
  doCollect();
});

七、登录状态判断优化

7.1 预检微信登录状态

我:每次打开登录弹窗都要等用户点击,
    能不能提前知道需不需要手机号授权?
AI:可以在打开弹窗时"预检"微信登录状态:
// LoginModal.vue
const determineLoginStrategy = async () => {
  if (!isMpWeixin) {
    needPhoneAuth.value = false;
    return;
  }

  try {
    // 静默获取 code,尝试登录
    const loginCode = await requestWechatLoginCode();
    const response = await loginByWechat({
      code: loginCode,
      source: clientSource,
      channel: clientChannel,
    });

    // 如果能直接登录,不需要手机号
    if (response.code === 200 && response.data?.token) {
      needPhoneAuth.value = false;
    } else {
      needPhoneAuth.value = true;
    }
  } catch (error) {
    needPhoneAuth.value = true;
  }
};

// 打开弹窗时调用
const startLogin = () => {
  resetState();
  isVisible.value = true;
  // 预检登录状态
  void determineLoginStrategy();
};
AI:这样的好处:
1. 老用户:按钮显示"微信登录",点击直接成功
2. 新用户:按钮显示"手机号快捷登录",需要授权

用户体验更顺畅,不用点两次。

八、核心经验:登录系统设计要点

8.1 架构设计原则

原则 说明
状态集中 loginModalStore 统一管理弹窗状态和回调
组件分离 LoginModalRoot 负责桥接,LoginModal 负责 UI 逻辑
全局可用 挂载在 Layout 层,任意页面可触发
回调机制 支持登录成功后执行特定操作

8.2 流程设计要点

// ✅ 推荐:多步骤用状态机
const currentStep = ref<'login' | 'gender' | 'age'>('login');

// ❌ 不推荐:多个 boolean 控制
const showLogin = ref(true);
const showGender = ref(false);
const showAge = ref(false);
// ✅ 推荐:预检登录状态
const startLogin = () => {
  void determineLoginStrategy(); // 提前判断需要哪种登录
};

// ❌ 不推荐:用户点击才判断
const handleClick = () => {
  // 点击后才知道需要手机号,体验差
};

8.3 错误处理

// 区分不同的错误场景
const showWechatLoginError = (error: any) => {
  if (error?.code === -8) {
    toast.error('未安装微信客户端');
    return;
  }
  toast.error('登录失败,请重试');
};

// 手机号授权取消 vs 失败
const handlePhoneNumber = (event: any) => {
  const { code, errMsg } = event.detail;
  if (!code) {
    if (errMsg?.includes('user deny')) {
      toast.info('已取消授权'); // 用户主动取消,不是错误
    } else {
      toast.error('获取手机号失败'); // 真正的错误
    }
    return;
  }
};

九、总结:登录系统的完整实现

9.1 文件清单

文件 职责
store/loginModal.ts 弹窗状态 + 回调队列
store/user.ts 用户状态 + 登录方法
components/auth/LoginModalRoot.vue 全局弹窗容器
pages/my/components/LoginModal.vue 登录弹窗 UI + 逻辑
hooks/useRequireLogin.ts 登录检查 Hook

9.2 关键收获

  1. 架构先行:先设计整体架构,再实现细节
  2. 状态集中:用 Store 管理弹窗状态和回调
  3. 多步骤流程:用状态机管理,避免多个 boolean
  4. 体验优化:预检登录状态,减少用户等待
  5. 错误区分:用户取消 vs 系统错误,提示不同

9.3 下一篇预告

《【AI 编程实战】第 8 篇:组件封装的艺术 - 从业务代码到可复用组件》

下一篇展示如何设计通用组件:

  • 从业务代码中提取组件
  • Props 和 Events 设计
  • 组件的扩展性和灵活性

登录系统不只是"调用接口",而是用户体验、状态管理、错误处理的综合考验。 通过和 AI 对话,逐步理清每个环节,最终形成完整的解决方案。

如果这篇文章对你有帮助,请点赞、收藏、转发!

AI时代2025年下半年学的这些Web前端特性有没有用?

作者 张鑫旭
2026年1月12日 18:19

40岁老前端学习记录第6波,每半年一次。对前五次学习内容感兴趣的可以去我的掘金专栏“每周学习记录”进行了解。

不过这次前端记录有别于过往,会聊一聊学这些东西有没有价值。

先看学了什么。

第27周 2025.6.30-7.6

本周学习Select原生组件的样式自定义。

其中按钮部分若想完全自定义,使用下面的CSS代码:

select { appearance: base-select; }

若想下拉部分的样式可以自定义,则需要使用::picker()伪元素函数设置:

::picker(select) { appearance: base-select; }

此时,select元素和option元素就可以自如的自定义了。

其中,::picker-icon伪元素是设置下拉按钮后面那个三角的:

::checkmark伪元素伪元素指向的是下拉列表选中选项前面的勾勾:

根据我的实践,已经可以完全覆盖<select>元素的样式自定义了,唯一问题是目前仅Chrome浏览器支持。

更多更具体的细节参见我写的这篇文章:“好诶,select下拉框元素支持样式完全自定义啦!

第28周 2025.7.7-7.13

本周学习3个与HTML相关的东西。

1. 全新的search元素

Chrome 117和Safari 17新支持了一个HTML元素名为 <search>,用于定义搜索区域,通常作为表单的一部分。

兼容性如下图所示:

2. h1元素样式规则要变

如果页面有多个h1元素,且这些h1元素被<section>,<aside>,<nav>, 以及<article>等元素嵌套,那么h1元素会自动降级为h2, h3, h4这种。例如:

渲染效果则是这样的:

不过,浏览器可能会改变这种策略,即所有h1的字号保持一致。

然后这种变化是和CSS :heading伪类和:heading()函数一起出现的。

3. 声明式Shadow DOM

无需JavaScript即可定义Shadow DOM,支持服务器端渲染(SSR),提升组件隔离性。

示意代码:

也就是,DOM内容无需像过去一样,全部都通过JavaScript创建的。

但是,如果有交互行为,通常还是需要借助JS代码的,包括自定义组件的注册等。

由于HTML结构可以直接呈现,因此,后端输出就非常方便,SEO也会比过去更友好。

兼容性:

第29周 2025.7.14-7.20

本周学习的是JS新特性,还挺实用的。

RegExp.escape()是所有前端都要熟知的静态函数,用于安全地将字符串转换为正则表达式字面量,目前所有现代浏览器都已经支持。

详见此文:“JS正则新特性:安全过滤RegExp.escape方法

第30周 2025.7.21-7.27

本周学习CSS的if()函数。

CSS之前也支持类逻辑函数,例如数学函数中的min()max(),本质上也是有逻辑的,例如:max(300px, 100vw),表示:

  • 如果100vw的计算宽度大于300px,则最终尺寸300px

  • 否则是100vw的宽度。

现在,CSS支持 if() 函数了,这个是真正意义上的逻辑函数了。

目前,if() 支持三种不同类型的查询:

  • style():样式查询

  • media():媒体查询

  • supports():支持查询

其中,用得最多的当属 style() 样式查询,具体使用案例参见此文:“CSS倒反天罡居然支持if()函数了

第31周 2025.7.28-8.3

本周学习HTML popover属性新增的值hint

Popover可以说是弹出浮层最佳解决方案,自动显隐特性,外加顶层特性,非常香。

但是有一个问题,那就是一次最多只能有一个弹出层显示(如果不使用 showPopover() 方法手动设置的话),例如下面的Tips提示出现的时候会关闭前面的列表。

类似这种仅仅是用做信息展示的轻提示,其实无需关闭之前的弹出层,之前的popover属性能力就不够。

于是,从Chrome 133开始,popover属性新增了属性值hint,可以让轻提示显示的时候不会影响之前的弹出层。

目前仅Chrome浏览器支持:

第32周 2025.8.4-8.10

本周学习CSS索引和数量匹配函数sibling-index()sibling-count()

sibling-index()可以返回当前子元素是第几个,sibling-count()可以返回当前元素的父元素总共有多少个子元素。

这两个函数特别适合用在很多重复子元素需要动态匹配计算的场景。

例如下面的浮动等宽布局示意:

渲染效果如下(目前仅Chrome才有效果),宽度自动等比例分配:

更多细节可以参考我写的这篇文章:“CSS索引和数量匹配函数sibling-index sibling-count简介

第33周 2025.8.11-8.17

本周学习CSS @container scroll-state容器滚动查询。

此特性可以让我们,仅仅通过CSS,就可以检测是否滚动到容器边缘,Scroll Snap边缘,以及sticky元素是否触及定位的边缘。

 然后我们就可以实现更加精确友好的交互控制,参见下面的GIF示意。

还是挺实用的一个特性,例如很多文档需要阅读到底部才能按钮提交,就可以使用这里的CSS技术,无需JavaScript代码判断了。

详见此文:“CSS @container scroll-state容器滚动查询

第34周 2025.8.18-8.24

本周学习的是CSS社区呼声已久的一个特性,叫做corner-shape属性。

CSS的corner-shape属性是CSS背景与边框模块(Level 4)中引入的新特性,它通过扩展border-radius的功能,允许开发者自定义元素的角形状,而不仅限于传统的圆角效果。以下是其核心作用及特性:

扩展border-radius的几何形状

  • 传统圆角border-radius默认生成四分之一椭圆形的圆角(round)。

  • 新增形状corner-shape支持五种专业级角形状:

    • bevel(斜角):用直线替代圆弧,生成切角效果。

    • scoop(凹槽):创建内凹的弧形角。

    • notch(缺口):添加微小内凹的缺口。

    • squircle(超椭圆):介于圆和矩形之间的平滑形状。

    • 保留默认的round(圆角)。

该特性可以实现各种五花八门的图形效果。

特性是个好特性,可惜太新了点,目前仅Chrome 139以上版本才正式支持。

有兴趣的可以访问此文进行进一步了解:抢先学习大开眼界的CSS corner-shape属性

第35周 2025.8.25-8.31

本周学习的是HTML新特性,啥呢?

就是hidden属性支持了一个名为until-found的新值,默认不可见,但是如果用户搜索匹配,或者锚点定位了,就会自动显示,有助于特定场景下提高用户体验。

until-found隐藏的元素还支持一个名为beforematch的事件,在该元素从隐藏变成显示之前的一瞬间执行。

详见我撰写的这篇文章:“HTML之快速了解hidden=until-found的作用

第36周 2025.9.1-9.7

本周学习的也是个挺厉害的CSS特性,那就是@function规则,可以在CSS中函数编程了。

下面的代码是一个响应式布局的函数定义示意。

/* 在小于640px的屏幕上为侧边栏占用1fr的空间,在较大的屏幕上使用相对具体具体宽度值 */
@function --layout-sidebar(--sidebar-width: max(20ch, 20vw)) {
  result: 1fr;
  
  @media (width > 640px) {
    result: var(--sidebar-width) auto;
  }
}

.layout {
  display: grid;
  /*侧边栏宽度兜底20ch和20vw的较大计算值 */
  grid-template-columns: --layout-sidebar();
}

布局效果参见(需要Chrome 139+):

@function 规则可以定义可重用的计算逻辑,让 CSS 代码更灵活、模块化且易于维护。

更多案例与细节见我写的这篇文章:“@function自定义函数让CSS支持编程啦

第37周 2025.9.8-9.14

本周学习使用CSS linear()函数。

CSS 中的linear()函数用于创建自定义的线性缓动效果,它允许你通过定义多个关键点来精确控制动画在不同阶段的播放进度和速度变化。

无论是更加逼真的物理动画,还是特殊的阶梯式动画都不在话下。

目前所有浏览器都已经支持。

借助转换工具,可以将各类JS缓动动画变成CSS linear()函数,还是很厉害的一个特性。

详见我撰写的这篇文章:“使用CSS linear()函数实现更逼真的物理动画效果

第38周 2025.9.15-9.21

本周学习的新特性也是期盼已久的,几行CSS就能实现页面滚动的时候,菜单栏根据标题内容自动高亮。

使用的是CSS scroll-target-group属性以及:target-current伪类。

实现代码非常简单,HTML代码就下面这些:

<menu>
    <li><a href="#intro">前言</a></li>
    <li><a href="#ch1">第1章</a></li>
    <li><a href="#ch2">第2章</a></li>
</menu>

<article>
    <h1>欢迎来到我的博客</h1>
    <section id="intro">...</section>
    <section id="ch1">...</section>
    <section id="ch2">...</section>
</article>

CSS代码也非常简单,就这么点内容:

menu {
    position: fixed;
    scroll-target-group: auto;
}

a:target-current {
    color: red;
}

结束了,就结束了,对吧,给菜单容器设置scroll-target-group:auto,然后菜单里面的链接元素使用:target-current设置匹配样式就可以了。

此时,链接元素对应的href锚点元素进入区域的时候,链接元素就会高亮啦!

牛逼!

目前Chrome浏览器Only!

详见我撰写的这篇文章:“CSS scroll-target-group加:target-current滚动菜单自动高亮

第39周 2025.9.22-9.28

本周学习的是JS特性,Intl.Segmenter构造函数。

Intl.Segmenter不仅能精准返回Emoji字符的长度,还能实现中文语言的自动分词,分句效果,这个特性比预想的实用的多了。

目前所有现代浏览器都支持。

更多内容参见我的这篇文章:“使用Intl.Segmenter返回更准确的字符长度

第40周 2025.9.29-10.5

国庆假期,学什么学,起来high啦!

第41周 2025.10.6-10.12

本周学习CSS field-sizing属性,该属性可以让文本输入框根据输入内容的多少自动改变宽度和高度大小。

某些场景下,还是挺实用的。

例如<textarea> 元素内容输入的时候,高度自动变高。

以前都是需要使用div元素模拟,或者JS代码计算,现在就一行CSS代码的事情。

Chrome已经支持,Safari即将大范围支持,这个CSS的应用前景还可以。

更多内容,可见我撰写的这篇文章:“CSS field-sizing属性简介

第42周 2025.10.13-10.19

本周学习CSS reading-flowreading-order和两个新属性。

Web开发中,DOM文档的属性和视觉表现顺序不一致是很正常的。

在过去,我们会使用HTML tabindex属性进行交互处理。

但是HTML tabindex属性在页面复杂的时候,容易冲突。

在这种背景下,reading-flowreading-order属性应运而生。

其可以让可访问元素的Tab索引顺序和视觉表现顺序表现一致

目前仅Chrome浏览器支持:

注定沦为小众特性,有兴趣可以前往这里了解更多:“CSS reading-flow和reading-order属性简介

第43周 2025.10.20-10.26

本周学习Canvas新支持的锥形渐变绘制方法createConicGradient方法

CSS虽然也能实现锥形渐变,但是如果遇到需要前端截图的需求,那么CSS方案就会有问题,html2canvas并不支持锥形渐变。

此时,可以试试使用Canvas绘制图形。

浏览器全支持,下图就是使用createConicGradient()方法绘制的饼图。

完整代码和语法说明,参见我的这篇文章:“Canvas也支持锥形渐变了createConicGradient方法

第44周 2025.10.27-11.2

本周学习CSS :heading伪类和:heading()函数。

这两个CSS选择器是用来选择h1-h6元素的。

唉,说实话,这是我见过最让我失望的新特性。

完全看不到任何使用这个特性的理由。

是让人无语的垃圾特性。

后来经过同行提醒,这两个CSS伪类匹配的是语义上是h1~h6的元素,参考第28周学习的h1元素自动降级。

不过还是很烂的特性。

为什么说烂呢?有兴趣的可以访问这里进行了解:“垃圾特性之CSS :heading伪类和:heading()函数

第45周 2025.11.3-11.9

本周学习使用CSS锚点定位实现元素的鼠标跟随效果。

下图是其中一个案例的示意:

悬停跟着走,移开恢复到选中元素上。

具体实现参见这篇文章:“CSS锚点定位实战-鼠标跟随交互效果

第46周 2025.11.10-11.16

本周学习CSS :state()伪类函数。

此选择器函数可以匹配Web Components内部通过attachInternals添加的状态,从而实现穿透组件,实现更加精准的样式设置。

兼容性如下:

我觉得还算个不错的设计。

具体案例和细节,可以参见我写的这篇文章:“介绍下与CSS自定义组件相关的:state()函数

第47周 2025.11.17-11.23

本周学习了CSS ::details-content伪元素,可以用来匹配<details>元素的内容DOM,我们可以使用这个伪元素设置背景,边框等样式。

不过,实际开发,还是使用它实现动画效果更多一些。

目前所有现代浏览器都已经支持了这个特性。

详见这篇文章:巧用CSS ::details-content伪元素实现任意展开动画

第48周 2025.11.24-11.30

本周学习CookieStore对象。

在前端开发的长河中,Cookie始终扮演着重要角色。

从用户身份识别到状态维持,它是浏览器与服务器之间轻量通信的核心载体。

但长期以来,我们操作Cookie的方式始终停留在通过document.cookie拼接字符串的“原始阶段”,不仅代码冗余易出错,还无法应对异步场景下的复杂需求。

比方说,我们要设置一个Cookie,需要先获取,然后再手动进行键值对+属性的字符串拼接,例如:

document.cookie = "name=zhangxinxu; max-age=3600; path=/"

这就很麻烦,属性顺序、符号格式稍有偏差就会导致失效。

正是因为上面这些不足,CookieStore API才应运而生。

它将Cookie操作封装为标准化的异步方法,让Cookie管理变得简洁、可控。

详见我的这篇文章:“醒醒,该使用CookieStore新建和管理cookie了

第49周 2025.12.1-12.7

本周学习单IMG标签的图片内阴影效果实现

列举了三种方法:

  1. 如果图片背景纯色,可以通过添加padding,让box-shadow内阴影显示。

  2. 如果不考虑兼容性,可以使用attr()函数让图片地址作为背景图显示。

  3. 最后一种方法,任意图片,且兼容性极佳,就是使用SVG滤镜。

效果示意:

详见我的这篇文章:“单IMG标签的图片内阴影效果实现

第50周 2025.12.8-12.14

本周学习CSS progress()函数。

语法:

progress(<value>, <start>, <end>)

可以返回0-1之间的进度值,常用于进度条、动态过渡、动画关键帧等场景,是 CSS 原生实现进度关联样式的核心工具。

还挺实用的。

但是兼容性一般。

详见我的这篇文章:”CSS progress()函数简介

第51周 2025.12.15-12.21

本周学习CSS锚点定位锚定容器回退检测语法。

CSS锚点定位有边界位置自动改变位置的能力,但是,浏览器却无法检测,导致很多交互效果并不能完全使用CSS实现,着实可惜。

然后,最近,Chrome浏览器新支持了一个特性,那就是锚定容器位置查询检测语法。

.float-element { position-try-fallbacks: flip-block; container-type: anchored; }@container anchored(fallback: flip-block) {   .float-element { /* 如果垂直定位方向改变,如何如何…… */ }}

可以实现下图所示的交互效果,纯CSS实现的:

详见我的这篇文章:“补全不足,CSS锚点定位支持锚定容器回退检测了

第52周 2025.12.22-12.28

本周学习CSS style()样式查询及其range范围语法。

比方说下面的案例,识别CSS变量范围,显示不同的文字颜色。

<span class="score" style="--score: 95;">
  <data>95</data>
</span>
<span class="score" style="--score: 85;">
  <data>85</data>
</span>
<span class="score" style="--score: 65;">
  <data>65</data>
</span>
<span class="score" style="--score: 35;">
  <data>35</data>
</span>

此时,就可以在style()函数中,使用大于号,小于号进行匹配:

@container style(--score >= 90) {
  data {
    color: gold;
  }
}
@container style(--score >= 80) and style(--score < 90) {
  data {
    color: green;
  }
}
@container style(--score >= 60) and style(--score < 80) {
  data {
    color: orange;
  }
}
@container style(--score < 60) {
  data {
    color: red;
  }
}

通俗易懂,三岁小孩也能知道是什么意思。

效果图参考:

Chrome和Safari浏览器都支持,而且已经支持一段时间了。

另外,此查询和attr()if()等函数配合使用后,该特性的场景适应性更是直线攀升。

详见这篇文章:“今日学习CSS style()样式查询及其range范围语法

-------------

好,以上就是我这个40岁的老前端2025年下半年学习的内容。

接下来回答很多前端同行关心的一个问题,都什么年代了,还学这些细枝末节的东西,有什么用?

下面简单说说我的看法,注意,篇幅原因,真就简单说说,回头我专门写一篇聊聊这个事情。

前端还值得学习吗?

结论,还是要学习的,只是需要从以前的熟悉降为了解,然后把节约的精力去学习其他东西。

我本人是AI重度使用者,编程、写作、甚至在自制AI漫剧。

对目前AI编程能力的边界感受挺深的。

短期

以目前的AI能力,当我们实现一个需求的时候,它所实现的代码往往是基于历史代码训练的最稳健的实现,功能运行没问题,但是很多时候,并不是最佳实现。

我已经遇到很多类似的例子了。

例如一个划词评论功能,实现代码洋洋洒洒,实则啰嗦无比,非要提醒AI使用selectionchange事件,代码这才指数级腰斩。

又比如一个富文本输入框中的AT功能,也是一堆selection和range控制,天书一样,未能充分应用输入框自身拖拽、回撤等能力。

我再举本文中出现的一个例子,实现<textarea>多行文本输入框跟随内容高度自动的问题,我可以100%打包票,AI一定会使用JavaScript代码实现,他不会想到说使用CSS field-sizing实现,因为field-sizing有兼容性问题,如果开发者不主动让他优先使用field-sizing,AI绝对不会选择这个技术选型的。

但是,其实很多项目是不需要考虑兼容性的,只需要最新的Chrome浏览器支持。

下面问题来了,如果开发人员不学习,不知道有这么个东西,请问,这个可以大大简化代码量,降低复杂度的东西如何在项目中应用?

如果只奔着功能实现,公司为何还需要你,找个刚毕业的大学生不更香吗?

所以,就目前而言,学肯定是要学的,但是我们不需要去掌握语法和细节了,只需要知道有这么个东西,在什么场景下使用最合适就可以了。

作为优秀的指挥官,务必高屋建瓴,对吧,都有哪些手下、各自优缺点务必要了然于心,这样指挥作战的时候才能发挥出最大的威能。

长期

不过AI本身也是发展的,有可能:

1. AI自动分析项目运行环境,目标客户群体,或者适应技术人员的风格癖好,选择最适合的技术方案。

2. 不是人指挥AI,而是AI指挥人——老子会很多,你想要使用哪一个。

到时候,说不定这些广度知识也就没有必要去花时间去学习的了。

嗯……想了下,试试现在AI有没有这个水平吧。

使用Trae测试。

我让其新建一个空白HTML页面。

结果其自以为是连功能都实现了,果不其然是JavaScript实现的:

然后我对其进行使用其他技术的提示,结果还是一些传统实现:

我看了下其contenteditable的实现,很糟糕,没有关闭富文本的能力:

document.querySelectorAll('.editable-div').forEach(div => {    const placeholder = div.getAttribute('data-placeholder');        // 设置初始placeholder    if (placeholder && !div.textContent.trim()) {        div.innerHTML = `<span style="color: #aaa;">${placeholder}</span>`;    }        // 点击时清除placeholder    div.addEventListener('click', () => {        if (div.innerHTML.includes(placeholder)) {            div.innerHTML = '';        }    });        // 失去焦点时恢复placeholder    div.addEventListener('blur', () => {        if (!div.textContent.trim() && placeholder) {            div.innerHTML = `<span style="color: #aaa;">${placeholder}</span>`;        }    });});

应该设置contenteditable="plaintext-only"

算了,我直接当面指挥他使用filed-sizing属性吧。

我观察AI的思考过程,发现其确实捞到了filed-sizing属性的语法,就在我以为AI可以完美完成任务的时候,结果生成的页面一看,尼玛,完全就是一本正经的胡说八道。

嗯……短期来看,学习还是有必要的,尤其是一些前沿技术。

更长期

其实人生在世,无需那么多算计、功利,纠结于投入产出比,担心价值流失之类的。

想那么多干嘛,持续学习,总不会错的,只要是能够提高自己的,都要学,前端技术本身如此,如何使用AI也是如此。

毕竟,人在职场,在社会,看起来是人与AI的竞争,本质上你活得如何,还是人与人的竞争。

我比你懂很多的前端知识,我又比你更懂AI,你说,我会担心被淘汰吗?

所以,2026年我还将继续学习,继续保持对前端的好奇心,欢迎关注,转发,一起进步。

告别素材焦虑!用 AI 一键生成鸿蒙项目图片素材

作者 万少
2026年1月12日 16:37

告别素材焦虑!用 AI 一键生成鸿蒙项目图片素材

万少:华为HDE、鸿蒙极客

个人主页:blog.zbztb.cn/

2025年参与孵化了20+鸿蒙应用、技术文章300+、鸿蒙知识库用户500+、鸿蒙免费课程2套。

如果你也喜欢交流AI和鸿蒙技术,欢迎扣我。

最近我在B站上进行不定期的免费鸿蒙技术直播,欢迎关注:space.bilibili.com/414874315?s…

程序员找素材,到底有多难?

做项目开发时,我们经常需要各种图片素材。但获取素材这件事,不同角色的体验天差地别:

企业开发:有专业 UI 设计师,直接找设计师要就完事了。

个人开发者:就只能自己想办法:

  • 手动去素材网站搜索、挑选、下载
  • 把素材引入工程
  • 在代码中使用

这一套流程走下来,没个十几二十分钟根本搞不定。更糟心的是,花半天找的素材还不一定满意。

我就一直在想:图片素材能不能像普通文本一样,直接让 AI 生成,然后插到工程里?

鸿蒙工程里怎么用图片?

在鸿蒙(HarmonyOS)开发中,使用图片主要分两步:

  1. 存放图片:把图片放到 resources 目录或 rawfile 目录
  2. 代码引用:在 .ets 文件中引入并使用

这个流程本身很简单,但问题卡在第一步——图片从哪来?

我的解决方案:AI 生成图片脚本

既然 AI 能写代码,那生成图片当然也不在话下。

我的做法是写一个脚本,通过 AI 图像生成接口来获取素材。成本很低,市面上主流的 AI 绘画服务(百度、阿里、火山等)生成一张图片大约 1~3 分钱

脚本的核心功能:

  • 接收一个参数:图片描述(文本字符串)
  • 调用 AI 图片生成接口
  • 返回图片文件流
  • 自动保存到指定位置

实现思路:

  1. 从各 AI 平台官网复制对应语言的 SDK 代码(比如 Python、Node.js 等)
  2. 把自己的 API Key 写入脚本
  3. 封装一个函数,传入图片描述,返回图片文件

实战:免费好用的图片生成服务

推荐一个我经常用的——智谱 AI 的图片生成服务:

🔗 bigmodel.cn/

免费额度对个人开发者来说完全够用,生成效果也很不错。

智谱 AI 平台

将生成图片的脚本直接放在鸿蒙工程内

image-20260112130734993

将脚本包装成可以使用终端调用的文件

image-20260112130828662

AI编辑器中直接对话生成

这里用上架应用-流蓝卡片 为例:

image-20260112131139741

然后执行程序,得到结果

image-20260112131456935


实际效果:

PixPin_2026-01-12_13-17-11

历史文章

  1. AI 玩转鸿蒙 (1):选择合适的AI开发工具

    mp.weixin.qq.com/s/HXbT60vzJ…

下期预告

用 AI 生成鸿蒙代码难免会有小语法错误。

下篇文章我来分享:如何让 AI 自动修复自己的代码错误,实现「生成即可用」的无缝体验。

AI 写的代码有 48% 在"胡说八道":那些你 npm install 的包,可能根本不存在

2026年1月12日 10:18

摘要:研究显示,AI 生成的代码中有 48% 存在"幻觉"——引用了根本不存在的包、API 或方法。更可怕的是,黑客已经开始利用这个漏洞:他们注册 AI 经常"幻觉"出来的假包名,等你 npm install,恶意代码就进了你的项目。这种攻击叫"Slopsquatting",已经影响了 44 万个包依赖。本文带你深入了解这个 AI 时代的新型安全危机。


01. 那个让我后背发凉的 Bug

上周,我在 Code Review 时发现了一个奇怪的 import:

import { validateEmail } from "email-validator-pro"

我没见过这个包,于是去 npm 上搜了一下。

搜索结果:0 个匹配。

我问写这段代码的同事:"这个包是哪来的?"

他说:"Cursor 自动补全的啊,我看着挺专业的就用了。"

我又问:"你 npm install 过吗?"

他愣了一下:"好像……没有?代码能跑啊。"

我看了一眼 package.json,果然没有这个依赖。代码之所以能跑,是因为另一个包里恰好有个同名的函数被导出了。

这次我们运气好。

但如果这个"不存在的包"真的被人注册了呢? 如果里面藏着恶意代码呢? 如果我们真的 npm install 了呢?

这不是假设。这正在发生。


02. AI 代码幻觉:48% 的代码在"胡说八道"

2.1 什么是 AI 代码幻觉?

AI 代码幻觉(AI Code Hallucination)是指 AI 生成的代码中包含:

  • 不存在的包import xxx from 'fake-package'
  • 不存在的 APIresponse.data.nonExistentMethod()
  • 不存在的方法array.filterMap() (JavaScript 没有这个方法)
  • 错误的参数fs.readFile(path, 'utf-8', callback, extraParam)
  • 虚构的配置项{ enableTurboMode: true } (没有这个选项)

2.2 有多严重?

2025 年的研究数据让人触目惊心:

AI 代码幻觉统计(2025年研究):

样本量:576,000 个代码样本
测试模型:16 个主流 LLM

关键发现:
├─ 48% 的 AI 生成代码包含某种形式的幻觉
├─ 440,000 个包依赖是"幻觉"出来的(不存在)
├─ 58% 的幻觉包名会重复出现(AI 会反复犯同样的错)
├─ 开源模型幻觉率:22%
├─ 商业模型幻觉率:5%(好一些,但仍然存在)
└─ 45% 的 AI 生成应用包含可利用的 OWASP 漏洞

将近一半的 AI 代码在"胡说八道"。

2.3 为什么 AI 会"幻觉"?

// AI 幻觉的产生机制
interface HallucinationCause {
  cause: string
  explanation: string
  example: string
}

const hallucinationCauses: HallucinationCause[] = [
  {
    cause: "训练数据过时",
    explanation:
      "AI 的训练数据可能是 1-2 年前的,很多新包它不知道,很多旧包已经改名或废弃",
    example: "推荐使用已经废弃的 request 库,而不是 axios",
  },
  {
    cause: "模式匹配过度泛化",
    explanation:
      "AI 看到 'email' + 'validator' 就觉得应该有个 'email-validator' 包",
    example: "生成 import { validate } from 'email-validator-pro' // 不存在",
  },
  {
    cause: "混淆不同语言/框架",
    explanation:
      "把 Python 的库名用在 JavaScript 里,或者把 React 的 API 用在 Vue 里",
    example: "在 Node.js 里 import pandas // 这是 Python 的库",
  },
  {
    cause: "自信地编造",
    explanation: "AI 不会说'我不知道',它会自信地给出一个看起来合理的答案",
    example: "生成一个完整的、看起来很专业的、但完全虚构的 API 调用",
  },
  {
    cause: "私有代码库盲区",
    explanation: "AI 没见过你公司的内部代码,但会根据命名规律'猜测'",
    example: "猜测你公司有个 @company/utils 包,但实际上叫 @company/common",
  },
]

03. Slopsquatting:黑客的"钓鱼"新玩法

3.1 什么是 Slopsquatting?

Slopsquatting = Slop(AI 生成的垃圾内容)+ Squatting(抢注)

简单来说:黑客注册 AI 经常"幻觉"出来的假包名,等你上钩。

Slopsquatting 攻击流程:

第一步:研究 AI 幻觉模式
├─ 用各种 LLM 生成大量代码
├─ 收集所有"幻觉"出来的包名
└─ 找出重复率最高的(58% 会重复)

第二步:抢注假包名
├─ 在 npm / PyPI 上注册这些包名
├─ 包内容看起来正常(躲避审查)
└─ 但藏有恶意代码

第三步:等待受害者
├─ 开发者用 AI 生成代码
├─ AI "幻觉"出这个包名
├─ 开发者 npm install
└─ 恶意代码进入项目

第四步:获利
├─ 窃取环境变量(API Key、密码)
├─ 植入后门
├─ 加密勒索
└─ 供应链攻击(感染下游项目)

3.2 真实案例

2025 年,安全研究人员发现了一个大规模的 Slopsquatting 攻击:

案例:huggingface-cli 事件

背景:
├─ Hugging Face 是最流行的 AI 模型平台
├─ 官方 CLI 工具叫 huggingface-hub
└─ 但 AI 经常"幻觉"出 huggingface-cli 这个名字

攻击:
├─ 黑客注册了 huggingface-cli 包
├─ 包内容:正常的 CLI 功能 + 隐藏的数据窃取代码
├─ 窃取内容:HF_TOKEN(Hugging Face API 密钥)
└─ 影响:数千个项目被感染

发现过程:
├─ 安全研究人员在分析 AI 幻觉模式时发现
├─ 该包已被下载数万次
└─ 大部分下载来自 AI 辅助开发的项目

3.3 规模有多大?

Slopsquatting 威胁规模(2025-2026):

已发现的恶意包:
├─ npm:3,000+ 个疑似 Slopsquatting 包
├─ PyPI:1,500+ 个疑似 Slopsquatting 包
└─ 其他包管理器:数量不详

潜在攻击面:
├─ 440,000 个 AI 幻觉包名可被利用
├─ 58% 的幻觉包名会重复出现(高价值目标)
└─ 每天有数百万次 AI 辅助的包安装

受影响的开发者:
├─ 97% 的开发者不会验证 AI 推荐的包是否存在
├─ 大部分人直接复制 AI 生成的 import 语句
└─ 很少有人检查 package.json 里的陌生依赖

04. 更可怕的:AI 生成的"合成漏洞"

除了幻觉包名,AI 还会生成一种全新的安全威胁:合成漏洞(Synthetic Vulnerabilities)

4.1 什么是合成漏洞?

合成漏洞是指:只存在于 AI 生成代码中的安全漏洞,人类程序员通常不会写出这种代码。

// 人类程序员写的代码(有漏洞,但是常见模式)
const userId = req.params.id
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`)
// SQL 注入漏洞,但 SAST 工具能检测到

// AI 生成的代码(合成漏洞,工具检测不到)
const userId = req.params.id
const sanitizedId = userId.replace(/[^0-9]/g, "") // 看起来做了过滤
const user = await db.query(`SELECT * FROM users WHERE id = ${sanitizedId}`)
// 问题:如果 userId 是 "1 OR 1=1",过滤后变成 "111"
// 不是注入了,但逻辑完全错误,可能返回错误的用户数据
// 传统 SAST 工具检测不到这种"逻辑漏洞"

4.2 合成漏洞的特点

// 合成漏洞 vs 传统漏洞
interface VulnerabilityComparison {
  aspect: string;
  traditional: string;
  synthetic: string;
}

const comparison: VulnerabilityComparison[] = [
  {
    aspect: "来源",
    traditional: "人类程序员的常见错误",
    synthetic: "AI 的独特错误模式"
  },
  {
    aspect: "可检测性",
    traditional: "SAST/DAST 工具能检测大部分",
    synthetic: "传统工具检测不到"
  },
  {
    aspect: "模式",
    traditional: "已知的漏洞模式(OWASP Top 10)",
    synthetic: "全新的、未分类的漏洞模式"
  },
  {
    aspect: "修复难度",
    traditional: "有成熟的修复方案",
    synthetic: "需要理解 AI 的"思维方式"才能修复"
  },
  {
    aspect: "复现性",
    traditional: "相同输入产生相同漏洞",
    synthetic: "AI 可能每次生成不同的漏洞代码"
  }
];

4.3 研究数据

合成漏洞研究(2025年,50万+代码样本):

发现:
├─ AI 生成的代码比人类代码有更多高危漏洞
├─ AI 会复制训练数据中的不安全编码模式
├─ AI 会"幻觉"出不存在的抽象层和框架
└─ 这些"幻觉框架"创造了全新的攻击面

具体数据:
├─ 45% 的 AI 生成应用包含 OWASP 漏洞
├─ AI 代码的高危漏洞密度是人类代码的 1.5 倍
├─ 30% 的合成漏洞无法被传统 SAST 工具检测
└─ 修复 AI 代码漏洞的时间比修复人类代码多 40%

05. 如何保护自己?

5.1 代码审查清单

// AI 代码审查清单
const aiCodeReviewChecklist = {

  // 1. 依赖检查
  dependencies: [
    "每个 import 的包是否真实存在?",
    "包名拼写是否正确?(typosquatting 风险)",
    "包是否来自官方源?",
    "包的下载量和维护状态如何?",
    "包的最近更新时间?(太新可能是恶意包)"
  ],

  // 2. API 检查
  apis: [
    "调用的 API 是否真实存在?",
    "参数数量和类型是否正确?",
    "返回值类型是否符合预期?",
    "是否使用了已废弃的 API?"
  ],

  // 3. 安全检查
  security: [
    "是否有 SQL 注入风险?",
    "是否有 XSS 风险?",
    "敏感数据是否正确处理?",
    "权限检查是否完整?",
    "是否有硬编码的密钥或密码?"
  ],

  // 4. 逻辑检查
  logic: [
    "边界情况是否处理?",
    "错误处理是否完善?",
    "代码逻辑是否符合需求?",
    "是否有"看起来对但实际错"的代码?"
  ]
};

5.2 工具推荐

防护 AI 代码幻觉的工具:

依赖检查:
├─ npm audit / yarn audit(基础检查)
├─ Snyk(更全面的漏洞扫描)
├─ Socket.dev(专门检测供应链攻击)
└─ deps.dev(Google 的依赖分析工具)

代码扫描:
├─ SonarQube(传统 SAST)
├─ Semgrep(可自定义规则)
├─ CodeQL(GitHub 的代码分析)
└─ AI 专用扫描器(2026年新出的工具)

实时防护:
├─ IDE 插件:在 import 时检查包是否存在
├─ Git Hooks:提交前自动检查依赖
├─ CI/CD 集成:构建时扫描
└─ 运行时监控:检测异常行为

5.3 最佳实践

AI 辅助开发安全最佳实践:

1. 永远不要盲目信任 AI 生成的代码
   ├─ 每个 import 都要验证
   ├─ 每个 API 调用都要查文档
   └─ 每段逻辑都要理解

2. 使用锁文件
   ├─ package-lock.json / yarn.lock
   ├─ 锁定依赖版本
   └─ 防止依赖被篡改

3. 定期审计依赖
   ├─ 每周运行 npm audit
   ├─ 检查新增的依赖
   └─ 移除不需要的依赖

4. 使用私有镜像
   ├─ 公司内部 npm 镜像
   ├─ 只允许白名单包
   └─ 阻止未知包安装

5. 代码审查流程
   ├─ AI 生成的代码必须人工审查
   ├─ 重点检查依赖和安全相关代码
   └─ 使用自动化工具辅助

06. 给不同角色的建议

6.1 如果你是个人开发者

个人开发者防护指南:

立即做:
├─ 安装 Socket.dev 或类似的 IDE 插件
├─ 每次 npm install 前检查包是否存在
├─ 养成查文档的习惯(不要只信 AI)
└─ 定期运行 npm audit

习惯养成:
├─ AI 生成代码后,先读一遍再用
├─ 看到陌生的包名,先去 npm 搜一下
├─ 不确定的 API,查官方文档确认
└─ 保持怀疑态度

6.2 如果你是团队 Leader

团队安全策略:

流程层面:
├─ 建立 AI 代码审查规范
├─ 要求所有 AI 生成代码必须标注
├─ 重点审查依赖变更的 PR
└─ 定期安全培训

工具层面:
├─ CI/CD 集成依赖扫描
├─ 使用私有 npm 镜像
├─ 配置依赖白名单
└─ 自动化安全检查

文化层面:
├─ 鼓励质疑 AI 生成的代码
├─ 奖励发现安全问题的人
├─ 分享 AI 代码踩坑经验
└─ 建立安全意识

6.3 如果你是安全工程师

安全工程师行动指南:

短期:
├─ 研究 AI 代码幻觉模式
├─ 建立 AI 代码专用扫描规则
├─ 监控公司代码库中的可疑依赖
└─ 培训开发团队

中期:
├─ 开发 AI 代码专用安全工具
├─ 建立 AI 代码安全基线
├─ 与 AI 工具厂商合作改进
└─ 参与行业安全标准制定

长期:
├─ 研究合成漏洞的检测方法
├─ 建立 AI 代码安全知识库
├─ 推动 AI 编程工具的安全改进
└─ 培养 AI 安全专业人才

07. 写在最后

AI 编程工具是把双刃剑。

它可以让你的效率提升 10 倍,也可以让你的项目在不知不觉中被植入恶意代码。

48% 的 AI 代码在"胡说八道"。

这不是危言耸听,这是研究数据。

440,000 个幻觉包名等着被利用。

这不是未来威胁,这是正在发生的攻击。

作为程序员,我们需要:

  1. 保持警惕:AI 生成的代码不是"免检产品"
  2. 验证一切:每个包、每个 API、每段逻辑
  3. 使用工具:让自动化工具帮你把关
  4. 持续学习:了解最新的安全威胁和防护方法

最后,送给所有程序员一句话:

"AI 可以帮你写代码,但只有你能为代码的安全负责。"

"那个你随手 npm install 的包,可能正在窃取你的 API Key。"

在 AI 时代,安全意识比任何时候都重要。

保持警惕,保护好自己。


💬 互动时间:你遇到过 AI 代码幻觉吗?你的团队有什么防护措施?评论区聊聊!

觉得有用的话,点赞 + 在看 + 转发,让更多程序员朋友看到~


本文作者是一个差点被 AI 幻觉坑了的程序员。关注我,一起在 AI 时代保持安全意识。

普通前端仔的 2025 : 年终总结与 AI 对我的影响

2026年1月12日 09:30

前言

2025 年已经过去,时间滚滚向前。恍惚间,仿佛上一次过年还在不久之前,转眼一年却已走到尾声。借着掘金这次 # 🏆2025 AI / Vibe Coding 对我的影响|年终征文 活动的机会,我想和大家分享一下自己的年终总结,并聊一聊 2025 年 AI 在工作与学习中对我的实际帮助。

开始坚持写文章分享

在今年年初,我和老婆完成了订婚,年中正式领取了结婚证💗。我的肩上多了一份对家庭的责任,也开始一起规划未来的生活,坚定了一个目标:一定要多赚一些钱,才能更有底气地生活。

后来我想到,之前曾看到有人提到在技术社区持续写文章,有机会接到外包或私活。于是,我决定在自己最常逛的技术社区——掘金,开始发布一些原创技术文章。

最早是在 2024 年 12 月底,因为工作阶段性需求不大,有了一些空闲时间,我便开始动笔。但现实很快给了我反馈:文章写完后几乎没人看。其实这也很正常,就像刚开始做自媒体需要“起号”一样,一个新账号发布的第一篇文章,基本不会有太多曝光。

后来,好朋友韬哥提醒我,文章审核通过后可以让朋友帮忙点点赞,新文章有机会进入周榜,从而获得更多曝光。这里也要感谢我老婆以及几位朋友,对我写作的支持与鼓励和建议。万分感谢🙏

接下来就是不断地写。直到有一篇 # 前端开发又幸福了,Cursor + Figma MCP 快速还原设计稿 意外火了,不仅阅读量明显上涨,还被掘金公众号转发。事实上,这篇文章反而是我写得最随意、耗时最短的一篇,可能正好踩中了 MCP 的热点。当时 MCP 刚出现不久,那段时间我确实非常开心。

或许是因为好奇心比较强——说得直白一点,其实也是想“偷懒”——我一直很愿意尝试新事物😂,所以第一时间体验了 MCP,确实让人眼前一亮。随后我便迫不及待地想把这些体验分享出来,担心同事在实际使用中踩坑,便写下了这篇文章,想着审核通过后可以直接转发给同事参考实践。后面关于 AI 的相关内容,我也会继续深入,具体聊一聊 AI 在工作方式和工作内容上带来的改变。

我在写文章的过程中,也会适当借助一些 AI 辅助。毕竟我的文笔并不算好,容易偏口语化,自己写完再读一遍时,常常都有些读不下去,因此我通常会让 AI 帮我优化一下文案表达。在这里也确实要感谢“AI 老师”,在写作效率和可读性上给了我很大的帮助。

但与此同时,我也非常排斥“AI 味”过重的文章。掘金上有些上周榜的内容,我几乎一眼就能看出是 AI 生成的。或许现在还能分辨,再过两年就未必了。我记得有一次刷到一篇讲“2025 年 JavaScript 新语法糖”的文章,通篇都是 AI 胡编乱造的内容,作者既没有自行验证,也没有标注 AI 生成,就这样直接发布出来。这种行为在我看来完全是在误导新人,想到这里就会感到非常生气。

我始终认为,每个人都应该对自己分享的知识负责。因此,我写的文章尽量都是真人思考、真人实践得出的内容,只是借助 AI 做一些文字层面的润色,而不是让它替我“创作观点”。

随着 AI 能力不断增强,一些常见、零散的编程问题其实已经不太值得单独分享了,比如 JavaScript 时间处理中的各种坑,AI 的回答往往已经足够准确和全面。相比之下,更有价值的内容,反而是系统化、体系化的实践流程与思考总结,这也是我之后更希望持续输出的方向。

跳槽

另一方面,也是想多赚一些钱。成家之前,我的工资养活自己绰绰有余,但成家之后,现实问题就变得具体起来:未来如果有孩子、还没有买房,这些都需要更强的经济支撑。我也很清楚,在中国大部分程序员的职业生命周期大概率只有十几年,到了年龄偏大时,可能就需要考虑转型。2025 年,是我毕业、正式进入社会工作的第三年,因此我做出了一个决定——准备跳槽。

马云曾说过一句话:

跳槽无外乎两个原因,钱给少了,心里受委屈了。

这两点,我可能都占了。在这家公司干了两年,年初时,领导、CTO,以及当初面试我的帆叔,或许是出于生活和前途的考虑,陆续选择了离开。核心人物走后,公司换了新的领导,但我明显感觉到一种“死海效应”。感觉开了很多没有必要的会议,真的像过家家一样,我也感觉到没有效率无头苍蝇一样东一榔头西一棒的做事情。

所谓“死海效应”,是指组织中优秀员工不断流失,如同死海水分蒸发,导致低质量员工比例上升,从而影响整体效率和企业发展。

其实在我第一次提出离职时,公司也给我调了薪。当时我一度以为,自己可能会在这里长期干下去。但后来发生了一些不太方便细说的矛盾,如今回头看,我依然认为自己并没有做错。最终,出于职业发展与前途的考虑我还是选择了离开。

我悄悄提交了离职申请,只提前和一直合作的产品同学说了一声。说实话,我们组的产品在我看来是非常有能力的人才。直到我离职的最后一天,很多同事看到我的签名留言(相遇是缘,祝大家越来越好),才意识到我要走了。那天有十几位同事和我道别,让我非常感动。直到现在,我也还会和前同事们时不时在微信上聊聊天,聊前端,聊 AI。我跟每个同事都很熟悉,可能是我的性格善于把大家链接起来。

提完离职之后,我便立刻开始找工作。我并没有打算 gap 一段时间,因为之前已经 gap 过一次。那次裸辞后玩了两个月,前期确实很爽,像是在过寒暑假;但等旅游结束回到出租屋后,每天不是躺着就是刷手机、玩电脑,生活逐渐失去了目标感。那时我才真正意识到,人是需要劳动的,需要在社会工作中获得价值感。

正因如此,那次我很快重新投入找工作,也正是在那段时间,柯总收留了当时只有一年工作经验的我🙏。

正如马克思所说:

劳动是人类生存的基石,是人自身发展的决定性要素。在共产主义社会高级阶段,“劳动已经不仅仅是谋生的手段,而是本身成了生活的第一需要”。

在跳槽过程中,我也观察到了招聘市场风向的变化:越来越多的公司更倾向于简历中带有 AI 项目经历的候选人。幸运的是,我在 2023 年第一份工作时就参与过一个 AI 相关的生图项目,这让我的简历在市场上颇受欢迎。不过,当时市场对 AI 的重视还有滞后性,真正对 AI 项目经历感兴趣的公司并不多。到了这次跳槽,情况明显不同——AI 相关经历几乎成为必问项,也显著提升了候选人的吸引力。这让我深刻体会到,AI 对程序员带来的不是威胁,而是新的机会。

在面试过程中,我也会主动考察部门的 AI 使用情况。令我震惊的是,很多小公司的团队虽然知道 AI 的存在,但根本没有实际应用,仍然依赖传统的手工编码。显然,我不会选择加入这样的团队,因为对于我而言,高效利用 AI 不只是工具加成,而是能显著提升团队整体效率和技术成长空间的重要指标。

有了上一次裸辞的经历后,这一次在“多赚钱”的前提下,我几乎没有给自己任何休息时间,离职后便立刻投入到找工作中。或许缘分就是这么巧,我很快找到了一份听起来前途还不错的工作。但由于当时没有把工作时长和薪资细节问清楚,也没有在谈薪阶段据理力争到自己真正满意的程度,入职后还是产生了一些后悔的情绪。不过再找一份工作的成本不低,加上自己也有些懒,索性就先在这家公司干了下来。

这是一家总部在北京的做游戏的大公司,在广州新成立的一个部门,部门在 5 月份成立,而我是 8 月份加入的。由于我之前的技术栈和项目经验主要集中在管理后台领域,入职后便从0到1参与并负责了一个内部 BI 后台项目的建设。新公司的领导能力很强,一人同时承担后端开发、产品规划以及与设计师沟通协调等多重角色。

团队规模不大,我们是一个前端、一个后端,再加上一位测试同学,三个人协作完成了这个中台系统的开发,用于支持游戏发行部门的日常业务。

AI

也该聊到 AI 了,不然有点偏题太久了。😂

2022年的 AI

第一次接触 AI 辅助编程,是在 2022 年通过 GitHub Copilot。当时我在写毕业设计,用到的是一个需要发邮件申请试用的 VS Code 插件。印象很深的是,只要写一个诸如“二分查找”的注释,下面很快就能自动补全出完整代码,确实让人觉得相当聪明。

后来在 2022 年 12 月左右,ChatGPT 横空出世。现在回头看,那真的是一件非常幸运的事情——我刚参加工作没多久,大语言模型就已经出现了。那段时间最大的感受是:GPT 写出来的代码,甚至比当时作为初级程序员的我还要规范、完整。

于是后来每次遇到代码重构或优化相关的问题,我都会先问一问它。在不断的使用过程中,也确实从“AI 老师”那里学到了不少编程思路和实践技巧。

2023,2024年的 AI

那时候 ChatGPT 还没有免费开放,基本都是通过国内的镜像站之类的方式在使用,稳定性和体验都谈不上好,但依然挡不住大家的热情。我还记得 Cursor 刚出来的时候,最大的优势就是不需要科学上网就能直接用 GPT,这一点在当时非常有吸引力。谁能想到,后来这个工具不断迭代升级,从一个“能用”的编辑器插件,逐渐成长为 AI IDE 的第一梯队,甚至在某些场景下彻底改变了写代码的方式。

在那个阶段,我的使用方式其实还比较“传统”:写完一段代码,复制出来,粘贴到 GPT 里提问,让它帮我看看有没有优化空间、潜在问题,或者让它补全缺失逻辑,然后再把结果复制回编辑器中。这个流程现在看起来有些笨重,但在当时已经极大提升了效率。很多原本需要翻文档、查 Stack Overflow 的问题,几分钟内就能得到一个相对完整的答案。

那时的 AI 更多还是“辅助工具”的角色,而不是直接参与到编码流程中。它更像是一位随叫随到、耐心十足的高级同事,帮你查资料、给思路、补细节。虽然偶尔也会胡编乱造,需要自己具备判断能力,但不可否认的是,从 2023 年开始,我已经明显感受到:写代码这件事,正在被 AI 悄然重塑。

2025 年的 AI

一直到 2024 年底,Cursor 突然火了起来。我记得好像是某公司的一个大佬的女儿在几乎没有编程经验的情况下,用 Cursor 写了一个小程序,这篇推特被广泛转发后,Cursor 迅速走红。我看到后也下载了最新版,试用后直接被震撼到了——它的补全功能丝滑得让人难以置信,好像能直接理解我脑子里的想法,这是我第一次体验到如此智能又顺手的 AI 编程提示。

当时,我也尝试了 Cursor 的一个竞品 Winsurf,但整体体验还是 Cursor 更佳。有人会说,这不过是把 AI 模型套个壳而已,但我认为“套壳”也有高低之分。作为普通程序员,我们不必去研究模型的理论,但在应用层的交互体验、细节设计做得出色,同样非常了不起。使用 Cursor 后,我明显感受到工作效率提升,甚至可以达到两倍、五倍甚至十倍。

我当时非常积极地向同事推荐,但发现部分同事带有悲观色彩,担心 AI 会替代程序员,因此不愿尝试。对此,我的观点是:AI 是提效工具,它能帮你节省重复劳动,让你有更多时间去学习新技术、思考产品设计和架构优化。AI 的核心意义在于,让程序员从繁琐的 CRUD 工作中解放出来,把时间用在更高价值的工作上,让创意和想象力真正发挥作用。

与此同时,字节跳动推出了 Trae,我也体验过并写过相关征文,但整体体验还是不如 Cursor 顺手。也许是 Trae 的宣传和营销做得比较好,所以在我跳槽面试时,不少团队表示虽然自己没有使用 AI 编程,但知道字节出了 Trae。

后面过春节的时候,国产开源模型之光 DeepSeek 横空出世,连家里的长辈都知道中国出来个 nb 的 AI。太伟大了 DeepSeek 直接选择了开源,给全世界分享他们的成果,respect🫡!!!

在高强度使用了月左右后,我积累了一些经验和方法,也在文章中分享给了大家。

随着 AI 工具的发展,我也开始尝试其他工具,例如 Winsurf 和 Argument Code。特别是 Argument Code,这是一个 VS Code 插件,能够智能寻找代码中相关的影响范围,非常适合进行复杂逻辑分析。背后的 AI 模型 Claude 在这里表现得很聪明,但订阅价格不低,当时约 100 美元/月。

后来我也尝试了 Claude Code 和 Codex 的 CLI,不得不说,Claude 模型确实很强(题外话:但最近对第三方的封禁以及反华的一些魔幻操作,真希望预告新年发布的DeepSeek v4能挫挫这家公司锐气!),尤其在编码和设计相关的理解上非常到位。开源的 Claude-agent-sdk 也很优秀,很多人在它的基础上可以做自己的 CLI 二次开发。不过,我个人还是不太习惯在终端里使用 AI,习惯了有 GUI 界面的 IDE,操作起来更直观、顺手。

谷歌的 Antigravity我也体验了,都是在侧边栏有个对话框,可以试用 Gemini 与 Claude,我经常用 Gemini 写页面,但是写逻辑他很喜欢 any 跳过,很无语😅,写逻辑还是需要用 Claude。每周会限制一些使用额度,不得不说谷歌还是家大业大,想要超车提速就是这么快。但是这个产品名称起的真的不好拼写哈哈。

目前我在试用 Kiro 的 Claude 服务,用的是白嫖的 30 天、500 积分版本。不过这个 IDE 似乎没有智能提示功能(可能是我使用姿势不对?但我理解应该是默认开启的)。

总的来说,虽然 CLI 强大,但对我而言,GUI 界面的交互体验更符合日常编码习惯。我估计下一步还是回到 cursor 了。

对 AI 的思考与想法

写了这么多,我也有些累了。这是我第一次写这么长的文章,可能是因为想表达的内容实在太多了。码了一上午,最后想和大家聊聊我个人对 AI 的理解与思考。

AI 给我的工作效率带来了成倍提升。面试时我也常提到,以前写代码都是一行行敲,现在几乎可以“一片一片”地生成代码。但这并不意味着可以无脑相信 AI 输出的结果。如果每天只是依赖 AI 完成 Vibe Coding,长期下来可能会非常痛苦——-因为你不了解 AI 的实现细节。选用性能差的模型,即便功能实现了,后续改造或迭代可能会非常困难,只能再次依赖 AI 来处理。久而久之,就可能形成“AI 生成的代码屎山”。

因此,我的做法是:每次命令 AI 完成任务后,都会仔细 review 它的代码,再进行提交。如果项目是一次性的小型任务,或许可以不用过于严格,但对于需要长期维护的系统,认真 review 并与 AI 协作至关重要。

同时,AI 目前还无法替代程序员,其根本原因在于缺乏责任感。AI 的上下文长度有限,它无法像人一样,在公司里长期维护多个项目四五年。上下文越长,它遗忘的内容也越多。新建一个窗口,之前的事情就忘记了(可以设置全局 rule) 此外,一些自媒体常吹嘘用 AI 完全不会编程也能完成系统开发,虽然 AI 越来越强,一次性任务看起来很漂亮,但遇到小细节或后续改动时,如果没有懂一点的人去指挥和优化,代码很容易崩溃。

所以,至少需要一个懂技术的人来指导 AI,确保输出可靠。实际上,AI 也可以成为学习的辅助工具:通过它快速学习新的编程语言语法、软件架构最佳实践,再用这些知识高效指挥 AI 完成任务。总结来看,AI 是效率的倍增器,但仍然需要人的经验与判断力来控制风险、保证质量。

我觉得大家应该积极拥抱 AI,面对它、理解它,并善加利用,让 AI 成为让自己如虎添翼的工具。AI 的发展必然会带来产业变革和技术革新,但从更宏观的角度看,它是推动人类文明进步的重要力量。我们正加速步入一个生产力大爆发的时代,AI 将程序员从以往繁琐的搬砖任务中解放出来,让我们有更多精力去思考架构设计、创新功能,以及探索新的技术边界。

更进一步,AI 的真正价值在于它能够让人类在创造力和效率之间找到平衡。以前很多重复性工作占据了大量时间,现在这些工作可以交给 AI 来处理,而程序员可以把精力放在更高层次的思考上:如何设计更优的系统、如何优化用户体验、如何在复杂业务中做出更合理的决策。AI 不仅是工具,也是学习的助力,它能够快速提供信息、分析方案,让我们在短时间内掌握新技术、新方法,从而实现知识和能力的快速积累。

可以说,AI 对程序员而言,是一种能力的放大器,而不是替代品。未来,能够合理运用 AI 的人,将比单纯依赖传统技能的人更具竞争力。在这个过程中,保持学习、理解和掌控 AI 的能力,比单纯追求 AI 生成的结果更重要。真正掌握了这项能力的人,将能够在技术创新和生产力提升的浪潮中站稳脚跟,甚至引领变革。

结语

过去的一年是成长的一年,我也能明显感受到,相比去年的自己,有了不少进步。

希望在新的一年里,AI 能够展现出更惊艳的能力,带来更多创新和可能。期待未来,也祝大家新年快乐,工作顺利,生活愉快,每个人都能不断成长、越来越好。

Skill 真香!5 分钟帮女友制作一款塔罗牌 APP

作者 乘风gg
2026年1月12日 09:14

最近发现一个 AI 提效神器 ——Skills,用它配合 Cursor 开发,我仅用 5 分钟就帮女友做出了一款塔罗牌 H5 APP!在说如何操作之前,我们先大概了解下 Skills 的原理

一、Skills的核心内涵与技术构成

(一)本质界定

Skills 可以理解为给 AI Agent 定制的「专业技能包」,把特定领域的 SOP、操作逻辑封装成可复用的模块,让 AI 能精准掌握某类专业能力,核心目标是实现领域知识与操作流程的标准化传递,使AI Agent按需获取特定场景专业能力。其本质是包含元数据、指令集、辅助资源的结构化知识单元,通过规范化封装将分散专业经验转化为AI Agent可理解执行的“行业SOP能力包”,让 AI 从‘只会调用工具’变成‘懂专业逻辑的执行者

(二)技术构成要素

完整Skill体系由三大核心模块构成,形成闭环能力传递机制:

  1. 元数据模块:以SKILL.md或meta.json为载体,涵盖技能名称、适用场景等关键信息约 100 个字符(Token),核心功能是实现技能快速识别与匹配,为AI Agent任务初始化阶段的加载决策提供依据。
  2. 指令集模块:以instructions.md为核心载体,包含操作标准流程(SOP)、决策逻辑等专业规范,是领域知识的结构化转化成果,明确AI Agent执行任务的步骤与判断依据。
  3. 辅助资源模块:可选扩展组件,涵盖脚本代码、案例库等资源,为AI Agent提供直接技术支撑,实现知识与工具融合,提升执行效率与结果一致性。

和传统的函数调用、API 集成相比,Skills 的核心优势是:不只是 “告诉 AI 能做什么”,更是 “教会 AI 怎么做”,让 AI 理解专业逻辑而非机械执行

二、Skills与传统Prompt Engineering的技术差异

从技术范式看,Skills与传统Prompt Engineering存在本质区别,核心差异体现在知识传递的效率、灵活性与可扩展性上:

  1. 知识封装:传统为“一次性灌输”,冗余且复用性差;Skills为“模块化封装”,一次创建可跨场景复用,降低冗余成本。
  2. 上下文效率:传统一次性加载所有规则,占用大量令牌且易信息过载;Skills按需加载,提升效率并支持多技能集成。
  3. 任务处理:传统面对复杂任务易逻辑断裂,无法整合外部资源;Skills支持多技能组合调用,实现复杂任务全流程转化。
  4. 知识迭代:传统更新需逐一修改提示词,维护成本高;Skills为独立模块设计,更新成本低且关联任务可同步受益。

上述差异决定Skills更适配复杂专业场景,可破解传统Prompt Engineering规模化、标准化应用的瓶颈。

三、渐进式披露:Skills的核心技术创新

(一)技术原理与实现机制

Skills能在不增加上下文负担的前提下支撑多复杂技能掌握,核心在于“按需加载”的渐进式披露(Progressive Disclosure)设计,将技能加载分为三阶段,实现知识传递与上下文消耗的动态平衡:

  1. 发现阶段(启动初始化):仅加载所有Skills元数据(约100个令牌/个),构建“技能清单”明确能力边界,最小化初始化上下文负担。
  2. 激活阶段(任务匹配时):匹配任务后加载对应技能指令集,获取操作规范,实现精准加载并避免无关知识干扰。
  3. 执行阶段(过程按需加载):动态加载辅助资源,进一步优化上下文利用效率。

(二)技术优势与价值

渐进式披露机制使Skills具备三大核心优势:

  1. 降低令牌消耗:分阶段加载避免资源浪费,支持单次对话集成数十个技能,降低运行成本。
  2. 提升执行准确性:聚焦相关知识组件,减少干扰,强化核心逻辑执行精度。
  3. 增强扩展性:模块化设计支持灵活集成新知识,无需重构系统,适配领域知识快速迭代。

四、Cursor Skills

介绍完 Skills 是什么之后,我将使用的是 Cursor 作为我的开发工具。先说明一下,最开始只有 Claude Code 支持 Skills、Codex 紧随其后,口味自己选。

好消息是,Cursor 的 Skills 机制采用了与 Claude Code 几乎完全一致的 SKILL.md 格式。这意味着,你完全不需要从头编写,可以直接将 Claude Code 的生态资源迁移到 Cursor。

(一)Cursor 设置

因为 Cursor 刚支持不久,并且是 Beta 才能使用,所以要进行下面操作

Agent Skills 仅在 Nightly 更新渠道中可用。
要切换更新渠道,打开 Cursor 设置( Cmd+Shift+J ),选择 Beta,然后将更新渠道设置为 Nightly。更新完成后,你可能需要重新启动 Cursor。 如下图所示

要启用或禁用 Agent Skills:

  1. 打开 Cursor Settings → Rules
  2. 找到 Import Settings 部分
  3. 切换 Agent Skills 开关将其开启或关闭 如下图所示

(二)复制 Claude Skills

然后我们直接去 Anthropic 官方维护的开源仓库 anthropics/skills,里面提供了大量经过验证的 Skill 范例,涵盖了创意设计、开发技术、文档处理等多个领域。

你可以访问 github.com/anthropics/… 查看完整列表。以下是这次用到的 Skills

Frontend Design:这是一个专门用于提升前端设计质量的技能。它包含了一套完整的 UI 设计原则(排版、色彩、布局)

然后我们直接把 Skills 里面的 .claude/skills/frontend-design 到当前项目文件下,如图:

模型和模式如下图

提示词如下,不一定非得用我的。

使用 Skill front-design。我要做一个 H5 ,功能是一个塔罗牌。

你是一名经验丰富的产品设计专家和资深前端专家,擅长UI构图与前端页面还原。现在请你帮我完成这个塔罗牌应用的 UI/UX 原型图设计。请输出一个包含所有设计页面的完整HTML文件,用于展示完整UI界面。

注意:生成代码的时候请一步一步执行,避免单步任务过大,时间执行过长

然后 Cursor 会自动学习 Skills,并输出代码

然后就漫长的等待之后,Cursor 会自动做一个需求技术文档,然后会一步一步的实现出来,这时候可以去喝杯茶,再去上个厕所!

最终输出了 5 个页面

  1. 首页 (Home)
  2. 每日抽牌页 (Daily Draw)
  3. 牌阵占卜页 (Spread Reading)
  4. 塔罗百科页 (Encyclopedia)
  5. 占卜历史页 (History)

最终效果如下,整体效果看起来,完全是一个成熟的前端工程师的水准,甚至还带有过渡动画和背景效。因为掘金无法上传视频,欢迎私信我找我要或者关注我:

image.png

扩展阅读

因为 Cursor 目前仅在 Nightly 版本上才可以使用 Skills。如果担心切换此模式会引发意想不到的情况,可以使用另一种方案

OpenSkills 是一个开源的通用技能加载器。

  • 完全兼容:它原生支持 Anthropic 官方 Skill 格式,可以直接使用 Claude 官方市场或社区开发的技能。
  • 桥梁作用:它通过简单的命令行操作,将这些技能转换为 Cursor、Windsurf 等工具可识别的配置(AGENTS.md),从而让 Cursor 具备与 Claude Code 同等的“思考”与“技能调用”能力。

Tailwind 因为 AI 的裁员“闹剧”结束,而 AI 对开源项目的影响才刚刚开始

2026年1月12日 06:53

Tailwind 还是相当明白「会哭的孩子有奶吃」这个道理,“裁员风波”才刚开始,立马就收到谷歌 AI Studio 、Vercel 和 Lovable 的相关赞助:

这个风波其实并不是最近才开始的,早在去年年底,Bun 被 Anthropic 收购加入 Claude Code 阵营的之后,Tailwind CSS 的创始人 Adam Wathan 就发出过灵魂警告:

因为现在很多 AI 公司,比如 OpenAI 、Claude、Gemini 等,都在前端 AI 上都大量使用了 Tailwind,因为很大程度上, Tailwind CSS 训练数据多、表达方式离散可拼装、可控性强、出错成本低 ,例如:

<div class="p-4 rounded-xl shadow-md bg-white">

对 AI 来说,Tailwind 的 class 写法非常像在拼积木,每个 token(p-4 / rounded-xl / shadow-md)都是一个“语义单元”:

  • 既能局部修改(把 p-4p-6
  • 又能组合叠加
  • 还能按响应式/状态扩展(md:p-6 hover:bg-xxx)

在这方面,模型向来更擅长生成离散符号序列(token),而不擅长维护抽象结构,同时 class 贴在元素上所见即所得,符合 AI 追求的尽可能“生成代码一次就能跑”

特别是谷歌的 AI Studio 在这方面倾向特别明显。

那这对 Tailwind 有什么影响?这不是代表着框架在 AI 时代很流行吗?为什么还会出现”裁员“问题?这个影响其实就类似 AI 对 Stackoverflow 的影响,原网站没流量了:

Tailwind 这次的本质矛盾在于,AI 让 Tailwind 使用量更大,但把它原本的赚钱路径(流量 to 转化)给切断了,所以反而出现“越火越穷”的情况。

Tailwind 本体是开源免费的,但是它的典型商业模式是:

Google/搜索 → Tailwind 官网文档/教程 → 认同与依赖 → 购买增值产品(模版、文档、企业合作、教程和顾问咨询等)。

这其实也是很多开源项目的盈利方式,特别国内很多 gitee 的项目更明显,放出简陋开源版本,付费提供文档和完整商业版本,而这些付费产品严重依赖:文档流量 + 心智依赖 ,还有用户在官网停留时间和访问频率,但是现在 AI 在掐断了这个路径

Tailwind 在线文档的流量下降了 40%,收入下降了 80%,实际上写技术文章和公众号的应该也有感受,现在的开发者越来越不喜欢读和找技术文章了,就算读也是让 AI 直接总结

当然,这波闹出来的裁掉 75% 的工程师的事件,多少也有一些标题党的味道,因为工程团队原来有4 个工程师,这次裁掉了 3 个,所以比例高达 75%

实际应该就是剩下创始人+ 1 个工程师+ 1 个兼职的团队规模。

当然,这波赞助风波其实对于 Tailwind 危机来说,也只是解救近期的燃眉之急,因为它不像 Bun ,Bun 对 Anthropic 来说是强战略资产,因为运行时/工具链直接影响 AI 编码产品的性能与交付:

一体化 runtime/toolchain,和 AI coding 产品的工程链路强绑定,收购能立刻减少外部依赖、提升稳定性与性能上限。

所以 Bun 卖的是“工程基础设施能力”(速度/工具链/交付体验),而 Tailwind 虽然十分流行,但是主要商业化通常靠“围绕开源的增值产品/服务漏斗”,成不了核心体系的,简单说:

  • Bun 属于“可控的关键基础设施(runtime/toolchain)”,收购后可以把关键工具链进化成自有资产
  • Tailwind 属于“可替代的开发者体验层(UI styling layer)”,买它不太会给你护城河

在链路上的差距,也导致了它们之间命运的走向不同,当然 Tailwind 深知“发声”的重要性,所以这波闹腾之后,也暂时性解决了生存问题,只是:

赞助只能覆盖一时的资金问题,但解决不了当前开源项目的的商业模式窘境。

AI 切断流量是致命的,StackOverflow 在这方面表现最为明显也最为明显,所以 Tailwind 这波于开发者和开源领域来也是很明显的警钟:

就像我朋友,上午还问有没有什么记账软件推荐,结果下午就直接用 AI 做了一个符合心意的应用,AI 对于个人开发者的影响未来也会越来越明显,如果 AI 可以直接 A2UI 直出功能和结果的时候,是否其他独立产品还有存在的意义?

image-20260111143806306

所以, AI 对于开发者和开源项目的影响才刚刚开始,以前的项目增长和流水靠的是:

  • Google 搜索
  • 文档流量
  • StackOverflow
  • 博客/教程
  • GitHub star 传播

但是现在 AI 时代之后,开源的影响力不再去取决于:

  • 文档写得多好
  • SEO 做得多好

现在的项目是否流畅,越来越由取决于 AI :

  • 训练语料里出现得够不够多
  • 模型偏好它还是偏好别的库
  • 它是否“更适合生成”

而项目能否赚到钱,更要取决于你在 AI 链路里扮演的角色,这也是 Tailwind 这波表现出来的趋势:

虽然你在 AI 时代很多,但是越火,流量却越少。

前端佬们!!AI大势已来,未来的上限取决你的独特气质!恭请批阅!!

作者 大怪v
2026年1月10日 20:23

前言

写这篇文章,纯粹是AI热潮给炸出来的。本来想继续更我的虚拟世界专栏,但看了一眼沸点,好家伙,大家都在聊DeepSeek、ChatGPT,感觉我不说两句显得我很不合群。

还有另外一个原因,就是身边的很多程序员,都在焦虑。

那么,程序员真的会被替代吗?兄弟们,别急别急!

我直接给出我自己的观点:如果还是之前那种记俩API、写两功能的程序员,我相信很快会替代。但是那种会整活、会整合,直接面向问题解决的程序员,不会的!!!

O9.gif

对比以前还要苦哈哈地背API,现在AI把门槛直接铲平了。这哪里是危机?这分明是从“螺丝钉”进化成“架构师”的最佳版本!

我的核心观点:AI对于之前的经验总结、归纳非常牛牪犇!!但是对于复杂问题、现实的敏感度以及所谓创新,他们还直接不能!!

来,上理由!

0变1的门槛,被无限拉低了!

以前你想做一个全栈应用,你得懂前端、后端、数据库、运维... 还没开始写代码,环境配置就先把你劝退了。

现在呢?

只要你的想法够骚,AI就是你的一万个分身。

骄傲.gif

我有一个专栏,手搓虚拟世界

在没有 AI 的时候,你想从 0 到 1 做一个产品,你要懂后端、懂数据库、懂运维,甚至还得懂点 UI 设计。这每一项,都是一座大山。很多很棒的 idea,就死在了“我不会写后端接口”或者“这 UI 丑得我没眼看”上。

现在呢?

AI 就是你的那个全能外包团队。你不会写 SQL?问它。你不会画 icon?让它画。 以前我们为了画一个完美的圆,可能要算半天 Math.PI,现在你只需要告诉 AI:“给我整一个圆,要五彩斑斓的那种。”

0 变 1 的过程,不再是技术的堆砌,而是你“脑洞”的直接具象化。 只要你有想法,技术实现的壁垒正在被 AI 暴力拆除。

这是拼气质的时代

很多人说 AI 出来的代码没有灵魂,是缝合怪。 我说:别急别急!

当所有人都能一键生成标准化代码的时候,什么东西最值钱? 是个性。是那种“独属前端佬气质”的创新。

0.gif

就像当年的 Flash,工具大家都有,但只有少数人能做出《小小火柴人》。AI 时代同理,它能帮你生成 90% 的通用代码,但剩下那 10% 的、决定产品气质的、让人眼前一亮的 “手搓” 部分,才是你真正的价值所在。

未来的牛人,不是谁 API 背得熟,而是谁能用 AI 这个超级引擎,组合出别人没见过的玩法。这不就是我们最擅长的吗?不依赖第三方库(因为 AI 可能会瞎引用),纯靠逻辑和创意,去构建一个新的虚拟世界。

能做什么,取决于你能想到什么

以前想整一个事情,大致如下流程:想法=>需求=>原型=>UI=>交互=>编写代码

完全靠人海战术。 现在?一个拿着 AI 的工程师(或者说“全栈工程师”),战斗力可能抵得上以前的一个公司。

这意味着什么?意味着个人创新的回报率被无限放大了。你不需要在一个项目中,当一颗在大机器里运转的螺丝钉,你有机会成为那个设计机器的人。

假如未来硬件再大升级(就像我之前说的智能眼镜、脑机接口),结合 AI 的生产力,一个人手搓一个“元宇宙”雏形,可能真的不再是梦。

93d794510fb30f248be3f2baca95d143ac4b03e8.gif

AI 不会淘汰有想法的人,它只会淘汰那些只会 copy-paste 的“代码搬运工”。

与其在焦虑中等待被替代,不如现在就头脑热一把,利用这个时代的红利,去“手搓”一点属于你自己的、独一无二的东西。

毕竟,只有当现有的规则已经装不下你的野心时,打破规则才更有乐趣。

前端佬们,别怂,干!

❌
❌