阅读视图

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

鸿蒙AVSession Kit

—— ArkTS 与 C/C++ 双语言、Provider 与 Controller 全角色

适用版本:HarmonyOS API 12+

覆盖:本地会话提供方(Provider)、会话控制方(Controller)、后台播放


一、设计总览(Mermaid)

graph TD
    subgraph 系统
        PC[播控中心/耳机/车机]
    end
    subgraph 应用进程
        A[AVSessionProvider<br>播放器] -->|setAVMetadata<br>setAVPlaybackState| S[AVSession]
        C[AVSessionController<br>控制逻辑] -->|sendControlCommand| S
    end
    PC <-->|控制命令| S
  • 任何进程都能 读 会话(获取元数据/状态)
  • 任何进程都能 写 会话(发命令)——只要拿到对应 AVSessionController

二、ArkTS 完整落地

  1. 依赖
import { avSession } from '@kit.AVSessionKit';
import { BackgroundTasks } from '@kit.BackgroundTasksKit';
  1. 会话提供方(Provider)
class MyProvider {
  private session: avSession.AVSession | null = null;

  async init(ctx: Context) {
    /* 1. 长时任务 */
    await BackgroundTasks.startBackgroundRunning(ctx, 'AUDIO_PLAYBACK', 'music');

    /* 2. 创建会话 */
    this.session = await avSession.createAVSession(ctx, 'MusicSession', 'audio');

    /* 3. 元数据 */
    await this.session.setAVMetadata({
      assetId: '10086',
      title: 'Bohemian Rhapsody',
      artist: 'Queen',
      duration: 355000,
      mediaImage: 'https://xx/cover.jpg',
      previousAssetId: '10085',
      nextAssetId: '10087'
    });

    /* 4. 播放状态 */
    await this.session.setAVPlaybackState({
      state: avSession.PlaybackState.PLAYBACK_STATE_PLAYING,
      position: { elapsedTime: 60000, updateTime: Date.now() },
      speed: 1.0,
      loopMode: avSession.LoopMode.LOOP_MODE_LIST
    });

    /* 5. 注册命令(被动) */
    this.session.on('play',     () => this.player.play());
    this.session.on('pause',    () => this.player.pause());
    this.session.on('seek',     (pos: number) => this.player.seek(pos));
    this.session.on('playNext', () => this.player.next());

    /* 6. 激活 */
    await this.session.activate();
  }

  private player = { play:()=>{}, pause:()=>{}, seek:()=>{}, next:()=>{} };

  async destroy() {
    this.session?.destroy();
    BackgroundTasks.stopBackgroundRunning(getContext(this));
  }
}
  1. 会话控制方(Controller)
class MyController {
  private ctrl: avSession.AVSessionController | null = null;

  async attach(sessionId: string) {
    this.ctrl = await avSession.createController(sessionId);
  }

  async play()  { await this.ctrl?.sendControlCommand({ command: 'play' }); }
  async pause() { await this.ctrl?.sendControlCommand({ command: 'pause' }); }
  async seek(ms: number) {
    await this.ctrl?.sendControlCommand({ command: 'seek', parameters: { position: ms } });
  }
}
  1. 枚举会话
const sessions = await avSession.getAllActiveSessions();
console.log('当前活跃会话', sessions.map(s => s.sessionTag));

三、C/C++(NDK)完整落地

  1. CMake
find_library(avsession-lib libohavsession.so)
target_link_libraries(your_target ${avsession-lib})
  1. 会话提供方(Provider)
// 1. 创建
OH_AVSession* session;
OH_AVSession_Create("MusicSess", SESSION_TYPE_AUDIO,
                    "com.demo", "MainAbility", &session);

// 2. 元数据
OH_AVMetadataBuilder* mb = OH_AVMetadataBuilder_Create();
OH_AVMetadataBuilder_SetAssetId(mb, "10086");
OH_AVMetadataBuilder_SetTitle(mb, "Rhapsody");
OH_AVMetadata* meta = OH_AVMetadataBuilder_Build(mb);
OH_AVSession_SetAVMetadata(session, meta);

// 3. 播放状态
OH_AVPlaybackState* ps = OH_AVPlaybackState_Create();
OH_AVPlaybackState_SetState(ps, PLAYBACK_STATE_PLAYING);
OH_AVSession_SetAVPlaybackState(session, ps);

// 4. 注册命令
OH_AVSession_RegisterCommandCallback(
    session, COMMAND_PLAY,
    [](void*){ /* real play */ }, nullptr);

OH_AVSession_Activate(session);
  1. 会话控制方(Controller)
// 枚举
AVSession_SessionInfo* infos;
size_t cnt;
OH_AVSessionManager_GetAllActiveSessions(&infos, &cnt);

// 创建 Controller
OH_AVSessionController* ctrl;
OH_AVSessionController_Create(infos[0].sessionId, &ctrl);

// 发命令
OH_AVControllerCommand cmd = { .command = COMMAND_SEEK, .parameters.position = 30000 };
OH_AVSessionController_SendCommand(ctrl, &cmd);
  1. 清理
OH_AVSessionController_Destroy(ctrl);
OH_AVSession_Destroy(session);

四、命令全集(两语言通用)

命令ArkTS 参数C++ 枚举 play{ command:'play' }COMMAND_PLAY pause{ command:'pause' }COMMAND_PAUSE stop{ command:'stop' }COMMAND_STOP seek{ command:'seek', parameters:{ position } }COMMAND_SEEK setSpeed{ command:'setSpeed', parameters:{ speed } }COMMAND_SET_SPEED setLoopMode{ command:'setLoopMode', parameters:{ loopMode } }COMMAND_SET_LOOP_MODE toggleFavorite{ command:'toggleFavorite' }COMMAND_TOGGLE_FAVORITE playNext / playPrevious{ command:'playNext'/'playPrevious' }COMMAND_PLAY_NEXT / PREVIOUS skipToQueueItem{ command:'skipToQueueItem', parameters:{ itemId } }COMMAND_SKIP_TO_QUEUE_ITEM commonCommand{ command:'commonCommand', parameters:{ command, extras } }COMMAND_COMMON


五、后台播放 checklist(上架必过)

  • createAVSession 的 type 与音频流类型匹配(audio / video / voice_call)
  • 激活前已 setAVMetadata + setAVPlaybackState + 注册命令
  • 已申请 AUDIO_PLAYBACK 长时任务且 pause/stop 时主动取消
  • 退出业务时 destroy 会话
  • 若支持歌单/冷启动续播,已注册 PlayMusicList / PlayAudio 意图

至此,ArkTS 与 C/C++ 双语言、Provider 与 Controller 全角色一次讲透。

Vue 事件绑定机制

Vue 将事件系统拆分为原生 DOM 事件与自定义组件事件两套正交实现,前者对接浏览器事件循环,后者基于发布–订阅模型。本文以 v-on(缩写 @)为线索,结合运行时源码路径,给出端到端的实现剖析。

一、架构概览

Vue 的事件绑定分为两条主线:

  • 原生事件绑定

    通过 @clickv-on:click 直接作用于普通 DOM 元素,最终调用浏览器的 addEventListener

  • 组件事件绑定

    通过 @click 作用于子组件标签时,实际上是父组件监听子组件的自定义事件,由子组件通过 $emit 触发,不经过 DOM。

二、原生事件绑定:从 AST 到 addEventListener

1.编译阶段

模板中的 @click="handler" 经模板编译器解析后,生成 AST,最终转化为 VNode 的 data.on = { click: handler }

2.运行时挂载

首次渲染时,patch 过程会调用 createElm,为真实 DOM 节点执行 invokeCreateHooks,其中 cbs.create 包含 updateDOMListeners(位于 src/platforms/web/runtime/modules/events.js)。

updateDOMListeners 的职责:

  • 归一化事件名,处理 IE 兼容性差异。
  • 生成包裹函数,处理 .once.passive.capture 等修饰符。
  • 调用 updateListenersaddtarget.addEventListener(type, wrappedHandler, useCapture)

3.更新阶段

当组件更新时,patch 再次调用 updateDOMListeners,通过 sameVnode 判断事件差异,按需移除旧事件并重新绑定新事件。

三、组件事件绑定:on + events + emit

1.父组件编译

<Child @click="handleClick" /> 编译后,VNode 的 componentOptions.listeners = { click: handleClick },不会出现在 DOM 属性上。

2.子组件初始化

子组件实例化时:

  • initInternalComponent 将父级 listeners 注入到 vm.$options._parentListeners
  • initEvents 创建 _events = Object.create(null) 作为事件中心。
  • _parentListeners 非空,执行 updateComponentListeners(vm, _parentListeners),内部通过 $on 注册事件:
function add(event, fn) {
  vm.$on(event, fn)
}

3.手动触发

子组件内部调用 this.$emit('click', payload) 时,执行:

const cbs = vm._events[event]
if (cbs) {
  cbs.forEach(cb => cb(payload))
}

整个过程与浏览器事件体系完全隔离,因此可跨层级通信,且参数可控。

四、.native:在组件根节点强制使用原生事件

<Child @click.native="handler" /> 编译为 nativeOn 而非 on,运行时由 updateDOMListeners 读取 nativeOn,流程与原生事件一致,绑定在组件根 DOM 上。

五、事件修饰符实现细节

  • .stop:包裹函数内调用 e.stopPropagation()
  • .prevent:包裹函数内调用 e.preventDefault()
  • .once:绑定后立即移除监听器,并标记 _withOnce
  • .passive:调用 addEventListener(type, fn, { passive: true })
  • .capture:第三个参数传入 useCapture: true

六、性能与内存考量

  • 原生事件由浏览器托管,Vue 仅在 VNode 销毁时执行 removeEventListener,无额外开销。
  • 组件事件存储在 JS 对象,组件销毁时统一 $off,防止内存泄漏。

结论

Vue 事件系统通过“编译期转换 + 运行时调度”实现高度抽象:

  • 原生事件:AST → VNode → patch → addEventListener,完全对齐浏览器。
  • 组件事件:父子间通过 VNode.listeners → vm.events → emit,脱离 DOM,实现跨组件通信。

理解这一分层设计,有助于在复杂场景(服务端渲染、微前端、自定义渲染器)中精准定位事件相关问题。

从0到1实现react(三):任务调度-最小堆基础

为什么要学最小堆?

想象一下,你是一个超级忙碌的项目经理,手上有无数个任务等待处理。有些任务紧急程度高,有些相对不那么着急。你会怎么安排处理顺序呢?

这就是 React 在进行任务调度时面临的问题!每当页面需要更新时,React 需要决定哪些任务优先级最高,应该先处理。而最小堆(Min Heap)就是 React Scheduler 用来解决这个问题的核心数据结构。

今天,我们就来揭开最小堆的神秘面纱,看看它是如何帮助 React 高效管理任务优先级的!

什么是最小堆?一个生动的比喻 🏆

想象最小堆就像一个"自动排序的金字塔":

  • 塔顶永远是最小的数(就像最高优先级的任务总在最前面)
  • 每个父节点都比子节点小(上司的优先级总是比下属高)
  • 插入新元素时,金字塔会自动重新排列(新任务来了会自动找到合适位置)

这种特性让我们可以在 O(log n) 的时间复杂度内插入任务,并在 O(1) 的时间内获取最高优先级的任务!

最小堆的结构:从抽象到具体

1. 树形结构视图

让我们先看看最小堆长什么样:

graph TD
    A[1] --> B[3]
    A --> C[4]
    B --> D[7]
    B --> E[10]
    C --> F[9]
    C --> G[6]
    D --> H[15]
    D --> I[14]
    E --> J[12]

    style A fill:#ff9999
    style B fill:#ffcc99
    style C fill:#ffcc99
    style D fill:#ffffcc
    style E fill:#ffffcc
    style F fill:#ffffcc
    style G fill:#ffffcc
    style H fill:#ccffcc
    style I fill:#ccffcc

看出规律了吗?

  • 根节点 1 是最小的
  • 每个父节点都比它的子节点小
  • 这就保证了堆顶永远是最小值!

2. 数组存储:化繁为简

虽然最小堆在逻辑上是树结构,但在实际存储时,我们使用数组!这是个巧妙的设计:

graph LR
    subgraph "数组索引"
        idx0[0]
        idx1[1]
        idx2[2]
        idx3[3]
        idx4[4]
        idx5[5]
        idx6[6]
        idx7[7]
        idx8[8]
        idx9[9]
    end

    subgraph "数组值"
        val1[1]
        val3[3]
        val4[4]
        val7[7]
        val10[10]
        val9[9]
        val6[6]
        val15[15]
        val14[14]
        val12[12]
    end

    idx0 --- val1
    idx1 --- val3
    idx2 --- val4
    idx3 --- val7
    idx4 --- val10
    idx5 --- val9
    idx6 --- val6
    idx7 --- val15
    idx8 --- val14
    idx9 --- val12

数组 = [1, 3, 4, 7, 10, 9, 6, 15, 14, 12]

3. 索引关系:数学的魅力

这里有个超酷的数学规律:

graph TD
subgraph "索引关系"
A["父节点索引 i"]
B["左子节点: 2*i + 1"]
C["右子节点: 2*i + 2"]
D["子节点 j 的父节点: (j-1)÷2"]

        A --> B
        A --> C
        D --> A
    end

举个例子:

  • 索引 0 的左子节点:2×0+1 = 1
  • 索引 0 的右子节点:2×0+2 = 2
  • 索引 3 的父节点:(3-1)÷2 = 1

这个规律让我们可以仅用数组就完美模拟树结构!

代码实现:从理论到实践

现在让我们动手实现一个最小堆类:

/**
 * 最小堆的实现 - React Scheduler 的核心数据结构
 * 让任务按优先级自动排序!
 */
class MiniHeap {
  heap = [];

  constructor() {
    console.log("🎯 最小堆初始化完成!");
  }

  /**
   * 插入新任务 - 就像给金字塔添加新砖块
   * @param {number} val 任务的优先级值(越小优先级越高)
   */
  insert(val) {
    console.log(`📥 插入新任务,优先级: ${val}`);
    this.heap.push(val);
    
    // 如果不是第一个元素,需要向上调整位置
    if (this.heap.length > 1) {
      this.heapifyUp();
    }
    console.log(`当前堆状态: [${this.heap.join(', ')}]`);
  }

  /**
   * 获取并移除最高优先级任务 - 取走金字塔顶端
   * @returns {number|undefined} 最高优先级的任务
   */
  pop() {
    if (this.heap.length === 0) {
      console.log("⚠️ 没有任务可以执行了!");
      return undefined;
    }
    
    if (this.heap.length === 1) {
      const task = this.heap.pop();
      console.log(`🎯 执行最后一个任务: ${task}`);
      return task;
    }

    // 保存最小值(堆顶)
    let min = this.heap[0];
    console.log(`🎯 执行最高优先级任务: ${min}`);
    
    // 将最后一个元素移到顶部,然后向下调整
    this.heap[0] = this.heap.pop();
    this.heapifyDown();
    
    console.log(`执行后堆状态: [${this.heap.join(', ')}]`);
    return min;
  }

  /**
   * 获取父节点索引 - 找到上级
   */
  getParentIndex(i) {
    return Math.floor((i - 1) / 2);
  }

  /**
   * 获取左子节点索引 - 找到左下属
   */
  getLeft(i) {
    return 2 * i + 1;
  }

  /**
   * 获取右子节点索引 - 找到右下属
   */
  getRight(i) {
    return 2 * i + 2;
  }

  /**
   * 向上调整 - 新员工可能比老板还优秀,需要晋升!
   */
  heapifyUp() {
    let currentIndex = this.heap.length - 1;
    
    while (currentIndex > 0) {
      let parentIndex = this.getParentIndex(currentIndex);
      
      // 如果当前节点比父节点小,就交换位置(向上晋升)
      if (this.heap[currentIndex] < this.heap[parentIndex]) {
        console.log(`🔄 交换位置: ${this.heap[currentIndex]}${this.heap[parentIndex]}`);
        [this.heap[currentIndex], this.heap[parentIndex]] = [
          this.heap[parentIndex],
          this.heap[currentIndex],
        ];
        currentIndex = parentIndex;
      } else {
        break; // 已经找到合适位置
      }
    }
  }

  /**
   * 向下调整 - 新来的老板可能不如下属,需要降级!
   */
  heapifyDown() {
    let currentIndex = 0;

    while (true) {
      let leftIndex = this.getLeft(currentIndex);
      let rightIndex = this.getRight(currentIndex);
      let smallestIndex = currentIndex;

      // 找到当前节点和其子节点中最小的
      if (
        leftIndex < this.heap.length &&
        this.heap[leftIndex] < this.heap[smallestIndex]
      ) {
        smallestIndex = leftIndex;
      }

      if (
        rightIndex < this.heap.length &&
        this.heap[rightIndex] < this.heap[smallestIndex]
      ) {
        smallestIndex = rightIndex;
      }

      // 如果当前节点已经是最小的,说明位置合适
      if (smallestIndex === currentIndex) {
        break;
      }
      
      console.log(`🔄 向下调整: ${this.heap[currentIndex]}${this.heap[smallestIndex]} 交换`);
      [this.heap[currentIndex], this.heap[smallestIndex]] = [
        this.heap[smallestIndex],
        this.heap[currentIndex],
      ];
      currentIndex = smallestIndex;
    }
  }

  /**
   * 获取堆大小
   */
  size() {
    return this.heap.length;
  }

  /**
   * 查看最高优先级任务(不移除)
   */
  peek() {
    return this.heap.length > 0 ? this.heap[0] : undefined;
  }
}

// 🎮 让我们来测试一下!
console.log("=== 最小堆演示开始 ===");

const heap = new MiniHeap();

console.log("\n📋 模拟 React 任务调度场景:");
console.log("优先级越小越重要(1=超高优先级,100=低优先级)");

// 插入一些"任务"
heap.insert(100); // 低优先级任务
heap.insert(10);  // 中等优先级
heap.insert(9);   // 较高优先级  
heap.insert(7);   // 高优先级任务

console.log("\n🎯 开始执行任务(按优先级从高到低):");
while (heap.size() > 0) {
  const nextTask = heap.pop();
  console.log(`执行任务,优先级: ${nextTask}`);
}

console.log("\n=== 演示结束 ===");

实战应用:React 中的任务调度

在 React 的 Scheduler 中,最小堆被用来管理任务队列:

  1. 插入任务:当组件需要更新时,React 会根据优先级插入任务
  2. 执行任务:Scheduler 总是取出优先级最高的任务执行
  3. 时间片管理:如果时间片用完,当前任务会被重新插入堆中

这种设计让 React 可以:

  • 响应用户交互(高优先级)
  • 🎨 处理动画更新(中等优先级)
  • 📊 执行后台数据同步(低优先级)

性能分析:为什么选择最小堆?

操作 时间复杂度 说明
插入 O(log n) 最多需要向上调整 log n 层
删除最小值 O(log n) 最多需要向下调整 log n 层
查看最小值 O(1) 直接访问数组第一个元素
构建堆 O(n) 批量构建时的优化算法

相比普通数组:

  • ❌ 插入后排序:O(n log n)
  • ❌ 查找最小值:O(n)
  • ✅ 最小堆插入:O(log n)
  • ✅ 最小堆取最小值:O(1)

也是用上webworker了

React 16.8,我自己写的的足球应用,问题是模态框没有立即弹出,反而是等了一会才弹出。

  useImperativeHandle(ref, () => ({
    showModal: (id: string) => {
      const newState = {
        ...state,
        id,
        teamCount: teamOddList.length,
        tableLoading: true,
        open: true,
      };
      setState(newState);
      setAddedItems(new Map());

      // 初始化已存在的奖金项目
      initializeAddedItems(id);

      getOddResultList(newState, teamOddList);
    },
  }));

我开始以为是 React 响应式设计导致的,因为此时 setState 的是异步的,虽然 showModal 中设置了 open 为 true,后续处理不当,还是会导致 open 隐式设置为 false

仔细检查一番发现没有,并且我已经把 newSate 传递过去了,通过注释代码发现,是getOddResultList导致的,其实在之前的写法中我是加了setTimeout的,只不过时间太久了忘记了为什么加。

如今再写一来觉得setTimeout这种解决方式并不好。而且延迟 20ms 是没用的,看起来像是在等计算完成后再显示模态框。

于是我问了下 Trace,他说是因为getOddResultList有大量的同步计算,建议我用 webworker 来处理。

  • 多重嵌套循环 :对每个球队的赔率信息进行多维度组合计算(胜平负、让球、比分、进球数、半场等)
  • 指数级复杂度 :通过递归函数 getTeamCombinationList 和 againForEach 生成所有可能的投注组合
  • 大量数据处理 :每个组合都需要计算赔率乘积,并进行排序
  • JavaScript 是单线程的,同步计算会完全阻塞主线程
  • 模态框的 open 状态虽然已设置为 true ,但 React 无法进行重新渲染
  • 用户界面会出现"卡顿",模态框无法立即显示

这个函数确实会消耗大量的时间,因为有 4 只球队会产生 9 百万种结果。

使用 webworker 的话,需要将 js 文件放到 public 目录下,通过onmessage来接收消息,通过postMessage来发送消息。

self.onmessage = function (e) {
  const { teamOddList } = e.data;
  try {
    const result = calculateOddResultList(teamOddList);
    self.postMessage({ success: true, data: result });
  } catch (error) {
    self.postMessage({ success: false, error: error.message });
  }
};

在 jsx 文件中使用 webworker

useEffect(() => {
    // 创建 Web Worker

    workerRef.current = new Worker('/oddResultWorker.js');
    workerRef.current.onmessageerror = (e) => {
      console.error('Worker message error:', e);
    };

    // 监听 Worker 消息
    workerRef.current.onmessage = (e) => {
      const { success, data, error } = e.data;
      if (success) {
        allOddResultListRef.current = data;
        setState((preState) => ({
          ...preState,
          total: data.length,
          tableLoading: false,
          oddResultList: data.slice(
            (preState.currentPage - 1) * pageSize,
            preState.currentPage * pageSize
          ),
        }));
      } else {
        console.error('Worker error:', error);
      }
    };

    return () => {
      if (workerRef.current) {
        workerRef.current.terminate();
      }
    };
  }, []);

调用时

    showModal: (id: string, teamOddList: Array<NFootball.ITeamRecordOdds>) => {
      ...
      setAddedItems(new Map());
      initializeAddedItems(id);

      workerRef.current.postMessage({ teamOddList });
    },

React 牵手 Ollama:本地 AI 服务对接实战指南

在这个 AI 大模型如雨后春笋般涌现的时代,让前端应用与本地大模型来一场 “亲密接触”,就像给你的 React 应用装上一个 “本地智囊团”。今天,我们就来实现一个看似高深实则简单的需求:用 React 对接本地 Ollama 服务。这就好比教两个素未谋面的朋友打招呼,Ollama 是守在本地的 “AI 达人”,React 则是活泼的 “前端信使”,我们要做的就是搭建它们之间的沟通桥梁。

底层原理:通信的奥秘

在开始编码前,我们得先搞明白这两个 “朋友” 是如何交流的。Ollama 作为本地运行的大模型服务,会在你的电脑上开启一个 “通信窗口”—— 也就是 HTTP 服务器,默认情况下这个窗口的地址是 http://localhost:11434。而 React 应用要做的,就是通过 HTTP 协议向这个窗口发送 “消息”(请求),并等待 “回复”(响应)。

这就像你去餐厅吃饭,Ollama 是后厨的厨师,React 是前厅的服务员,http://localhost:11434 就是厨房的传菜口。服务员把顾客的订单(请求)通过传菜口递给厨师,厨师做好菜后再通过传菜口把菜(响应)送回给服务员。

准备工作:工具就位

在正式开始前,我们需要准备好 “食材” 和 “厨具”:

  1. 安装 Ollama:去 Ollama 官网下载并安装,这一步就像把厨师请到厨房里。安装完成后,打开命令行,输入 ollama run llama3 来启动一个基础模型,这里我们用 llama3 作为示例,你也可以换成其他喜欢的模型。
  1. 创建 React 应用:如果你还没有 React 项目,可以用 Create React App 快速创建一个,命令是 npx create-react-app ollama-demo,这就像搭建好前厅的场地。

代码实现:搭建沟通桥梁

一切准备就绪,现在我们来编写核心代码,实现 React 与 Ollama 的通信。

首先,我们需要一个发送请求的函数。在 React 组件中,我们可以用 fetch API 来发送 HTTP 请求到 Ollama 的 API 端点。Ollama 的聊天接口是 http://localhost:11434/api/chat,我们需要向这个接口发送包含模型名称和消息内容的 JSON 数据。

import { useState } from 'react';
function OllamaChat() {
  const [message, setMessage] = useState('');
  const [response, setResponse] = useState('');
  const sendMessage = async () => {
    try {
      // 构建请求体,指定模型和消息
      const requestBody = {
        model: 'llama3',
        messages: [{ role: 'user', content: message }],
        stream: false // 不使用流式响应,等待完整回复
      };
      // 发送 POST 请求到 Ollama 的聊天接口
      const response = await fetch('http://localhost:11434/api/chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
      });
      // 解析响应数据
      const data = await response.json();
      
      // 提取并显示 AI 的回复
      if (data.message && data.message.content) {
        setResponse(data.message.content);
      }
    } catch (error) {
      console.error('与 Ollama 通信出错:', error);
      setResponse('抱歉,无法连接到 AI 服务,请检查 Ollama 是否正在运行。');
    }
  };
  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
      <h2>React × Ollama 聊天 Demo</h2>
      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="输入你的问题..."
          style={{ width: '70%', padding: '8px', marginRight: '10px' }}
        />
        <button onClick={sendMessage} style={{ padding: '8px 16px' }}>
          发送
        </button>
      </div>
      <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '4px' }}>
        <h3>AI 回复:</h3>
        <p>{response}</p>
      </div>
    </div>
  );
}
export default OllamaChat;

代码解析:庖丁解牛

让我们来仔细看看这段代码的工作原理,就像拆解一台精密的机器。

  1. 状态管理:我们用 useState 钩子创建了两个状态变量,message 用来存储用户输入的消息,response 用来存储 AI 的回复。这就像两个储物盒,分别存放要发送的消息和收到的回复。
  1. 发送消息函数:sendMessage 是核心函数,它通过 fetch 发送请求到 Ollama。请求体中指定了要使用的模型(llama3)和用户的消息。这里的 stream: false 表示我们希望一次性收到完整的回复,而不是逐字接收。
  1. 处理响应:当 Ollama 处理完请求后,会返回一个 JSON 格式的响应。我们从中提取出 AI 的回复内容,并更新 response 状态,这样页面上就会显示出 AI 的回答了。
  1. 错误处理:如果通信过程中出现错误(比如 Ollama 没有运行),我们会捕获错误并显示友好的提示信息。

运行测试:见证奇迹的时刻

现在,让我们来测试一下这个 Demo 是否能正常工作。

  1. 确保 Ollama 正在运行:打开命令行,输入 ollama run llama3,等待模型加载完成。
  1. 启动 React 应用:在项目目录下运行 npm start,打开浏览器访问 http://localhost:3000
  1. 发送消息:在输入框中输入一个问题,比如 “你好,Ollama!”,然后点击 “发送” 按钮。稍等片刻,你应该就能看到 AI 的回复了。

如果一切顺利,你会看到 React 应用和 Ollama 成功 “牵手”,完成了一次愉快的对话。如果遇到问题,先检查 Ollama 是否正在正常运行,模型名称是否正确,网络连接是否通畅。

进阶思考:拓展可能性

这个简单的 Demo 只是一个开始,就像我们只是搭建了一座简陋的小桥。你可以基于这个基础进行很多拓展:

  1. 实现流式响应:将 stream 设置为 true,然后处理流式响应,让 AI 的回复像打字一样逐字显示,提升用户体验。
  1. 增加聊天历史:用状态管理存储聊天记录,让对话可以上下文连贯。
  1. 切换不同模型:在界面上增加模型选择功能,让用户可以根据需要切换不同的 Ollama 模型。
  1. 优化错误处理:增加更详细的错误提示,帮助用户排查问题。

总结:本地 AI 的魅力

通过这个 Demo,我们展示了 React 对接本地 Ollama 服务的全过程。相比于调用云端的 AI 服务,本地部署的 Ollama 具有隐私性好、响应速度快、无需网络连接等优点,就像把 AI 助手请到了自己家里,随时可以交流。

希望这篇文章能帮助你理解 React 与本地 AI 服务对接的原理和方法。现在,你可以基于这个基础,开发出更强大、更有趣的本地 AI 应用了。让我们一起探索前端与 AI 结合的无限可能吧!

Next.js 全栈开发基础:在 pages/api/*.ts 中创建接口的艺术

在 Web 开发的世界里,前端与后端就像一对需要默契配合的舞者。前端负责优雅地展示数据,后端则默默在幕后准备数据,而接口就是它们之间传递信号的乐谱。在 Next.js 的舞台上,pages/api/*.ts就是谱写这份乐谱的最佳创作室。今天,我们就来揭开在 Next.js 中创建接口的神秘面纱,用 TypeScript 为你的全栈应用搭建起高效的数据桥梁。

接口的本质:数据交换的高速公路

在深入技术细节之前,让我们先理解接口的本质。想象你在餐厅点餐,你(前端)告诉服务员(接口)想要什么,服务员把需求传达给厨房(数据库 / 业务逻辑),然后把做好的食物(数据)端给你。这个过程中,服务员就是接口,负责规范请求格式、处理业务逻辑并返回结果。

在计算机科学中,接口本质上是客户端与服务器之间约定的数据交换格式和规则。Next.js 的 API 路由之所以强大,是因为它允许我们在同一个项目中同时编写前端页面和后端接口,就像在同一个屋檐下同时拥有餐厅大堂和厨房,大大提高了开发效率。

初探 pages/api:Next.js 的接口魔法

Next.js 的 API 路由基于一个简单而强大的约定:在pages/api目录下创建的文件会自动成为 API 接口。这个机制背后其实是 Next.js 的文件系统路由在起作用,当服务器启动时,它会扫描pages/api目录下的所有文件,为每个文件创建对应的路由端点。

比如我们创建pages/api/hello.ts文件,访问http://localhost:3000/api/hello就能调用这个接口。这种设计就像给每个接口分配了独立的办公室,它们互不干扰又能协同工作。

第一个接口:Hello World 的进阶版

让我们从经典的 Hello World 开始,创建一个能返回个性化问候的接口。在pages/api目录下新建greet.ts文件,输入以下代码:

export default function handler(req, res) {
  // 从请求中获取查询参数name
  const { name = "World" } = req.query;
  
  // 设置响应状态码为200(成功)
  res.status(200).json({ 
    message: `Hello, ${name}!`,
    timestamp: new Date().toISOString()
  });
}

这个接口做了三件事:

  1. 从请求的查询参数中获取 name,如果没有提供则默认使用 "World"
  1. 设置 HTTP 响应状态码为 200,表示请求成功
  1. 返回一个 JSON 对象,包含问候消息和当前时间戳

运行你的 Next.js 应用,访问http://localhost:3000/api/greet?name=Next.js,你会看到类似这样的响应:

{
  "message": "Hello, Next.js!",
  "timestamp": "2025-08-17T12:34:56.789Z"
}

处理不同的 HTTP 方法:接口的多面手

一个健壮的接口应该能处理不同的 HTTP 方法,就像一个多才多艺的演员能胜任不同的角色。常见的 HTTP 方法有 GET(获取数据)、POST(创建数据)、PUT(更新数据)和 DELETE(删除数据)。

让我们创建一个简单的任务管理接口,支持 GET 和 POST 方法:

// pages/api/tasks.ts
let tasks = [
  { id: 1, title: "学习Next.js", completed: false },
  { id: 2, title: "创建API接口", completed: true }
];
export default function handler(req, res) {
  // 获取请求方法
  const { method } = req;
  switch (method) {
    case 'GET':
      // 处理GET请求:返回所有任务
      res.status(200).json(tasks);
      break;
    case 'POST':
      // 处理POST请求:创建新任务
      const { title } = req.body;
      
      // 验证请求数据
      if (!title) {
        return res.status(400).json({ error: "任务标题不能为空" });
      }
      
      // 创建新任务
      const newTask = {
        id: tasks.length + 1,
        title,
        completed: false
      };
      
      // 添加到任务列表
      tasks.push(newTask);
      
      // 返回创建的任务,状态码201表示资源创建成功
      res.status(201).json(newTask);
      break;
    default:
      // 处理不支持的方法
      res.setHeader('Allow', ['GET', 'POST']);
      res.status(405).end(`方法 ${method} 不被允许`);
  }
}

这个接口展示了如何根据不同的 HTTP 方法执行不同的操作:

  • 当使用 GET 方法访问时,它返回所有任务列表
  • 当使用 POST 方法并发送包含 title 的 JSON 数据时,它创建一个新任务
  • 当使用不支持的方法(如 PUT 或 DELETE)时,它返回 405 错误

你可以使用工具如 Postman 或 curl 来测试这个接口:

# 测试GET请求
curl http://localhost:3000/api/tasks
# 测试POST请求
curl -X POST -H "Content-Type: application/json" -d '{"title":"新任务"}' http://localhost:3000/api/tasks

接口参数处理:精准获取请求数据

在实际开发中,我们经常需要从不同位置获取请求数据。Next.js 的 API 路由提供了多种方式来获取这些数据,就像有多个入口可以进入一个建筑:

  1. 查询参数(Query Parameters) :位于 URL 中?后面的键值对,通过req.query获取
  1. 路径参数(Path Parameters) :URL 路径中的动态部分,通过文件名中的[param]定义
  1. 请求体(Request Body) :POST、PUT 等方法发送的数据,通过req.body获取

让我们创建一个支持路径参数的接口,用于获取单个任务:

// pages/api/tasks/[id].ts
// 假设tasks数组与前面的例子相同
let tasks = [
  { id: 1, title: "学习Next.js", completed: false },
  { id: 2, title: "创建API接口", completed: true }
];
export default function handler(req, res) {
  const { id } = req.query;
  // 将id转换为数字
  const taskId = parseInt(id, 10);
  
  // 验证id是否有效
  if (isNaN(taskId)) {
    return res.status(400).json({ error: "无效的任务ID" });
  }
  
  // 查找任务
  const task = tasks.find(t => t.id === taskId);
  
  if (task) {
    res.status(200).json(task);
  } else {
    res.status(404).json({ error: "任务不存在" });
  }
}

现在,访问http://localhost:3000/api/tasks/1会返回 ID 为 1 的任务,而访问http://localhost:3000/api/tasks/99会返回 404 错误。

错误处理:接口的安全网

就像现实生活中需要应急预案一样,接口也需要完善的错误处理机制。一个好的错误处理策略应该:

  • 返回适当的 HTTP 状态码
  • 提供清晰的错误信息
  • 避免暴露敏感信息

让我们改进前面的任务接口,添加更完善的错误处理:

// pages/api/tasks/[id].ts(改进版)
let tasks = [
  { id: 1, title: "学习Next.js", completed: false },
  { id: 2, title: "创建API接口", completed: true }
];
export default function handler(req, res) {
  try {
    const { id } = req.query;
    const taskId = parseInt(id, 10);
    
    if (isNaN(taskId)) {
      // 400 Bad Request:请求参数无效
      return res.status(400).json({ 
        error: "无效的任务ID",
        details: "ID必须是数字"
      });
    }
    
    const task = tasks.find(t => t.id === taskId);
    
    if (task) {
      // 200 OK:请求成功
      res.status(200).json(task);
    } else {
      // 404 Not Found:资源不存在
      res.status(404).json({ 
        error: "任务不存在",
        details: `没有ID为${taskId}的任务`
      });
    }
  } catch (error) {
    // 500 Internal Server Error:服务器内部错误
    console.error("处理请求时出错:", error);
    res.status(500).json({ 
      error: "服务器内部错误",
      details: "请稍后再试"
    });
  }
}

这个改进版接口使用 try-catch 块捕获可能的错误,并为不同类型的错误返回相应的状态码和详细信息,同时避免将内部错误直接暴露给客户端。

接口的性能考量:让数据流动更快

随着应用规模的增长,接口的性能变得越来越重要。以下是一些提高 API 路由性能的小贴士:

  1. 数据缓存:对于不经常变化的数据,可以使用缓存减少重复计算
  1. 请求验证:尽早验证请求数据,避免不必要的处理
  1. 分页处理:对于大量数据,使用分页减少数据传输量
  1. 异步处理:对于耗时操作,考虑使用异步处理避免阻塞

让我们实现一个带分页功能的任务列表接口:

// pages/api/tasks/paginated.ts
let tasks = [
  // 假设这里有很多任务...
  { id: 1, title: "任务1", completed: false },
  { id: 2, title: "任务2", completed: true },
  // ...更多任务
];
export default function handler(req, res) {
  try {
    // 获取分页参数,默认页码为1,每页10条
    const { page = 1, limit = 10 } = req.query;
    const pageNum = parseInt(page, 10);
    const limitNum = parseInt(limit, 10);
    
    // 验证分页参数
    if (isNaN(pageNum) || isNaN(limitNum) || pageNum < 1 || limitNum < 1) {
      return res.status(400).json({ 
        error: "无效的分页参数",
        details: "页码和每页数量必须是正整数"
      });
    }
    
    // 计算总页数
    const totalPages = Math.ceil(tasks.length / limitNum);
    
    // 计算起始索引
    const startIndex = (pageNum - 1) * limitNum;
    
    // 获取当前页的任务
    const paginatedTasks = tasks.slice(startIndex, startIndex + limitNum);
    
    res.status(200).json({
      data: paginatedTasks,
      pagination: {
        total: tasks.length,
        page: pageNum,
        limit: limitNum,
        totalPages
      }
    });
  } catch (error) {
    console.error("分页查询出错:", error);
    res.status(500).json({ error: "服务器内部错误" });
  }
}

这个接口支持通过page和limit参数控制返回的数据量,减轻了服务器和网络的负担。

部署与注意事项:让接口飞向生产环境

当你的接口准备好部署到生产环境时,有几个重要的注意事项:

  1. 环境变量:敏感信息如数据库连接字符串应该使用环境变量,而不是硬编码在代码中
  1. CORS 设置:如果你的前端和后端不在同一个域名下,需要配置跨域资源共享(CORS)
  1. 速率限制:为了防止滥用,考虑添加速率限制功能
  1. 日志记录:添加适当的日志记录以便调试和监控

在 Next.js 中配置 CORS 非常简单,你可以使用cors中间件:

// pages/api/with-cors.ts
import cors from 'cors';
// 初始化cors中间件
const corsMiddleware = cors({
  origin: process.env.NEXT_PUBLIC_FRONTEND_URL || '*',
  methods: ['GET', 'POST', 'PUT', 'DELETE']
});
// 辅助函数:将中间件转换为Promise
function runMiddleware(req, res, fn) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
}
export default async function handler(req, res) {
  // 应用CORS中间件
  await runMiddleware(req, res, corsMiddleware);
  
  // 处理请求
  res.status(200).json({ message: "这个接口支持跨域请求!" });
}

总结:接口开发的艺术与科学

在 Next.js 中创建 API 接口就像在构建一座连接前端和后端的桥梁,它需要扎实的技术基础,也需要对用户需求的深刻理解。通过pages/api/*.ts文件,我们可以快速创建功能完善的接口,处理各种 HTTP 方法,获取不同来源的请求数据,并返回结构化的响应。

记住,一个好的接口应该是清晰、健壮、高效且安全的。它不仅要能正确处理正常情况,还要能优雅地应对错误;不仅要能满足当前需求,还要为未来的扩展留有余地。

随着你对 Next.js API 路由的深入了解,你可以尝试更高级的功能,如数据库集成、身份验证、文件上传等。全栈开发的世界充满了可能性,而接口就是打开这个世界的钥匙。现在,拿起这把钥匙,开始构建你的全栈应用吧!

鸿蒙各种生命周期

说真的记不住,用的时候自然会

层级 对象 主要回调 触发时机一句话
UI-组件 @Component aboutToAppear → build → onDidBuild → onReady → (onWillDestroy) → onDestroy → aboutToDisappear 创建到销毁完整链路
UI-页面 @Entry onPageShow / onPageHide / onBackPress 页面可见/隐藏/返回键
UI-窗口 WindowStage onWindowStageCreate / Destroy / Show / Hide 多窗口/悬浮窗
UI-复用 @Reusable aboutToReuse / aboutToRecycle 列表滚动复用池
Ability UIAbility onCreate → onWindowStageCreate → onForeground → onBackground → onWindowStageDestroy → onDestroy Stage 模型
Ability ServiceAbility onStart / onCommand / onConnect / onDisconnect / onStop 启动/绑定/停止
Ability DataAbility onInitialize + CRUD 跨应用数据共享
Ability FormAbility onCreateForm / onUpdateForm / onDeleteForm… 桌面卡片
扩展点 ExtensionAbility onCreate → 业务回调 → onDestroy 输入法/分享/打印/VoIP…
模块级 AbilityStage onCreate → onAcceptWant → onConfigurationUpdated → onMemoryLevel → onDestroy 每个 HAP 一个实例
并发 Worker onCreate → onMessage → onError → onDestroy 耗时计算
并发 TaskDispatcher onStart / onProgress / onComplete / onFail 轻量并发
硬件 Camera onCreate → onCaptureStarted/Ended → onRelease 相机
硬件 AudioRenderer onStateChange / onMarkReach / onPeriodReach 音频播放
硬件 Sensor onSensorChanged / onAccuracyChanged 传感器
图形 XComponent onSurfaceCreated / Changed / Destroyed Native 渲染
分布式 Continuation onStartContinuation → onSaveData → onRestoreData → onCompleteContinuation 跨设备迁移
分布式 RemoteAbility ServiceAbility(但跑在远端) 分布式后台
Native NativeWindow Create → … → Destroy C++ 层图形
Native NativeAudioRenderer Create → … → Release C++ 层音频
系统事件 netAvailable / lowBattery / orientationChange 全局广播
调试 HotReload / HiTrace 热重载/性能追踪

Node.js 中 require 函数的原理深度解析

Node.js 中 require 函数的原理深度解析

引言

在 Node.js 开发中,require 函数是我们每天都会使用的基础功能之一,它让我们能够轻松地模块化代码并引入各种功能。但你是否曾好奇过这个看似简单的函数背后是如何工作的?本文将深入探讨 Node.js 中 require 函数的实现原理。

一、模块系统概述

Node.js 采用 CommonJS 模块规范,这与浏览器端的 ES Modules 有着显著的不同。CommonJS 模块系统的核心特点包括:

  • 同步加载
  • 适用于服务器端
  • 每个文件都是一个独立的模块
  • 模块加载是运行时发生的

二、require 的基本工作流程

当你在代码中调用 require('./moduleA') 时,Node.js 会执行以下步骤:

  1. 路径解析:将相对路径转换为绝对路径
  2. 缓存检查:检查模块是否已被缓存
  3. 文件加载:如果未缓存,则加载文件内容
  4. 模块编译:将文件内容编译为可执行代码
  5. 缓存模块:将编译后的模块加入缓存
  6. 返回导出:返回模块的 exports 对象

三、深入 require 的各个阶段

1. 路径解析

Node.js 的模块分为三类:

  • 核心模块:如 fs、http 等,直接使用名称引入
  • 文件模块:通过相对路径(./)或绝对路径(/)引入
  • 第三方模块:通过 node_modules 引入

解析顺序遵循以下规则:

require('moduleA') // 核心模块 → node_modules
require('./moduleA') // 文件模块
require('/absolute/path/moduleA') // 绝对路径文件模块

2. 缓存机制

Node.js 通过 Module._cache 对象缓存已加载的模块,这可以避免重复加载和循环依赖带来的问题。

// 伪代码展示缓存机制
const cachedModule = Module._cache[filename];
if (cachedModule) {
  return cachedModule.exports;
}

3. 文件加载

根据文件扩展名,Node.js 采用不同的加载策略:

  • .js:作为 JavaScript 文件编译
  • .json:作为 JSON 文件解析
  • .node:作为编译的插件模块加载

4. 模块编译

这是最有趣的部分。Node.js 实际上会将模块代码包装在一个函数中:

(function(exports, require, module, __filename, __dirname) {
  // 你的模块代码在这里
});

这种包装实现了:

  • 模块作用域的隔离
  • 注入模块系统相关变量
  • 保持全局命名空间的干净

四、循环依赖的处理

Node.js 如何处理循环依赖是一个常见的面试题。关键在于理解模块加载的阶段性:

// a.js
exports.loaded = false;
const b = require('./b');
console.log('在 a 中,b.loaded =', b.loaded);
exports.loaded = true;

// b.js
exports.loaded = false;
const a = require('./a');
console.log('在 b 中,a.loaded =', a.loaded);
exports.loaded = true;

运行 node a.js 时,输出结果如下:

在 b 中,a.loaded = false
在 a 中,b.loaded = true
详细执行过程解析:
  1. 开始执行 a.js:

    • exports.loaded = false (a 模块的 loaded 设为 false)
    • 遇到 require('./b'),开始加载 b.js
  2. 开始执行 b.js:

    • exports.loaded = false (b 模块的 loaded 设为 false)
    • 遇到 require('./a'),尝试加载 a.js
      • 此时 a.js 已经开始加载但尚未完成
      • Node.js 会返回 a.js 当前的部分导出对象(此时 loaded 为 false)
    • 输出 在 b 中,a.loaded = false (此时 a.js 还未执行完,loaded 仍是 false)
    • exports.loaded = true (b 模块的 loaded 设为 true)
    • b.js 执行完成,返回 b 模块的 exports 对象
  3. 回到 a.js 继续执行:

    • 现在拿到了完整的 b 模块 exports 对象(loaded 为 true)
    • 输出 在 a 中,b.loaded = true
    • exports.loaded = true (a 模块的 loaded 设为 true)
    • a.js 执行完成
关键点说明:
  1. 模块加载是同步且阶段性的:当遇到 require 时会暂停当前模块执行,先加载被引用的模块。

  2. 循环依赖处理:Node.js 通过以下方式处理循环依赖:

    • 在模块完全加载前就将其放入缓存
    • 返回部分完成的模块导出对象
  3. 状态冻结:在 b.js 中获取的 a 模块状态是 require 时刻的状态,后续 a.js 的修改不会影响 b.js 中已经获取的值。

这个例子很好地展示了 Node.js 模块系统如何处理循环依赖,以及模块加载的顺序如何影响程序行为。

五、require 的内部实现

让我们看一下简化版的 require 实现:

function require(path) {
  // 1. 解析路径为绝对路径
  const filename = Module._resolveFilename(path);
  
  // 2. 检查缓存
  if (Module._cache[filename]) {
    return Module._cache[filename].exports;
  }
  
  // 3. 创建新模块实例
  const module = new Module(filename);
  
  // 4. 加载前缓存 (处理循环依赖)
  Module._cache[filename] = module;
  
  // 5. 尝试加载模块
  try {
    module.load(filename);
  } catch (err) {
    delete Module._cache[filename];
    throw err;
  }
  
  // 6. 返回 exports 对象
  return module.exports;
}

六、模块查找算法

当 require 一个非核心模块且不是相对路径时,Node.js 会按照以下顺序查找:

  1. 当前目录下的 node_modules
  2. 父目录下的 node_modules
  3. 一直向上直到根目录的 node_modules
  4. 环境变量 NODE_PATH 指定的目录

七、性能优化建议

了解 require 的原理后,我们可以得出一些性能优化建议:

  1. 合理组织模块结构,减少查找时间
  2. 对于频繁使用的模块,可以考虑提前 require
  3. 避免过深的依赖层级
  4. 合理使用缓存机制

八、ES Modules 与 CommonJS 的差异

随着 ES Modules 的引入,了解两者的区别变得重要:

  1. ES Modules 是静态的,CommonJS 是动态的
  2. ES Modules 支持顶层 await,CommonJS 不支持
  3. ES Modules 的 import 是只读视图,CommonJS 的 require 是值拷贝
  4. ES Modules 的 this 是 undefined,CommonJS 的 this 是当前模块

结语

require 函数看似简单,但背后隐藏着 Node.js 模块系统的精妙设计。理解这些原理不仅能帮助我们更好地组织代码,还能在遇到模块相关问题时快速定位原因。随着 Node.js 的发展,模块系统也在不断演进,但 CommonJS 的 require 仍将是 Node.js 生态中的重要组成部分。

希望本文能帮助你更深入地理解 Node.js 的模块系统。下次当你使用 require 时,或许会对这个小小的函数产生新的认识。

【 设计模式】常见前端设计模式

什么是设计模式?

设计模式是软件开发中常见问题的通用解决方案。

打个比方,设计模式就像做饭的“菜谱”,它不是具体的菜,而是一种做菜的方法,

能让你更快、更好地完成任务。


就像全家只有一台电视机,不管谁用,都是同一台。

在代码里,这就是——单例模式。

1、单例模式

📌 场景:全局状态管理(Vuex、Pinia)、缓存管理

💡 单例模式的作用:确保某个类只有一个实例,并提供一个访问它的全局入口。

✅ 实际案例:Vuex / Pinia


class Store {
  constructor() {
    if (!Store.instance) {
      this.state = {};
      Store.instance = this;
    }
    return Store.instance;
  }
}

const store1 = new Store();
const store2 = new Store();

console.log(store1 === store2); // ✅ true,保证全局唯一

“Vuex/Pinia 本质上就是一个单例模式的应用,保证全局状态只有一个实例,避免数据不一致。”(也就是数据应该只有一个“唯一的来源”)


不过,光有唯一的数据来源还不够,

我们还需要一种机制:当数据发生变化时,相关地方能自动收到通知并更新界面。

——这就是 观察者模式。


2、观察者模式

📌 场景:Event Bus(Vue 事件总线)、Vue 2 响应式原理

💡 观察者模式的作用:一对多的依赖管理,当一个对象(发布者)发生变化时,所有监听它的对象(订阅者)都会收到通知。

✅ 实际案例:Event Bus(Vue 事件总线)

在 Vue 2 里,我们有时需要让没有直接关系的组件进行通信(比如兄弟组件),

这时可以用 Event Bus 作为一个“中间人”,订阅事件(on) 和 触发事件(emit)。

// EventBus.js
class EventBus {
  constructor() {
    this.events = {};
  }
  on(event, callback) {
    this.events[event] = this.events[event] || [];
    this.events[event].push(callback);
  }
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }
}

const bus = new EventBus();
export default bus;

// A:发送事件
<template>
  <button @click="sendMessage">发送消息</button>
</template>

<script>
import bus from "@/utils/EventBus";

export default {
  methods: {
    sendMessage() {
      bus.emit("hello", "来自 A.vue 的问候 👋");
    }
  }
};
</script>


// B: 监听接收
<template>
  <div>消息:{{ message }}</div>
</template>

<script>
import bus from "@/utils/EventBus";

export default {
  data() {
    return {
      message: ""
    };
  },
  created() {
    bus.on("hello", this.receiveMessage);
  },
  beforeDestroy() {
    bus.off("hello", this.receiveMessage); // 卸载时解绑
  },
  methods: {
    receiveMessage(data) {
      this.message = data;
    }
  }
};
</script>


补充:

Event Bus: 本质是一个 Vue 实例,利用 Vue 内部的 事件机制(emit/emit / on) 来实现跨组件通信,符合观察者模式, 因为多个组件可以订阅同一个事件。

Vue 3 :移除了 Vue 实例上的 onon 和 emit,所以不能直接用 new Vue() 作为 Event Bus。


但除了通信,开发中还有一个常见问题:

我们经常需要“根据不同条件,去创建不同的对象或组件”。

每天早上想喝豆浆或咖啡,如果每次都要自己动手磨豆、煮、泡,那就太麻烦了。

工厂模式就像一个智能饮料机:把整个制作流程都封装起来,

你只需要告诉它“我要豆浆”或者“我要咖啡”,机器就帮你完成剩下的所有步骤。

在代码里,这就是——工厂模式:封装对象创建逻辑,只管需求,不用管具体怎么造。


3、工厂模式

📌 场景:封装 API 请求、组件动态创建、创建 Vue 组件实例。

💡 工厂模式的作用:提供一个通用的接口,根据不同条件创建不同的实例,而不是直接 new。

✅ 实际案例:封装 API 请求


class AxiosFactory {
  static createInstance(baseURL) {
    return axios.create({
      baseURL,
      timeout: 5000, // 统一设置超时时间
    });
  }
}

// 这样就可以随时创建不同 `baseURL` 的实例
const apiClient1 = AxiosFactory.createInstance("https://api.example.com");
const apiClient2 = AxiosFactory.createInstance("https://api.another.com");

// 直接调用,不用关心 axios 实例的创建细节
apiClient1.get("/users").then(console.log);
apiClient2.get("/products").then(console.log);

使用工厂模式封装 axios,可以根据不同的 baseURL 轻松创建不同的请求实例。

补充static:

static: 定义“类本身的方法”,而不是实例的方法。

class Person {
  static sayHi() {
    console.log("Hi,我是类本身的方法!");
  }

  sayHello() {
    console.log("Hello,我是实例的方法!");
  }
}

// 调用方式
Person.sayHi();       // ✅ 不用 new,直接调用
const p = new Person();
p.sayHello();         // ✅ 要先 new 出一个对象,才能调用

工厂模式帮我们造出了对象,但直接用对象,有时候不方便或不安全。

买了一个智能家电:

  • 不能直接自己去拆电路板操作;

  • 需要遥控器或 App 来控制它;

  • 遥控器就帮你拦截、检查、处理指令,你只用它就能安全操作家电。

👉 在代码里,这个“遥控器”就是 代理(Proxy),它控制了对真实对象的访问。


4、代理模式

📌 场景:Vue 3 响应式、图片懒加载、权限管理

💡 代理模式的作用:创建一个“代理”对象,来控制对目标对象的访问。

✅ 实际案例:Vue 3 响应式

 const data = { name: "Alice", age: 25 };
 
 const reactiveData = new Proxy(data, {
   get(target, prop) {
     console.log(`获取属性: ${prop}`);
     return target[prop];
   },
   set(target, prop, value) {
     console.log(`修改属性: ${prop} = ${value}`);
     target[prop] = value;
     return true;
   }
 });
 
 console.log(1, reactiveData.name);  // ✅ 输出:获取属性: name
 reactiveData.age = 30; // ✅ 输出:修改属性: age = 30

Proxy 创建了一个“代理对象” reactiveData, 它能监听对象的 get 和 set 操作,实现响应式更新。


有了代理模式,对象的访问安全了,但有时候同一个动作可能有多种做法。

像控制智能家电:

  • 想开灯,你可以用遥控器开,也可以用手机 App,甚至语音控制;

  • 不同情况选择不同操作方式,而不用每次都写一大堆 if-else。

在代码里,这种“根据情况切换操作方法”的思路,就是 策略模式。


5、策略模式

📌 场景:表单校验、算法切换、权限控制

💡 策略模式的作用:把不同的逻辑(策略)封装成独立模块,然后根据不同情况动态切换,而不是 if-else 一大堆。

✅ 实际案例:表单校验

const strategies = {
  isNotEmpty: value => value.trim() !== "" || "不能为空",
  isEmail: value => /\S+@\S+\.\S+/.test(value) || "邮箱格式不正确",
  minLength: (value, length) => value.length >= length || `至少 ${length} 个字符`
};

function validate(value, type, param) {
 return strategies[type](value, param);
}

console.log(validate("", "isNotEmpty")); //  "不能为空"
console.log(validate("test@example.com", "isEmail")); //  true
console.log(validate("123", "minLength", 5)); // "至少 5 个字符"

在表单校验中,使用策略模式,把不同的校验规则封装到对象里,

避免了 if-else 过多导致的代码混乱。


😉 记录学习,分享经验,持续成长


p5.js 3D 形状 "预制工厂"——buildGeometry ()

点赞 + 关注 + 收藏 = 学会了

如果你已经会用box()sphere()画简单 3D 形状,想组合它们做出复杂模型,又担心画面卡顿,那么buildGeometry()就是你的 "性能救星"。这个函数能把多个简单形状 "焊接" 成一个自定义 3D 模型,让绘制效率飙升。

什么是 buildGeometry ()?

buildGeometry()是 p5.js 中用于组装复杂 3D 模型的工具函数。它的核心作用就像 "预制构件厂":

  • 把多个简单 3D 形状(比如box()sphere())组合成一个完整的p5.Geometry对象(可以理解为 "自定义 3D 零件");
  • 这个 "零件" 只需要在程序启动时制作一次,之后每次绘制直接调用即可,大幅减少重复计算;
  • 必须在WebGL 模式下使用(和所有 3D 函数一样)。

buildGeometry()就是来解决这个问题的:它能把多个简单 3D 形状 “打包” 成一个p5.Geometry对象,只需创建一次,之后反复绘制都不会卡顿。就像快递打包,把多个小包裹捆成一个大包裹,搬运起来更高效~

基础用法

buildGeometry打包一个球体,然后绘制它。

01.png

let myShape; // 存储打包好的3D对象

function setup() {
  // 开启WebGL模式(3D绘图必备)
  createCanvas(400, 400, WEBGL);
  // 用buildGeometry创建3D对象,回调函数是makeShape
  myShape = buildGeometry(makeShape);
}

function draw() {
  background(200); // 灰色背景
  orbitControl(); // 允许鼠标拖动旋转视角
  lights(); // 添加光照(3D物体需要光照才看得见)
  model(myShape); // 绘制打包好的3D对象
}

// 回调函数:定义要打包的形状
function makeShape() {
  sphere(50); // 画一个半径50的球体
}

旋转的几何花朵

buildGeometry组合多个锥体,形成一朵 “花”,然后让它随时间旋转并变色,展示高性能复杂 3D 动画的实现。

02.gif

let flower;
let hueValue = 0; // 色相值(用于颜色变化)

function setup() {
  createCanvas(600, 600, WEBGL);
  // 创建几何花朵
  flower = buildGeometry(makeFlower);
}

function draw() {
  background(0); // 黑色背景
  orbitControl(); // 允许鼠标旋转视角
  lights(); // 光照
  
  // 颜色随时间变化(HSB模式:色相、饱和度、亮度)
  colorMode(HSB);
  fill(hueValue % 360, 80, 90);
  hueValue += 0.5;
  
  // 整体旋转(X和Y轴同时转,更有动感)
  // rotateX(frameCount * 0.005);
  rotateY(frameCount * 0.008);
  
  model(flower); // 绘制花朵
}

// 构建花朵形状的回调函数
function makeFlower() {
  // 中心球体
  sphere(15);
  
  // 周围的“花瓣”:12个锥体
  for (let i = 0; i < 12; i++) {
    push();
    // 绕Y轴均匀分布(360度/12=30度一个)
    rotateY(i * PI / 6);
    // 沿Z轴向外移动
    translate(0, 0, 40);
    // 锥体:底面半径10,高30,朝上
    cone(10, 30);
    pop();
  }
  
}

以上就是本文的全部内容啦,想了解更多 P5.js 用法欢迎关注 《P5.js中文教程》

也可以➕我 green bubble 吹吹水咯

qrcode.jpeg

点赞 + 关注 + 收藏 = 学会了

158.gif

HiCharts??与Echarts一样的图表库??

HiCharts也是一款可视化图表库,适用于Javascript, Angular, React, VueJS, ios, R, NET,但是如果需要商用需要额外付费

对比点 Echarts Hicharts
性能 大数据量性能较好 中等数据量性能优秀
文档支持 中文文档完善 英文文档为主
特点 功能丰富,适合复杂可视化需求 功能完善,偏向传统商业图表

中文文档:highcharts.com.cn/

英文文档:www.highcharts.com/

一、入门学习

是好是坏,接下来代码里见真章,开码!!!

安装HiCharts依赖

pnpm install highcharts --save

HiCharts的使用起来与Echarts差不多,也是定义元素、配置项,然后去渲染图表,只是API使用不同,接下来我们定义简单的折线图渲染看看效果。

  1. 定义渲染元素:注意设置宽高
<div ref="chartRef" class="chart"></div>

2. 定义图表配置项

  // 初始化图表配置
  const options = {
    title: {
      text: '手机品牌月度销量',
      align: 'left'
    },
    subtitle: {
      text: '市场调研',
      align: 'left'
    },
    yAxis: {
      title: {
        text: '销量 (百万台)'
      }
    },
    xAxis: {
      categories: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
    },
    legend: {
      enabled: true, // 显示图例
      align: 'right',
      verticalAlign: 'top'
    },
    tooltip: {
      valueSuffix: ' 百万台'
    },
    plotOptions: {
      series: {
        marker: {
          enabled: true // 显示数据点
        }
      }
    },
    series: [
      {
        name: '小米',
        data: [4.2, 3.8, 4.5, 5.1, 5.7, 6.3, 6.8, 7.2, 7.5, 8.1, 8.6, 9.2]
      },
      {
        name: '华为',
        data: [8.5, 7.8, 9.1, 6.2, 5.4, 10.9, 12.2, 11.8, 10.5, 8.3, 9.2, 11.8]
      }
    ]
  }
  // 初始化图表
  Highcharts.chart(chartRef.value, options);
}

3. 页面初始化完成后渲染图表

onMounted(() => {
    initCharts();
})

HiCharts渲染效果如下图,为了方便作对比,笔者使用Echarts实现了同款图表,大家觉得哪个更好?

Echarts实现同款图表效果

在上述的例子中,在图表的右下角有着HiCharts的Logo,只需要加入如下配置即可消除

credits: {
  enabled: false
},

二、常见图表

(一) 柱状图(column)

基本结构与折线图类似,需要修改图表类型为column

{
  chart: {
    // 定义图表类型
    type: 'column'
  },
  title: {
    text: '手机品牌月度销量',
        align: 'center'
  },
  subtitle: {
    text: '市场调研',
        align: 'center'
  },
  xAxis: {
    categories: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  },
  yAxis: {
    min: 0,
        title: {
      text: '销量 (百万台)'
    }
  },
  tooltip: {
    valueSuffix: ' 百万台'
  },
  plotOptions: {
    column: {
      pointPadding: 0.2,
          borderWidth: 0
    }
  },
  series: [
    {
      name: '小米',
      data: [4.2, 3.8, 4.5, 5.1, 5.7, 6.3, 6.8, 7.2, 7.5, 8.1, 8.6, 9.2]
    },
    {
      name: '华为',
      data: [8.5, 7.8, 9.1, 6.2, 5.4, 10.9, 12.2, 11.8, 10.5, 8.3, 9.2, 11.8]
    }
  ]
}

(二) 饼图(Pie)

const options = {
  chart: {
    type: 'pie',
    zooming: {
      type: 'xy'
    },
    panning: {
      enabled: true,
      type: 'xy'
    },
    panKey: 'shift'
  },
  title: {
    text: '手机品牌月度销量占比',
    align: 'center'
  },
  subtitle: {
    text: '市场调研',
    align: 'center'
  },
  tooltip: {
    valueSuffix: '%'
  },
  series: [
    {
      name: '手机品牌',
      colorByPoint: true,
      data: [
        {
          name: '小米',
          y: 55.02
        },
        {
          name: '华为',
          y: 26.71
        },
        {
          name: 'OPPO',
          y: 16.71
        },
        {
          name: '魅族',
          y: 16.71
        },
      ]
    }
  ]
}

(三) 面积图(area)

const options = {
  chart: {
    type: 'area'
  },
  title: {
    text: '手机品牌月度销量'
  },
  subtitle: {
    text: '市场调研'
  },
  xAxis: {
    categories: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  },
  yAxis: {
    title: {
      text: '销量 (百万台)'
    }
  },
  credits: {
    enabled: false
  },
  legend: {
    enabled: true, // 显示图例
    align: 'right',
    verticalAlign: 'top'
  },
  tooltip: {
    valueSuffix: ' 百万台'
  },
  plotOptions: {
    area: {
      pointStart: 1940,
      marker: {
        enabled: false,
        symbol: 'circle',
        radius: 2,
        states: {
          hover: {
            enabled: true
          }
        }
      }
    }
  },
  series: [
    {
      name: '小米',
      data: [4.2, 3.8, 4.5, 5.1, 5.7, 6.3, 6.8, 7.2, 7.5, 8.1, 8.6, 9.2]
    },
    {
      name: '华为',
      data: [8.5, 7.8, 9.1, 6.2, 5.4, 10.9, 12.2, 11.8, 10.5, 8.3, 9.2, 11.8]
    }
  ]
};

(四) 组合图

const options = {
  chart: {
    zooming: {
      type: 'x'
    }
  },
  credits: {
    enabled: false
  },
  title: {
    text: '手机品牌月度销量与市场份额'
  },
  xAxis: {
    categories: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  },
  yAxis: [
    {
      labels: {
        format: '{value}%'  // 左侧Y轴:市场份额百分比
      },
      title: {
        text: '市场份额'
      },
      lineColor: Highcharts.getOptions().colors[1],
      lineWidth: 2
    },
    {
      labels: {
        format: '{value} 百万台'  // 右侧Y轴:销量
      },
      title: {
        text: '销量'
      },
      lineColor: Highcharts.getOptions().colors[0],
      lineWidth: 2,
      opposite: true
    }
  ],
  tooltip: {
    shared: true
  },
  legend: {
    align: 'left',
    verticalAlign: 'top'
  },
  series: [
    {
      name: '华为',
      type: 'column',
      yAxis: 1,
      data: [12.5, 15.2, 18.7, 22.3, 25.1, 28.6, 30.2, 32.8, 29.4, 26.7, 20.5, 17.3],
      tooltip: {
        valueSuffix: ' 百万台'
      }
    },
    {
      name: '市场份额',
      type: 'spline',
      data: [18, 20, 22, 25, 27, 30, 32, 35, 33, 30, 25, 22],
      tooltip: {
        valueSuffix: '%'
      }
    }
  ]
};

三、中文配置

HiCharts默认使用的英文,配置中文的话,需要对应js文件

const protocol = window.location.protocol;
const defaultOptionsZhCn = {
  lang: {
    contextButtonTitle: "图表导出菜单",
    decimalPoint: ".",
    downloadJPEG: "下载JPEG图片",
    downloadPDF: "下载PDF文件",
    downloadPNG: "下载PNG文件",
    downloadSVG: "下载SVG文件",
    drillUpText: "返回 {series.name}",
    invalidDate: "无效的时间",
    loading: "加载中...",
    months: [
      "一月",
      "二月",
      "三月",
      "四月",
      "五月",
      "六月",
      "七月",
      "八月",
      "九月",
      "十月",
      "十一月",
      "十二月"
    ],
    noData: "没有数据",
    numericSymbols: null,
    printChart: "打印图表",
    resetZoom: "重置缩放比例",
    resetZoomTitle: "重置为原始大小",
    shortMonths: [
      "一月",
      "二月",
      "三月",
      "四月",
      "五月",
      "六月",
      "七月",
      "八月",
      "九月",
      "十月",
      "十一月",
      "十二月"
    ],
    thousandsSep: ",",
    weekdays: [
      "星期天",
      "星期一",
      "星期二",
      "星期三",
      "星期四",
      "星期五",
      "星期六"
    ],
    rangeSelectorFrom: "开始时间",
    rangeSelectorTo: "结束时间",
    rangeSelectorZoom: "范围",
    zoomIn: "缩小",
    zoomOut: "放大"
  },
  global: {
    canvasToolsURL:
      protocol + "//cdn.hcharts.cn/highcharts/modules/canvas-tools.js",
    VMLRadialGradientURL:
      protocol + +"//cdn.hcharts.cn/highcharts/gfx/vml-radial-gradient.png"
  },
  title: { text: "图表标题" },
  tooltip: {
    dateTimeLabelFormats: {
      millisecond: "%H:%M:%S.%L",
      second: "%H:%M:%S",
      minute: "%H:%M",
      hour: "%H:%M",
      day: "%Y-%m-%d",
      week: "%Y-%m-%d",
      month: "%Y-%m",
      year: "%Y"
    },
    split: false
  },
  exporting: { url: protocol + "//export.highcharts.com.cn" },
  credits: {
    text: "Highcharts.com.cn",
    href: "https://www.highcharts.com.cn"
  },
  xAxis: {
    dateTimeLabelFormats: {
      millisecond: "%H:%M:%S.%L",
      second: "%H:%M:%S",
      minute: "%H:%M",
      hour: "%H:%M",
      day: "%Y-%m-%d",
      week: "%Y-%m",
      month: "%Y-%m",
      year: "%Y"
    }
  },
  rangeSelector: {
    inputDateFormat: "%Y-%m-%d",
    buttonTheme: {
      width: "auto",
      style: { fontSize: "12px", padding: "4px" }
    },
    buttons: [
      { type: "month", count: 1, text: "月" },
      { type: "month", count: 3, text: "季度" },
      { type: "month", count: 6, text: "半年" },
      { type: "ytd", text: "YTD" },
      { type: "year", count: 1, text: "年" },
      { type: "all", text: "所有" }
    ]
  },
  plotOptions: {
    series: {
      dataGrouping: {
        dateTimeLabelFormats: {
          millisecond: [
            "%Y-%m-%d %H:%M:%S.%L",
            "%Y-%m-%d %H:%M:%S.%L",
            " ~ %H:%M:%S.%L"
          ],
          second: ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S", " ~ %H:%M:%S"],
          minute: ["%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M", " ~ %H:%M"],
          hour: ["%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M", " ~ %H:%M"],
          day: ["%Y-%m-%d", "%Y-%m-%d", " ~ %Y-%m-%d"],
          week: ["%Y-%m-%d", "%Y-%m-%d", " ~ %Y-%m-%d"],
          month: ["%Y-%m", "%Y-%m", " ~ %Y-%m"],
          year: ["%Y", "%Y", " ~ %Y"]
        }
      }
    },
    ohlc: {
      tooltip: {
        split: false,
        pointFormat:
          '<span style="color:{point.color}">●</span> <b> {series.name}</b><br/>' +
          "开盘:{point.open}<br/>" +
          "最高:{point.high}<br/>" +
          "最低:{point.low}<br/>" +
          "收盘:{point.close}<br/>"
      }
    },
    candlestick: {
      tooltip: {
        split: false,
        pointFormat:
          '<span style="color:{point.color}">●</span> <b> {series.name}</b><br/>' +
          "开盘:{point.open}<br/>" +
          "最高:{point.high}<br/>" +
          "最低:{point.low}<br/>" +
          "收盘:{point.close}<br/>"
      }
    }
  }
};

export default defaultOptionsZhCn;
import defaultOptionsZhCn from "../../assets/highcharts-zh_CN.js"
    
// 应用中文配置
Highcharts.setOptions(defaultOptionsZhCn);

四、结语

HiCharts的用法与Echarts使用差不多,只是些许区别,不过商用需要付费,而Echarts不需要,那这个HiCharts,但是存在即合理,下一章让我们一起学习一下它的进阶用法!!

手把手教你入门 MCP:模型上下文协议与 Trae IDE 中的实践

✨点击上方关注☝️,追踪不迷路!

一、什么是 MCP?

MCP(Model Context Protocol)是一种开放协议,它标准化了应用程序如何向大型语言模型(LLMs)提供上下文。可以将 MCP 想象成 AI 应用程序的 USB-C 接口——正如 USB-C 提供了标准化的方式来连接设备与各种外设和配件一样,MCP 提供了标准化的方式来连接 AI 模型与不同的数据源和工具。

MCP 使你能够在 LLM 之上构建代理和复杂工作流,并将你的模型与外部世界连接起来。它提供了一套标准规则,让 AI 模型能够理解如何获取上下文信息、如何与外部工具交互,以及如何处理和利用返回的结果。

二、为什么要学习 MCP?

MCP 为 AI 生态系统带来了许多重要价值:

  1. 丰富的预构建集成:MCP 提供了一系列预构建的集成,你的 LLM 可以直接接入使用
  2. 标准化的自定义集成方式:为 AI 应用程序提供了标准化的方式来构建自定义集成
  3. 开放的协议:每个人都可以自由实现和使用 MCP 协议
  4. 灵活性:允许在不同应用之间切换并携带你的上下文

对于开发者和 AI 用户来说,学习 MCP 意味着你可以充分利用 AI 模型与外部工具的协同能力,构建更加强大和实用的 AI 应用。

三、MCP 的基本工作原理

MCP 协议的核心工作原理是建立 AI 模型与外部数据源、工具之间的标准化通信桥梁。以下是 MCP 的基本工作流程:

image.png

  1. 上下文获取:MCP 服务从各种数据源收集上下文信息
  2. 标准化处理:将上下文信息转换为标准格式
  3. 模型交互:将标准化的上下文传递给 AI 模型
  4. 工具调用:根据模型需求,通过 MCP 协议调用外部工具
  5. 结果返回:将工具执行结果格式化后返回给模型

在 Trae IDE 这样的开发环境中,AI 助手正是通过 MCP 协议来调用代码搜索、文件编辑、命令执行等各种功能,从而更好地理解代码库上下文,为开发者提供更精准的帮助。

四、如何在 Trae IDE 中配置 Gitee MCP 服务

Gitee 提供了 MCP 服务,允许 Trae IDE 通过 MCP 协议与 Gitee 平台进行交互。下面是在 Trae 中配置 Gitee MCP 服务的步骤:

1. 获取 Gitee 访问令牌

首先,你需要在 Gitee 上创建一个访问令牌,用于 Trae IDE 与 Gitee MCP 服务之间的身份验证:

  1. 登录你的 Gitee 账号
  2. 进入「设置」→「安全设置」→「私人令牌」
  3. 点击「生成新令牌」,选择需要的权限(如 repouser_info 等)
  4. 保存生成的令牌,这将用于后续配置

2. 在 Trae IDE 中配置 Gitee MCP 服务

在 Trae IDE 中,你可以通过以下 JSON 配置来连接 Gitee MCP 服务:

{
  "mcpServers": {
    "gitee": {
      "url": "https://api.gitee.com/mcp",
      "headers": {
        "Authorization": "Bearer <私人令牌>"
      }
    }
  }
}

其中,<私人令牌> 是你在第一步中获取的 Gitee 访问令牌。你可以将此配置添加到 Trae IDE 的 MCP 服务配置文件中。

3. 测试 Gitee MCP 服务连接

在 Trae IDE 中测试 Gitee MCP 服务连接的实际操作步骤如下:

  1. 打开 Trae IDE 的 MCP 服务配置面板
  2. 选择你刚刚配置的 Gitee MCP 服务
  3. 点击服务配置旁边的「开启连接」按钮
  4. Trae IDE 会自动向 Gitee MCP 服务发送一个简单的请求来验证连接是否正常
  5. 观察测试结果:如果连接成功,你会看到服务会有一个绿色勾,并可能显示一些基本的用户信息摘要;如果连接失败,会有红色感叹号,点击会显示具体的错误信息

通过这种方式,你可以确认 Gitee MCP 服务配置是否正确,以及能否正常与 Gitee 平台通信。

4. 开始使用

在trae对话中输入@切换到对应的builder with MCP 智能体,并输入“帮我查询我的 gitee 有多少个仓库?” 如下图

iShot_2025-08-17_12.28.41.png

五、在 Trae IDE 中配置 MCP 服务

在 Trae IDE 中,你可以通过两种方式添加和配置 MCP 服务:从市场添加或手动添加。下面将详细介绍这两种配置方法。

5.1 从市场添加 MCP 服务

Trae IDE 提供了 MCP 市场,其中包含了社区中热门的 MCP 服务,你可以直接从中选择并添加:

操作步骤:

  1. 在 AI 侧边对话框的右上角,点击「设置」图标
  2. 选择「MCP」页签
  3. 点击「+ 添加 MCP Servers」按钮;或点击右上角的「+ 添加」按钮,然后在菜单中选择「从市场添加」
  4. 在打开的 MCP 市场中,浏览并找到你需要的 MCP 服务
  5. 点击该服务右侧的「+」按钮
  6. 在弹出的配置窗口中,填写必要的配置信息

注意事项:

  • 对于标记为「Local」的 MCP 服务,需要在本地安装 NPX 或 UVX 后才能使用
  • 配置内容中的环境变量信息(如 API Key、Token、Access Key 等字段)须替换为你的真实信息
  • 填写完成后,点击「确认」按钮保存配置

5.2 手动添加 MCP 服务

如果你需要添加的 MCP 服务不在市场中,或者你想使用自己开发的 MCP 服务,可以通过手动添加的方式:

操作步骤:

  1. 在 AI 侧边对话框的右上角,点击「设置」图标,然后在菜单中选择「MCP」
  2. 进入 MCP 配置界面后,点击右上角的「+ 添加」按钮,然后在菜单中选择「手动添加」
  3. 在弹出的「手动配置」窗口中,填写 MCP 服务的详细配置

配置建议:

  • 优先使用 NPX 或 UVX 配置方式
  • 如果你希望添加一个全新的 MCP 服务,可以参考 MCP 官方文档中的协议规范
  • 配置完成后,点击「确认」按钮保存设置

5.3 配置系统环境

为确保 MCP 服务能够正常启动和运行,你可能需要安装以下工具:

5.3.1 安装 Node.js

NPX 工具依赖于 Node.js 环境(版本需大于等于 18):

  1. 前往 Node.js 官网,下载并安装 Node.js 18 或更高版本
  2. 安装完成后,在终端中运行以下命令确认安装成功:
    node -v
    npx -v
    
  3. 若安装成功,终端会输出相应的版本号
  4. 重启 Trae IDE,使 Node.js 环境生效

5.3.2 安装 uvx(Python 工具)

uvx 是基于 Python 的快速执行工具,用于运行 Python 脚本的 MCP 服务:

  1. 前往 Python 官网,下载并安装 Python 3.8 或更高版本
  2. 安装完成后,在终端中运行以下命令确认安装成功:
    python --version
    
  3. 执行以下命令安装 uv(包含 uvx):
    • macOS / Linux 系统:
      curl -LsSf https://astral.sh/uv/install.sh | sh
      
    • Windows 系统(PowerShell):
      powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
      
  4. 安装完成后,运行以下命令验证:
    uvx --version
    

5.3.3 安装 Docker(可选)

如果你需要使用 GitHub MCP Server,则需要安装 Docker:

macOS 系统:

  • 手动安装:前往 Docker 官方网站,下载适用于 macOS 的 Docker Desktop 安装包(.dmg 文件)
  • 使用 Homebrew 安装:brew install --cask docker

Windows 系统:

  • 前往 Docker 官方网站,下载并安装 Docker Desktop,确保勾选「Use the WSL 2 based engine」选项

验证安装:

 docker --version
docker info

5.4 MCP 服务传输类型

在 Trae IDE 中,MCP Server 支持三种传输类型:

  1. stdio 传输:通过标准输入/输出进行通信,适用于本地运行的 MCP 服务
  2. SSE 传输:Server-Sent Events 传输,支持服务端向客户端推送事件
  3. Streamable HTTP 传输:支持流式 HTTP 通信的传输方式

根据你选择的 MCP 服务类型,Trae IDE 会自动选择合适的传输方式。

六、MCP 开发资源

如果你想深入了解 MCP 协议或开发自己的 MCP 服务,可以参考以下资源:

  1. 官方 SDK:MCP 提供了多种语言的官方 SDK,包括 Python、JavaScript、Java 等,这些 SDK 处理了协议细节,让你可以专注于构建功能
  2. 协议规范文档:详细说明了 MCP 协议的请求和响应格式、数据类型等技术细节
  3. 示例代码库:包含了各种 MCP 服务和客户端的示例实现
  4. 社区论坛:可以与其他开发者交流 MCP 开发经验和问题

七、总结

MCP(模型上下文协议)是连接 AI 模型与外部世界的重要桥梁,它通过标准化的方式,让 AI 模型能够获取上下文信息并调用各种外部工具。在 Trae IDE 中,AI 助手正是通过 MCP 协议来提供代码搜索、文件编辑、命令执行等强大功能。

通过本文的介绍,你应该已经了解了 MCP 的基本概念、工作原理以及在 Trae IDE 中配置和使用 Gitee MCP 服务的方法。随着 AI 技术的不断发展,MCP 协议的重要性将会越来越凸显,它将为 AI 应用提供更广阔的可能性。

如果你想进一步学习 MCP,可以访问官方文档或加入 MCP 社区,与其他开发者一起探索 MCP 的更多可能性。


延伸阅读:

最后,创作不易请允许我插播一则自己开发的小程序广告,感兴趣可以访问体验:

【「合图图」产品介绍】

  • 主要功能为:本地添加相册图片进行无限长图高清拼接,各种布局拼接等

  • 安全:无后台服务无需登录,全程设备本地运行,隐私100%安全;

  • 高效:自由布局+实时预览,效果所见即所得;

  • 高清:秒生高清拼图,一键保存相册。

  • 立即体验 →合图图 或微信小程序搜索「合图图」

如果觉得本文有用,欢迎点个赞👍和收藏⭐支持我吧!

第一章: Mac Flutter Engine开发准备工作

快速开始 - 操作流程总览

必要条件

  • macOS 11+ 系统
  • 至少 100GB 可用磁盘空间
  • 16GB+ 内存(推荐32GB)
  • 稳定的网络连接

第一步:安装基础工具

# 1. 安装 Xcode Command Line Tools
xcode-select --install

# 2. 安装 Homebrew(如果没有)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 3. 安装基础依赖
brew install git python3

第二步:安装 depot_tools 到根目录

# 1. 克隆到根目录
sudo git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /depot_tools

# 2. 设置权限
sudo chown -R $(whoami):staff /depot_tools
sudo chmod -R 755 /depot_tools

# 3. 配置环境变量
echo 'export PATH="/depot_tools:$PATH"' >> ~/.zshrc
echo 'export DEPOT_TOOLS_UPDATE=0' >> ~/.zshrc
source ~/.zshrc

第三步:创建工作目录和配置文件

# 1. 创建 Flutter SDK 目录
mkdir -p ~/flutter_sdk
cd ~/flutter_sdk

# 2. 创建 .gclient 配置文件
cat > .gclient << 'EOF'
solutions = [
  {
    "managed": False,
    "name": ".",
    "url": "https://github.com/flutter/engine.git@3.29.0",
    "custom_deps": {},
    "deps_file": "DEPS",
    "safesync_url": "",
  },
]
EOF

第四步:同步 Flutter Engine 源码

# 同步源码(耗时 1-3 小时,约 30GB)
gclient sync --verbose

第五步:编译 Flutter Engine

# 1. 配置构建环境(以 Android Release 为例)
./flutter/tools/gn --android --android-cpu arm64 --runtime-mode release --optimized

# 2. 执行编译
ninja -C out/android_release_arm64 -j$(sysctl -n hw.ncpu)

验证安装

# 检查工具是否正确安装
which gclient gn ninja
ls -la ~/flutter_sdk/flutter/

# 检查编译产物
ls -la out/android_release_arm64/flutter.jar
ls -la out/android_release_arm64/libflutter.so

详细说明目录

1.1 开发环境要求与系统配置

硬件配置要求

推荐配置:

  • CPU: Apple M1 Pro/Max 或 Intel i7 8核以上
  • 内存: 32GB(最低16GB)
  • 存储: 1TB SSD,至少500GB可用空间
  • 网络: 能访问 GitHub 和 Google 服务

存储空间分配:

/depot_tools/               # 约 500MB
~/flutter_sdk/              # 源码约 30GB
~/flutter_sdk/out/          # 编译产物 10-50GB

macOS 系统要求

系统版本: macOS 11.0+ (推荐 macOS 12+)

必需工具安装:

# 安装 Xcode Command Line Tools
xcode-select --install

# 验证安装
xcode-select -p
# 输出: /Applications/Xcode.app/Contents/Developer

# 检查系统信息
sw_vers

1.2 depot_tools 详细安装配置

为什么安装到根目录?

  1. 统一路径: 避免不同用户的路径差异
  2. 权限管理: 便于团队开发时的权限控制
  3. 脚本兼容: 与现有构建脚本保持一致

详细安装步骤

# 第一步:克隆 depot_tools 到根目录
sudo git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /depot_tools

# 第二步:设置合适的权限
sudo chown -R $(whoami):staff /depot_tools
sudo chmod -R 755 /depot_tools

# 第三步:配置环境变量
# 编辑 shell 配置文件
nano ~/.zshrc  # macOS 默认使用 zsh

# 添加以下内容
export PATH="/depot_tools:$PATH"
export DEPOT_TOOLS_UPDATE=0

# 使配置生效
source ~/.zshrc

验证安装

# 检查工具路径
which gclient    # 应该输出: /depot_tools/gclient
which gn         # 应该输出: /depot_tools/gn
which ninja      # 应该输出: /depot_tools/ninja

# 检查版本
gclient --version
gn --version
ninja --version

# 测试功能
gclient help

1.3 Flutter Engine 源码获取详解

.gclient 配置文件解析

solutions = [
  {
    "managed": False,           # 不使用自动管理模式
    "name": ".",               # 存储到当前目录
    "url": "https://github.com/flutter/engine.git@3.29.0",  # 指定版本
    "custom_deps": {},         # 自定义依赖
    "deps_file": "DEPS",       # 依赖文件
    "safesync_url": "",        # 安全同步URL
  },
]

关键参数说明:

  • "name": ".": 直接存储到 ~/flutter_sdk/ 目录,不创建子目录
  • "@3.29.0": 指定 Flutter Engine 版本,与 Flutter SDK 3.29.0 对应
  • GitHub 仓库: 相比 Chromium 源,GitHub 访问更稳定

同步过程详解

# 执行同步命令
cd ~/flutter_sdk
gclient sync --verbose

# 同步过程包含:
# 1. 克隆主仓库 (flutter/engine)
# 2. 解析 DEPS 文件,获取依赖列表
# 3. 下载第三方依赖 (Skia, Dart VM, ICU 等)
# 4. 应用 Flutter 特定补丁
# 5. 运行预编译钩子脚本

同步完成后的目录结构:

~/flutter_sdk/
├── .gclient                 # gclient 配置文件
├── .gclient_entries        # 同步状态记录
├── build/                  # 构建系统文件
├── buildtools/             # 构建工具
├── flutter/                # Flutter Engine 核心源码
├── third_party/            # 第三方依赖
└── tools/                  # 辅助工具

1.4 编译脚本使用说明

compile_engine.sh 脚本

为了统一编译流程,必须使用 compile_engine.sh 脚本进行编译:

# 基本用法
./compile_engine.sh --mode <profile|release> --platform <android|ios> --cpu <arm64|x64>

# 常用示例
./compile_engine.sh --mode release --platform android --cpu arm64      # Android 发布版(推荐)
./compile_engine.sh --mode profile --platform android --cpu arm64      # Android 性能分析版
./compile_engine.sh --mode release --platform ios --cpu arm64          # iOS 发布版

# 清理构建
./compile_engine.sh --mode release --platform android --cpu arm64 --clean

Mac 版本脚本内容

#!/bin/bash
# 文件名:compile_engine.sh

set -e

# 检测操作系统
if [[ "$OSTYPE" != "darwin"* ]]; then
    echo "错误: 此脚本仅适用于macOS系统"
    exit 1
fi

echo "=== Mac Flutter Engine 编译脚本 ==="

# 配置参数
BUILD_MODE="release"
TARGET_PLATFORM="android"
TARGET_CPU="arm64"
CLEAN_BUILD=false

# 解析命令行参数
while [[ $# -gt 0 ]]; do
    case $1 in
        --mode)
            BUILD_MODE="$2"
            # 禁用debug模式
            if [ "$BUILD_MODE" = "debug" ]; then
                echo "错误: 不支持debug模式,请使用 release 或 profile"
                exit 1
            fi
            shift 2
            ;;
        --platform)
            TARGET_PLATFORM="$2"
            shift 2
            ;;
        --cpu)
            TARGET_CPU="$2"
            shift 2
            ;;
        --clean)
            CLEAN_BUILD=true
            shift
            ;;
        --help|-h)
            echo "用法: $0 [选项]"
            echo "选项:"
            echo "  --mode <profile|release>         构建模式(不支持debug)"
            echo "  --platform <android|ios>        目标平台"
            echo "  --cpu <arm|arm64|x64>           目标CPU架构"
            echo "  --clean                         清理构建"
            echo ""
            echo "示例:"
            echo "  $0 --mode release --platform android --cpu arm64"
            echo "  $0 --mode profile --platform android --cpu arm64"
            echo "  $0 --mode release --platform ios --cpu arm64"
            exit 0
            ;;
        *)
            echo "未知参数: $1"
            echo "使用 --help 查看帮助"
            exit 1
            ;;
    esac
done

# 检查当前目录
if [ ! -f ".gclient" ] || [ ! -d "flutter" ]; then
    echo "错误: 请在Flutter Engine源码根目录下运行此脚本"
    echo "当前目录应该包含 .gclient 文件和 flutter/ 目录"
    exit 1
fi

echo "构建配置:"
echo "  模式: $BUILD_MODE"
echo "  平台: $TARGET_PLATFORM"
echo "  CPU: $TARGET_CPU"
echo "  清理构建: $CLEAN_BUILD"

# 清理构建目录
if [ "$CLEAN_BUILD" = true ]; then
    echo "清理构建目录..."
    rm -rf out/
fi

# 构建输出目录
OUTPUT_DIR="out/${TARGET_PLATFORM}_${BUILD_MODE}_${TARGET_CPU}"

# 配置GN参数
echo "配置构建环境..."
GN_ARGS=""
GN_ARGS+=" --${TARGET_PLATFORM}"

# 处理CPU架构参数
if [ "$TARGET_PLATFORM" = "android" ]; then
    GN_ARGS+=" --android-cpu ${TARGET_CPU}"
elif [ "$TARGET_PLATFORM" = "ios" ]; then
    GN_ARGS+=" --ios-cpu ${TARGET_CPU}"
fi

GN_ARGS+=" --runtime-mode ${BUILD_MODE}"

# 优化设置(强制使用优化)
GN_ARGS+=" --optimized"

# 执行GN配置
echo "执行: ./flutter/tools/gn $GN_ARGS"
./flutter/tools/gn $GN_ARGS

# 获取CPU核心数(Mac专用)
CORES=$(sysctl -n hw.ncpu)
echo "使用 $CORES 个CPU核心进行编译"

# 开始编译
echo "开始编译..."
start_time=$(date +%s)

ninja -C "$OUTPUT_DIR" -j"$CORES"

end_time=$(date +%s)
build_time=$((end_time - start_time))

echo "=== 编译完成 ==="
echo "编译时间: ${build_time}秒"
echo "输出目录: $OUTPUT_DIR"

# 显示产物大小
echo "构建产物大小:"
du -sh "$OUTPUT_DIR"

# 验证关键文件
echo "验证构建产物..."
if [ "$TARGET_PLATFORM" = "android" ]; then
    if [ -f "$OUTPUT_DIR/flutter.jar" ]; then
        echo "✓ flutter.jar 构建成功"
        ls -lh "$OUTPUT_DIR/flutter.jar"
    else
        echo "✗ flutter.jar 构建失败"
    fi
    
    if [ -f "$OUTPUT_DIR/libflutter.so" ]; then
        echo "✓ libflutter.so 构建成功"
        ls -lh "$OUTPUT_DIR/libflutter.so"
    else
        echo "✗ libflutter.so 构建失败"
    fi
elif [ "$TARGET_PLATFORM" = "ios" ]; then
    if [ -f "$OUTPUT_DIR/Flutter.framework/Flutter" ]; then
        echo "✓ Flutter.framework 构建成功"
        ls -lh "$OUTPUT_DIR/Flutter.framework/Flutter"
    else
        echo "✗ Flutter.framework 构建失败"
    fi
fi

echo "Mac编译脚本执行完成"

Linux 版本差异

Linux 环境使用 compile_engine_linux.sh,主要差异:

# Linux 特有检查
check_dependencies() {
    required_packages=("build-essential" "git" "python3" "pkg-config")
    # ...检查逻辑...
}

# CPU 核心数获取
CORES=$(nproc)

# 内存检查
MEMORY_GB=$(free -g | awk '/^Mem:/{print $2}')
if [ "$MEMORY_GB" -lt 8 ]; then
    CORES=$((CORES / 2))  # 内存不足时减少并行任务
fi

1.5 环境验证与测试

快速验证脚本

#!/bin/bash
# 检查关键组件

echo "=== Flutter Engine 环境检查 ==="

# 1. 检查 depot_tools
echo "depot_tools: $(which gclient)"
gclient --version

# 2. 检查源码
if [ -f "~/flutter_sdk/.gclient" ] && [ -d "~/flutter_sdk/flutter" ]; then
    echo "✓ Flutter Engine 源码已准备完成"
else
    echo "✗ Flutter Engine 源码未准备"
fi

# 3. 检查编译脚本
if [ -x "~/flutter_sdk/compile_engine.sh" ]; then
    echo "✓ 编译脚本已准备完成"
else
    echo "✗ 编译脚本未准备"
fi

# 4. 磁盘空间检查
available_space=$(df -h ~ | tail -1 | awk '{print $4}')
echo "可用磁盘空间: $available_space"

性能基准测试

# 编译性能测试
time ./compile_engine.sh --mode release --platform android --cpu arm64

# 预期时间参考(Mac M1 Pro):
# Release 模式:60-120分钟
# Profile 模式:45-90分钟

1.6 常见问题解决

问题1:depot_tools 找不到

# 症状
command not found: gclient

# 解决方案
echo $PATH | grep depot_tools  # 检查PATH
echo 'export PATH="/depot_tools:$PATH"' >> ~/.zshrc
source ~/.zshrc

问题2:gclient sync 失败

# 症状
HTTP 403 错误或网络超时

# 解决方案
# 1. 检查网络连接
ping github.com

# 2. 重新同步
gclient sync --delete_unversioned_trees --force

# 3. 使用代理(如需要)
git config --global http.proxy http://proxy:8080

问题3:编译内存不足

# 症状
c++: internal compiler error: Killed

# 解决方案
# 1. 关闭其他应用程序
# 2. 限制并行任务数
# 编辑 compile_engine.sh,将 -j"$CORES" 改为 -j4

问题4:权限问题

# 症状
Permission denied

# 解决方案
sudo chown -R $(whoami):staff /depot_tools
chmod +x compile_engine.sh

1.7 实用脚本工具

一键环境安装脚本

#!/bin/bash
# 文件名:setup_flutter_engine_macos.sh

echo "=== Flutter Engine 一键安装脚本 ==="

# 检查系统
if [[ "$OSTYPE" != "darwin"* ]]; then
    echo "错误: 仅支持 macOS"
    exit 1
fi

# 安装步骤
echo "1/6 安装 Xcode Command Line Tools..."
xcode-select --install 2>/dev/null || echo "已安装"

echo "2/6 安装 Homebrew..."
if ! command -v brew &> /dev/null; then
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi

echo "3/6 安装依赖..."
brew install git python3

echo "4/6 安装 depot_tools..."
if [ ! -d "/depot_tools" ]; then
    sudo git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /depot_tools
    sudo chown -R $(whoami):staff /depot_tools
fi

echo "5/6 配置环境变量..."
if ! grep -q "/depot_tools" ~/.zshrc; then
    echo 'export PATH="/depot_tools:$PATH"' >> ~/.zshrc
    echo 'export DEPOT_TOOLS_UPDATE=0' >> ~/.zshrc
fi

echo "6/6 创建工作目录..."
mkdir -p ~/flutter_sdk
cd ~/flutter_sdk

cat > .gclient << 'EOF'
solutions = [
  {
    "managed": False,
    "name": ".",
    "url": "https://github.com/flutter/engine.git@3.29.0",
    "custom_deps": {},
    "deps_file": "DEPS",
    "safesync_url": "",
  },
]
EOF

echo "=== 安装完成 ==="
echo "下一步:"
echo "1. 重启终端或运行: source ~/.zshrc"
echo "2. cd ~/flutter_sdk && gclient sync"
echo "3. 创建 compile_engine.sh 并编译"

版本切换脚本

#!/bin/bash
# 文件名:switch_engine_version.sh

ENGINE_DIR="$HOME/flutter_sdk"

switch_version() {
    local version="$1"
    cd "$ENGINE_DIR"
    
    echo "切换到版本: $version"
    sed -i.bak "s/@[^\"]*/@$version/" .gclient
    gclient sync --delete_unversioned_trees
    
    echo "切换完成"
}

# 使用示例
# ./switch_engine_version.sh 3.29.0
switch_version "$1"

自动化构建脚本

#!/bin/bash
# 文件名:build_all_variants.sh

# 构建所有常用变体(仅 release 和 profile 模式)
CONFIGS=(
    "profile android arm64"
    "release android arm64" 
    "release ios arm64"
)

for config in "${CONFIGS[@]}"; do
    echo "构建: $config"
    ./compile_engine.sh --mode $(echo $config | cut -d' ' -f1) \
                       --platform $(echo $config | cut -d' ' -f2) \
                       --cpu $(echo $config | cut -d' ' -f3)
done

小结

本章提供了 Mac 环境下 Flutter Engine 开发的完整准备流程:

核心步骤回顾:

  1. 安装基础工具(Xcode、Homebrew、Git、Python)
  2. 安装 depot_tools 到根目录并配置环境变量
  3. 创建工作目录和 .gclient 配置文件
  4. 同步 Flutter Engine 源码
  5. 使用 compile_engine.sh 脚本编译(仅支持 release 和 profile 模式)

关键要点:

  • depot_tools 必须安装到 /depot_tools 根目录
  • 使用 GitHub 仓库地址提高同步成功率
  • 编译必须使用统一的 compile_engine.sh 脚本
  • 全程使用 release 模式,不支持 debug 模式
  • 区分 Mac 和 Linux 环境的差异处理

准备时间:

  • 环境安装:30分钟
  • 源码同步:1-3小时
  • 首次编译:60-120分钟(release 模式)

完成本章配置后,即可进入下一章的 Docker 环境配置,为跨平台开发提供更多选择。

直接使用ninja编译

ninja -C out/android_release_arm64 -j(sysctl -n hw.ncpu) # Mac ninja -C out/android_release_arm64 -j(nproc) # Linux

限制内存使用的编译

ninja -C out/android_release_arm64 -j4 -l4

清理构建目录

ninja -C out/android_release_arm64 -t clean


## 1.5 环境验证与性能测试

### 完整环境检查脚本

```bash
#!/bin/bash
# 文件名:check_flutter_engine_env.sh

echo "=== Flutter Engine开发环境检查 ==="

# 检查操作系统
echo "1. 操作系统信息:"
if [[ "$OSTYPE" == "darwin"* ]]; then
    sw_vers
    echo "CPU信息: $(sysctl -n machdep.cpu.brand_string)"
    echo "CPU核心数: $(sysctl -n hw.ncpu)"
    echo "内存: $(system_profiler SPHardwareDataType | grep "Memory:" | awk '{print $2 $3}')"
else
    uname -a
    echo "CPU核心数: $(nproc)"
    echo "内存: $(free -h | grep '^Mem:' | awk '{print $2}')"
fi

# 检查磁盘空间
echo -e "\n2. 磁盘空间:"
df -h ~

# 检查depot_tools安装
echo -e "\n3. depot_tools检查:"
if [ -d "/depot_tools" ]; then
    echo "✓ depot_tools已安装到根目录"
    echo "PATH中的depot_tools: $(echo $PATH | grep -o '/depot_tools')"
else
    echo "✗ depot_tools未安装到根目录"
fi

# 检查工具版本
echo -e "\n4. 工具版本:"
which gclient && gclient --version
which gn && gn --version
which ninja && ninja --version

# 检查Python
echo -e "\n5. Python环境:"
python3 --version
which python3

# 检查Git
echo -e "\n6. Git配置:"
git --version
git config --get user.name
git config --get user.email

# 检查Flutter Engine源码
echo -e "\n7. Flutter Engine源码检查:"
if [ -f "~/flutter_sdk/.gclient" ]; then
    echo "✓ .gclient配置文件存在"
    if [ -d "~/flutter_sdk/flutter" ]; then
        echo "✓ Flutter Engine源码已下载"
        cd ~/flutter_sdk/flutter
        echo "当前Engine版本: $(git describe --tags 2>/dev/null || git rev-parse --short HEAD)"
    else
        echo "✗ Flutter Engine源码未下载"
        echo "请运行: cd ~/flutter_sdk && gclient sync"
    fi
else
    echo "✗ Flutter Engine未配置"
    echo "请创建 ~/flutter_sdk/.gclient 文件"
fi

# 检查编译脚本
echo -e "\n8. 编译脚本检查:"
if [ -f "compile_engine.sh" ]; then
    echo "✓ compile_engine.sh 存在"
else
    echo "✗ compile_engine.sh 不存在"
fi

# 网络检查
echo -e "\n9. 网络连接测试:"
ping -c 1 github.com >/dev/null 2>&1 && echo "✓ GitHub可访问" || echo "✗ GitHub不可访问"
ping -c 1 chromium.googlesource.com >/dev/null 2>&1 && echo "✓ Chromium可访问" || echo "✗ Chromium不可访问"

echo -e "\n=== 环境检查完成 ==="

编译性能测试脚本

#!/bin/bash
# 文件名:benchmark_compile.sh

echo "=== Flutter Engine编译性能基准测试 ==="

# 检查环境
if [ ! -f "compile_engine.sh" ]; then
    echo "错误: 找不到 compile_engine.sh 脚本"
    exit 1
fi

# 测试配置
TEST_CONFIGS=(
    "debug android arm64"
    "profile android arm64"
    "release android arm64"
)

# 执行测试
for config in "${TEST_CONFIGS[@]}"; do
    IFS=' ' read -r mode platform cpu <<< "$config"
    
    echo "测试配置: $mode $platform $cpu"
    
    # 清理之前的构建
    ./compile_engine.sh --mode "$mode" --platform "$platform" --cpu "$cpu" --clean
    
    # 记录开始时间
    start_time=$(date +%s)
    
    # 执行编译
    ./compile_engine.sh --mode "$mode" --platform "$platform" --cpu "$cpu"
    
    # 记录结束时间
    end_time=$(date +%s)
    compile_time=$((end_time - start_time))
    
    echo "$mode $platform $cpu: ${compile_time}秒"
    
    # 记录产物大小
    output_dir="out/${platform}_${mode}_${cpu}"
    if [ -d "$output_dir" ]; then
        size=$(du -sh "$output_dir" | cut -f1)
        echo "产物大小: $size"
    fi
    
    echo "---"
done

echo "=== 性能测试完成 ==="

1.6 问题排查与解决方案

depot_tools相关问题

问题1: depot_tools路径找不到

# 症状:command not found: gclient

# 检查安装路径
ls -la /depot_tools/

# 检查PATH配置
echo $PATH | grep depot_tools

# 解决方案:重新配置PATH
echo 'export PATH="/depot_tools:$PATH"' >> ~/.zshrc
source ~/.zshrc

问题2: depot_tools权限问题

# 症状:Permission denied

# 修复权限
sudo chown -R $(whoami):staff /depot_tools
sudo chmod -R 755 /depot_tools

gclient同步问题

问题3: gclient sync失败

# 症状:HTTP 403 错误或网络超时

# 解决方案1:检查.gclient配置
cat .gclient
# 确保URL正确:https://github.com/flutter/engine.git@3.29.0

# 解决方案2:强制重新同步
gclient sync --delete_unversioned_trees --force --reset

# 解决方案3:清理并重新开始
rm -rf .gclient_entries
gclient sync --verbose

问题4: DEPS文件解析错误

# 症状:Error parsing DEPS file

# 解决方案:更新depot_tools
cd /depot_tools
git pull origin main

# 重新同步
cd ~/flutter_sdk
gclient sync --force

编译相关问题

问题5: compile_engine.sh脚本权限

# 症状:Permission denied: ./compile_engine.sh

# 解决方案:添加执行权限
chmod +x compile_engine.sh

问题6: GN配置失败

# 症状:gn gen failed

# 检查Python环境
python3 --version

# 检查源码完整性
ls flutter/tools/gn

# 重新运行hooks
gclient runhooks --force

问题7: Ninja编译内存不足

# 症状:c++: internal compiler error: Killed

# Mac解决方案:限制并行任务
./compile_engine.sh --mode debug --platform android --cpu arm64

# 在脚本中修改并行数
# 编辑compile_engine.sh,将 -j"$CORES" 改为 -j4

1.7 实用脚本与自动化工具

一键环境安装脚本

#!/bin/bash
# 文件名:setup_flutter_engine_macos.sh

set -e

echo "=== Flutter Engine macOS开发环境一键安装 ==="

# 检查系统
if [[ "$OSTYPE" != "darwin"* ]]; then
    echo "错误: 此脚本仅适用于macOS"
    exit 1
fi

# 安装Xcode Command Line Tools
echo "1. 安装Xcode Command Line Tools..."
if ! xcode-select -p >/dev/null 2>&1; then
    xcode-select --install
    echo "请完成Xcode Command Line Tools安装后继续..."
    read -p "按回车键继续..."
fi

# 安装Homebrew
echo "2. 检查Homebrew..."
if ! command -v brew >/dev/null 2>&1; then
    echo "安装Homebrew..."
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi

# 安装基础工具
echo "3. 安装基础工具..."
brew install git python3

# 安装depot_tools到根目录
echo "4. 安装depot_tools到根目录..."
if [ ! -d "/depot_tools" ]; then
    sudo git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /depot_tools
    sudo chown -R $(whoami):staff /depot_tools
    sudo chmod -R 755 /depot_tools
fi

# 配置环境变量
echo "5. 配置环境变量..."
SHELL_RC="$HOME/.zshrc"
if ! grep -q "/depot_tools" "$SHELL_RC"; then
    echo 'export PATH="/depot_tools:$PATH"' >> "$SHELL_RC"
    echo 'export DEPOT_TOOLS_UPDATE=0' >> "$SHELL_RC"
fi

# 创建Flutter SDK目录
echo "6. 创建Flutter SDK目录..."
mkdir -p ~/flutter_sdk

# 创建.gclient配置
echo "7. 创建.gclient配置..."
cat > ~/flutter_sdk/.gclient << 'EOF'
solutions = [
  {
    "managed": False,
    "name": ".",
    "url": "https://github.com/flutter/engine.git@3.29.0",
    "custom_deps": {},
    "deps_file": "DEPS",
    "safesync_url": "",
  },
]
EOF

# 创建编译脚本
echo "8. 创建编译脚本..."
cp compile_engine.sh ~/flutter_sdk/ 2>/dev/null || echo "请手动复制compile_engine.sh到~/flutter_sdk/"
chmod +x ~/flutter_sdk/compile_engine.sh 2>/dev/null || true

echo "=== 安装完成 ==="
echo ""
echo "下一步操作:"
echo "1. 重新启动终端或运行: source ~/.zshrc"
echo "2. 进入源码目录: cd ~/flutter_sdk"
echo "3. 同步源码: gclient sync"
echo "4. 编译Engine: ./compile_engine.sh --mode debug --platform android --cpu arm64"

版本管理脚本

#!/bin/bash
# 文件名:manage_engine_version.sh

ENGINE_DIR="$HOME/flutter_sdk"
CURRENT_VERSION=""

# 切换到Engine目录
cd "$ENGINE_DIR" || {
    echo "错误: Flutter Engine目录不存在: $ENGINE_DIR"
    exit 1
}

# 获取当前版本
get_current_version() {
    if [ -d "flutter/.git" ]; then
        cd flutter
        CURRENT_VERSION=$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)
        echo "当前Engine版本: $CURRENT_VERSION"
        cd ..
    else
        echo "Flutter Engine源码未下载"
    fi
}

# 列出可用版本
list_versions() {
    cd flutter
    echo "最新的10个stable版本:"
    git tag | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -10
    cd ..
}

# 切换版本
switch_version() {
    local target_version="$1"
    
    if [ -z "$target_version" ]; then
        echo "错误: 请指定目标版本"
        return 1
    fi
    
    echo "切换Engine到版本: $target_version"
    
    # 修改.gclient文件
    sed -i.bak "s/@[^\"]*/@$target_version/" .gclient
    
    # 重新同步
    gclient sync --delete_unversioned_trees
    
    echo "版本切换完成"
    get_current_version
}

# 同步最新代码
sync_latest() {
    echo "同步最新Engine代码..."
    gclient sync --verbose
    echo "同步完成"
    get_current_version
}

# 主函数
main() {
    case "$1" in
        "current"|"")
            get_current_version
            ;;
        "list")
            list_versions
            ;;
        "switch")
            switch_version "$2"
            ;;
        "sync")
            sync_latest
            ;;
        *)
            echo "用法: $0 {current|list|switch <version>|sync}"
            echo ""
            echo "命令说明:"
            echo "  current  - 显示当前版本"
            echo "  list     - 列出可用版本"
            echo "  switch   - 切换到指定版本"
            echo "  sync     - 同步最新代码"
            echo ""
            echo "示例:"
            echo "  $0 current"
            echo "  $0 list"
            echo "  $0 switch 3.29.0"
            echo "  $0 sync"
            exit 1
            ;;
    esac
}

main "$@"

自动化构建脚本

#!/bin/bash
# 文件名:auto_build_all.sh

echo "=== Flutter Engine自动化构建脚本 ==="

# 检查环境
if [ ! -f "compile_engine.sh" ]; then
    echo "错误: 找不到compile_engine.sh"
    exit 1
fi

# 构建配置列表
BUILD_CONFIGS=(
    "debug android arm64"
    "profile android arm64"
    "release android arm64"
    "debug ios arm64"
    "release ios arm64"
)

# 构建结果记录
SUCCESS_BUILDS=()
FAILED_BUILDS=()

echo "开始自动化构建..."
echo "构建配置数量: ${#BUILD_CONFIGS[@]}"

for config in "${BUILD_CONFIGS[@]}"; do
    IFS=' ' read -r mode platform cpu <<< "$config"
    
    echo ""
    echo "===================="
    echo "构建: $mode $platform $cpu"
    echo "===================="
    
    # 执行构建
    if ./compile_engine.sh --mode "$mode" --platform "$platform" --cpu "$cpu"; then
        SUCCESS_BUILDS+=("$config")
        echo "✓ 构建成功: $config"
    else
        FAILED_BUILDS+=("$config")
        echo "✗ 构建失败: $config"
    fi
done

# 输出构建结果
echo ""
echo "=== 构建结果汇总 ==="
echo "成功构建 (${#SUCCESS_BUILDS[@]}):"
for build in "${SUCCESS_BUILDS[@]}"; do
    echo "  ✓ $build"
done

echo ""
echo "失败构建 (${#FAILED_BUILDS[@]}):"
for build in "${FAILED_BUILDS[@]}"; do
    echo "  ✗ $build"
done

# 显示产物大小
echo ""
echo "=== 构建产物大小 ==="
if [ -d "out" ]; then
    du -sh out/*/ 2>/dev/null | sort -hr
fi

echo ""
echo "自动化构建完成"

小结

本章针对Mac环境下的Flutter Engine开发,详细介绍了完整的准备工作:

  1. 环境配置: 针对Mac系统的硬件要求和系统配置
  2. 工具安装: depot_tools安装到根目录的具体步骤
  3. 源码管理: 使用指定的.gclient配置和GitHub仓库
  4. 编译脚本: 区分Mac和Linux的compile_engine.sh脚本
  5. 问题解决: 针对实际开发中的常见问题
  6. 自动化工具: 提高开发效率的实用脚本

通过本章的学习,开发者能够:

  • 在Mac环境下正确配置Flutter Engine开发环境
  • 使用统一的compile_engine.sh脚本进行编译
  • 管理不同版本的Flutter Engine
  • 解决开发过程中的常见问题
  • 使用自动化工具提高工作效率

下一章将介绍Docker环境配置,为需要Linux环境的开发场景提供支持。

借助CSS实现自适应屏幕边缘的tooltip

欢迎关注我的公众号:前端侦探

tooltip是一个非常常见的交互,一般用于补充文案说明。比如下面这种(蓝色边框表示屏幕边缘)

image-20250523223912070

通常tooltip都会有一个固定的方向,比如top表示垂直居中向上。

但是,如果提示文案比较多,提示区域右比较靠近屏幕边缘,就可能出现这种情况

image-20250523224309231

直接超出屏幕!这很显然是不能接受的。

你可能会想到改变一下对齐方向,比如top-right,但是这里的文案可能是不固定的,也就是会出现这样

image-20250523224706671

嗯...感觉无论怎么对齐都会有局限。那么如何解决呢?一起看看吧

一、理想中的自适应对齐

我们先想想,最完美的对齐是什么样的。

其实没那么复杂,就分两种情况,一个居左,一个居右

1.居左

正常情况下,就是垂直居中朝上

image-20250523225707868

如果提示文本比较多,那就靠左贴近文本容器对齐

image-20250523225826347

如果提示文本继续增加,那就整行换行,并且不超过文本容器

image-20250523230041333

2. 居右

正常情况下,也是垂直居中朝上

image-20250523230849249

如果提示文本比较多,那就靠右贴近文本容器对齐

image-20250523230936187

如果提示文本继续增加,也是整行换行,并且不超过文本容器

image-20250523231035167

那么如何实现这样的对齐方式呢?

二、左自适应对齐的思路

我们先看第一种情况,看似好像有3种对齐方式,而且还要监测是否到了边界,好像挺复杂。其实换个角度,其实是这样一种规则

  1. 当内容较少时,居中对齐
  2. 当内容较多时,居左对齐
  3. 当内容多到换行时,有一个最大宽度

既然涉及到了对齐,那就有对齐的容器和被对齐的对象。

我们可以想象一个虚拟容器,以对齐中心(下图问号图标)向两边扩展,一直到边界处,如下所示(淡蓝色区域)

image-20250523233837924

假设HTML如下

<span class="tooltip" title="提示"></span>

当气泡文本比较少时,可以通过文本对齐实现居中,气泡可以直接通过伪元素实现

.tooltip{
  width: 50px; /*虚拟容器宽度,暂时先固定 */
  text-align:center;
}
.tooltip::before{
  content: attr(title);
  display: inline-block;
  color: #fff;
  background-color: #000;
  padding: .5em 1em;
  border-radius: 8px;
  box-sizing: border-box;
}
/*居中箭头*/
.tooltip::after{
  content: '';
  position: absolute;
  width: 1em;
  height: .6em;
  background: #000;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  top: 0;
  left:0;
  right:0;
  margin: 0 auto;
  transform: translateY(-150%)
}

使用文本居中,也就是text-align: center有个好处,当文本不超过容器时,居中展示,就如同上图展示一样。

当文本比较多时,默认会换行,效果如下

image-20250524112135696

这样应该很好理解吧。

我们需要气泡里的文本在多行时居左,可以直接给气泡设置居左对齐

.tooltip::before{
  /*...*/
  text-align: left;
}

效果如下

image-20250524112333960

这样就实现了单行居中,多行居左的效果了。

现在还有一个问题,如何在气泡文本较多时,不被对齐容器束缚呢?

首先可以想到的是禁止换行,也就是

.tooltip::before{
  /*...*/
  white-space: nowrap
}

这样在文本不超过一行时确实可以

image-20250524112641734

看,已经突破了容器束缚。但是文本继续增加时,也会出现无法换行的问题

image-20250524112800984

我们可以想一想,还有什么方式可以控制换行呢?

这里,我们需要设置宽度为最大内容宽度,相当于文本有多少,文本容器就有多宽

.tooltip::before{
  /*...*/
  width: max-content
}

看似好像和不换行一样

image-20250524112800984

实则不然,我们并没用禁止换行。只要给一个最大宽度,立马就换行了

.tooltip::before{
  /*...*/
  width: max-content;
  max-width: 300px;
}

效果如下

image-20250524113318010

是不是几乎实现了我们想要的效果了?

不过,这里涉及了两个需要动态计算的宽度,一个是虚拟容器宽度,还有一个是外层最大宽度,

image-20250524152008282

下面看如何实现

三、借助JS计算所需宽度

现如今,外层的最大宽度倒是可以通过容器查询获得,但内部的虚拟容器宽度还无法直接获取,只能借助JS了。

不过我们这里可以先只计算左侧偏移,也就是一半的宽度

image-20250524155257344

具体实现如下

//问号中心到左侧距离
const x = this.offsetLeft - 8
// 问号的宽度
const w = this.clientWidth
// 外层整行文本容器宽度
const W = this.offsetParent.clientWidth - 32
// 左侧偏移
this.style.setProperty('--x', x + 'px')
// 外层文本容器宽度(气泡最大宽度)
this.style.setProperty('--w', W + 'px')

然后给前面待定的宽度绑定这些变量就行了

.tooltip{
  /*...*/
  width: calc(var(--x) * 2);
}
.tooltip::before{
  /*...*/
  max-width: var(--w);
}

这样左侧就完全实现自适应了,无需实时计算,仅需初始化一次就好了

Kapture 2025-05-24 at 15.56.13转存失败,建议直接上传图片文件

四、完全自适应对齐

前面是左侧,那右侧如何判断呢?我们可以比较左侧距离的占比,如果超过一半,就表示现在是居右了

这里用一个属性表示

this.tooltip.dataset.left = x/W < 0.5 //是否居左

然后就右侧虚拟容器的宽度了,和左侧还有有点不一样

image-20250524160146516

前面我们已经算出了左侧距离,由于超过了一半,所以需要先减然后再乘以二

.tooltip[data-left="false"]::before{
  /*...*/
  width: calc( (var(--w) - var(--x)) * 2);
  max-width: var(--w);
}

其实这里还是有个小问题的,当气泡文字比较长时,仍然是朝右突破了边界,如下所示

image-20250524160531721

这是因为默认的语言流向造成的(从左往右),解决这个问题也非常简单,仅需改变语言方向就可以了,要用到direction:rtl,如下

.tooltip[data-left="false"]::before{
  /*...*/
  width: calc( (var(--w) - var(--x)) * 2);
  max-width: var(--w);
  direction: rtl;
}

这样就完美了

image-20250524160856055

现在来看一下所有边界情况的演示

Kapture 2025-05-24 at 16.10.06

你也可以访问在线demo真实体验:codepen.io/xboxyan/pen…

如果你是 vue3 项目,可以直接用这段封装好的组件(其实没几行代码,大部分自适应都是CSS完成的)

<!-- 极度自适应的tooltips -->
<script setup lang="ts">
const props = defineProps({
  text: String,
  gap: {
    type: Number,
    default: 12,
  },
})

const show = ref(false)
const pos = reactive({
  x: 0,
  w: 0,
  top: 0,
  gap: 0,
  isLeft: true,
})
const click = (ev: MouseEvent) => {
  // console.log()
  // if (ev.target) {
  //   ev.stopPropagation()
  // }
  const target = ev.target as Element | null
  console.log('xxxxxxxxxxx', target)
  if (target) {
    const { x, y, width } = target.getBoundingClientRect()
    pos.top = y + window.scrollY
    pos.gap = props.gap
    pos.x = x + width / 2 - props.gap
    pos.w = window.innerWidth - props.gap * 2
    show.value = true
  }
}

const wrap = ref<HTMLElement>()

document.body.addEventListener('touchstart', (ev) => {
  // 没有点击当前触发对象就隐藏tooltips
  if (!(wrap.value && ev.target && wrap.value.contains(ev.target as Node))) {
    show.value = false
  }
})
</script>

<template>
  <span class="wrap" ref="wrap" @click="click">
    <slot></slot>
  </span>
  <Teleport to="body">
    <div
      class="tooltip"
      v-show="show"
      :data-title="text"
      :data-left="pos.x / pos.w < 0.5"
      :style="{
        '--x': pos.x + 'px',
        '--top': pos.top + 'px',
        '--gap': pos.gap + 'px',
        '--w': pos.w + 'px',
      }"
    ></div>
  </Teleport>
</template>
<style>
.wrap {
  display: contents;
}
.tooltip {
  position: absolute;
  top: var(--top);
  text-align: center;
  pointer-events: none;
}
.tooltip[data-left='true'] {
  width: calc(var(--x) * 2);
  left: var(--gap);
}
.tooltip[data-left='false'] {
  width: calc((var(--w) - var(--x)) * 2);
  right: var(--gap);
  direction: rtl;
}

.tooltip::before {
  content: attr(data-title);
  display: inline-block;
  color: #fff;
  background-color: #191919;
  padding: 0.5em 0.8em;
  border-radius: 8px;
  transform: translateY(calc(-100% - 0.5em));
  width: max-content;
  max-width: var(--w);
  box-sizing: border-box;
  text-align: left;
}
.tooltip::after {
  content: '';
  position: absolute;
  width: 1.2em;
  height: 0.6em;
  background: #000;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  top: 0;
  left: 0;
  right: 0;
  margin: 0 auto;
  transform: translateY(calc(-100% - 0.2em));
}
</style>

五、推荐一个开源库

其实市面上有一个库可以完成类似的交互,叫做 float-ui

image-20250817104551464转存失败,建议直接上传图片文件

这个是专门做popover这类交互的,其中有一个shift属性,可以做这种跟随效果

image-20250817104816034

不过对于大部分情况,引入一个单独的库还是成本偏大,建议还是纯原生实现。

这样一个极度自适应的气泡组件,你学会了吗,赶紧在项目中用起来吧~最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤

iconfont 阿里巴巴免费矢量图标库超级好用!

前言

之前我介绍过一款非常好用的前端开发图标字体库FontAwesome

但是除了它还有一款非常好用并且也是免费的图标字体库也是非常不错,并且我自己开发时也是经常在用,那就是iconfont 阿里巴巴矢量图标库 毕竟也要支持国产嘛,你说对不对!

官网地址: www.iconfont.cn/

如图

在使用之前必须先登录一下

没有账号的可以自己注册一下,用手机直接注册就OK了, 然后登录!

使用教程

下载图标字体

登录之后,我们通常在菜单栏中选择素材库里面的图标库 根据需求自己选择!

如图

然后根据自己的需求找一组自己觉得合适的图标,这里都有很多作者自己设计的图标字体

单色图标彩色图标

如图

这里我们一般采用的是单色图标, 因为可以根据自己的需求修改颜色,彩色图标就固定好了的!

当我们选择好了一组之后,点击进去,然后鼠标放到某个图标字体上之后,会出现三个选项按钮

如图

具体意思如下:

  1. 加入购物车
  2. 收藏
  3. 直接下载

其中这里的直接下载就是直接把这个图标当成文件图片的形式下载到本地进行使用, 你也可以根据需求调整颜色和图标格式, 支持svg、ai、png

如图

但是这样使用太麻烦,相当于图片一样了,我们还是需要下载它的图标字体格式

所以我们先要把想要用的图标添加到购物车

如图

然后点击右上角的购物车小图标

如图

侧边栏会弹出一个购物车清单页面,我们选择的图标字体就在这里

因为阿里巴巴矢量图标库它这里是以项目为一个单位,所以我们添加的图标要打包成一个项目给我们使用!

所以这里就直接点击添加至项目

如图

然后自定义新建一个项目名称, 建议用英文

如图

接着会自动跳转到你自己账号的项目管理页面, 你所新建的项目和添加到项目中的图标字体都在这里!

如图

我们直接点击下载到本地 就可以得到一个zip压缩文件,至此图标的下载完成了!

使用本地图标字体

当我们下载好之后,解压,并且重命名一个你自己比较好记忆的名称!

然后你会得到一堆文件

如图

其实到这里,就跟我们之前使用FontAwesome是一样的道理

我们只需要把这些文件拷贝到我们项目文件夹下就可以了!

如图

关于具体如何使用到我们的html页面中, 在打包解压出来的文件中有一个叫demo_index.html的文件

你可以打开它,里面全部都是你刚刚所添加的图标字体和具体的使用方式

并且它这里提供了三种使用方式:Unicode实体编码方式、FontClass类名称调用、Symbol

如图

这里我就以Unicode实体编码方式、FontClass类名称调用方式演示一下

Unicode实体编码方式调用

Unicode是字体在网页端最原始的应用方式,特点如下:

  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 默认情况下不支持多色,直接添加多色图标会自动去色。

我们可以使用CSS中的@font-face在页面上引入图标字体

例如

@font-face {
  font-family: 'iconfont';
  src: url('iconfont.woff2?t=1706238093360') format('woff2'),
       url('iconfont.woff?t=1706238093360') format('woff'),
       url('iconfont.ttf?t=1706238093360') format('truetype');
}

也可以直接把iconfont.css文件通过link标签引入到我们的页面中

<link rel="stylesheet" type="text/css" href="iconfont/iconfont.css">

然后我们就可以使用了

调用方式

<span class="iconfont">Unicode实体编码</span>

这里在标签中一定要加上classiconfont才有效果

至于Unicode实体编码名称你可以在刚刚的案例文档中招到

举个栗子

<style type="text/css">
    #content {
        width: 300px;
        border: 1px dotted red;
        padding: 10px;
        margin: 100px auto;
        text-align: center;
    }
</style>


<div id="content">
    <span class="iconfont">&#xe614;</span>
    <span class="iconfont">&#xe615;</span>
    <span class="iconfont">&#xe616;</span>
    <span class="iconfont">&#xe617;</span>
    <span class="iconfont">&#xe618;</span>
    <span class="iconfont">&#xe619;</span>
</div>

效果如下

怎么样,是不是很简单,你再也不用担心图标的问题了!

FontClass类名称调用

FontClass类名称调用方式其实是 Unicode 使用方式的一种变种, 主要是解决Unicode书写不直观,语意不明确的问题

Unicode使用方式相比,具有如下特点:

  • 相比于Unicode语意明确,书写更直观,可以很容易分辨这个icon代表什么意思!
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。

其实如果你仔细打开观察一下iconfont.css这个源码文件,你就会知道,其实里面就是对Unicode的有种封装

如图

调用方式

<span class="iconfont icon-xxx"></span>

在文档页面中挑选相应图标并获取类名,应用页面元素就可以了!

举个栗子

<style type="text/css">

    #content {
        width: 300px;
        border: 1px dotted red;
        padding: 10px;
        margin: 100px auto;
        text-align: center;
    }


    #content>.icon-shujuzhanshi{
        color: yellow;
        font-size: 12px;
    }

    #content>.icon-xiangouhuodong{
        color: pink;
        font-size: 14px;
    }

    #content>.icon-pingfen{
        color: yellowgreen;
        font-size: 16px;
    }

    #content>.icon-dianpukanbanmoren{
        color: blue;
        font-size: 18px;
    }

    #content>.icon-youhuiquan{
        color: green;
        font-size: 20px;
    }

    #content>.icon-XyuanhuodongD{
        color: red;
        font-size: 22px;
    }

</style>

<div id="content">
    <span class="iconfont icon-shujuzhanshi"></span>
    <span class="iconfont icon-xiangouhuodong"></span>
    <span class="iconfont icon-pingfen"></span>
    <span class="iconfont icon-dianpukanbanmoren"></span>
    <span class="iconfont icon-youhuiquan"></span>
    <span class="iconfont icon-XyuanhuodongD"></span>
</div>

效果跟刚刚是一模一样!

并且你也可以通过CSS去自定义他们的颜色和大小,可以说非常方便!

如下

最后

总的来说iconfont 阿里巴巴矢量图标库还是很不错的,但是缺点可能就是版权问题,如果你是学习那应该没什么问题,但是如果是商用,那么最好在使用这些图标之前先咨询一下作者,以免版权纠纷!

这一点我个人感觉确实是没有FontAwesome做得好,搞得不清不楚的真麻烦! 嘿嘿嘿~~

事件委托的深层逻辑:当冒泡不够时⁉️

前言

在项目不断扩大之时,管理用户交互变的越来越重要,为每个交互元素附加一个事件监听器是一种糟糕的做法,因为它会导致代码混乱、内存消耗增加以及性能瓶颈。这时,事件委托就派上用场了。

认识dom事件传播

三个阶段

当事件在 DOM 元素上触发时,它不会简单地到达目标并停止。相反,它会经历以下阶段:

  1. 捕获阶段旅程从window级别开始,沿着 DOM 树向下移动,经过每个祖先元素,直到到达目标的父级。带有(中的第三个参数)的事件监听器在此触发useCapture = true``addEventListener
  2. 目标阶段在此阶段,事件到达预期的目标元素。所有直接附加到此元素的监听器都会被触发
  3. 冒泡阶段命中目标后,事件会沿着 DOM 向上“冒泡”,从目标的父元素到祖父元素,依此类推,直到到达目标window。默认情况下,大多数事件监听器都在此阶段运行

事件在dom树中流动过程

< div id = "grandparent" >
< div id = "parent" >
< button id = "child" >点击我</ button > 
</ div > 
</ div > 

如果您单击,则事件流程如下:<button id="child"> click

  1. 捕获 -window -> document -> <html> -> <body> -> <div id="grandparent"> -> <div id="parent">
  2. 目标<button id="child">
  3. 冒泡 - <button id="child"> -> <div id="parent"> -> <div id="grandparent"> -> <body> -> <html> ->document -> window

什么是事件委托

事件委托是一种将事件监听器添加到多个子元素的父元素上,而不是分别添加到每个子元素上的方法。当子元素上发生事件时,它会触发父元素上的监听器,父元素会检查是哪个子元素触发了该事件。

假设一个<ul>包含<li>以下项目的简单列表:

< ul id = "myList" > 
< li >项目 1 </ li > 
< li >项目 2 </ li >
< li >项目 3 </ li >
< li >项目 4 </ li > 
</ ul > 
  

而不是为每个添加一个点击监听器<li>

const listItems = document . querySelectorAll ( '#myList li' ); 
listItems . forEach ( item => { 
  item . addEventListener ( 'click' , ( event ) => { console . log ( `点击于: $ { event . target . textContent } ` );
  });
 });           

通过事件委托,你可以将一个监听器附加到<ul>父级:

onst myList =文档. getElementById ( 'myList' ); 

myList . addEventListener ( 'click' , ( event ) => { 
// 检查点击的元素是否为 <li>
if ( event . target . tagName === 'LI' ) {
console . log ( ` Clicked on : $ { event . target . textContent } ` );
} 
});   
  

在此示例中,当<li>点击任意一个时,click事件都会冒泡到。然后,myList上的单个事件监听器会检查是否是触发了该事件,并采取相应的措施:myList``event.target.tagName``<li>

为什么事件委托如此重要

  • 无需添加数百或数千个监听器,只需几个父容器就足够了,从而大大减少内存占用
  • 更少的监听器可以提高浏览器整体系统内存的使用率,并减少 JavaScript 引擎在事件管理和调度方面的工作量
  • 它支持动态创建元素,这非常实用。假设在页面加载后(例如,在 API 调用后)<li>添加了新元素,监听器仍然有效。无需重新连接监听器。#myList``#myList

事件委托中常见的误区

event.target vs event.currentTarget
  • event.target 是触发事件的特定元素。
  • event.currentTarget 是事件监听器实际附加到的元素。
stopPropagation ()和stopImmediatePropagation () 
  • event.stopPropagation() – 此方法仅允许事件停止沿 DOM 树向上或向下冒泡或捕获。如果在子元素的事件处理程序中执行此方法,则其祖先元素上的任何委托监听器都将无法访问该事件
  • event.stopImmediatePropagation() 这不stopPropagation()的复制粘贴。它的相似之处仅限于添加了这个效果:它阻止进一步的事件传播,并阻止绑定到同一元素的任何其他监听器被执行。

在某些情况下,它们会破坏委托处理程序,例如:子元素的事件处理程序调用stopPropagation将导致位于 DOM 层次结构中更高层级的任何委托监听器的功能失效。委托监听器将无法接收事件。这对于分析、集中式 UI 逻辑或可访问的自定义控件功能尤其麻烦。

非冒泡事件

最突出的非冒泡事件包括:

  • focus– 当元素获得焦点时触发
  • blur– 当元素失去焦点时触发
  • mouseenter– 当指针进入元素时触发
  • mouseleave– 当指针离开元素时触发

为什么它们不起泡

由于浏览器的工作方式以及过去的兼容性问题,通常无法触发此类事件。focusblur旨在在获得或失去焦点的特定元素上触发,因此不存在冒泡。mouseentermouseleavemouseover 和 mouseout配对(它们会产生冒泡);但是与mouseover 和 mouseout不同,mouseenter 和 mouseleave仅在指针位于元素上(而不是其子元素上)时触发。

对于非冒泡事件只能通过自定义冒泡事件来替代

总结

事件委托通过将单个监听器附加到父元素来简化事件处理。当子元素触发事件时,它会向上冒泡到父元素,从而减少内存占用并简化代码。

这种技术在管理大量相似元素(例如列表项或按钮)时非常有效,尤其是在它们动态生成的情况下。父级监听器无需额外配置即可处理新添加元素的事件。

并非所有事件都会冒泡 focusblurmouseleave 等是例外。对于这些事件,可以用 focusinfocusout 或自定义冒泡事件等替代方法。

🐙 Git 从入门到面试能吹的那些事

“会用 Git 不稀奇,能讲明白才值钱。”
本文让你从 git add . 的机械工人,变成能聊底层原理 + 面试加分的 Git 社交达人。


1. Git 是什么?

先来个官方说法:

Git 是一个分布式版本控制系统,用来记录代码变更历史,方便多人协作。

翻译成人话:

  • 它是代码界的时光机
  • 支持你随时穿越回过去(reflog 就像游戏存档)
  • 多人协作时,它就像一群厨子一起炒菜,每个人有自己的灶台,最后再把菜端到一张桌上。

2. Git 的三大区域

  • 工作区(Working Directory) :你正在写的代码文件。
  • 暂存区(Staging Area) :已经打包好,等着快递的改动。
  • 本地仓库(Local Repository) :正式存档的历史版本。

命令速记:

git add .       # 把工作区改动送到暂存区
git commit -m "fix: 修复登录 Bug"  # 把暂存区的改动送进历史

3. 常用 Git 命令(带脑洞解释)

命令 作用 脑洞版记忆
git status 看当前状态 “摸一摸脉搏”
git log 看历史记录 “翻家谱”
git diff 看改了啥 “照镜子对比一下”
git branch 看分支 “看看我开了几条平行世界”
git switch / git checkout 切换分支 “从一个世界跳到另一个世界”
git merge 合并分支 “两个世界融合”
git rebase 变基 “时空线性整理”

4. 分支:Git 的平行宇宙

  • 主分支(main/master) :上线版本的世界线。
  • 功能分支(feature/xxx) :新功能试验田。
  • 修复分支(hotfix/xxx) :紧急修 bug 的世界。
# 创建并切换
git switch -c feature/login

# 合并回主分支
git switch main
git merge feature/login

小贴士

  • 合并用 merge 安全可靠
  • 想要历史好看,可以 rebase(别在别人用的分支上乱 rebase)

5. 远程协作的日常

git clone <url>         # 拿到别人的代码副本
git fetch               # 拉取最新改动(不影响你当前文件)
git pull                # 拉取 + 合并(或 rebase)
git push origin main    # 推送你的改动

脑补场景:

  • fetch:去看快递柜里有什么新快递,但先不取
  • pull:看完直接取回家
  • push:你把自己的菜送到团队大锅里

6. 史诗级救命技能

  1. 撤销最近一次提交(保留改动)

    git reset --soft HEAD~1
    
  2. 回到某个提交

    git reset --hard <commit-id>
    
  3. 找回“丢失”的提交

    git reflog
    git reset --hard <reflog-id>
    
  4. 挑一个提交到当前分支

    git cherry-pick <commit-id>
    

记住reset --hard 像是核弹,一定确认无误再按。


7. 面试常考 Git 题

Q1: Git pull 和 Git fetch 有什么区别?

  • fetch:只下载远程最新记录,本地不动。
  • pull:相当于 fetch + merge(或 rebase)。
    面试加分:有时先 fetch 再手动合并更安全。

Q2: merge 和 rebase 的区别?

  • merge:保留分支的合并历史,可能有多叉结构。
  • rebase:将提交“搬到”目标分支顶部,历史线性更清爽。
    加分点:团队协作时,在共享分支用 merge,自己分支可以 rebase 保持整洁。

Q3: 如何撤销已经 push 上去的错误提交?

  • 如果要保留历史:用 git revert 生成一个反向提交。
  • 如果可以改历史(风险大):用 git reset --hard + git push --force-with-lease,但要确保没人基于你的提交工作。

Q4: .gitignore 是干嘛的?

  • 用来指定 Git 不跟踪的文件(如 node_modules/dist/.env)。
  • 注意 .gitignore 只能忽略未被追踪的文件,已经提交过的需要用 git rm --cached 移除追踪。

Q5: Git rebase -i 有什么用?

  • -i 是交互式变基,可以合并提交(squash)、修改提交信息(reword)、删除提交(drop)、调整顺序等。
  • 面试加分:常用来清理杂乱的历史提交,让 PR 更优雅。

8. 总结 & 面试吹法

  • 会基本命令:加、提、切、合、推、拉。

  • 会救命操作:reset、reflog、stash、cherry-pick。

  • 理解原理:三大区域 + 分支模型。

  • 面试吹点:

    1. 团队分支策略(Git Flow / GitHub Flow)
    2. 规范化提交(Conventional Commits)
    3. 在 CI/CD 流程中结合 Git Hooks 提升质量

9. 送你一份 Git 冷笑话

面试官:你会 Git 吗?
我:会啊,我是 Git 大师。
面试官:那帮我 reset 一下刚才问的问题。
我:git reset --hard
面试官:……你回家等通知吧。

鸿蒙音频编码

【HarmonyOS 音频编码开发速览】

文档地址:developer.huawei.com/consumer/cn…

  1. 能力总览

    • 将任意来源的 PCM 数据编码为 AAC、FLAC、MP3、G.711μ、OPUS、AMR-NB/WB 等格式。

    • 典型使用场景:录音后封装、PCM 编辑后导出文件。

    • AAC 默认使用 VBR,码率可能与设定值存在偏差。

  2. 核心流程(12 步)

    1. 引入头文件。

    2. 创建编码器(OH_AVCodec):可按 MIME 或编解码器名称创建。

    3. 注册回调:

      • onError、onNeedInputBuffer、onNewOutputBuffer(onStreamChanged 暂不支持)。
    4. 配置参数:

      必须项:采样率、声道数、采样格式;

      可/必须项:码率、声道布局、ADTS、Profile 等,各格式差异见表。

    5. Prepare → 6) Start → 7) 逐帧输入 PCM(需按格式计算单次样点数) →

    6. 取出码流 → 9) FreeOutputBuffer →

    10)(可选)Flush / Reset → 11) Stop → 12) Destroy。

  3. 关键参数表

    • 采样率/声道数范围:AAC-LC 8–96 kHz、1–8 ch;FLAC 8–96 kHz、1–8 ch;MP3 8–48 kHz、1–2 ch;OPUS 8–48 kHz、1–2 ch;AMR-NB 8 kHz/1 ch,AMR-WB 16 kHz/1 ch。

    • 单次输入样点数:AAC-LC=1024;HE-AAC=2048;FLAC 按采样率查表(如 44.1 kHz→4608)。

  4. 开发注意

    • 必须按顺序调用 API,否则可能异常。

    • 最后一帧需置 AVCODEC_BUFFER_FLAGS_EOS。

    • 调用 Flush/Reset/Stop 后需重新 Start 并可能重新输入数据。

以下 Mermaid 流程图对应「最开始回答」中的 12 步开发顺序,可直接复制到 Mermaid Live Editor 渲染。

图中:

  • 实线框为「必选步骤」
  • 虚线框为「可选步骤」
  • 箭头旁文字为调用接口名
%% HarmonyOS 音频编码 12 步调用流程(对应官方文档顺序)
flowchart TD
    A([开始]) --> B[1. 添加头文件\n#include]
    B --> C[2. 创建编码器\nOH_AudioCodec_CreateByMime / CreateByName]
    C --> D[3. 注册回调\nOH_AudioCodec_RegisterCallback]
    D --> E[4. 配置参数\nOH_AudioCodec_Configure]
    E --> F[5. 就绪\nOH_AudioCodec_Prepare]
    F --> G[6. 启动\nOH_AudioCodec_Start]
    G --> H[7. 循环输入 PCM\nOH_AudioCodec_PushInputBuffer]
    H --> I[8. 取出码流\nOH_AudioCodec_FreeOutputBuffer]

    I --> J{是否继续?}
    J -->|是| H
    J -->|否| K[11. 停止\nOH_AudioCodec_Stop]

    %% 可选分支
    H -.->|9. 刷新队列| L[OH_AudioCodec_Flush]
    L -.-> G
    K -.->|10. 重置| M[OH_AudioCodec_Reset]
    M -.-> E
    K --> N[12. 销毁\nOH_AudioCodec_Destroy]

    N --> Z([结束])

下面给出

  1. 用 Mermaid 绘制的「音频编码状态/调用流程图」
  2. 一份可直接在 DevEco Studio 4.0(API 10+)里跑的 C++ 最小完整示例(CMake 工程),演示把 44.1 kHz/16-bit/立体声 PCM 编码为 AAC-LC(ADTS)并写入本地文件。

  1. Mermaid 图
%% 音频编码器状态机 + 调用时序(HarmonyOS AVCodec)
stateDiagram-v2
    [*] --> Init: OH_AudioCodec_CreateByMime(...)
    Init --> Configured: Configure(params)
    Configured --> Prepared: Prepare()
    Prepared --> Running: Start()

    Running --> Running: PushInputBuffer() ➜ FreeOutputBuffer()
    Running --> Flushed: Flush()        %% 可选
    Flushed --> Running: Start()

    Running --> Stopped: Stop()
    Stopped --> Prepared: Start()      %% 可再次启动
    Stopped --> Reset: Reset()         %% 可选
    Reset --> Configured: Configure()

    Running --> [*]: Destroy()
    Stopped --> [*]: Destroy()
    Reset --> [*]: Destroy()

  1. 最小可编译示例

目录结构

AudioEncoderDemo/
 ├─ entry/
 │   ├─ src/main/cpp/
 │   │   ├─ native_audio_encoder.cpp (下面代码)
 │   │   └─ CMakeLists.txt
 │   └─ src/main/resources/rawfile/
 │       └─ test_44k_16bit_2ch.pcm   (原始 PCM, 任意长度)

2.1 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(audio_encoder_demo)

set(CMAKE_CXX_STANDARD 17)

# HarmonyOS NDK
find_library(hilog-lib hilog_ndk.z)
find_library(native-buffer-lib native_buffer)
find_library(avcodec-lib libavcodec_base.z)

add_library(entry SHARED native_audio_encoder.cpp)
target_link_libraries(entry
        ${hilog-lib}
        ${native-buffer-lib}
        ${avcodec-lib}
        ohaudio
)

2.2 native_audio_encoder.cpp

#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>

#include "napi/native_api.h"
#include "multimedia/audio_codec/audio_codec_api.h"
#include "hilog/log.h"

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0x0001
#define LOG_TAG "AudioEncoder"

static const int SAMPLE_RATE   = 44100;
static const int CHANNEL_COUNT = 2;
static const int BIT_RATE      = 128000; // 128 kbps
static const int PCM_FRAME_SAMPLES = 1024; // AAC-LC 每帧 1024 样点
static const int PCM_FRAME_BYTES =
        PCM_FRAME_SAMPLES * CHANNEL_COUNT * sizeof(int16_t);

static OH_AVCodec *g_encoder = nullptr;
static int32_t g_fd_out = -1;          // 输出 ADTS 文件描述符
static bool g_input_done = false;

/* ---------- 工具 ---------- */
static void write_adts_header(uint8_t *buf, int frameLen) {
    const int profile = 2;     // AAC-LC
    const int freqIdx = 4;     // 44.1 kHz
    const int chanCfg = 2;     // 2 ch

    int fullLen = frameLen + 7;
    buf[0] = 0xFF;
    buf[1] = 0xF1;
    buf[2] = (profile - 1) << 6 | (freqIdx << 2) | (chanCfg >> 2);
    buf[3] = ((chanCfg & 3) << 6) | (fullLen >> 11);
    buf[4] = (fullLen >> 3) & 0xFF;
    buf[5] = ((fullLen & 7) << 5) | 0x1F;
    buf[6] = 0xFC;
}

/* ---------- 回调 ---------- */
static void OnError(OH_AVCodec *codec, int32_t errorCode, void *userData) {
    OH_LOG_ERROR(LOG_APP, "Encoder error %{public}d", errorCode);
}

static void OnOutputFormatChanged(OH_AVCodec *codec, OH_AVFormat *format, void *userData) {
    // AAC 暂未支持
}

static void OnNeedInputBuffer(OH_AVCodec *codec,
                              uint32_t index,
                              OH_AVBuffer *buffer,
                              void *userData) {
    if (g_input_done) return;

    uint8_t *addr = OH_AVBuffer_GetAddr(buffer);
    int32_t capacity = OH_AVBuffer_GetCapacity(buffer);
    ssize_t bytes = read(0, addr, capacity); // 从 stdin 读 PCM
    if (bytes <= 0) {
        OH_AudioCodec_Stop(codec);
        g_input_done = true;
        return;
    }
    OH_AVCodec_PushInputBuffer(codec, index);
}

static void OnNewOutputBuffer(OH_AVCodec *codec,
                              uint32_t index,
                              OH_AVBuffer *buffer,
                              OH_AVFormat *attr,
                              void *userData) {
    uint8_t *data = OH_AVBuffer_GetAddr(buffer);
    int32_t size  = OH_AVBuffer_GetSize(buffer);

    uint8_t adts[7];
    write_adts_header(adts, size);
    write(g_fd_out, adts, 7);
    write(g_fd_out, data, size);

    OH_AudioCodec_FreeOutputBuffer(codec, index);
}

static OH_AVCodecCallback g_callback = {
        .onError = OnError,
        .onStreamChanged = OnOutputFormatChanged,
        .onNeedInputBuffer = OnNeedInputBuffer,
        .onNewOutputBuffer = OnNewOutputBuffer,
};

/* ---------- NAPI 接口 ---------- */
static napi_value EncodeFile(napi_env env, napi_callback_info info) {
    g_encoder = OH_AudioCodec_CreateByMime(OH_AVCODEC_MIMETYPE_AUDIO_AAC, true);
    if (!g_encoder) {
        OH_LOG_ERROR(LOG_APP, "Create encoder failed");
        return nullptr;
    }

    OH_AVFormat *fmt = OH_AVFormat_Create();
    OH_AVFormat_SetIntValue(fmt, OH_MD_KEY_AUD_SAMPLE_RATE, SAMPLE_RATE);
    OH_AVFormat_SetIntValue(fmt, OH_MD_KEY_AUD_CHANNEL_COUNT, CHANNEL_COUNT);
    OH_AVFormat_SetIntValue(fmt, OH_MD_KEY_AUDIO_SAMPLE_FORMAT, SAMPLE_S16LE);
    OH_AVFormat_SetLongValue(fmt, OH_MD_KEY_BITRATE, BIT_RATE);
    OH_AVFormat_SetIntValue(fmt, OH_MD_KEY_AAC_IS_ADTS, 1); // 输出 ADTS

    OH_AudioCodec_RegisterCallback(g_encoder, &g_callback, nullptr);
    OH_AudioCodec_Configure(g_encoder, fmt);
    OH_AVFormat_Destroy(fmt);

    OH_AudioCodec_Prepare(g_encoder);

    g_fd_out = open("/data/storage/el2/base/haps/entry/files/out.aac",
                    O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (g_fd_out < 0) {
        OH_LOG_ERROR(LOG_APP, "open output failed");
        return nullptr;
    }

    OH_AudioCodec_Start(g_encoder);
    while (!g_input_done) {
        usleep(10 * 1000); // 简单阻塞等待
    }

    OH_AudioCodec_Stop(g_encoder);
    OH_AudioCodec_Destroy(g_encoder);
    close(g_fd_out);
    OH_LOG_INFO(LOG_APP, "encode done");
    return nullptr;
}

/* ---------- 注册 NAPI ---------- */
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"encodeFile", nullptr, EncodeFile, nullptr, nullptr, nullptr, napi_default, nullptr}
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",
    .nm_priv = nullptr,
    .reserved = {0},
};

extern "C" __attribute__((constructor)) void RegisterEntryModule() {
    napi_module_register(&demoModule);
}

2.3 使用方式

  1. test_44k_16bit_2ch.pcm 推送到 /data/storage/el2/base/haps/entry/files/in.pcm
  2. ArkTS 侧调用:
import entry from '@ohos.entry';
entry.encodeFile();
  1. 运行后可在同目录得到 out.aac(ADTS 封装,可直接播放验证)。

至此,完整的 Mermaid 流程图 + 可跑代码实现已给出,可直接集成到 HarmonyOS 工程中。

鸿蒙音频解码

以下是对鸿蒙音频解码模块的总结,包含核心流程、API设计与使用要点,以及Mermaid示意图:

一、鸿蒙音频解码核心流程

以下内容基于官方文档,用一张 Mermaid 时序图 把「鸿蒙音频解码(AVCodec Kit)」的核心流程、API 设计与使用要点一次性梳理出来。可直接复制到 mermaid.live 预览。

仅保留开发者最常调用的接口与顺序,省略了可选项与异常分支,方便快速上手。

%% 鸿蒙音频解码(AVCodec Kit)API 调用时序
sequenceDiagram
    participant App as 应用层
    participant Codec as OH_AVCodec* 解码器实例
    participant PCM as 音频输出(PCM)

    Note over App,PCM: 1. 准备阶段
    App ->> App: 1.1 添加头文件 & CMake 链接 libnative_media_codecbase.so
    App ->> Codec: 1.2 OH_AudioCodec_CreateByMime("audio/mp4a-latm") 或通过 codecName
    App ->> Codec: 1.3 OH_AudioCodec_RegisterCallback(cb) 注册 4 个回调
    App ->> Codec: 1.4 OH_AudioCodec_Configure(cfg) 配置采样率/声道/格式
    App ->> Codec: 1.5 OH_AudioCodec_Prepare()
    App ->> Codec: 1.6 OH_AudioCodec_Start()

    Note over App,PCM: 2. 运行阶段
    loop 解码循环
        Codec -->> App: OH_AVCodecOnNeedInputBuffer(idx, buf)
        App ->> Codec: OH_AudioCodec_PushInputBuffer(idx, buf, size, flags)
        Codec -->> App: OH_AVCodecOnNewOutputBuffer(idx, info, buf)
        App ->> PCM: 取走 PCM 数据
        App ->> Codec: OH_AudioCodec_FreeOutputBuffer(idx)
    end

    Note over App,PCM: 3. 结束阶段
    App ->> Codec: OH_AudioCodec_Stop()
    App ->> Codec: OH_AudioCodec_Destroy()
  1. 支持的输入格式

    AAC、FLAC、MP3、Vorbis、G711、AMR、APE、Opus、Audio Vivid 等,具体采样率/声道范围见文档表格。

  2. PCM 输出格式

    通过 OH_MD_KEY_AUDIO_SAMPLE_FORMAT 可选 SAMPLE_S16LESAMPLE_F32LE,默认 S16LE。

  3. 线程模型

    所有回调都在内部工作线程,请勿阻塞;应用需要保证线程安全。

  4. DRM 解密

    若内容加密,需在 Prepare 前调用 OH_AudioCodec_SetDecryptionConfig,并在 PushInputBuffer 时把 cencInfo 通过 OH_AVCencInfo_SetAVBuffer 写入。

  5. EOS 处理

    输入最后一包数据时把 flags 设为 AVCODEC_BUFFER_FLAGS_EOS,解码器会在回调中同样给出 EOS,应用即可进入停止流程。

  6. CMake 链接

   target_link_libraries(xxx
       native_media_codecbase.so
   )

一句话总结
「鸿蒙音频解码」遵循 创建→配置→启动→循环喂数据→取 PCM→停止销毁 的极简五步模型;所有细节都围绕 OH_AVCodec* 句柄与 4 个回调完成,无需额外线程或同步,直接嵌入现有播放 / 编辑管线即可。

audio-decode.png

二、API设计与使用要点

1. 核心API组件

组件类型 关键API 作用说明
解码器实例 OH_AudioCodec_CreateByMime() 通过MIME类型创建解码器
OH_AudioCodec_CreateByName() 通过编解码器名称创建
回调注册 OH_AudioCodec_RegisterCallback() 注册错误/数据流/缓冲区回调
参数配置 OH_AudioCodec_Configure() 设置采样率/声道数等参数
运行时控制 OH_AudioCodec_Start() 启动解码器
OH_AudioCodec_Flush() 刷新缓冲区(可选)
OH_AudioCodec_Reset() 重置解码器(可选)
数据操作 OH_AudioCodec_PushInputBuffer() 送入压缩数据
OH_AudioCodec_FreeOutputBuffer() 释放PCM输出数据
资源管理 OH_AudioCodec_Destroy() 销毁解码器实例

2. 关键开发步骤

  1. 创建实例

    // 通过MIME创建
    OH_AVCodec *decoder = OH_AudioCodec_CreateByMime(OH_AVCODEC_MIMETYPE_AUDIO_AAC, false);
    
    // 或通过名称创建
    const char *name = OH_AVCapability_GetName(capability);
    OH_AVCodec *decoder = OH_AudioCodec_CreateByName(name);
    
  2. 注册回调

    OH_AVCodecCallback cb = {OnError, OnOutputFormatChanged, 
                            OnInputBufferAvailable, OnOutputBufferAvailable};
    OH_AudioCodec_RegisterCallback(decoder, cb, userData);
    
  3. 配置参数

    OH_AVFormat *format = OH_AVFormat_Create();
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_SAMPLE_RATE, 44100);
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_CHANNEL_COUNT, 2);
    OH_AudioCodec_Configure(decoder, format);
    
  4. **数据流处理

    • 输入:在OnInputBufferAvailable回调中填充压缩数据

      OH_AVBuffer_SetBufferAttr(buffer, &attr); // 设置PTS/flag等属性
      OH_AudioCodec_PushInputBuffer(decoder, index);
      
    • 输出:在OnOutputBufferAvailable中获取PCM数据

      OH_AVBuffer_GetBufferAttr(data, &attr); // 获取解码数据属性
      OH_AudioCodec_FreeOutputBuffer(decoder, index); // 释放缓冲区
      

3. 特殊功能支持

  • DRM解密

    OH_AudioCodec_SetDecryptionConfig(decoder, session, false);
    OH_AVCencInfo_SetAVBuffer(cencInfo, buffer); // 设置加密信息
    
  • Audio Vivid元数据

    OH_AVFormat_GetBuffer(format, OH_MD_KEY_AUDIO_VIVID_METADATA, &metadata, &metaSize);
    

三、重要注意事项

  1. 调用顺序强制要求

    创建 → 配置 → 准备 → 启动必须顺序执行,否则引发异常

  2. 解码格式限制

    • 支持AAC/FLAC/MP3/Vorbis等主流格式
    • 不同格式有特定参数要求(如Vorbis需ID Header)
  3. 资源释放

    OH_AudioCodec_Stop(decoder);   // 先停止
    OH_AudioCodec_Destroy(decoder); // 再销毁
    
  4. 动态库依赖

    target_link_libraries(sample 
        libnative_media_codecbase.so
        libnative_media_core.so
        libnative_media_acodec.so)
    

文档链接音频解码-音视频编解码-AVCodec Kit(音视频编解码服务)-媒体 - 华为HarmonyOS开发者

❌