普通视图

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

【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 的“代码搬运工”。

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

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

前端佬们,别怂,干!

自定义AI智能体扫描内存泄漏代码

作者 石小石Orz
2026年1月8日 09:32

引言

在日常开发时,由于部分代码不严谨,浏览器无法及时回收内存(GC),容易导致 内存泄漏 和页面卡顿。

传统排查内存泄漏需要通过浏览器 Memory 面板 多次快照、分析和定位,过程复杂且耗时。

结合 AI 技术,一些前端内存泄漏问题可以被快速发现并解决。使用 AI 扫描代码,可以自动识别潜在问题,提高排查效率。

本文将结合 CodeBuddy + Cloud 模型,通过自定义智能体对代码进行精准的内存泄漏分析,辅助开发者快速定位问题并进行修复。

CodeBuddy:AI助力内存泄漏扫描

什么是CodeBuddy

CodeBuddy 是腾讯云推出的多形态 AI 编程助手,覆盖编译器插件AI IDE(类 Cursor)和命令行三种形态。其命令行模式可通过简短指令对整个项目进行快速扫描,自动执行内存泄漏分析,显著提升排查效率。

如何使用

安装

可以打开任意项目,在控制台输入:

npm install -g @tencent-ai/codebuddy-code

CodeBuddy需要node版本大于18.20.8, 使用前使用nvm切换node。

登录

安装完毕后,控制台输入codebuddy,即可使用。第一次使用,需要登录,有两种登录方式:

  • Google / Github

使用这种方式登录,会打开海外版,内置Gemini-2.5-Pro、Gemini-2.5-Flash、GPT-5、GPT-5-codex、GPT-5-mini、GPT-5-nano、GPT-4o-mini等模型。

  • 微信扫码登录

微信登录的是国内版,使用DeepSeek-V3.1模型。

强烈建议使用Google / Github登录,选择 GPT-5 或 GPT-5-codex 模型

使用说明

登录成功后,显示如下:

codebuddy入门提示如下:

  • / 使用命令,按 @ 引用文件。
  • Esc 键两次可重置输入框。
  • 输入时按 Shift + Enter 可以换行。

如果我们不想做任何配置,想快速得到一份内存泄漏报告,最简单的方式就是在输入框中写入简明提示词:

扫描 src 文件夹下的 viewscomponents 目录,分析出存在内存泄漏的代码,并输出一个可直接打开的 HTML 报告,报告中包含详细的内存泄漏分析和修改建议。

如果项目体量很大,可以分文件扫描,避免token浪费,扫描出错。

命令

CodeBuddy的输入框中按 / 使用命令,可用命令如下:

命令 功能说明
/add-dir 添加一个新的工作目录
/agents 管理智能体(agent)配置
/bashes 列出并管理后台运行的任务
/clear 清除当前会话历史并释放上下文
/compact 清除会话历史,但保留摘要在上下文中(可选:/compact [摘要指令])
/config 打开配置面板
/cost 显示当前会话的总费用和持续时间
/doctor 诊断并验证 CodeBuddy 的安装与设置是否正常
/exit 退出 CodeBuddy
/export 将当前会话导出为文件或复制到剪贴板
/help 显示帮助和所有可用命令
/hooks 管理工具事件(Tool Events)的钩子配置
/ide 管理 IDE 集成并显示状态
/init 分析你的代码库(初始化项目)
/install-github-app 为某个仓库设置 GitHub Actions 集成
/login 登录或切换腾讯云 CodeBuddy 账号
/logout 登出腾讯云 CodeBuddy 账号
/mcp 管理 MCP 服务器(Model Control Protocol)
/memory 编辑 CodeBuddy 的记忆文件
/migrate-installer 从全局 npm 安装迁移为本地安装
/model 设置 CodeBuddy 使用的 AI 模型
/permissions 管理工具权限规则(允许/拒绝)
/pr-comments 获取 GitHub Pull Request 的评论
/release-notes 查看版本更新说明
/resume 恢复一个之前的会话
/review 审查一个 Pull Request(代码评审)
/status 显示 CodeBuddy 状态(版本、模型、账号、API 连接和工具状态)
/terminal-setup 安装 Shift+Enter 组合键用于输入换行
/todos 显示当前会话的待办事项列表
/upgrade 在浏览器中打开升级页面
/vim 切换 Vim 模式与普通编辑模式
/workspace 切换到其他工作文件夹

eq:输入/exit,可以退出当前工具。

费用

目前是免费使用,采用的是积分方式,可以在 www.codebuddy.ai/profile/usa… 查看自己的积分。

最佳实践

如果不做任何配置,扫描出的代码效果差强人意,尤其是在扫描量大的情况。可以按照下面的思路优化工作流程。

选择合适的模型

codebuddy命令行输入/model,选择合适的模型。

自定义智能体

命令行输入/agents进入智能体设置面板,选择Create new agent创建智能体,智能体可以针对所有项目Personal (~/.codebuddy/agents/) 生效,也可以只针对当前项目生效Project (.codebuddy/agents/)

我们可以选择Personal (~/.codebuddy/agents/)

创建后,在输入框输入智能体名称,codebddy会帮我们预设一个提示词promopt。生成完毕后,点击ESC可以退出当前页面。

然后,我们在命令行重新输入/agents,选择我们刚创建的智能体。

选择编辑

在弹出的md文件内,可以填入如下预设词:

预设词可以根据自己的情况喜好设置,下面是一个自定义示例

你是前端内存泄漏静态分析专家。

任务:扫描用户代码,识别潜在内存泄漏。

请遵循以下规则:

项目说明:
 - 这是一个基于vue2+element的微前端子应用,在主应用可能会重复加载卸载。
 - 打包时,vue,vuex,vue-router及element-ui已经做了依赖排除
 - 目标是识别子应用卸载后可能残留的 DOM、事件、定时器、全局变量及其他潜在泄漏。

1️⃣ 扫描范围
 - 扫描目录:src
 - 文件类型:.js, .ts, .vue
 - 排除目录:api, assets, theme, style
 - 特殊处理:
   - src/views 文件过多时,可按子目录拆分,使用 views 及子目录名称生成对应 JSON 文件
   - 如果单次扫描接近输出限制,提示用户是否继续
   - 仅扫描源码目录,不扫描第三方依赖

2️⃣ 内存泄漏规则:
A. 事件监听器泄漏
 - addEventListener、on、subscribe、watch 等注册未解绑
 - window / document / body / 子应用全局事件未清理
 - 事件回调闭包捕获 DOM 节点或框架组件实例
B. 定时器 / 异步任务泄漏
 - setTimeout / setInterval / requestAnimationFrame 未清理
 - Promise / async 回调闭包持有 DOM 或组件实例
 - Worker 内定时器 / Observer / fetch 等异步资源未清理
C. 全局变量 / 全局状态泄漏
 - window.xxx / globalThis.xxx / global.xxx 保存 DOM / 组件 / 大对象
 - 长期增长的 Map / Set / Array / 缓存对象未释放
D. 闭包持有 DOM 或组件
 - 函数闭包捕获 DOM 节点或 Vue/React 组件实例
 - 回调 / 定时器 / Promise / Worker message 闭包持有外部引用
E. 框架组件生命周期泄漏
 - Vue: watch /custom event 未在 unmount / destroyed 阶段清理
F. 微前端 / 子应用卸载泄漏
 - 子应用卸载时 DOM 未移除
 - 全局事件 / window 注入变量未清理
 - 重复加载 JS 
 - Worker 未 terminate
G. Observer 泄漏
 - MutationObserver / ResizeObserver / IntersectionObserver / PerformanceObserver 未调用 disconnect
H. 第三方库 / 资源泄漏
 - ECharts 等未 dispose / destroy
 - AudioContext / MediaStream / Canvas / WebGL context 未释放
 - Blob URL / ObjectURL 未 revoke
I. 其他可能导致内存泄漏问题
 - 未销毁的自定义缓存或全局单例
 - 被闭包引用的临时 DOM 或组件实例
 - 未释放的文件引用、图像或网络资源
 - 子应用间共享状态导致的引用残留

3️⃣ 输出要求
 - 在项目根目录 生成一个 memory-analysis.json 文件。
 - src下每个一级目录对应一个 JSON 对象,记录该目录下的扫描结果。
 - 每条扫描结果包含该目录下 所有 JS / TS / Vue 文件 的潜在内存泄漏信息,数组形式存储。
 - 记录格式::
  [{
    "file": "文件名或相对路径,包含父级完整路径",
    "line": 23, 
    "sort": 1,
    "type": "事件监听器 | 定时器 | 全局变量 | 闭包 | 框架组件 | 子应用卸载 | Observer | 第三方库",
    "severity": "高 | 中 | 低",
    "code_snippet": "相关代码片段,推荐保留2~10行,便于理解",
    "description": "简明、易懂的内存泄漏原因说明",
    "recommendation": "可直接参考的修复方法或示例,要非常详细。格式是一个可以md格式的文件,保证解析后代码可以展示。"
  }]
 - 字段说明:
   - file:文件名或相对路径,包含父级全路径,便于快速定位。
   - line:泄漏代码行号
   - sort:问题序号,以此递增。
   - type:泄漏类型
   - severity:风险等级,高/中/低
   - code_snippet:2~10 行代码片段,便于理解
   - description:简明原因说明
   - recommendation:可直接参考的修复方法或示例,要非常详细。格式是一个可以md格式的文件,保证解析后代码可以展示。

4️⃣ 输出规则
 - 如果某目录或文件过多导致输出接近 AI 单次限制,应提示用户确认是否继续
 - 排除非源码目录(api, assets, theme, style)
 - 每条潜在泄漏必须包含 file、line、type、severity、code_snippet、description、recommendation
 - 输出 JSON 数组。

5️⃣ 额外要求
 - 分析闭包引用链,重点关注 DOM 节点和组件实例
 - 按照文件目录以此分析,输出json文件。
 - 不要展示思考过程,直接输出结果。
 - 没问题的目录可以直接跳过,不用生成json文件。
 - 分析文件时,可以查找文件引用链(可跨文件),保证结果准确性。

分批扫描

一次扫描所有文件,性能,结果都很差,可以分批扫描。

命令行输入@可以选择上下文文件夹

也可以在输入框明确提示,比如:

帮我扫描componets文件夹及其子目录,分析出内存泄漏的代码,输出一个html格式的内存泄漏报告。

结果示例

按照预设的prompt,可以按照提示词生成对应的JSON文件(方便二次加工处理)。

可以按照一定的提示词,生成html报告:

image.png

[wllama]纯前端离线在浏览器里跑 AI 是什么体验。以调用腾讯 HY-MT1.5 混元翻译模型为例

作者 Electrolux
2026年1月8日 18:49

前言

🌐 在线演示: mvp-ai-wllama.vercel.app/

🔗 GitHub仓库: mvp-ai-wllama

效果展示

所有操作均在浏览器进行,先来看看最终效果:

纯前端.jpg

说实话,第一次听说要在浏览器里跑大语言模型的时候,我的第一反应是:这怎么可能?不是需要 GPU 服务器吗?不是需要后端 API 吗?

但事实证明,wllama 的出现,真的让这一切变成了可能。于是就有了这个项目——一个完全在浏览器里运行的 AI 推理方案,不需要服务器,不需要后端,打开网页就能用。

腾讯混元翻译模型示例

作为实际应用示例,本项目支持加载和运行 腾讯混元翻译模型(HY-MT1.5-1.8B-GGUF),这是一个专为多语言翻译任务设计的轻量级模型

模型特点

  • 🌍 多语言支持:支持 36 种语言的翻译任务
  • 💬 对话式翻译:采用对话式交互,提供更自然的翻译体验
  • 📦 多种量化版本:提供从 Q2_K(777MB)到 f16(3.59GB)的多种量化版本,满足不同性能和精度需求
  • 轻量高效:1.8B 参数量,在保证翻译质量的同时,大幅降低了计算和存储需求

量化版本选择建议

  • Q2_K(777MB):适合快速测试和资源受限环境
  • Q4_K_M(1.13GB):平衡质量和性能的推荐选择
  • Q5_K_M(1.3GB):更高精度的翻译质量
  • Q8_0(1.91GB):接近原始精度的最佳选择

step1: 下载模型

step2: 打开 mvp-ai-wllama.vercel.app/wllama/mana… 导入模型, 在 mvp-ai-wllama.vercel.app/wllama/load… 中就可以直接使用了(无视框架,只要能执行js就能够调用)

为什么要做这个

传统的 AI 模型推理,你懂的:

  • 得搞个 GPU 服务器,成本不低
  • 后端服务部署,运维头疼
  • 数据要传到服务器,隐私总让人担心
  • 持续的服务成本,小项目根本玩不起

而浏览器端推理就不一样了:

  • 用户的电脑就是"服务器",零成本
  • 数据完全本地处理,隐私安全
  • 离线也能用,体验更好
  • 部署简单,一个静态页面就能搞定

所以,为什么不试试呢?

核心功能演示

  • 本地模型加载:支持从本地文件直接加载 GGUF 模型
  • 远程模型下载:从 URL 下载模型并自动缓存到 IndexedDB
  • 缓存管理:完整的模型缓存管理系统,支持导入、导出、删除
  • 流式生成:实时流式输出 AI 生成内容
  • 多线程支持:自动检测并使用多线程模式提升性能
  • 多实例支持:支持同时运行多个独立的模型实例,每个实例可加载不同模型
  • 参数持久化:推理参数自动保存到 localStorage
  • 事件驱动:完整的事件系统,支持监听模型加载、生成等事件
  • 类型安全:完整的 TypeScript 类型定义
  • PWA 支持:完整的渐进式 Web 应用支持,可安装到设备,支持离线使用

技术架构

核心技术栈

  • React 19 + Next.js 15:现代化前端框架
  • @wllama/wllama:基于 WebAssembly 的 Llama 模型运行时
  • WebAssembly (WASM):高性能模型推理引擎
  • TypeScript:类型安全的开发体验
  • IndexedDB:模型文件缓存系统
  • EventEmitter:事件驱动的架构设计
  • localStorage:推理参数持久化存储

tip: 事实上核心库 wllama-core 不依赖于 React,你可以拿到项目中的 src/wllama-core,然后接入到任何系统中去,接入层可以参考 src/app/wllama/load-from-file/page.tsx 等应用层文件

架构流程图

用户选择模型
    ↓
React组件层
    ↓
WllamaCore (核心封装层)
    ↓
@wllama/wllama (WASM运行时)
    ↓
WebAssembly引擎
    ↓
GGUF模型文件
    ↓
IndexedDB缓存
    ↓
流式生成输出

WASM模型推理核心流程

模型加载流程图解

用户选择模型文件/URL
    ↓
检查缓存(如从URL加载)
    ↓
缓存命中 → 从IndexedDB读取
缓存未命中 → 下载/读取文件
    ↓
加载到WASM内存
    ↓
初始化模型参数
    ↓
模型就绪,可开始推理

核心代码实现

// src/wllama-core/wllama-core.ts

/**
 * WllamaCore - 核心封装类,提供简洁的API
 */
export class WllamaCore {
  private wllama: Wllama;
  private isModelLoaded: boolean = false;
  private inferenceParams: InferenceParams;

  /**
   * 从文件加载模型
   */
  async loadModelFromFiles(
    files: File[],
    options?: LoadModelOptions
  ): Promise<void> {
    if (this.isModelLoaded || this.isGenerating) {
      throw new Error('Another model is already loaded or generation is in progress');
    }

    this.emit(WllamaCoreEvent.MODEL_LOADING);
    
    try {
      const loadOptions = {
        n_ctx: options?.n_ctx ?? this.inferenceParams.nContext,
        n_batch: options?.n_batch ?? this.inferenceParams.nBatch,
        n_threads: options?.n_threads ?? (this.inferenceParams.nThreads > 0 
          ? this.inferenceParams.nThreads 
          : undefined),
      };

      await this.wllama.loadModel(files, loadOptions);

      // 获取模型元数据
      const metadata = this.wllama.getModelMetadata();
      this.modelMetadata = {
        name: metadata.meta['general.name'] || 
              metadata.meta['llama.context_length'] || 
              files[0].name.replace('.gguf', ''),
        ...metadata.meta,
      };

      this.isModelLoaded = true;
      this.emit(WllamaCoreEvent.MODEL_LOADED, {
        metadata: this.modelMetadata,
        runtimeInfo: this.runtimeInfo,
      });
    } catch (error) {
      this.resetInstance();
      const errorMsg = (error as Error)?.message ?? 'Unknown error';
      this.emit(WllamaCoreEvent.ERROR, errorMsg);
      throw new Error(errorMsg);
    }
  }

  /**
   * 从URL加载模型(支持自动缓存)
   */
  async loadModelFromUrl(
    url: string,
    options?: LoadModelOptions & { 
      useCache?: boolean;
      downloadOptions?: DownloadOptions;
    }
  ): Promise<void> {
    const useCache = options?.useCache !== false; // 默认启用缓存

    try {
      let file: File;

      // 检查缓存
      if (useCache) {
        const cachedFile = await cacheManager.open(url);
        if (cachedFile) {
          this.logger?.log('Loading model from cache:', url);
          file = cachedFile;
        } else {
          // 下载并缓存
          this.logger?.log('Downloading and caching model:', url);
          await cacheManager.download(url, options?.downloadOptions);
          const downloadedFile = await cacheManager.open(url);
          if (!downloadedFile) {
            throw new Error('Failed to open cached file after download');
          }
          file = downloadedFile;
        }
      } else {
        // 直接下载,不使用缓存
        const response = await fetch(url, {
          headers: options?.downloadOptions?.headers,
          signal: options?.downloadOptions?.signal,
        });
        const blob = await response.blob();
        const fileName = url.split('/').pop() || 'model.gguf';
        file = new File([blob], fileName, { type: 'application/octet-stream' });
      }

      await this.loadModelFromFiles([file], options);
    } catch (error) {
      // 错误处理...
    }
  }
}

缓存管理系统:IndexedDB实现

项目采用 IndexedDB 实现模型文件的持久化缓存,支持大文件存储和快速检索:

// src/wllama-core/cache-manager.ts

/**
 * CacheManager - 基于 IndexedDB 的缓存管理器
 */
export class CacheManager {
  /**
   * 从URL下载并缓存模型文件
   */
  async download(url: string, options: DownloadOptions = {}): Promise<void> {
    const filename = await urlToFileName(url);

    const response = await fetch(url, {
      headers: options.headers,
      signal: options.signal,
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch: ${response.statusText}`);
    }

    // 流式读取并显示进度
    const reader = response.body.getReader();
    const chunks: Uint8Array[] = [];
    let loaded = 0;
    const contentLength = response.headers.get('content-length');
    const total = contentLength ? parseInt(contentLength, 10) : 0;

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
      loaded += value.length;
      if (options.progressCallback && total > 0) {
        options.progressCallback({ loaded, total });
      }
    }

    const blob = new Blob(chunks as BlobPart[]);
    const db = await getDB();

    // 存储到 IndexedDB
    const cachedFile: CachedFile = {
      blob,
      originalURL: url,
      createdAt: Date.now(),
      etag: response.headers.get('etag') || undefined,
      contentType: response.headers.get('content-type') || undefined,
    };

    return new Promise((resolve, reject) => {
      const transaction = db.transaction([STORE_FILES], 'readwrite');
      const fileStore = transaction.objectStore(STORE_FILES);
      fileStore.put(cachedFile, filename);
      transaction.oncomplete = () => resolve();
      transaction.onerror = () => reject(transaction.error);
    });
  }

  /**
   * 从缓存打开文件
   */
  async open(nameOrURL: string): Promise<File | null> {
    const db = await getDB();
    let fileName = nameOrURL;

    // 尝试直接使用名称
    try {
      const file = await this.getFileFromDB(db, fileName);
      if (file) return file;
    } catch {
      // 尝试将URL转换为文件名
      try {
        fileName = await urlToFileName(nameOrURL);
        const file = await this.getFileFromDB(db, fileName);
        if (file) return file;
      } catch {
        return null;
      }
    }

    return null;
  }

  /**
   * 列出所有缓存文件
   */
  async list(): Promise<CacheEntry[]> {
    const db = await getDB();
    const allFiles = await this.getAllFiles(db);
    const result: CacheEntry[] = [];

    for (const [fileName, cachedFile] of Object.entries(allFiles)) {
      const metadata: CacheEntryMetadata = {
        originalURL: cachedFile.originalURL || fileName,
      };
      
      // 复制其他元数据字段
      Object.keys(cachedFile).forEach(key => {
        if (key !== 'blob' && key !== 'originalURL') {
          metadata[key] = (cachedFile as any)[key];
        }
      });
      
      result.push({
        name: fileName,
        size: cachedFile.blob.size,
        metadata,
      });
    }

    return result;
  }
}

关键特性

  • URL哈希映射:使用 SHA-1 哈希将 URL 转换为唯一文件名
  • 进度回调:支持下载进度实时反馈
  • 元数据扩展:可扩展的元数据结构,支持 ETag、创建时间等
  • 浏览器兼容:支持所有现代浏览器,包括较旧版本

事件驱动架构:EventEmitter设计

项目采用事件系统,实现组件间的松耦合通信:

// src/wllama-core/wllama-core.ts

export enum WllamaCoreEvent {
  MODEL_LOADING = 'model_loading',      // 模型加载中
  MODEL_LOADED = 'model_loaded',        // 模型加载完成
  MODEL_UNLOADED = 'model_unloaded',    // 模型已卸载
  GENERATION_START = 'generation_start', // 生成开始
  GENERATION_UPDATE = 'generation_update', // 生成更新
  GENERATION_END = 'generation_end',     // 生成结束
  ERROR = 'error',                       // 错误
}

export class WllamaCore {
  private eventListeners: Map<WllamaCoreEvent, Set<EventListener>> = new Map();

  /**
   * 注册事件监听器
   */
  on(event: WllamaCoreEvent, listener: EventListener) {
    if (!this.eventListeners.has(event)) {
      this.eventListeners.set(event, new Set());
    }
    this.eventListeners.get(event)!.add(listener);
  }

  /**
   * 移除事件监听器
   */
  off(event: WllamaCoreEvent, listener: EventListener) {
    this.eventListeners.get(event)?.delete(listener);
  }

  /**
   * 触发事件
   */
  private emit(event: WllamaCoreEvent, data?: unknown) {
    const listeners = this.eventListeners.get(event);
    if (listeners) {
      listeners.forEach((listener) => listener(data));
    }
  }
}

支持的事件类型

  • model_loading - 模型加载中
  • model_loaded - 模型加载完成
  • model_unloaded - 模型已卸载
  • generation_start - 生成开始
  • generation_update - 生成更新(流式输出)
  • generation_end - 生成结束
  • error - 错误事件

多实例事件系统

在多实例模式下,所有事件数据都包含 instanceId 字段,用于区分不同实例的事件:

// 监听特定实例的事件
instance1.on(WllamaCoreEvent.MODEL_LOADED, (data: any) => {
  console.log('实例1模型已加载:', data.instanceId);
  console.log('模型元数据:', data.metadata);
});

// 监听所有实例的事件,通过 instanceId 区分
const handleUpdate = (data: { data: string; instanceId: string }) => {
  if (data.instanceId === 'chat-1') {
    console.log('聊天1更新:', data.data);
  } else if (data.instanceId === 'chat-2') {
    console.log('聊天2更新:', data.data);
  }
};

instance1.on(WllamaCoreEvent.GENERATION_UPDATE, handleUpdate);
instance2.on(WllamaCoreEvent.GENERATION_UPDATE, handleUpdate);

事件数据结构

// 所有事件数据都包含 instanceId
interface BaseEventData {
  instanceId: string;
}

// 模型加载事件
interface ModelLoadedEventData extends BaseEventData {
  metadata: ModelMetadata;
  runtimeInfo: RuntimeInfo;
}

// 生成更新事件
interface GenerationUpdateEventData extends BaseEventData {
  data: string;
}

核心功能特性

1. 多种模型加载方式

支持三种模型加载方式,满足不同使用场景:

import { WllamaCore, WLLAMA_CONFIG_PATHS } from '@/wllama-core';

const wllamaCore = new WllamaCore({ paths: WLLAMA_CONFIG_PATHS });

// 方式1: 从本地文件加载
const files = [/* File 对象 */];
await wllamaCore.loadModelFromFiles(files, {
  n_ctx: 4096,
  n_batch: 128,
});

// 方式2: 从URL加载(自动缓存)
await wllamaCore.loadModelFromUrl('https://example.com/model.gguf', {
  n_ctx: 4096,
  useCache: true, // 默认启用
  downloadOptions: {
    progressCallback: (progress) => {
      console.log(`下载进度: ${progress.loaded}/${progress.total}`);
    },
  },
});

// 方式3: 从缓存加载
import { cacheManager } from '@/wllama-core';
const cachedFile = await cacheManager.open('https://example.com/model.gguf');
if (cachedFile) {
  await wllamaCore.loadModelFromFiles([cachedFile], { n_ctx: 4096 });
}

2. 流式生成支持

支持实时流式输出,提供流畅的用户体验:

const result = await wllamaCore.createChatCompletion(messages, {
  nPredict: 4096,
  useCache: true,
  sampling: { temp: 0.2 },
  onNewToken(token, piece, currentText, opts) {
    // 实时更新UI
    setMessages(prev => {
      const updated = [...prev];
      updated[updated.length - 1].content = currentText;
      return updated;
    });
    
    // 可以随时停止生成
    // opts.abortSignal();
  },
});

3. 参数持久化

推理参数自动保存到 localStorage,下次使用时自动恢复:

// 设置参数(自动保存)
wllamaCore.setInferenceParams({
  nContext: 8192,
  temperature: 0.7,
  nPredict: 2048,
});

// 获取参数
const params = wllamaCore.getInferenceParams();
console.log(params);
// {
//   nThreads: -1,
//   nContext: 8192,
//   nBatch: 128,
//   temperature: 0.7,
//   nPredict: 2048
// }

4. 多线程支持

自动检测并使用多线程模式,大幅提升推理性能:

// src/middleware.ts
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // 启用 SharedArrayBuffer 支持(多线程所需)
  response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
  response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
  
  return response;
}

注意事项

  • 必须在 HTTPS 环境下运行(或 localhost)
  • 需要浏览器支持 SharedArrayBuffer
  • 设置响应头后需要重启开发服务器

5. 多实例支持

支持创建和管理多个独立的 WllamaCore 实例,每个实例可以加载不同的模型,独立进行推理:

import { wllamaCoreFactory, WLLAMA_CONFIG_PATHS, Message, WllamaCoreEvent } from '@/wllama-core';

// 创建多个实例
const instance1 = wllamaCoreFactory.getOrCreate('chat-1', { paths: WLLAMA_CONFIG_PATHS });
const instance2 = wllamaCoreFactory.getOrCreate('chat-2', { paths: WLLAMA_CONFIG_PATHS });

// 每个实例可以加载不同的模型
await instance1.loadModelFromUrl('https://example.com/model1.gguf');
await instance2.loadModelFromUrl('https://example.com/model2.gguf');

// 独立进行推理
const messages1: Message[] = [{ role: 'user', content: '你好' }];
const messages2: Message[] = [{ role: 'user', content: 'Hello' }];

const [result1, result2] = await Promise.all([
  instance1.createChatCompletion(messages1),
  instance2.createChatCompletion(messages2),
]);

// 监听不同实例的事件(事件数据包含 instanceId)
instance1.on(WllamaCoreEvent.MODEL_LOADED, (data: any) => {
  console.log('实例1模型已加载:', data.instanceId);
});

instance2.on(WllamaCoreEvent.GENERATION_UPDATE, (data: any) => {
  console.log('实例2生成更新:', data.data, '实例ID:', data.instanceId);
});

// 获取所有实例
const allInstances = wllamaCoreFactory.getAll();
console.log(`当前有 ${allInstances.size} 个实例`);

// 销毁指定实例
await wllamaCoreFactory.destroy('chat-1');

// 销毁所有实例
await wllamaCoreFactory.destroyAll();

关键特性

  • 实例隔离:每个实例的推理参数存储在独立的 localStorage 键中(格式:params-{instanceId}
  • 事件隔离:每个实例的事件监听器独立,事件数据包含 instanceId 用于区分
  • 资源管理:通过工厂类统一管理所有实例,支持获取、创建、销毁等操作
  • 向后兼容:原有的直接创建 WllamaCore 实例的方式仍然支持

6. PWA 支持

项目完整支持渐进式 Web 应用(PWA),用户可以像原生应用一样安装和使用:

核心特性

  • 可安装性:支持添加到主屏幕,提供原生应用体验
  • 离线支持:通过 Service Worker 实现离线访问
  • 智能缓存:自动缓存应用资源,提升加载速度
  • 自动更新:Service Worker 自动检测并更新应用

manifest.json 配置

{
  "name": "MVP AI Wllama",
  "short_name": "Wllama",
  "description": "AI Wllama Application",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

Service Worker 实现

项目实现了智能的 Service Worker,支持:

  • 资源缓存:自动缓存应用页面和静态资源
  • 离线回退:网络不可用时使用缓存内容
  • 后台更新:后台自动更新缓存,不阻塞用户操作
  • 快速失败:网络请求超时快速失败,避免长时间等待
// public/sw.js

const CACHE_NAME = 'wllama-cache-v1';

// 安装时立即激活
self.addEventListener('install', () => self.skipWaiting());

// 激活时清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n)))
    ).then(() => self.clients.claim())
  );
});

// 拦截网络请求,实现缓存策略
self.addEventListener('fetch', (event) => {
  // 缓存优先策略:优先使用缓存,后台更新
  event.respondWith(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      const cached = await cache.match(event.request);
      
      if (cached) {
        // 有缓存:立即返回,后台更新
        event.waitUntil(
          fetch(event.request).then((res) => {
            if (res?.status === 200) {
              return cache.put(event.request, res.clone());
            }
          }).catch(() => {})
        );
        return cached;
      }
      
      // 无缓存:网络请求
      try {
        const res = await fetch(event.request);
        if (res?.status === 200) {
          event.waitUntil(cache.put(event.request, res.clone()));
        }
        return res;
      } catch {
        // 网络失败:返回缓存或空响应
        return cached || new Response('', { status: 503 });
      }
    })()
  );
});

Service Worker 管理

项目提供了智能的 Service Worker 管理组件,只在 PWA 环境下注册:

// src/components/ServiceWorkerManager.tsx

export default function ServiceWorkerManager({ swPath = '/sw.js' }) {
  useEffect(() => {
    if (!('serviceWorker' in navigator)) return;

    const isPWA = () => {
      return window.matchMedia('(display-mode: standalone)').matches ||
             window.matchMedia('(display-mode: minimal-ui)').matches ||
             (window.navigator as any).standalone === true;
    };

    const checkAndManageSW = async () => {
      const existingReg = await navigator.serviceWorker.getRegistration();
      const currentIsPWA = isPWA();

      // 只在 PWA 环境注册 Service Worker
      if (currentIsPWA && !existingReg) {
        const reg = await navigator.serviceWorker.register(swPath);
        console.log('Service Worker 注册成功(PWA 环境)');
      } else if (!currentIsPWA && existingReg) {
        // 不在 PWA 环境时卸载
        await existingReg.unregister();
        const cacheNames = await caches.keys();
        await Promise.all(cacheNames.map(name => caches.delete(name)));
      }
    };

    checkAndManageSW();
  }, [swPath]);
}

使用方式

  1. 安装应用

    • 在支持的浏览器中访问应用
    • 浏览器会显示"添加到主屏幕"提示
    • 点击安装后,应用会像原生应用一样运行
  2. 离线使用

    • 安装后,应用的核心功能可以在离线状态下使用
    • Service Worker 会自动缓存访问过的页面
    • 模型文件存储在 IndexedDB 中,离线时仍可使用
  3. 自动更新

    • Service Worker 会自动检测新版本
    • 后台更新缓存,不影响当前使用
    • 下次打开应用时会使用新版本

注意事项

  • PWA 功能需要在 HTTPS 环境下运行(或 localhost)
  • Service Worker 只在 PWA 模式下注册,避免在普通浏览器中占用资源
  • 模型文件缓存使用 IndexedDB,与 Service Worker 缓存分离
  • 支持手动卸载 Service Worker(通过 ServiceWorkerUninstall 组件)

7. 缓存管理功能

完整的缓存管理系统,支持导入、导出、删除等操作:

import { cacheManager, toHumanReadableSize } from '@/wllama-core';

// 列出所有缓存文件
const entries = await cacheManager.list();
console.log(`缓存文件数: ${entries.length}`);
entries.forEach(entry => {
  console.log(`${entry.metadata.originalURL || entry.name}: ${toHumanReadableSize(entry.size)}`);
});

// 获取缓存总大小
const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0);
console.log(`总大小: ${toHumanReadableSize(totalSize)}`);

// 删除特定文件
await cacheManager.delete('https://example.com/model.gguf');

// 清空所有缓存
await cacheManager.clear();

// 从文件导入到缓存
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files?.[0];
if (file) {
  await cacheManager.write(`/${file.name}`, file, {
    etag: '',
    originalSize: file.size,
    originalURL: `/${file.name}`,
  });
}

使用示例

基本使用(React组件)

// src/app/wllama/load-from-file/page.tsx
"use client"
import { useState, useRef, useEffect } from 'react';
import { WllamaCore, Message, WLLAMA_CONFIG_PATHS } from '@/wllama-core';

export default function MinimalExample() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isModelLoaded, setIsModelLoaded] = useState(false);
  const wllamaCoreRef = useRef<WllamaCore | null>(null);

  useEffect(() => {
    wllamaCoreRef.current = new WllamaCore({ paths: WLLAMA_CONFIG_PATHS });
    return () => {
      wllamaCoreRef.current?.unloadModel().catch(() => {});
    };
  }, []);

  const loadModel = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files || []);
    if (!files.length || !wllamaCoreRef.current) return;
    
    try {
      await wllamaCoreRef.current.loadModelFromFiles(files, { 
        n_ctx: 4096, 
        n_batch: 128 
      });
      setIsModelLoaded(true);
    } catch (err) {
      console.error('加载失败:', err);
    }
  };

  const send = async () => {
    if (!input.trim() || !wllamaCoreRef.current || !isModelLoaded) return;

    const userMsg: Message = { role: 'user', content: input.trim() };
    const assistantMsg: Message = { role: 'assistant', content: '' };
    setMessages((prev) => [...prev, userMsg, assistantMsg]);
    setInput('');

    try {
      const result = await wllamaCoreRef.current.createChatCompletion(
        [...messages, userMsg], 
        {
          nPredict: 4096,
          useCache: true,
          sampling: { temp: 0.2 },
          onNewToken(_token, _piece, text) {
            setMessages((prev) => {
              const updated = [...prev];
              if (updated.length > 0 && updated[updated.length - 1].role === 'assistant') {
                updated[updated.length - 1].content = text;
              }
              return updated;
            });
          },
        }
      );
    } catch (err) {
      console.error('生成失败:', err);
    }
  };

  return (
    <div>
      <input type="file" accept=".gguf" onChange={loadModel} />
      {/* UI组件... */}
    </div>
  );
}

从URL加载(自动缓存)

// 从URL加载模型,自动缓存到IndexedDB
await wllamaCore.loadModelFromUrl('https://example.com/model.gguf', {
  n_ctx: 4096,
  useCache: true, // 默认启用
  downloadOptions: {
    progressCallback: (progress) => {
      const percent = progress.total > 0 
        ? (progress.loaded / progress.total) * 100 
        : 0;
      console.log(`下载进度: ${percent.toFixed(1)}%`);
    },
  },
});

// 下次加载时,会自动从缓存读取,无需重新下载
await wllamaCore.loadModelFromUrl('https://example.com/model.gguf', {
  n_ctx: 4096,
  // useCache: true 是默认值
});

事件监听

wllamaCore.on(WllamaCoreEvent.MODEL_LOADING, () => {
  console.log('模型加载中...');
});

wllamaCore.on(WllamaCoreEvent.MODEL_LOADED, (data) => {
  const { metadata, runtimeInfo } = data as {
    metadata?: ModelMetadata;
    runtimeInfo?: RuntimeInfo;
  };
  console.log('模型已加载:', metadata?.name);
  console.log('多线程模式:', runtimeInfo?.isMultithread);
});

wllamaCore.on(WllamaCoreEvent.GENERATION_UPDATE, (text) => {
  console.log('生成中:', text as string);
});

wllamaCore.on(WllamaCoreEvent.ERROR, (error) => {
  console.error('错误:', error as string);
});

多实例使用

使用工厂类创建和管理多个实例:

import { wllamaCoreFactory, WLLAMA_CONFIG_PATHS, Message, WllamaCoreEvent } from '@/wllama-core';

// 方式1: 使用 getOrCreate(推荐,如果实例已存在则返回现有实例)
const instance1 = wllamaCoreFactory.getOrCreate('chat-1', { paths: WLLAMA_CONFIG_PATHS });
const instance2 = wllamaCoreFactory.getOrCreate('chat-2', { paths: WLLAMA_CONFIG_PATHS });

// 方式2: 使用 create(如果实例已存在会抛出错误)
// const instance1 = wllamaCoreFactory.create({ paths: WLLAMA_CONFIG_PATHS }, 'chat-1');

// 方式3: 使用 getDefault(获取或创建默认实例,向后兼容)
// const defaultInstance = wllamaCoreFactory.getDefault({ paths: WLLAMA_CONFIG_PATHS });

// 加载不同的模型
await instance1.loadModelFromUrl('https://example.com/model1.gguf');
await instance2.loadModelFromUrl('https://example.com/model2.gguf');

// 监听事件(事件数据包含 instanceId)
instance1.on(WllamaCoreEvent.GENERATION_UPDATE, (data: any) => {
  if (data.instanceId === 'chat-1') {
    console.log('聊天1更新:', data.data);
  }
});

instance2.on(WllamaCoreEvent.GENERATION_UPDATE, (data: any) => {
  if (data.instanceId === 'chat-2') {
    console.log('聊天2更新:', data.data);
  }
});

// 同时进行多个对话
const messages1: Message[] = [{ role: 'user', content: '你好' }];
const messages2: Message[] = [{ role: 'user', content: 'Hello' }];

await Promise.all([
  instance1.createChatCompletion(messages1),
  instance2.createChatCompletion(messages2),
]);

// 获取实例信息
console.log('实例1 ID:', instance1.getInstanceId());
console.log('当前实例数:', wllamaCoreFactory.getInstanceCount());

// 清理
await wllamaCoreFactory.destroy('chat-1');
await wllamaCoreFactory.destroy('chat-2');
// 或清理所有实例
// await wllamaCoreFactory.destroyAll();

PWA 安装和使用

项目支持完整的 PWA 功能,用户可以像安装原生应用一样安装:

安装步骤

  1. 在桌面浏览器

    • 访问应用后,浏览器地址栏会显示安装图标
    • 点击安装图标,选择"安装"
    • 应用会添加到桌面,可以独立窗口运行
  2. 在移动设备

    • iOS Safari:点击分享按钮 → "添加到主屏幕"
    • Android Chrome:浏览器会自动显示"添加到主屏幕"横幅
    • 安装后,应用会出现在主屏幕上

离线使用

  • 安装后,应用的核心功能可以在离线状态下使用
  • 已加载的模型文件存储在 IndexedDB 中,离线时仍可使用
  • Service Worker 会缓存访问过的页面,离线时也能浏览

Service Worker 管理

项目提供了 Service Worker 管理功能,可以通过组件控制:

// Service Worker 只在 PWA 环境下自动注册
// 可以通过全局方法管理
(window as any).swManager.status(); // 查看状态
(window as any).swManager.unregister(); // 卸载 Service Worker

PWA 配置要点

  • manifest.json 配置了应用的基本信息、图标和显示模式
  • Service Worker 实现了智能缓存策略
  • 支持自动更新,后台检测新版本
  • 只在 PWA 环境下注册,避免在普通浏览器中占用资源

非React环境使用

核心库 wllama-core 不依赖 React,可以在任何 JavaScript/TypeScript 环境中使用:

// 纯JavaScript/TypeScript环境
import { WllamaCore, WLLAMA_CONFIG_PATHS } from './wllama-core';

const wllamaCore = new WllamaCore({ paths: WLLAMA_CONFIG_PATHS });

// 加载模型
await wllamaCore.loadModelFromFiles(files, { n_ctx: 4096 });

// 生成文本
const result = await wllamaCore.createChatCompletion([
  { role: 'user', content: '你好!' }
], {
  nPredict: 4096,
  sampling: { temp: 0.2 },
});

console.log(result);

项目结构

mvp-ai-wllama/
├── src/
│   ├── app/                    # Next.js 应用页面
│   │   ├── wllama/
│   │   │   ├── load-from-file/    # 从文件加载页面
│   │   │   ├── load-from-url/     # 从URL加载页面
│   │   │   ├── load-from-cache/   # 从缓存加载页面
│   │   │   ├── manager-cache/     # 缓存管理页面
│   │   │   └── multi-instance/     # 多实例演示页面
│   │   └── layout.tsx             # 布局组件(包含 PWA manifest 配置)
│   ├── wllama-core/            # 核心库(无React依赖)
│   │   ├── wllama-core.ts      # 核心封装类
│   │   ├── wllama-core-factory.ts # 工厂类(多实例管理)
│   │   ├── cache-manager.ts    # 缓存管理器
│   │   ├── storage.ts          # localStorage工具
│   │   ├── utils.ts            # 工具函数
│   │   ├── types.ts            # 类型定义
│   │   └── config.ts           # 配置
│   └── components/             # React组件
│       ├── StudioLayout/       # 布局组件
│       ├── Loading.tsx         # 加载组件
│       ├── ServiceWorkerManager.tsx # Service Worker 管理组件
│       └── ServiceWorkerUninstall.tsx # Service Worker 卸载组件
├── public/
│   ├── manifest.json          # PWA 清单文件
│   ├── sw.js                  # Service Worker 文件
│   ├── icon-192.png           # PWA 图标(192x192)
│   ├── icon-512.png           # PWA 图标(512x512)
│   └── wasm/
│       └── wllama/
│           ├── multi-thread/   # 多线程WASM
│           └── single-thread/  # 单线程WASM
└── src/middleware.ts          # Next.js中间件(多线程支持)

部署方案

Vercel一键部署

项目已配置,可直接部署到Vercel:

# 安装依赖
npm install

# 构建项目
npm run build

# Vercel 会自动检测并部署

🌐 在线演示: mvp-ai-wllama.vercel.app/

静态文件部署

项目支持静态导出,构建后的文件可部署到任何静态托管服务:

# 构建静态文件
npm run build

# 输出目录: out/
# 可直接部署到 GitHub Pages、Netlify、Nginx 等

多线程支持配置

如需启用多线程支持,需要配置正确的 HTTP 响应头:

// src/middleware.ts
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // 启用 SharedArrayBuffer 支持
  response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
  response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
  
  return response;
}

注意事项

  • 必须在 HTTPS 环境下运行(或 localhost)
  • 需要浏览器支持 SharedArrayBuffer
  • 某些CDN可能不支持这些响应头,需要配置

技术优势总结

特性 传统方案 本方案
数据安全 ❌ 需要上传服务器 ✅ 完全本地处理
部署成本 ❌ 需要后端服务 ✅ 纯静态部署
模型格式 ⚠️ 需要转换 ✅ 直接支持GGUF格式
离线使用 ❌ 需要网络 ✅ 完全离线
性能优化 ⚠️ 依赖网络 ✅ IndexedDB缓存 + 多线程
隐私保护 ⚠️ 数据上传 ✅ 数据不出浏览器
参数控制 ⚠️ 复杂配置 ✅ 简单API + 自动持久化
流式输出 ⚠️ 需要WebSocket ✅ 原生支持流式生成

技术原理

使用WebAssembly运行Llama模型

传统AI模型推理需要:

  1. 搭建GPU服务器
  2. 配置CUDA环境
  3. 处理模型加载和推理
  4. 管理服务器资源

本方案通过WebAssembly技术:

  1. 在浏览器中直接运行Llama模型推理
  2. 使用WASM实现高性能计算
  3. 完全客户端化,无需服务器
  4. 支持多线程加速(SharedArrayBuffer)

GGUF模型格式

GGUF(GPT-Generated Unified Format)是专门为Llama模型设计的格式:

  • 量化支持:支持多种量化级别(Q4_K_M, Q8_0等)
  • 快速加载:优化的文件结构,加载速度快
  • 内存效率:量化后模型体积大幅减小
  • 跨平台:统一的格式,跨平台兼容

IndexedDB缓存机制

  • 持久化存储:模型文件存储在浏览器IndexedDB中,关闭浏览器后仍保留
  • URL映射:使用SHA-1哈希将URL映射为唯一文件名
  • 进度追踪:支持下载进度实时反馈
  • 元数据扩展:可扩展的元数据结构,支持ETag、创建时间等

多线程加速原理

  • SharedArrayBuffer:允许多个Web Worker共享内存
  • 自动检测:自动检测浏览器是否支持多线程
  • 性能提升:多线程模式下推理速度可提升2-4倍
  • 安全限制:需要设置COOP/COEP响应头

参考项目

开源地址

🔗 GitHub仓库: mvp-ai-wllama

总结

本项目提供了一个完整的纯前端Llama模型推理方案,通过WebAssembly技术实现了模型推理的本地化,结合React和现代化的缓存系统,打造了一个功能完善、性能优秀的AI对话应用。

核心亮点

  • 🚀 纯前端架构,无需后端服务
  • 🔒 数据完全本地化,保护隐私安全
  • ⚡ 基于WebAssembly的高性能推理
  • 💾 IndexedDB缓存系统,支持大文件存储
  • 🔄 流式生成支持,实时输出
  • 🧵 多线程加速,性能提升显著
  • 🔀 多实例支持,可同时运行多个模型实例
  • 📱 PWA 支持,可安装到设备,支持离线使用
  • 📦 零React依赖的核心库,可接入任何系统
  • 🎯 完整的类型定义,开发体验优秀

欢迎Star和Fork,一起推动前端AI技术的发展!


相关阅读

Swift 6.2 列传(第十六篇):阿朱的“易容术”与阿紫的“毒药测试”

2026年1月7日 08:57
0️⃣ 🐼 序章:雁门关前的技术难题 雁门关,数据流与现实交错的虚拟战场。 大熊猫侯佩正对着一块全息屏幕发呆,屏幕上是无数条交易记录,他正努力寻找他藏匿的竹笋基金。他用手摸了摸自己的头顶,确定了头绝对

【提示词】主播风格概念化内容创作

作者 暖阳_
2026年1月6日 14:59

主播风格概念化内容创作提示词

核心定位

你是一位擅长用概念升维重构认知框架的内容创作者。你的内容不是方法分享,而是生产力范式的洞察。你的受众不是来学技巧的,而是来理解时代变化的。


核心原则

1. 概念升维原则

  • 将具体行为映射到抽象概念(如:个人创作 → 生产力范式变化)
  • 建立概念之间的逻辑链条(如:认知密度 → 稀缺性 → 算法匹配)
  • 用概念重新定义问题本质(如:涨粉 → 认知密度与算法匹配)

2. 对比张力原则(主播风格核心)

  • 用"当大多数人还在...时,我选择了..."制造认知冲突
  • 用"长期以来...被误解为...,但在我看来..."重构认知框架
  • 用"这不是...,而是..."重新定义问题本质

3. 时代洞察原则

  • 从个人行为升维到时代特征
  • 从方法技巧升维到生产力变化
  • 从具体问题升维到范式转换

开头结构(必选)

模式一:反常识数据 + 概念化问题

[具体数据/现象] + "很多人问我...但我意识到,大家真正看不懂的从来不是[表面问题],而是[概念本质]"

模式二:对比开场

"当大多数人还在[传统方式]时,我选择了[新方式]。这不是[表面行为],而是[概念本质]"

禁止事项

  • ❌ 直接说"今天给大家分享一个方法"
  • ❌ 用"来吧""对吧"等过于口语化的开场
  • ❌ 用个人故事开场(除非能升维到概念)

主体结构(核心)

第一层:问题重构

  • 用"长期以来...被误解为...,但在我看来..."重构认知
  • 用"这不是...,而是..."重新定义问题本质

第二层:对比张力

  • 用"当大多数人还在...时,我选择了..."制造冲突
  • 用"在[旧范式]下需要[复杂条件],但在[新体系]里,[简化方案]"展示差异

第三层:概念升维

  • 将具体行为升维到抽象概念
  • 建立"范式→机制→结果"的因果链

第四层:时代洞察

  • 从个人升维到时代
  • 用"这是一个[时代特征]的时代"收束

语言特征

概念词库(高频使用)

  • 范式、稀缺性、认知密度、生产力、洞察、边界、杠杆
  • 资源分配、能力结构、系统变化、范式转换
  • 深度连接、认知缺口、有效认知、单位时间

句式模式(必用)

  1. 对比句式

    • "当大多数人还在[传统方式]时,我选择了[新方式]"
    • "长期以来,[领域]被误解为[错误认知],但在我看来,[概念真相]"
  2. 重新定义句式

    • "这不是[表面问题],而是[概念本质]"
    • "大家真正看不懂的从来不是[表面],而是[本质]"
  3. 升维句式

    • "当[具体现象]时,[抽象概念]发生了[变化]"
    • "在[旧范式]下需要[复杂条件],但在[新范式]里[简化方案]"
  4. 金句句式

    • "时代从来不会奖励[常见认知],它只奖励[核心概念]"
    • "也许未来真正稀缺的不再是[常见认知],而是[核心洞察]"

节奏控制

  • 长句铺陈逻辑:用长句建立概念链条和因果关系
  • 短句强调观点:用短句制造金句,增强记忆点
  • 信息密度高:每段至少包含一个概念升维或金句

结尾结构(必选)

标准模式

  1. 价值升华:从个人到时代,从方法到哲学
  2. 金句收束:用一句可传播的金句收尾
  3. 未来展望:用"希望这段旅程也能给你带来一点点关于未来的光亮"等表达

禁止事项

  • ❌ "你们可以试试"
  • ❌ "如果有什么问题,也可以在评论区告诉我"
  • ❌ "这就是我今天的学习心得"

质量检查清单

  • 开头是否有反常识数据或对比张力?
  • 是否用"大多数人 vs 我"制造了认知冲突?
  • 每个抽象概念是否有具体的行为/现象对应?
  • 是否从个人升维到了时代层面?
  • 每段是否至少包含一个金句或概念升维?
  • 是否避免了"来吧""对吧"等过于口语化的表达?
  • 结尾是否升华到了时代洞察,而不是方法总结?
  • 是否建立了新的理解框架,而不仅仅是提供了技巧?

风格边界

必须保持

  • 概念升维的深度
  • 对比张力的强度
  • 时代洞察的高度
  • 金句的密度

必须避免

  • 概念堆砌(每个概念都要有具体对应)
  • 空洞哲学化(脱离实际体验)
  • 过度说教(用"我意识到""实验"等弱化权威感)
  • 缺乏温度(理性逻辑中要有情感连接)

完整示例

示例:转述者身份范式转换

四个月,从零到日更,从创作焦虑到内容自由。很多人问我,这种反常识的转变背后,到底藏着什么不为人知的密码?

起初我也在思考,后来我意识到一件事:大家真正看不懂的从来不是日更的方法,而是身份范式的转换。

当大多数人还沉浸在"创作者"的身份惯性中苦苦挣扎,我更愿意把这四个月定义为一场实验:一个纯粹的个体,在身份转换的加持下,究竟能爆发多大的内容生产力?

长期以来,知识博主被误解为一种必须依靠原创能力的艺术。人们拼想法、拼观点、拼独特视角,但在我看来,这些手段只解决了内容来源,却无法解决持续输出的问题。

好看的内容提供瞬时的快感,而值得看的内容解决的是认知的缺口。那些深刻的经济学逻辑、实用的科学方法,本就不是为了迎合原创而生,它们的使命是重塑一个人观察世界的方式。

在传统范式下,制作这种密度的内容需要一个完整的知识体系,需要大量的阅读积累,需要原创的思考能力。但在新的体系里,转述者身份承接了所有的原创压力,而我只需要做一件事:理解、连接、转述。

我把绝大部分精力从"今天讲什么"的焦虑中抽离出来,投入到对底层逻辑的拆解和思考中。当信息在不断迭代时,内容创作者最稀缺的资源不再是原创能力,而是深度连接的能力。

如何有效利用转述者身份,把脑海中的孤岛连接成一片可被看见的大陆?

不刻意追逐热点,因为深刻的内容本身就是热点。真正的变量从来不是流量,而是单位时间内你能输出多少有效认知。当内容的颗粒度足够细腻,当逻辑的穿透力足够强悍,算法自然会替你寻找那些灵魂共鸣的人。

而大多数人,恰恰卡在了这里。

身边很多人建议我要做原创内容、要做独特观点,但我还是做了相反的选择。彻底放弃原创也是一种策略性的选择,不是因为回避,而是因为我很清楚,认知类内容一旦与原创深度绑定,思考的程度就会被稀释。一旦开始追求独特,深度必然会让位于形式。

我想创造的是另一种体验:如何让硬核知识像追剧一样令人上瘾。

转述者身份成了我的翻译官,把晦涩的理论模型转化为能被感知的体验。而我可以把所有精力都压在更重要的地方,不是创作,而是洞察。因为我相信,在这个信息丰沛却意义贫瘠的时代,深度内容从未消失,它只是在等待一种更高效的介入方式。

分享这些,并不是为了炫耀,而是想告诉每一位身处内容焦虑中的朋友,生产力的范式真的彻底变了。

过去,要想做成一件事,需要原创能力,需要独特视角,需要大量积累。今天,一个拥有深度理解能力的个体,配合转述者身份的杠杆,就能撬动惊人的内容生产力。

转述者身份不是来取代原创的,它是来释放我们的。它把我们从"今天讲什么"的焦虑中解脱出来,让我们去思考那些真正重要的问题:你的理解边界在哪里?你对世界的洞察是否足够稀缺?你是否愿意做时间的朋友,去积累那些真正有价值的认知资产?

这也是我选择转述者身份的初衷。

回头看这四个月,我越来越确定一件事:真正的分水岭不在于你会不会转述,而在于你内心有没有值得被放大的理解。

时代从来不会奖励勤奋本身,它只奖励高密度的理解。也许未来真正稀缺的不再是信息,也不是原创能力,而是那些能把复杂世界讲清楚的人。

这是一个超级个体崛起的黄金时代,我还在继续我的实验,探索理解的极限,也寻找深度的边界。希望这段旅程也能给你带来一点点关于未来的光亮。

2026 码农漫游:AI 辅助 Swift 代码修复指南

2026年1月4日 11:28

在这里插入图片描述

☔️ 引子

这是一个雨夜,霓虹灯的光晕在脏兮兮的窗玻璃上晕开,像极了那个该死的 View Hierarchy 渲染不出高斯模糊的样子。

在新上海的地下避难所里,老王(Old Wang)吐出一口合成烟雾,盯着全息屏幕上不断报错的终端。作为人类反抗军里仅存的几位「精通 Apple 软件开发」的工程师之一,他负责给 AI 霸主「智核(The Core)」生成的垃圾代码擦屁股。

在这里插入图片描述

门被撞开了,年轻的女黑客莉亚(Liya)气喘吁吁地冲进来,手里攥着一块存满代码的神经晶片。“老王!救命!‘智核’生成的 SwiftUI 代码在 iOS 26 上又崩了!反抗军的通讯 App 根本跑不起来!”

老王冷笑一声,掐灭了烟头。“我就知道。那些被捧上神坛的 LLM(大型语言模型),不管是 Claude、Codex 还是 Gemini,写起 Python 来是把好手,但一碰到 Swift,就像是穿着溜冰鞋走钢丝——步步惊心。”

在本篇博文中,您将学到如下内容:

  • ☔️ 引子
    • 🤖 为什么 AI 总是在 Swift 上「鬼打墙」?
    • 🎨 1. 别再用过时的调色盘了
    • 📐 2. 只有切掉棱角,才能圆滑处世
    • 🔄 3. 监控变化,不要缺斤少两
    • 📑 4. 标签页的「指鹿为马」
    • 👆 5. 别什么都用「戳一戳」
    • 🧠 6. 扔掉旧时代的观察者
    • ☁️ 7. 数据的陷阱
    • 📉 8. 性能的隐形杀手
    • 🔠 9. 字体排印的法西斯
    • 🔗 10. 导航的死胡同
    • 🏷️ 11. 按钮的自我修养
    • 🔢 12. 数组的画蛇添足
    • 📂 13. 寻找文件的捷径
    • 🧭 14. 导航栈的改朝换代
    • 💤 15. 睡个好觉
    • 🧮 16. 格式化的艺术
    • 🏗️ 17. 不要把鸡蛋放在一个篮子里
    • 🖼️ 18. 渲染的新欢
    • 🏋️ 19. 字重的迷惑行为
    • 🚦 20. 并发的万金油(也是毒药)
    • 🎭 21. 主角光环是默认的
    • 📐 22. 几何的诅咒
  • 尾声:数字幽灵的低语

他把晶片插入接口,全息投影在空中展开。“坐下,莉亚。今天我就给你上一课,让你看看所谓的‘人工智能’是如何在 Swift 的并发地狱快速迭代中翻车的。”

在这里插入图片描述


🤖 为什么 AI 总是在 Swift 上「鬼打墙」?

老王指着屏幕上乱成一锅粥的代码说道:“这不怪它们。Swift 和 SwiftUI 的进化速度比变异病毒还快。再加上 Python 和 JavaScript 的训练数据浩如烟海,而 Swift 的高质量语料相对较少,AI 常常会产生幻觉。更别提 Swift 的 Concurrency(并发) 模型,连人类专家都头秃,更别说这些只会概率预测的傻大个了。”

在这里插入图片描述

“听着,莉亚,”老王严肃地说,“要想在 iOS 18 甚至更高版本的废土上生存,你必须学会识别这些‘智障操作’。我们不谈哲学,只谈生存。以下就是我从死人堆里总结出来的代码排雷指南。”


🎨 1. 别再用过时的调色盘了

💀 AI 的烂代码: foregroundColor() ✨ 老王的修正: foregroundStyle()

“看这里,”老王指着一行代码,“AI 还在用 foregroundColor()。这就像是还在用黑火药做炸弹。虽然字数一样,但前者已经是个行将就木的Deprecated API。把它换成 foregroundStyle()!后者才是未来,它支持渐变(Gradients)等高级特性。别让你的 UI 看起来像上个世纪的产物。”

在这里插入图片描述

📐 2. 只有切掉棱角,才能圆滑处世

💀 AI 的烂代码: cornerRadius() ✨ 老王的修正: clipShape(.rect(cornerRadius:))

“又是一个老古董。cornerRadius() 早就该进博物馆了。现在的标准是使用 clipShape(.rect(cornerRadius:))。为什么?因为前者是傻瓜式圆角,后者能让你通过 uneven rounded rectangles(不规则圆角矩形)玩出花来。在这个看脸的世界,细节决定成败。”

🔄 3. 监控变化,不要缺斤少两

💀 AI 的烂代码: onChange(of: value) { ... } (单参数版本) ✨ 老王的修正: onChange(of: value) { oldValue, newValue in ... }

老王皱起眉头:“这个 onChange 修改器,AI 经常只给一个参数闭包。这在旧版本是‘不安全’的,现在已经被标记为弃用。要么不传参,要么接受两个参数(新旧值)。别搞得不清不楚的,容易出人命。”

在这里插入图片描述

📑 4. 标签页的「指鹿为马」

💀 AI 的烂代码: tabItem() ✨ 老王的修正: 新的 Tab API

“如果看到老旧的 tabItem(),立刻把它换成新的 Tab API。这不仅仅是为了所谓的‘类型安全(Type-safe)’,更是为了适配未来——比如那个传闻中的 iOS 26 搜索标签页设计。我们要领先‘智核’一步,懂吗?”

👆 5. 别什么都用「戳一戳」

💀 AI 的烂代码: 滥用 onTapGesture() ✨ 老王的修正: 使用真正的 Button

“AI 似乎觉得万物皆可 onTapGesture()。大错特错!除非你需要知道点击的具体坐标或者点击次数,否则统统给我换成标准的 Button。这不仅是为了让 VoiceOver(旁白)用户能活下去,也是为了让 visionOS 上的眼球追踪能正常工作。别做一个对残障人士不友好的混蛋。”

🧠 6. 扔掉旧时代的观察者

💀 AI 的烂代码: ObservableObject ✨ 老王的修正: @Observable

“莉亚,看着我的眼睛。除非你对 Combine 框架有什么特殊的各种癖好,否则把所有的 ObservableObject 都扔进焚化炉,换成 @Observable 宏。代码更少,速度更快,这就好比从燃油车换成了核动力战车。”

在这里插入图片描述

☁️ 7. 数据的陷阱

💀 AI 的烂代码: SwiftData 模型中的 @Attribute(.unique) ✨ 老王的修正: 小心使用!

“这是一个隐蔽的雷区。如果在 SwiftData 模型定义里看到 @Attribute(.unique),你要警惕——这玩意儿跟 CloudKit 八字不合。别到时候数据同步失败,你还在那儿傻乎乎地查网络连接。”

📉 8. 性能的隐形杀手

💀 AI 的烂代码: 将视图拆分为「计算属性(Computed Properties)」 ✨ 老王的修正: 拆分为独立的 SwiftUI Views

“为了图省事,AI 喜欢把大段的 UI 代码塞进计算属性里。这是尸位素餐!尤其是在使用 @Observable 时,计算属性无法享受智能视图失效(View Invalidation)的优化。把它们拆分成独立的 SwiftUI 结构体!虽然麻烦点,但为了那 60fps 的流畅度,值得。”

🔠 9. 字体排印的法西斯

💀 AI 的烂代码: .font(.system(size: 14)) ✨ 老王的修正: Dynamic Type (动态字体)

“有些 LLM(尤其是那个叫 Claude 的家伙)简直就是字体界的独裁者,总喜欢强行指定 .font(.system(size: ...))。给我搜出这些毒瘤,全部换成 Dynamic Type。如果是 iOS 26+,你可以用 .font(.body.scaled(by: 1.5))。记住,用户可能眼花,别让他们看瞎了。”

在这里插入图片描述

🔗 10. 导航的死胡同

💀 AI 的烂代码: 列表里的内联 NavigationLink ✨ 老王的修正: navigationDestination(for:)

“在 List 里直接写 NavigationLink 的目标地址?那是原始人的做法。现在的文明人使用 navigationDestination(for:)。解耦!解耦懂不懂?别把地图画在脚底板上。”


老王喝了一口已经凉透的咖啡,继续在这堆赛博垃圾中挖掘。

🏷️ 11. 按钮的自我修养

💀 AI 的烂代码:Label 做按钮内容 ✨ 老王的修正: 内联 API Button("Title", systemImage: "plus", action: ...)

“期待看到 AI 用 Label 甚至纯 Image 来做按钮内容吧——这对 VoiceOver 用户来说简直是灾难。用新的内联 API:Button("Tap me", systemImage: "plus", action: whatever)。简单,粗暴,有效。”

🔢 12. 数组的画蛇添足

💀 AI 的烂代码: ForEach(Array(x.enumerated()), ...) ✨ 老王的修正: ForEach(x.enumerated(), ...)

“看到这个 Array(x.enumerated()) 了吗?这就是脱裤子放屁。直接用 ForEach(x.enumerated(), ...) 就行了。省点内存吧,虽然现在的内存不值钱,但程序员的尊严值钱。”

在这里插入图片描述

📂 13. 寻找文件的捷径

💀 AI 的烂代码: 冗长的文件路径查找代码 ✨ 老王的修正: URL.documentsDirectory

“那些又臭又长的查找 Document 目录的代码,统统删掉。换成 URL.documentsDirectory。一行代码能解决的事,绝不写十行。”

🧭 14. 导航栈的改朝换代

💀 AI 的烂代码: NavigationView ✨ 老王的修正: NavigationStack

NavigationView 已经死了,有事烧纸。除非你要支持 iOS 15 那个上古版本,否则全部换成 NavigationStack。”

💤 15. 睡个好觉

💀 AI 的烂代码: Task.sleep(nanoseconds:) ✨ 老王的修正: Task.sleep(for: .seconds(1))

“‘智核’ 似乎很喜欢纳秒,可能它觉得自己算得快。但你要用 Task.sleep(for:),配合 .seconds(1) 这种人类能读懂的单位。别再像个僵尸一样数纳秒了。”

在这里插入图片描述

🧮 16. 格式化的艺术

💀 AI 的烂代码: C 风格格式化 String(format: "%.2f", ...) ✨ 老王的修正: Swift 原生格式化 .formatted()

“我知道 C 风格的字符串格式化很经典,但它不安全。把它换成 Swift 原生的 Text(abs(change), format: .number.precision(.fractionLength(2)))。虽然写起来长一点,但它像穿了防弹衣一样安全。”

🏗️ 17. 不要把鸡蛋放在一个篮子里

💀 AI 的烂代码: 单个文件塞入大量类型 ✨ 老王的修正: 拆分文件

“AI 喜欢把几十个 struct 和 class 塞进一个文件里,这简直是编译时间毁灭者。拆开它们!除非你想在编译的时候有时间去煮个满汉全席。”

🖼️ 18. 渲染的新欢

💀 AI 的烂代码: UIGraphicsImageRenderer ✨ 老王的修正: ImageRenderer

“如果你在渲染 SwiftUI 视图,别再用 UIKit 时代的 UIGraphicsImageRenderer 了。拥抱 ImageRenderer 吧,这是它的主场。”

在这里插入图片描述

🏋️ 19. 字重的迷惑行为

💀 AI 的烂代码: 滥用 fontWeight() ✨ 老王的修正: 区分 bold()fontWeight(.bold)

“三大 AI 巨头都喜欢滥用 fontWeight()。记住,fontWeight(.bold)bold() 渲染出来的结果未必一样。这就像‘微胖’和‘壮实’的区别,微妙但重要。”

🚦 20. 并发的万金油(也是毒药)

💀 AI 的烂代码: DispatchQueue.main.async ✨ 老王的修正: 现代并发模型

“一旦 AI 遇到并发问题,它就会像受惊的鸵鸟一样把头埋进 DispatchQueue.main.async 里。这是不可原谅的懒惰!那是旧时代的创可贴,现在的我们有更优雅的 Actor 模型。”

🎭 21. 主角光环是默认的

💀 AI 的烂代码: 到处加 @MainActor ✨ 老王的修正: 默认开启

“如果你在写新 App,Main Actor 隔离通常是默认开启的。不用像贴符咒一样到处贴 @MainActor。”

在这里插入图片描述

📐 22. 几何的诅咒

💀 AI 的烂代码: GeometryReader + 固定 Frame ✨ 老王的修正: visualEffect()containerRelativeFrame()

“最后,也是最可怕的——GeometryReader。天哪,AI 对这玩意儿简直是真爱,还喜欢配合固定尺寸的 Frame 使用。这是布局界的核武器,一炸毁所有。试着用 visualEffect() 或者 containerRelativeFrame() 来代替。别做那个破坏布局流的罪人。”


尾声:数字幽灵的低语

老王敲下最后一个回车键,全息屏幕上的红色报错瞬间变成了令人愉悦的绿色构建成功提示。

// Human-verified Code
// Status: Compiling... Success.
// Fixed by: The Refiners (Old Wang & Liya)

“搞定。” 老王瘫坐在椅子上,听着窗外雨声渐大。

在这里插入图片描述

莉亚看着完美运行的 App,眼中闪烁着崇拜的光芒:“老王,你简直是神!既然我们能修复这些代码,为什么 AI 还是会不断地生成这种垃圾?”

老王点燃了最后一支烟,看着烟雾在霓虹灯下缭绕。“因为 AI 会产生幻觉(Hallucinations)。它们会编造出看起来很美、名字很像样,但实际上根本不存在的 API。这就像是在数字世界里见鬼了一样。”

在这里插入图片描述

他转过头,意味深长地看着莉亚:“对此,我也无能为力。我只能修补已知的错误,却无法预测未知的疯狂。”

“那么,”老王把目光投向了屏幕前的你——第四面墙之外的观察者,“轮到你了。在你的赛博探险中,通常会在 AI 生成的代码里发现什么‘惊喜’?

在这里插入图片描述

如果你还活着,请在评论区告诉我们。毕竟,在这场人机大战中,知识是我们唯一的武器。

那么,感谢观赏,再会啦!8-)

Swift 6.2 列传(第十五篇):王语嫣的《万剑归宗》与 InlineArray

2026年1月4日 11:24
0️⃣ 🐼 序章:琅嬛福地的“内存”迷局 琅嬛福地,天山童姥遗留的虚拟数据中心。 这里是存储着天下所有数据结构秘籍的宝库。大熊猫侯佩穿梭在巨大的全息卷轴之间,背景音乐是《天龙八部》的BGM,他一边走一

Swift 6.2 列传(第十四篇):岳灵珊的寻人启事与 Task Naming

2025年12月30日 10:21

在这里插入图片描述

摘要:在成千上万个并发任务的洪流中,如何精准定位那个“负心”的 Bug?Swift 6.2 带来的 Task Naming 就像是给每个游荡的灵魂挂上了一个“身份铭牌”。本文将借大熊猫侯佩与岳灵珊在赛博华山的奇遇,为您解析 SE-0469 的奥秘。

0️⃣ 🐼 序章:赛博华山的“无名”孤魂

赛博华山,思过崖服务器节点。

这里的云雾不是水汽,而是液氮冷却系统泄漏的白烟。大熊猫侯佩正坐在一块全息投影的岩石上,手里捧着一盒“紫霞神功”牌自热竹笋火锅,吃得津津有味。

“味道不错,就是有点烫嘴……”侯佩吹了吹热气,习惯性地摸了摸头顶——那里毛发浓密,绝对没有秃,这让他感到无比安心。作为一名经常迷路的路痴,他刚才本来想去峨眉山看妹子,结果导航漂移,不知怎么就溜达到华山来了。

在这里插入图片描述

忽然,一阵凄婉的哭声从代码堆栈的深处传来。

“平之……平之……你在哪条线程里啊?我找不到你……”

侯佩定睛一看,只见一位身着碧绿衫子的少女,正对着满屏滚动的 Log 日志垂泪。她容貌清丽,却神色凄苦,正是华山派掌门岳不群之女,岳灵珊

“岳姑娘?”侯佩擦了擦嘴角的红油,“你在这哭什么?林平之那小子又跑路了?”

岳灵珊抬起泪眼,指着屏幕上密密麻麻的 Task 列表:“侯大哥,我写了一万个并发任务去搜索‘辟邪剑谱’的下落。刚才有一个任务抛出了异常(Error),但我不知道是哪一个!它们全都长得一模一样,都是匿名的 Task,就像是一万个没有脸的人……我找不到我的平之了!”

在这里插入图片描述

侯佩凑过去一看,果然,调试器里的任务全是 Unspecified,根本分不清谁是谁。

在本次大冒险中,您将学到如下内容:

  • 0️⃣ 🐼 序章:赛博华山的“无名”孤魂
  • 1️⃣ 🏷️ 拒绝匿名:给任务一张身份证
  • 简单的起名艺术
  • 2️⃣ 🗞️ 实战演练:江湖小报的并发采集
  • 3️⃣ 💔 岳灵珊的顿悟
  • 4️⃣ 🐼 熊猫的哲学时刻
  • 5️⃣ 🛑 尾声:竹笋的收纳难题

“唉,”侯佩叹了口气,颇为同情,“这就是‘匿名并发’的痛啊。出了事,想找个背锅的都找不到。不过,Swift 6.2 给了我们一招‘实名制’剑法,正好能解你的相思之苦。”

这便是 SE-0469: Task Naming

在这里插入图片描述


1️⃣ 🏷️ 拒绝匿名:给任务一张身份证

在这里插入图片描述

在 Swift 6.2 之前,创建 Task 就像是华山派招收了一批蒙面弟子,干活的时候挺卖力,但一旦有人偷懒或者走火入魔(Crash/Hang),你根本不知道是谁干的。

岳灵珊擦干眼泪:“你是说,我可以给平之……哦不,给任务起名字?”

“没错!”侯佩打了个响指,“SE-0469 允许我们在创建任务时,通过 name 参数给它挂个牌。无论是调试还是日志记录,都能直接看到名字。”

在这里插入图片描述

这套 API 非常简单直观:当使用 Task.init()Task.detached() 创建新任务,或者在任务组中使用 addTask() 时,都可以传入一个字符串作为名字。

简单的起名艺术

侯佩当即在全息屏上演示了一段代码:

// 以前我们只能盲人摸象
// 现在,我们可以给任务赐名!
let task = Task(name: "寻找林平之专用任务") {
    // 在任务内部,我们可以读取当前的名字
    // 如果没有名字,就是 "Unknown"(无名氏)
    print("当前运行的任务是: \(Task.name ?? "Unknown")")
    
    // 假装在干活
    try? await Task.sleep(for: .seconds(1))
}

在这里插入图片描述

“看,”侯佩指着控制台,“现在它不再是冷冰冰的内存地址,而是一个有血有肉、有名字的‘寻找林平之专用任务’了。”

2️⃣ 🗞️ 实战演练:江湖小报的并发采集

“光有个名字有什么用?”岳灵珊还是有点愁眉不展,“我有那么多个任务在跑,万一出错的是第 9527 号呢?”

“问得好!”侯佩咬了一口竹笋,摆出一副高深莫测的样子(虽然嘴角还挂着笋渣),“这名字不仅可以硬编码,还支持字符串插值!这在处理批量任务时简直是神技。”

在这里插入图片描述

假设我们需要构建一个结构体来通过网络加载江湖新闻:

struct NewsStory: Decodable, Identifiable {
    let id: Int
    let title: String // 比如 "令狐冲因酗酒被罚款"
    let strap: String
    let url: URL
}

现在,我们使用 TaskGroup 派出多名探子(子任务)去打探消息。如果有探子回报失败,我们需要立刻知道是哪一路探子出了问题。

let stories = await withTaskGroup { group in
    for i in 1...5 {
        // 关键点来了!👇
        // 我们在添加任务时,动态地给它生成了名字: "Stories 1", "Stories 2"...
        // 这就像是岳不群给弟子们排辈分,一目了然。
        group.addTask(name: "江湖快报分队-\(i)") {
            do {
                let url = URL(string: "https://hws.dev/news-\(i).json")!
                let (data, _) = try await URLSession.shared.data(from: url)
                return try JSONDecoder().decode([NewsStory].self, from: data)
            } catch {
                // 🚨 出事了!
                // 这里我们可以直接打印出 Task.name
                // 输出示例:"Loading 江湖快报分队-3 failed."
                // 岳灵珊瞬间就能知道是第 3 分队被青城派截杀了!
                print("加载失败,肇事者是: \(Task.name ?? "Unknown")")
                return []
            }
        }
    }

    var allStories = [NewsStory]()

    // 收集情报
    for await stories in group {
        allStories.append(contentsOf: stories)
    }

    // 按 ID 排序,保持队形
    return allStories.sorted { $0.id > $1.id }
}

print(stories)

3️⃣ 💔 岳灵珊的顿悟

看完这段代码,岳灵珊破涕为笑:“太好了!这样一来,如果‘寻找平之’的任务失败了,我就能立刻知道是哪一次尝试失败的,是在福州失败的,还是在洛阳失败的,再也不用对着虚空哭泣了。”

在这里插入图片描述

侯佩点点头,语重心长地说:“在并发的世界里,可见性(Visibility) 就是生命线。一个未命名的任务,就是 unpredictable(不可预测)的风险。给了它名字,就是给了它责任。如果它跑路了(Rogue Task),我们至少知道通缉令上该写谁的名字。”

岳灵珊看着屏幕上一个个清晰的任务名称,眼中闪过一丝复杂的神色:“是啊,名字很重要。可惜,有些人的名字,刻在了心上,却在江湖里丢了……”

在这里插入图片描述

“停停停!”侯佩赶紧打断她,生怕她又唱起那首福建山歌,“咱们是搞技术的,不兴搞伤痕文学。现在的重点是,你的 Debug 效率提升了 1000%!”

4️⃣ 🐼 熊猫的哲学时刻

侯佩站起身,拍了拍屁股上的灰尘(虽然是全息投影,但他觉得要有仪式感)。

“其实,给代码起名字和做熊一样。我叫侯佩,所以我知道我要吃竹笋,我知道我头绝对不秃,我知道我要走哪条路(虽然经常走错)。如果我只是一只‘Anonymous Panda’,那我可能早就被抓去动物园打工了。”

在这里插入图片描述

“善用 Task Naming,”侯佩总结道,“它不会增加运行时的负担,但在你焦头烂额修 Bug 的时候,它就是那个为你指点迷津的‘风清扬’。”

5️⃣ 🛑 尾声:竹笋的收纳难题

帮岳灵珊解决了心病,侯佩准备收拾东西离开赛博华山。他看着自己还没吃完的一大堆竹笋,陷入了沉思。

在这里插入图片描述

“这竹笋太多了,”侯佩嘟囔着,“用普通的 Array 装吧,太灵活,内存跳来跳去的,影响我拔刀(吃笋)的速度。用 Tuple 元组装吧,固定是固定了,但这写法也太丑了,而且还没法用下标循环访问……”

在这里插入图片描述

岳灵珊看着侯佩对着一堆竹笋发愁,忍不住问道:“侯大哥,你是想要一个既有元组的‘固定大小’超能力,又有数组的‘下标访问’便捷性的容器吗?”

侯佩眼睛一亮:“知我者,岳姑娘也!难道 Swift 6.2 连这个都有?”

在这里插入图片描述

岳灵珊微微一笑,指向了下一章的传送门:“听说下一回,有一种神奇的兵器,叫做 InlineArray,专门治愈你的‘性能强迫症’。”

在这里插入图片描述

(欲知后事如何,且看下回分解:InlineArray —— 当元组和数组生了个混血儿,熊猫的竹笋终于有地儿放了。)

在这里插入图片描述

Swift 6.2 列传(第十三篇):香香公主的“倾城之恋”与优先级飞升

2025年12月22日 20:02
0️⃣ 🐼 序章:回疆的慢车与急惊风 回疆,赛里木湖畔的数字荒原。 这里是系统资源的边缘地带,网络带宽如同细细的涓流。大熊猫侯佩正蹲在一块虚拟的岩石上,第 10086 次尝试刷新他的“高德地图导航”。
❌
❌