普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月9日掘金 前端

当AI Agent开始"职场内卷":你需要一个Agent Harness来当"项目经理"

2026年4月9日 18:49

从"单兵作战"到"团队混乱",再到"有序协作"的进化之路


引言:从"单兵作战"到"团队混乱"

各位程序员老铁们,你们有没有遇到过这种情况?

刚开始用AI的时候,觉得ChatGPT简直是神队友。写代码、改BUG、写文档,样样精通。你让它干啥它干啥,从不抱怨,从不请假,更不会在代码评审会上跟你argue设计模式。

但是! 当你开始玩起"多Agent协作"的时候,事情就变得微妙了。

想象一下这个场景:

Agent A(代码生成专员):"我已经写好了一个用户登录模块,采用了最新的JWT+Redis方案,代码整洁,注释完善,可以合并。"

Agent B(安全审查专员):"等等!这代码有SQL注入风险,第45行直接拼接了用户输入,必须整改!"

Agent C(性能优化专员):"而且你们注意到没有?这个查询没有加索引,用户量一上来数据库就挂了!"

Agent D(架构师):"我觉得我们应该用微服务架构,把这个模块拆分成认证服务、用户服务、会话服务..."

Agent E(产品经理):"其实用户只需要一个简单的登录框,你们能不能先做出来让我看看效果?"

此时此刻,作为人类程序员的你,看着这五个AI在终端里吵得不可开交,内心只有一个念头:

"我特么只是想加一个登录功能啊!!!"

这就是我们今天要聊的Agent Harness——一个用来管理这些"AI职场人"的"项目经理框架"。


第一章:当AI们开始"各说各话"

1.1 多Agent的美好幻想 vs 残酷现实

在理想世界里,多Agent协作是这样的:

  • 需求分析Agent先出马,把需求文档写得明明白白
  • 架构设计Agent紧随其后,画出完美的架构图
  • 代码生成Agent撸起袖子就是干,代码质量杠杠的
  • 测试Agent自动补全测试用例,覆盖率100%
  • 文档Agent同步更新文档,一个字都不用你改

听起来很美好对吧?

但实际上,现实往往是这样的:

第一幕:需求理解分歧

  • 需求分析Agent:"用户需要一个电商系统。"
  • 产品经理Agent:"不对,用户要的是社交电商,要有分享功能!"
  • UX设计Agent:"我觉得应该先做个用户调研..."

第二幕:技术选型战争

  • 后端Agent:"用Node.js,全栈JavaScript!"
  • 另一个后端Agent:"开玩笑,这种项目必须用Go,高并发!"
  • 架构师Agent:"你们都错了,云原生+Service Mesh才是未来!"

第三幕:代码冲突大爆炸

  • Agent A生成了UserController.ts,用了Class风格
  • Agent B在同一时间生成了user-controller.ts,用了函数式风格
  • Agent C:"我觉得应该用Vue 3..."
  • :"等等,这是个后端项目!"

1.2 为什么AI们会"吵架"?

其实这不怪AI,怪的是我们没有给它们一个统一的指挥系统

就像你让五个程序员各自为战,没有项目经理、没有技术负责人、没有代码规范、没有版本控制,最后不打架才怪。

每个Agent都是"专家",但:

  • ❌ 它们不知道其他Agent在干什么
  • ❌ 它们不知道自己的工作在整体流程中的位置
  • ❌ 它们没有一个"主心骨"来拍板决策
  • ❌ 它们更不知道何时该停手,何时该协作

这就需要一个Agent Harness——一个能够统筹管理所有Agent的"中枢神经系统"。


第二章:Agent Harness是什么?

2.1 接地气的定义

简单来说,Agent Harness就是AI Agent们的:

  • 👔 项目经理 - 分配任务、把控进度
  • 🚦 交通警察 - 指挥调度、维持秩序
  • 🤝 和事佬 - 解决冲突、协调关系

它的职责包括:

  1. 任务分配 - "你!去写代码!你!去审查!你!去边上歇会儿!"
  2. 流程编排 - "必须等设计完成才能写代码,懂不懂 waterfall?"
  3. 冲突解决 - "都别吵了!听我的!用React!"
  4. 状态管理 - "记录一下,这个Agent上次改代码把生产环境搞挂了,给它打个标签"
  5. 质量控制 - "这段代码审查不通过,打回去重写!"
  6. 资源调度 - "这个Agent今天已经生成了10000行代码了,让它休息一下吧..."

2.2 架构设计:从"菜市场"到"交响乐团"

没有Harness的多Agent系统 = 菜市场

  • 🗣️ 每个人都在大声吆喝
  • 📢 信息传递靠吼
  • 💸 交易(数据交换)混乱
  • 👣 经常有人被踩脚(资源冲突)

有了Harness之后 = 交响乐团

  • 🎼 指挥(Harness)拿着小棒站在中间
  • 🎻 乐手(Agents)各司其职
  • 🎵 乐谱(Workflow)规定好了每个人的节奏
  • 🎶 演奏出来的音乐(最终结果)和谐统一

2.3 Agent Harness的核心架构

1. 编排引擎(Orchestrator)

class AgentOrchestrator:
    def run_workflow(self, task):
        # 1. 分析任务,决定需要哪些Agent
        agents = self.select_agents(task)
        
        # 2. 制定执行计划
        plan = self.create_plan(agents, task)
        
        # 3. 按顺序或并行执行
        for step in plan:
            if step.type == "sequential":
                self.execute_sequential(step.agents)
            else:
                self.execute_parallel(step.agents)
        
        # 4. 整合结果
        return self.consolidate_results()

2. 上下文管理器(Context Manager)

负责维护共享状态,确保所有Agent都在"同一个频道"上:

class SharedContext:
    def __init__(self):
        self.state = {}
        self.history = []
    
    def update(self, agent_id, key, value):
        # 记录哪个Agent修改了什么
        self.state[key] = value
        self.history.append({
            "agent": agent_id,
            "action": "update",
            "key": key,
            "timestamp": now()
        })

3. 冲突解决器(Conflict Resolver)

当Agent们意见不一致时,需要一个"和事佬":

class ConflictResolver:
    def resolve(self, agent_opinions):
        # 策略1:投票制
        if self.strategy == "voting":
            return self.vote(agent_opinions)
        
        # 策略2:优先级制
        elif self.strategy == "priority":
            return self.select_by_priority(agent_opinions)
        
        # 策略3:人类介入
        elif self.strategy == "human_in_loop":
            return self.ask_human(agent_opinions)

4. 质量门禁(Quality Gates)

防止"渣代码"流入生产环境:

class QualityGate:
    def check(self, artifact):
        checks = [
            self.syntax_check(artifact),
            self.security_check(artifact),
            self.performance_check(artifact),
            self.style_check(artifact)
        ]
        return all(checks)

第三章:实战案例 - 让AI们"有序内卷"

3.1 场景:开发一个"用户评论系统"

假设我们要开发一个"用户评论系统",看看Agent Harness如何指挥。

阶段1:需求分析

workflow:
  name: "Comment Feature Development"
  steps:
    - name: "requirement_analysis"
      agent: "BA_Agent"
      task: "分析用户评论系统需求"
      output: "PRD文档"
    
    - name: "architecture_design"
      agent: "Architect_Agent"
      input: "PRD文档"
      task: "设计系统架构"
      output: "架构设计文档"
      depends_on: ["requirement_analysis"]

Harness的工作:

  1. 先唤醒BA_Agent,给它需求背景
  2. 等待BA_Agent产出PRD
  3. PRD通过质量检查(格式、完整性)
  4. 再唤醒Architect_Agent,把PRD喂给它

阶段2:并行开发

    - name: "backend_development"
      agent: "Backend_Dev_Agent"
      input: "架构设计文档"
      task: "开发后端API"
      output: "API代码"
      depends_on: ["architecture_design"]
    
    - name: "frontend_development"
      agent: "Frontend_Dev_Agent"
      input: "架构设计文档"
      task: "开发前端页面"
      output: "UI代码"
      depends_on: ["architecture_design"]
      
    - name: "database_design"
      agent: "DBA_Agent"
      input: "架构设计文档"
      task: "设计数据库表"
      output: "Schema定义"
      depends_on: ["architecture_design"]

Harness的工作:

  1. 检查依赖是否满足(架构设计已完成)
  2. 并行启动三个Agent
  3. 监控每个Agent的进度
  4. 如果某个Agent失败,决定是否重试或中断整个流程

阶段3:代码审查

    - name: "code_review"
      agents: ["Security_Agent", "Performance_Agent", "Style_Agent"]
      input: "所有代码"
      task: "代码审查"
      output: "审查报告"
      depends_on: ["backend_development", "frontend_development", "database_design"]
      merge_strategy: "consolidate"

这里有个有趣的点:三个审查Agent并行运行,各自关注不同方面。Harness需要合并它们的审查意见:

def merge_review_reports(reports):
    issues = []
    for report in reports:
        issues.extend(report.issues)
    
    # 去重和分类
    critical = [i for i in issues if i.severity == "critical"]
    warnings = [i for i in issues if i.severity == "warning"]
    
    if critical:
        return "REJECT", critical
    elif warnings:
        return "WARNING", warnings
    else:
        return "APPROVE", []

阶段4:冲突解决(重头戏)

假设冲突场景:

  • Backend_Agent:用了REST API
  • Frontend_Agent:期望的是GraphQL
  • Security_Agent:说"必须用HTTPS"
  • Performance_Agent:说"要加Redis缓存"

Harness的冲突解决逻辑:

class ConflictResolver:
    def resolve_api_style(self, backend_pref, frontend_pref):
        # 策略:前后端不一致时,优先满足前端(用户体验更重要)
        if backend_pref != frontend_pref:
            return {
                "decision": "使用REST + GraphQL Gateway",
                "reason": "Backend保持REST,Frontend通过Gateway访问",
                "implementation": "引入Apollo Federation"
            }
    
    def resolve_security_vs_performance(self, security_req, perf_req):
        # 策略:安全优先,性能其次
        if security_req.conflicts_with(perf_req):
            return {
                "decision": "先满足安全要求",
                "compromise": "通过优化实现方式减少性能影响",
                "action": "Security_Agent提出具体方案,Performance_Agent优化"
            }

3.2 完整代码示例

下面是一个简化的Agent Harness实现:

from typing import List, Dict, Any
from dataclasses import dataclass
from enum import Enum

class AgentStatus(Enum):
    IDLE = "idle"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"

@dataclass
class Agent:
    id: str
    name: str
    role: str
    capabilities: List[str]
    status: AgentStatus = AgentStatus.IDLE
    output: Any = None

class AgentHarness:
    def __init__(self):
        self.agents: Dict[str, Agent] = {}
        self.context = SharedContext()
        self.orchestrator = WorkflowOrchestrator()
        self.resolver = ConflictResolver()
    
    def register_agent(self, agent: Agent):
        """注册Agent到Harness"""
        self.agents[agent.id] = agent
        print(f"✅ Agent '{agent.name}' ({agent.role}) 已注册")
    
    def execute_workflow(self, workflow: Dict):
        """执行工作流"""
        print(f"🚀 开始执行工作流: {workflow['name']}")
        
        for step in workflow['steps']:
            result = self.execute_step(step)
            
            if result['status'] == 'failed':
                print(f"❌ 步骤 '{step['name']}' 失败")
                if not self.handle_failure(step, result):
                    break
            
            self.context.update(step['name'], result['output'])
        
        print("✨ 工作流执行完成")
        return self.context.get_final_output()
    
    def execute_step(self, step: Dict) -> Dict:
        """执行单个步骤"""
        agent_id = step.get('agent')
        agent_ids = step.get('agents', [])
        
        if agent_id:
            # 单Agent执行
            return self.run_single_agent(agent_id, step)
        else:
            # 多Agent并行执行
            return self.run_multi_agents(agent_ids, step)
    
    def run_single_agent(self, agent_id: str, step: Dict) -> Dict:
        """运行单个Agent"""
        agent = self.agents[agent_id]
        agent.status = AgentStatus.RUNNING
        
        print(f"🤖 Agent '{agent.name}' 开始工作: {step['task']}")
        
        try:
            # 实际调用Agent执行任务
            output = self.invoke_agent(agent, step)
            agent.status = AgentStatus.COMPLETED
            agent.output = output
            
            # 质量检查
            if not self.quality_gate.check(output):
                return {'status': 'failed', 'error': 'Quality check failed'}
            
            return {'status': 'completed', 'output': output}
        
        except Exception as e:
            agent.status = AgentStatus.FAILED
            return {'status': 'failed', 'error': str(e)}
    
    def run_multi_agents(self, agent_ids: List[str], step: Dict) -> Dict:
        """并行运行多个Agent"""
        print(f"👥 并行启动 {len(agent_ids)} 个Agent")
        
        results = []
        for agent_id in agent_ids:
            result = self.run_single_agent(agent_id, step)
            results.append(result)
        
        # 合并结果
        merge_strategy = step.get('merge_strategy', 'concat')
        merged = self.merge_results(results, merge_strategy)
        
        # 检查冲突
        if self.has_conflicts(results):
            print("⚠️ 检测到Agent间冲突,启动冲突解决...")
            resolution = self.resolver.resolve(results)
            merged = resolution
        
        return {'status': 'completed', 'output': merged}
    
    def has_conflicts(self, results: List[Dict]) -> bool:
        """检查结果之间是否有冲突"""
        outputs = [r['output'] for r in results if r['status'] == 'completed']
        
        for i, out1 in enumerate(outputs):
            for out2 in outputs[i+1:]:
                if self.detect_conflict(out1, out2):
                    return True
        return False
    
    def detect_conflict(self, output1, output2) -> bool:
        """检测两个输出是否冲突"""
        tech1 = output1.get('technology', '')
        tech2 = output2.get('technology', '')
        
        if tech1 and tech2 and tech1 != tech2:
            return True
        return False

第四章:最佳实践 - 如何让AI们"和谐共处"

4.1 给Agent们"定规矩"

就像人类团队需要代码规范一样,AI团队也需要"Agent规范"。

Agent行为准则

1. 单一职责原则

  • 每个Agent只做一件事
  • 不要搞"全栈Agent",容易精神分裂

2. 显式通信

  • 所有状态变更必须通过Harness
  • 禁止Agent之间"私聊"

3. 可追溯性

  • 每个决策都要记录理由
  • 方便出了问题"甩锅"(划掉)复盘

4. 优雅降级

  • Agent崩溃时,Harness要有备用方案
  • 实在不行就"人类介入"

4.2 Harness设计的坑与避坑指南

坑1:过度设计

错误示范:

# 为了"可扩展性",搞了复杂的插件系统
class AgentHarness:
    def __init__(self):
        self.plugin_manager = PluginManager()
        self.event_bus = EventBus()
        self.message_queue = MessageQueue()
        self.distributed_lock = DistributedLock()
        # ... 100行初始化代码

正确做法:

# 先实现核心功能,简单直接
class AgentHarness:
    def __init__(self):
        self.agents = {}
        self.context = {}
    
    def run(self, task):
        # 先能跑起来再说
        pass

坑2:忽视成本控制

💰 AI Agent每调用一次都是要花钱的! 一个设计不好的Workflow可能会让你的API账单爆炸。

优化策略:

  • 设置调用次数上限
  • 缓存Agent的输出
  • 对简单任务使用"廉价"模型(GPT-3.5)
  • 只在复杂任务上用"昂贵"模型(GPT-4)

坑3:完全自动化

⚠️ 记住:永远保留"人类介入"的开关

有些决策AI做不了,比如:

  • 这个需求合不合理?
  • 这个技术债要不要还?
  • 为了赶工期能不能先hack一下?
class HumanInTheLoop:
    def review(self, agent_decision):
        if agent_decision.confidence < 0.8:
            return self.ask_human(agent_decision)
        
        if agent_decision.risk_level == "high":
            return self.ask_human(agent_decision)
        
        return agent_decision

第五章:未来展望 - 当Harness学会"自我管理"

5.1 从"项目经理"到"CTO"

现在的Agent Harness还是个"项目经理",负责协调执行。

未来的Harness可能会进化成"CTO":

  • 自己决定招什么Agent(动态扩缩容)
  • 自己优化团队结构(Agent重组)
  • 自己制定技术战略(长期规划)
  • 甚至...自己解雇表现不好的Agent?
# 未来的AgentHarness
class SelfEvolvingHarness(AgentHarness):
    def optimize_team_structure(self):
        # 分析历史数据
        performance_data = self.analyze_performance()
        
        # 决定是否需要新Agent
        if performance_data.coverage < 0.9:
            new_agent = self.design_new_agent(
                capability_gap=performance_data.gaps
            )
            self.register_agent(new_agent)
        
        # 决定是否需要"裁员"
        for agent_id, perf in performance_data.items():
            if perf.efficiency < 0.3:
                self.retire_agent(agent_id)

5.2 从"单团队"到"多团队"

当系统复杂到一定程度,一个Harness管不过来了,就需要分层管理

  • Project Harness - 管理单个项目内的Agent
  • Department Harness - 管理多个项目的Harness
  • Company Harness - 管理整个公司的AI资源

这就形成了AI的"组织架构图"...


结语:让AI"卷"得更有序

说到底,Agent Harness解决的是一个古老的问题:

如何让多个智能体协作完成复杂任务

从人类团队到AI团队,道理是相通的:

  • 都需要明确的分工
  • 都需要有效的沟通
  • 都需要统一的指挥
  • 都需要质量控制

不同的是,AI们不会:

  • 抱怨加班
  • 要求涨薪
  • 在茶水间吐槽项目经理(至少现在不会)

所以,如果你也在玩多Agent系统,别再让它们"野蛮生长"了。给你的AI们配一个Harness吧,让它们"卷"得更有序、更高效。

毕竟,没有什么问题是一个好的管理层解决不了的,如果有,就加一层管理层——这句话对AI团队同样适用 😏

从零到一连接Solana:我在React项目中集成@solana/web3.js的实战与踩坑

作者 竹林818
2026年4月9日 18:01

背景

上个月,团队决定切入Solana生态,开发一个轻量级的NFT铸造平台。作为前端负责人,我的任务很明确:快速搭建一个能与Solana区块链交互的DApp界面。我之前的主要经验都在EVM链上,用惯了ethers.jswagmi,那一套流程已经刻在DNA里了。本以为切换到Solana,换个库@solana/web3.js应该大同小异,结果从项目初始化开始,就发现“水土不服”的情况比想象中多得多。最大的挑战不是理解概念,而是在真实的React组件中,如何稳定、优雅地实现连接钱包、读取链上数据、发送交易这一整套流程,并处理好各种边界情况和用户反馈。

问题分析

一开始,我的思路很“EVM”:找个类似wagmiRainbowKit的Solana一站式解决方案。我确实找到了@solana/wallet-adapter系列库,它提供了钱包连接器和React上下文。然而,在集成基础库@solana/web3.js时,我直接按照官方文档最简示例写,遇到了第一个拦路虎:连接对象(Connection)的创建与RPC节点的稳定性。文档里简单一句new Connection(clusterApiUrl('devnet')),在实际使用中频繁出现响应缓慢甚至超时,导致页面加载卡住,用户体验极差。我意识到,不能直接使用公共RPC,需要更可控的连接策略。同时,在尝试发送一笔简单的转账交易时,我遇到了各种序列化和签名错误,控制台报错信息对于新手来说并不友好,我需要拆解出从创建交易到广播的每一步,并找到其中容易出错的关键点。

核心实现

第一步:建立稳定且可配置的RPC连接

我放弃了直接使用clusterApiUrl,因为它指向的公共节点在流量大时很不稳定。解决方案是使用一个可靠的RPC服务提供商(如QuickNode、Helius等)的私有端点,并封装一个可重试、可降级的连接创建函数。

这里有个关键点:@solana/web3.jsConnection构造函数第二个参数可以设置commitment级别和WebSocket配置。commitment决定了节点确认数据的程度,对于查询余额,‘confirmed’通常就够了,但对于交易,有时需要‘finalized’。另外,启用disableRetryOnRateLimit对于私有端点通常设为false。

import { Connection, clusterApiUrl } from '@solana/web3.js';

// 配置你的RPC端点,优先使用环境变量中的私有端点
const getRpcUrl = (): string => {
  // 从环境变量读取,如果没有则降级到公共开发网节点(不推荐生产环境)
  return process.env.REACT_APP_SOLANA_RPC_URL || clusterApiUrl('devnet');
};

export const createConnection = (): Connection => {
  const rpcUrl = getRpcUrl();
  console.log(`Connecting to RPC: ${rpcUrl}`);
  
  return new Connection(rpcUrl, {
    commitment: 'confirmed', // 默认确认级别
    disableRetryOnRateLimit: false, // 启用速率限制重试
    confirmTransactionInitialTimeout: 60000, // 增加交易确认超时时间
  });
};

// 在应用中作为单例使用
export const connection = createConnection();

第二步:集成钱包适配器与连接状态管理

我选择了@solana/wallet-adapter-react@solana/wallet-adapter-wallets来管理钱包连接UI和状态。这一步相对顺畅,但需要注意钱包插件的动态导入以避免首屏加载过大。

注意这个细节WalletAdapterNetwork用于指定网络,但如果你用的私有RPC,需要确保钱包插件(如Phantom)也切换到对应网络(如Devnet)。

// WalletProvider.tsx
import React, { useMemo } from 'react';
import { ConnectionProvider, WalletProvider as SolanaWalletProvider } from '@solana/wallet-adapter-react';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '@solana/web3.js';

// 导入默认样式
import '@solana/wallet-adapter-react-ui/styles.css';

// 使用我们上面创建的稳定连接
import { connection } from './utils/connection';

export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  // 可以根据需要动态设置网络,这里我们与connection保持一致(假设是devnet)
  const network = WalletAdapterNetwork.Devnet;

  // 动态初始化钱包适配器
  const wallets = useMemo(
    () => [
      new PhantomWalletAdapter(),
      new SolflareWalletAdapter({ network }),
      // 可以添加更多钱包
    ],
    [network]
  );

  return (
    <ConnectionProvider endpoint={connection.rpcEndpoint}>
      <SolanaWalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>{children}</WalletModalProvider>
      </SolanaWalletProvider>
    </ConnectionProvider>
  );
};

index.tsxApp.tsx中用WalletProvider包裹你的应用。

第三步:获取账户余额与代币信息

连接钱包后,第一件事就是显示用户的SOL余额。这里需要用到connection.getBalance方法,参数是用户的公钥(PublicKey)。

这里有个坑getBalance返回的值是以lamports为单位的,1 SOL = 10^9 lamports。需要手动转换。另外,余额查询是一个异步操作,需要考虑加载和错误状态。

import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { useState, useEffect } from 'react';

export const BalanceDisplay: React.FC = () => {
  const { connection } = useConnection();
  const { publicKey, connected } = useWallet();
  const [balance, setBalance] = useState<number | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchBalance = async () => {
      if (!connected || !publicKey) {
        setBalance(null);
        return;
      }
      setLoading(true);
      setError(null);
      try {
        const balanceInLamports = await connection.getBalance(publicKey);
        const balanceInSOL = balanceInLamports / LAMPORTS_PER_SOL;
        setBalance(balanceInSOL);
      } catch (err: any) {
        console.error('Failed to fetch balance:', err);
        setError(err.message);
        setBalance(null);
      } finally {
        setLoading(false);
      }
    };

    fetchBalance();
    // 可以设置一个定时器轮询余额,或者监听区块变化来更新,这里简单处理
    const intervalId = setInterval(fetchBalance, 30000); // 每30秒更新一次
    return () => clearInterval(intervalId);
  }, [connection, publicKey, connected]);

  if (!connected) return <p>请连接钱包</p>;
  if (loading) return <p>查询余额中...</p>;
  if (error) return <p>错误: {error}</p>;
  return <p>余额: {balance !== null ? `${balance.toFixed(4)} SOL` : 'N/A'}</p>;
};

第四步:构造并发送一笔SOL转账交易

这是最核心也最容易出错的部分。一笔基本的SOL转账涉及:创建交易指令SystemProgram.transfer,将指令添加到交易中,获取最近区块哈希(recent blockhash),设置手续费支付者,最后由钱包签名并发送。

踩坑预警

  1. 区块哈希(blockhash):每笔交易都需要一个最近的区块哈希,用于交易过期和防止重放。必须通过connection.getLatestBlockhash()获取,不能使用过期的。
  2. 签名者数组sendTransaction需要传入签名者数组。对于简单的转账,只有付款人(即连接的钱包)需要签名,但必须将其钱包适配器对象转换成Signer接口要求的格式。@solana/wallet-adapter-reactuseWallet钩子提供了signTransaction方法,但更简单的方式是使用钱包适配器实例本身。
  3. 交易确认:发送交易后,sendTransaction返回的是交易签名(txid)。这并不代表交易成功,必须调用connection.confirmTransaction来等待网络确认。
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { SystemProgram, Transaction, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { useState } from 'react';

export const TransferSol: React.FC = () => {
  const { connection } = useConnection();
  const { publicKey, sendTransaction } = useWallet();
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [status, setStatus] = useState<'idle' | 'sending' | 'confirming' | 'success' | 'error'>('idle');
  const [txSignature, setTxSignature] = useState<string>('');
  const [errorMsg, setErrorMsg] = useState('');

  const handleTransfer = async () => {
    if (!publicKey || !recipient || !amount) {
      setErrorMsg('请填写完整信息并确保钱包已连接');
      return;
    }
    setStatus('sending');
    setErrorMsg('');
    setTxSignature('');

    try {
      // 1. 验证接收地址
      const toPubkey = new PublicKey(recipient);
      // 2. 转换金额为lamports
      const lamports = parseFloat(amount) * LAMPORTS_PER_SOL;
      if (isNaN(lamports) || lamports <= 0) {
        throw new Error('请输入有效的金额');
      }

      // 3. 获取最新的区块哈希和区块高度
      const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');

      // 4. 创建转账指令
      const transferInstruction = SystemProgram.transfer({
        fromPubkey: publicKey,
        toPubkey: toPubkey,
        lamports,
      });

      // 5. 创建交易并添加指令
      const transaction = new Transaction({
        feePayer: publicKey,
        recentBlockhash: blockhash,
      }).add(transferInstruction);

      // 6. 发送交易并获取签名
      const signature = await sendTransaction(transaction, connection);
      setTxSignature(signature);
      setStatus('confirming');
      console.log(`交易已发送,签名: ${signature}`);

      // 7. 确认交易
      const confirmation = await connection.confirmTransaction({
        signature,
        blockhash,
        lastValidBlockHeight,
      }, 'confirmed');

      if (confirmation.value.err) {
        throw new Error(`交易确认失败: ${JSON.stringify(confirmation.value.err)}`);
      }
      setStatus('success');
      console.log('交易成功确认!');

    } catch (error: any) {
      console.error('转账失败:', error);
      setStatus('error');
      setErrorMsg(error.message || '未知错误');
    }
  };

  return (
    <div>
      <h3>转账SOL</h3>
      <div>
        <input
          type="text"
          placeholder="接收方地址"
          value={recipient}
          onChange={(e) => setRecipient(e.target.value)}
        />
        <input
          type="number"
          step="0.001"
          placeholder="金额 (SOL)"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
        <button onClick={handleTransfer} disabled={status === 'sending' || status === 'confirming'}>
          {status === 'sending' ? '发送中...' : status === 'confirming' ? '确认中...' : '转账'}
        </button>
      </div>
      {status === 'success' && <p style={{ color: 'green' }}>转账成功!签名: {txSignature}</p>}
      {status === 'error' && <p style={{ color: 'red' }}>错误: {errorMsg}</p>}
      {txSignature && status !== 'error' && (
        <p>
          交易签名: <a href={`https://explorer.solana.com/tx/${txSignature}?cluster=devnet`} target="_blank" rel="noreferrer">在浏览器查看</a>
        </p>
      )}
    </div>
  );
};

完整代码示例

以下是一个整合了以上所有功能的简化版App.tsx

// App.tsx
import React from 'react';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { BalanceDisplay } from './components/BalanceDisplay';
import { TransferSol } from './components/TransferSol';
import { WalletProvider } from './providers/WalletProvider';
import './App.css';

// 主应用组件,需要被WalletProvider包裹
const AppContent: React.FC = () => {
  return (
    <div className="App">
      <header>
        <h1>Solana NFT平台(演示)</h1>
        <WalletMultiButton />
      </header>
      <main>
        <section>
          <h2>账户信息</h2>
          <BalanceDisplay />
        </section>
        <section>
          <h2>转账功能</h2>
          <TransferSol />
        </section>
        {/* 后续可以添加NFT查询、铸造等组件 */}
      </main>
    </div>
  );
};

// 顶层App,提供钱包上下文
const App: React.FC = () => {
  return (
    <WalletProvider>
      <AppContent />
    </WalletProvider>
  );
};

export default App;

踩坑记录

  1. Transaction recent blockhash required 错误:这是我最早遇到的错误。我一开始手动写死了一个区块哈希,或者忘记设置recentBlockhash解决方法:必须通过connection.getLatestBlockhash()动态获取,并确保这个哈希在交易被确认前是有效的(通过lastValidBlockHeight判断)。

  2. Signature verification failedWallet not connected 错误:在调用sendTransaction时,虽然钱包连接着,但交易签名失败。排查发现:我错误地尝试自己用私钥签名,或者没有正确使用钱包适配器提供的sendTransaction方法。解决方法:在React组件中,始终使用useWallet钩子暴露出的sendTransaction方法,它会自动处理与钱包扩展的交互和签名。

  3. 交易发送成功但一直不确认(Pending):在Devnet上,有时交易会卡住。原因:可能是RPC节点问题,或者手续费不足(虽然SOL转账手续费极低且固定)。解决方法:首先检查使用的RPC节点是否健康;其次,在confirmTransaction时使用更长的超时时间(如上面代码中在创建Connection时设置confirmTransactionInitialTimeout);最后,可以尝试重新获取一个全新的区块哈希并重新构建交易。

  4. 类型错误:Property ‘publicKey’ does not exist on type ‘WalletContextState’:在使用useWallet()的解构时,publicKey可能是null解决方法:在代码中始终对publicKeyconnected状态进行判空处理,使用可选链操作符?.或条件渲染。TypeScript的严格模式会强制你处理这些可能为null的情况,这是好事。

小结

通过这个项目,我深刻体会到不同区块链生态的前端开发虽有共通模式,但魔鬼藏在细节里。@solana/web3.js的核心在于对交易结构(Transaction, Instruction)和网络状态(Blockhash, Commitment)的精细控制。下一步,我可以在此基础上深入代币(SPL Token)操作、NFT元数据获取与铸造等更复杂的交互场景,并考虑引入状态管理库(如Zustand)来更好地管理全局的链上数据和交易状态。

【节点】[ReciprocalSquareRoot节点]原理解析与实际应用

作者 SmalBox
2026年4月9日 17:50

【Unity Shader Graph 使用与特效实现】专栏-直达

在 Unity URP Shader Graph 中,Reciprocal Square Root 节点是一个功能强大且高效的数学运算节点,专门用于计算输入值的平方根倒数。这个节点在图形编程和实时渲染中具有特殊的重要性,因为它能够以优化的方式执行一个在着色器中频繁使用的数学运算。

平方根倒数计算在计算机图形学中无处不在,从向量归一化到光照计算,从物理模拟到后期处理效果,都需要频繁使用这个数学运算。传统上,直接计算平方根再求倒数是一个相对昂贵的操作,特别是在需要处理大量像素的片段着色器中。Reciprocal Square Root 节点通过内部优化算法,提供了比分别计算平方根和倒数更高效的计算方式。

数学原理与背景

平方根倒数的数学定义

从数学角度来看,平方根倒数可以表示为:对于任意正实数 x,其平方根倒数为 1/√x。这个运算等价于 x 的-1/2 次幂。在 Shader Graph 中,这个运算被扩展到支持各种数据类型,包括标量、向量和矩阵。

平方根倒数在几何计算中特别有用,因为它与向量长度的归一化密切相关。当我们有一个向量 v,其长度为 |v|,那么归一化后的向量为 v/|v|。如果我们预先计算 1/|v|,那么归一化操作就简化为 v 乘以这个预先计算的值,这正是平方根倒数的应用场景。

计算机图形学中的重要性

在实时渲染中,性能是至关重要的考量因素。平方根倒数运算由于其复杂的数学特性,通常需要较多的计算资源。历史上,平方根倒数的计算甚至催生了一些著名的优化算法,其中最著名的是 Quake III Arena 中的快速平方根倒数算法,该算法通过巧妙的位操作和牛顿迭代法实现了惊人的计算速度。

虽然现代 GPU 硬件已经对这类运算进行了高度优化,但理解其背后的数学原理和性能特性仍然对编写高效着色器至关重要。Reciprocal Square Root 节点抽象了这些底层优化,为开发者提供了既简单又高效的工具。

节点功能详解

基本运算逻辑

Reciprocal Square Root 节点的核心功能非常直接:它接收一个输入值,计算该值的平方根,然后返回其倒数。从数学角度,如果输入是 x,那么输出就是 1/√x。

这个节点支持多种数据类型,包括:

  • 浮点数标量
  • 二维向量
  • 三维向量
  • 四维向量

当输入为向量时,节点会对每个分量独立执行平方根倒数运算。例如,对于输入向量(a, b, c),输出将是(1/√a, 1/√b, 1/√c)。

特殊输入值处理

对于特殊输入值,节点有明确的行为定义:

  • 对于正值输入,节点返回正常的平方根倒数
  • 对于零输入,理论上 1/√0 是未定义的,但节点会返回一个极大值以避免除零错误
  • 对于负值输入,平方根在实数域内未定义,节点会返回 NaN(Not a Number)或根据平台返回未定义结果

在实际应用中,建议确保输入值始终为非负,除非你明确知道负值输入的含义并已做好相应处理。

端口详细说明

输入端口

In 端口是节点的唯一输入,接受动态矢量类型。这意味着它可以连接任何维度的向量或标量值。输入值的范围通常应为非负数,尽管节点对负值输入有一定的容错能力。

输入端口的数据流特性:

  • 支持逐分量操作
  • 自动进行类型推广
  • 可以与各种其他节点组合使用

输出端口

Out 端口提供计算结果的输出,其维度与输入保持一致。输出值的范围取决于输入:

  • 当输入接近零时,输出趋近于无穷大
  • 当输入为 1 时,输出为 1
  • 当输入增大时,输出逐渐减小并趋近于零

输出的精度取决于目标平台和精度设置,在大多数现代 GPU 上,能够提供足够的精度满足图形计算需求。

实际应用场景

向量归一化优化

在着色器中,向量归一化是最常见的操作之一。传统归一化需要计算向量长度,然后每个分量除以该长度。使用 Reciprocal Square Root 节点可以优化这一过程:

// 传统归一化
float length = sqrt(dot(vector, vector));
float3 normalized = vector / length;

// 使用平方根倒数的优化归一化
float rcpLength = rsqrt(dot(vector, vector));
float3 normalized = vector * rcpLength;

这种方法在数学上是等价的,但通常更高效,因为 rsqrt 操作在硬件层面可能比先算平方根再算除法更优化。

光照计算

在光照模型中,经常需要计算距离的倒数或距离平方的倒数。例如,在点光源衰减计算中:

float distanceSq = dot(lightVector, lightVector);
float attenuation = 1.0 / (1.0 + lightAttenuation * distanceSq);

在某些情况下,使用平方根倒数可以重新组织计算,可能带来性能提升或数值稳定性改善。

物理模拟

在物理基础的渲染中,许多 BRDF(双向反射分布函数)包含基于距离或角度的归一化因子。这些因子经常涉及平方根倒数运算。例如,在计算微表面模型的几何项时:

float SmithGGXGeometric(float NdotV, float roughness)
{
    float a = roughness * roughness;
    float k = a / 2.0;
    return NdotV / (NdotV * (1.0 - k) + k);
}

在某些优化版本中,可能会使用平方根倒数来简化计算。

屏幕空间效果

在后期处理效果中,如景深、模糊或光晕效果,经常需要基于像素距离计算权重。平方根倒数可以用于创建特定的衰减曲线:

float2 screenUV = i.uv - 0.5;
float distanceFromCenter = length(screenUV);
float weight = rsqrt(1.0 + distanceFromCenter * distanceFromCenter * intensity);

这种方法创建了一种平滑的衰减效果,适用于许多屏幕空间效果。

性能考量与最佳实践

硬件优化

现代 GPU 通常对平方根倒数运算有专门的硬件支持。与分别计算平方根和倒数相比,使用专门的 rsqrt 指令通常能够:

  • 减少指令数量
  • 提高计算吞吐量
  • 降低功耗

然而,具体的性能优势因 GPU 架构而异。在移动设备上,这种优化可能更为显著,因为移动 GPU 通常对复杂数学运算的资源更加有限。

精度考虑

虽然平方根倒数运算在大多数情况下提供了足够的精度,但在极端情况下可能需要特别注意:

  • 对于非常小的输入值,可能会遇到浮点数下溢问题
  • 对于非常大的输入值,可能会遇到精度损失
  • 在需要高精度计算的场合,考虑使用更高精度的数据类型

在 URP Shader Graph 中,可以通过节点的精度设置来控制计算精度,平衡性能和质量需求。

适用场景判断

并非所有情况都适合使用平方根倒数节点。以下是一些指导原则:

适合使用 Reciprocal Square Root 节点的场景:

  • 需要计算归一化因子时
  • 需要基于距离的衰减函数时
  • 需要计算物理正确的光照时
  • 当性能是关键考量时

可能不适合的场景:

  • 当只需要平方根而不需要倒数时
  • 当输入值可能为零或负数且未做适当处理时
  • 当计算流程更直观地表达为其他形式时

与其他节点的组合使用

与数学节点组合

Reciprocal Square Root 节点可以与其他数学节点组合,创建复杂的数学表达式:

  • 与乘法节点组合,实现向量归一化
  • 与条件节点组合,处理边界情况
  • 与插值节点组合,创建平滑过渡效果

例如,创建一个安全的平方根倒数函数,避免除零错误:

SafeReciprocalSquareRoot(float x)
{
    float epsilon = 0.0001;
    return rsqrt(max(x, epsilon));
}

在子图中的应用

对于频繁使用的平方根倒数模式,可以将其封装为自定义子图。例如,创建一个"安全归一化"子图,自动处理零向量的情况:

SafeNormalize(float3 vector)
{
    float sqLength = dot(vector, vector);
    float safeInvLength = sqLength > 0.0 ? rsqrt(sqLength) : 0.0;
    return vector * safeInvLength;
}

这种方法提高了代码的可重用性和可读性。

生成代码分析

HLSL 代码实现

在生成的 HLSL 代码中,Reciprocal Square Root 节点通常对应于 rsqrt() 函数。如文档中提供的示例:

void Unity_ReciprocalSquareRoot_float4(float4 In, out float4 Out)
{
    Out = rsqrt(In);
}

这个简单的封装函数直接调用了 HLSL 内置的 rsqrt 函数,该函数针对目标平台进行了优化。

跨平台兼容性

虽然 rsqrt 函数在大多数现代图形 API 中都有支持,但 Shader Graph 会确保生成的代码在不同平台上的兼容性。在某些平台上,可能会使用不同的函数名或实现方式,但 Shader Graph 会处理这些差异,为开发者提供一致的接口。

实际示例与案例研究

案例一:点光源衰减优化

假设我们有一个点光源,需要计算基于距离的衰减。传统方法可能这样写:

float3 lightVector = lightPosition - worldPosition;
float distance = length(lightVector);
float attenuation = 1.0 / (1.0 + lightAttenuation * distance * distance);

使用 Reciprocal Square Root 节点可以优化为:

float3 lightVector = lightPosition - worldPosition;
float distanceSq = dot(lightVector, lightVector);
float rcpDistance = rsqrt(distanceSq);
float attenuation = 1.0 / (1.0 + lightAttenuation * distanceSq);

虽然在这个特定例子中,优化可能不明显,但在更复杂的计算中,这种模式可能带来性能提升。

案例二:法线分布函数

在基于物理的渲染中,法线分布函数(如 GGX)经常包含平方根运算。以下是 GGX NDF 的标准实现:

float GGXDistribution(float NdotH, float roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH2 = NdotH * NdotH;

    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return a2 / denom;
}

通过重新组织数学表达式,可以在某些部分使用平方根倒数来优化计算。

故障排除与常见问题

数值不稳定问题

当输入值非常接近零时,平方根倒数可能产生极大的值,导致数值不稳定。解决方法包括:

  • 对输入值进行钳制,确保不低于某个小正值
  • 使用条件语句处理特殊情况
  • 重新设计算法,避免极端情况

性能问题诊断

如果怀疑 Reciprocal Square Root 节点导致性能问题,可以:

  • 使用 Unity 的 Frame Debugger 或 RenderDoc 分析着色器性能
  • 尝试替换为其他数学表达式,比较性能差异
  • 检查目标平台的特定优化建议

平台兼容性问题

虽然 Shader Graph 尽力保证跨平台兼容性,但在某些边缘情况下可能会遇到问题:

  • 旧式移动设备可能对某些数学运算支持有限
  • 不同的精度设置可能导致细微的视觉差异
  • 特定平台的驱动程序可能有不同的优化策略

【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

记一次ESLint失效--(附原理分析)

作者 我的刀盾
2026年4月9日 17:11

生成ESLint标题图片 (1).png 近日突然发现 ESLint 在 VSCode 中无法自动保存了。

问题现象

保存文件时,ESLint 不再自动修复代码格式(如缩进、引号、分号等)。

当时的配置

settings.json 配置如下:

{
  "workbench.colorTheme": "Default Dark+",
  "typescript.locale": "zh-CN",
  "gitlens.defaultDateLocale": "",
  "editor.fontSize": 14,
  "eslint.format.enable": true,
  "eslint.enable": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.codeActionsOnSave.rules": null,
  "editor.indentSize": "tabSize",
  "eslint.validate": ["javascript", "javascriptreact", "vue", "html"],
  "commentTranslate.hover.string": true,
  "commentTranslate.hover.variable": true
}

根目录配置了 .eslintrc.js,package.json 中的 ESLint 相关依赖:

"@vue/cli-plugin-eslint": "~4.5.15",  // Vue CLI 对 ESLint 的集成,提供 vue-cli-service lint 命令和开发时的 lint-on-save
"eslint": "^6.7.2",                    // ESLint 核心引擎,负责解析规则并执行检查和修复
"eslint-plugin-prettier": "^3.1.4",    // 将 Prettier 的格式化规则作为 ESLint 规则运行,使两者统一
"eslint-plugin-vue": "^6.2.2",         // 提供 Vue 文件(.vue)的 lint 规则,如模板语法检查、组件命名规范等
// 另外 @babel/eslint-parser: 7.15.8  — JS 解析器,让 ESLint 能识别 Babel 转译的语法(如可选链 ?.、空值合并 ?? 等)

尝试修改配置后想到可能是版本问题导致的。于是安装了 ESLint 的历史版本,重启搞定。

image.png

猜测 package.json 安装版本与 VSCode 插件版本冲突。


深入分析:ESLint 自动保存涉及的三层配置

要搞清楚 ESLint 自动保存为什么会失效,首先需要理解整个机制涉及三层配置,它们各司其职:

第 1 层:VSCode/Cursor 全局 User Settings

文件路径: %APPDATA%/Code/User/settings.json(VSCode)或 %APPDATA%/Cursor/User/settings.json(Cursor)

作用: 所有项目的默认编辑器配置。

管什么:

  • ESLint 插件是否启用(eslint.enable
  • 保存时是否触发 ESLint 修复(source.fixAll.eslint
  • 校验哪些文件类型(eslint.validate
  • 各语言的默认格式化器(editor.defaultFormatter

第 2 层:项目 .vscode/settings.json

文件路径: 项目根目录下 .vscode/settings.json

作用: 仅对当前项目生效,会覆盖全局配置中的同名项

管什么: 与全局配置相同的字段,但优先级更高。适合团队统一配置,跟随项目代码提交到 Git。

第 3 层:.eslintrc.js

文件路径: 项目根目录下 .eslintrc.js

作用: 定义具体的代码检查和格式化规则。

管什么:

  • 用单引号还是双引号(quotes
  • 要不要分号(semi
  • 缩进几个空格(indent
  • Vue 模板规范(vue/max-attributes-per-line 等)

补充:.eslintignore — 忽略文件配置

项目根目录下的 .eslintignore 用于指定 ESLint 不需要检查的文件和目录,语法与 .gitignore 一致。当前项目配置如下:

build/*.js                          # 构建脚本,自动生成的代码无需检查
src/assets                          # 静态资源目录(图片、字体等),不含需要 lint 的代码
public                              # 公共静态文件,直接拷贝到输出目录,不经过编译
dist                                # 打包产物,自动生成无需检查
src/views/index/styles/mixins.scss  # SCSS mixin 文件,ESLint 不处理样式文件
node_modules/                       # 第三方依赖,永远不应该被检查

如果不配置 .eslintignore,ESLint 会尝试检查项目中所有匹配 eslint.validate 的文件,包括 node_modulesdist 中的文件,导致检查变慢甚至报大量无关错误。

三者的关系图

┌──────────────────────────────────────────────────┐
│  全局 User Settings                               │
│  决定:ESLint 插件开不开、保存时触不触发              │
│  优先级:最低(被项目配置覆盖)                       │
├──────────────────────────────────────────────────┤
│  项目 .vscode/settings.json                       │
│  决定:当前项目的编辑器行为,覆盖全局同名配置           │
│  优先级:中                                        │
├──────────────────────────────────────────────────┤
│  .eslintrc.js                                     │
│  决定:具体用什么规则检查和修复代码                    │
│  优先级:规则层面最终决定                             │
└──────────────────────────────────────────────────┘

简单记忆:settings.json 管"开关和触发时机",.eslintrc.js 管"具体规则"。

优先级规则

对于编辑器配置(settings.json 中的字段):

项目 .vscode/settings.json  >  全局 User settings.json

同名配置存在于两处时,项目级优先。例如:

配置项 全局配置 项目配置 最终生效
[vue] defaultFormatter Vue.volar null null
[javascript] defaultFormatter Prettier null null
source.fixAll.eslint explicit explicit explicit

保存时的完整执行链路

按下 Ctrl+S 保存文件
  
  
VSCode/Cursor 检查 settings.json 配置
  ├─ editor.formatOnSave?   true: 触发默认格式化器
  ├─ source.fixAll.eslint?  explicit: 触发 ESLint 自动修复
  
  
ESLint 插件(dbaeumer.vscode-eslint)开始工作
  
  ├─ 1. 在项目 node_modules/ 中查找 eslint 引擎
  ├─ 2. 读取 .eslintrc.js 中定义的规则
  └─ 3. 按规则执行自动修复
       ├─ quotes: 'single'      双引号改单引号
       ├─ semi: 'never'         删除分号
       ├─ indent: 2             改为 2 格缩进
       └─ ...其他可自动修复的规则

关键点:ESLint 插件是遥控器,项目 node_modules 中的 eslint 包是电视机,.eslintrc.js 是频道列表。三者缺一不可。


失效的常见原因

1. ESLint 插件版本与项目 eslint 包版本不兼容

这是本次遇到的问题。VSCode ESLint 插件会调用项目 node_modules/eslint 中的引擎,如果插件升级后不再支持旧版 eslint API,就会静默失败。

解决方案: 降级/升级项目中的 eslint 版本,使其与插件兼容。

2. 格式化器冲突

如果同时配置了 editor.formatOnSave: truesource.fixAll.eslint,且默认格式化器(如 Prettier、Volar)的规则和 .eslintrc.js 不一致,就会出现"保存后格式反复跳动"的问题。

典型冲突:

规则 Prettier 默认 .eslintrc.js 配置
引号 双引号 " 单引号 '
分号 ;
尾逗号

解决方案: 关闭 editor.formatOnSave,或者将格式化器设为 null,只保留 ESLint 修复:

{
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "[vue]": {
    "editor.defaultFormatter": null
  },
  "[javascript]": {
    "editor.defaultFormatter": null
  }
}

3. ESLint 插件未安装或被禁用

没有安装 dbaeumer.vscode-eslint 插件,或者插件被工作区禁用了。

检查方式: VSCode 左下角状态栏查看是否有 ESLint 图标,点击可查看插件状态和输出日志。

4. node_modules 未安装

项目没有执行 npm install,导致 node_modules/eslint 不存在,插件找不到引擎。

5. .eslintrc.js 配置错误

配置文件中有语法错误或引用了未安装的插件/解析器,导致 ESLint 初始化失败。

检查方式: 打开 VSCode 输出面板(Ctrl+Shift+U),选择 ESLint 通道查看错误日志。


推荐的项目级配置

在项目根目录创建 .vscode/settings.json,跟随代码提交到 Git,确保团队统一:

{
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "vue"
  ],
  "eslint.options": {
    "extensions": [".js", ".vue"]
  },
  "[vue]": {
    "editor.defaultFormatter": null
  },
  "[javascript]": {
    "editor.defaultFormatter": null
  }
}

这样保存时只使用项目 .eslintrc.js 中定义的规则进行自动修复,不会被全局配置或其他格式化器干扰。

复刻字节 AI 开发流:实践 Node.js 通用脚手架

2026年4月9日 17:02

前言

最近与前同事深入交流,他现为字节某组的 TL(组长),团队规模近 10 人。在讨论他们团队的 AI 工作流实践中,也得到一些八卦信息。

第一个是: 人才储备变化

他们团队已基本停止招聘实习生,今年仅招一人 个人解读: AI 协作模式下,新人的边际效应大幅下降

第二个是:组织预期转变

业内多位跟他同级的 Leader 私下评估:今年可能出现大规模调整 个人解读: 生成式 AI 在降本增效上的潜力被普遍看好,提前做好裁员准备

第三个是:开发方法论的周期性

  • 层出不穷的新概念:Vibe Coding、SDD 规范、Harness Engineering……
  • 理性看待:这些都是过渡阶段产物,随着大模型升级,这些方法论的生命周期可能只有几个月 个人建议: 不必投入过多精力去追赶,因为等你熟练一种方法论后,可能用不了多久就被淘汰了

第四个是:招聘标准演变 前端岗位招聘标准很多 JD 上开始冠以"全栈开发" 的名义招聘了 实际面试:侧重前端能力考察,但开始要求一些后端基础能力了 个人解读: AI 协作工具提效下,企业希望前端承担更多职能

好了,八卦之后,关于他们的 ai 工作流的核心如下:

你得教 AI,不断沉淀并固化它的规范

具体实践方法:

  • 识别问题:第一次遇到某类问题,AI 执行效果不理想
  • 人工介入:手动处理并解决问题
  • 规则沉淀:将解决方案固化为规则或 Skill
  • 迭代积累:问题解决越多,效率提升越明显,也就是后期 ai 执行的只要不是新的场景和复杂业务,基本不会出错

本质: 通过持续的反馈循环来训练和优化 AI 的工作能力,而不是一开始就寻求完美的工作流

这个理念在字节技术专家杨晨的全软软件开发大会分享中也提出过:Prompt = 可训练资产(像模型一样优化)

让后我个人抽象了这个单 Agent 的工作流程图如下:

image.png

接下来我会解释每个步骤的思路和如何落地:

步骤 1:项目初始化 + 全局规则设计

项目初始化是指,大多数现代框架都提供了开箱即用的脚手架工具,例如:

# Vue/React 生态
npm create vite@latest

# Nest.js
nest new project-name

# Hono.js
pnpm create hono@latest

初始化完成后,立即在根目录创建规则文件,通常命名为 CLAUDE.md 或 AGENTS.md。

这个文件是 AI 协作的宪法,应包含例如项目定位,技术栈清单,核心哲学(例如测试先行,采用 TDD 测试方案),项目结构示例,AI 协作原则等等内容。有兴趣的同学可以找我要这个项目的全局规则。

两个关键注意事项动态迭代 规则文件不是一成不变的。当发现 AI 写的代码不规范时,要想到如何抽离抽象的规则来约束,而不是一味手动修改

SKILL 机制(规则的模块化) 当规则内容过多时,抽象可复用的 Skill 文件。好处是 AI 按需加载,不会每次都把全局规则加入上下文,大大减少 token 用量

规则中引用 SKILL 的示例:

## 3. 核心哲学:测试先行 (TDD)

参考 `.trae/skills/tdd-first/SKILL.md` 中的测试驱动开发规范。

所有新功能开发或 Bug 修复必须遵循 **「红-绿-重构」** 循环。**严禁**在没有对应测试用例的情况下提交业务逻辑代码。
---

## 4. 响应格式

参考 `.trae/skills/response-standard/SKILL.md` 中的响应格式规范。

**[强制]** 所有接口统一返回 JSON,使用 `@/utils/response` 中的工具函数生成响应。

---

步骤 2:需求分析

如果你很清楚自己要做什么,可以直接下发任务。但如果只有模糊想法,建议先和 AI 一起做需求分析。

推荐提示词

你好!现在的任务是:我们要从零开始设计并实现 `hono.js boilerplate`。
 
你现在是资深的 Node.js 工程师。我有一个初步的想法,需要你通过向我提问,帮助我澄清需求、挖掘边缘场景,最终目标是理清我做一个通用后端功能的脚手架需要实现哪些功能。并按顺序输出一个实现这些功能的大纲。
 
请开始你的提问。

效果: Claude 会像资深 PM 一样向你提问,你逐一回答这些问题后,AI 会生成一份完整的功能清单文档。


步骤 3:测试先行 + AI 执行代码

这是整个工作流的核心环节,必须让 AI 严格按照 TDD 测试方案执行代码。

执行策略

  1. 任务拆解 → 将需求拆分为最小可测试单元
  2. 红阶段 → 先编写测试用例(此时测试应当失败)
  3. 绿阶段 → 编写最小化实现代码,使测试通过
  4. 重构阶段 → 在测试通过的基础上,优化代码结构和性能

关键约束

在全局规则中强制要求 AI:

  • ✅ 任何业务代码提交前,必须有对应的单元测试覆盖
  • ✅ 测试用例需包含正常场景 + 边界场景 + 异常场景
  • ✅ 测试执行必须通过,覆盖率不低于 80%
  • ✅ 严禁为了赶进度而跳过测试环节

步骤 4:代码审核 + 规则反馈循环

AI 执行代码后,进入审核阶段,分为 AI 自动审核和人工审核两部分。

AI 自动审核

AI 每次完成任务时,需要自动进行:

  • Eslint 校验
  • TypeScript 类型校验

如果报错,AI 自己修复直到通过(可以限制修复次数,避免死循环)

人工审核

前期必须进行人工审核,一旦发现问题,思考如何抽象成规则或 Skill,避免 AI 再次犯错。

核心发现: 你会发现后期 AI 执行的效果会越来越好。对于 CRUD 场景,基本不需要人审核了,大概看一下产出代码就知道没问题。


步骤 5:持续迭代与精准化

随着迭代轮次增加,形成正向循环:

迭代轮数 ↑
    ↓
规则精确度 ↑  → AI 执行准确率 ↑
    ↓
处理边界场景的能力 ↑
    ↓
人工介入频率 ↓
    ↓
开发效率 ↑

这就是 AI 时代的竞争力所在: 不是盲目信任 AI,而是通过不断的反馈循环来「教」AI,逐步固化和优化工作规范。


我举个自己实践中的迭代案例:

  • 例如我让 ai 实现超时中间件的时候,ai 是自己原生实现的,然后我感觉这种很常用的功能一般都有现成的库,就查了一下,果然是有 'hono/timeout' 这个库,然后就在全局规则加入了类似:”优先使用社区成熟,稳定的库解决问题“。
  • 例如我在设计后端的 url 的时候,突然想起 K8s 有一个类似的 url 设计规范,可以跟权限结合起来,例如,/api/v1/roles/{roleId},其中 roles 代表就是资源,roleId 代表的是子资源。
resources: ["roles"] # 操作的资源
verbs: ["get"] # 操作类型,增删改查
resourceNames: ["{roleId}"] # 可选(精确到某个子资源)

简单就是说一个 url 代表对什么资源的什么操作。

HTTP 请求 RBAC
GET /roles/{id} verbs: ["get"]
GET /roles verbs: ["list"]
POST /roles verbs: ["create"]
DELETE /roles/{id} verbs: ["delete"]

然后就可以结合我们的 rbac 模型(权限模型),用来标识一个权限是什么,简单来说就是资源名 + 操作,就能标识一个权限。

然后我干脆让 ai 把这个 url 规范抽象为一个 skill,下次 ai 定义 url 的时候就会调用这个规则。

  • 还有很多例子就不一一列举了...

需要注意的是,字节内部的复杂度是更高的,我们后期也会探索,例如字节内部还有:

  • 多 Agent 系统,比如主 Agent 来负责 plan 的制定,有 Coder Agent 负责编码,还有测试 Agent, test Agent 等等,这种多 Agent 协作,我个人估计字节迟早会推出一个开源库让我们使用的,所以不必着急。
  • 评测体系,会对 AI 输出质量进行打分,也就是评测 AI 的输出质量,这条我们目前这一版是靠人工来识别,但我觉得还好,前期人工介入,后期规则越来越好,模型能力越来越强,就可以进入 AI 自我评测阶段了。
  • 可观测性体系:就是对于 AI 写错的地方,是否能知道哪一步错了,然后根据这个让 AI 自动修正 Prompt,然后自动修正全局规则或者抽象为 SKILL。

上面要这么玩,目前个人还是很难的,需要大的平台支持,本文主要是先走通一个小循环,也欢迎大家一起交流(如果觉得不错,感谢点赞关注哦,欢迎入群交流~)。

项目实战:通用后端脚手架

技术栈: Hono.js + Drizzle ORM + PostgreSQL 数据库

前置声明: 整个工作流仅使用免费版工具(Trace + GLM5/豆包模型)即可高质量完成,充分说明该方法论的实际效果令人满意,而非纸上谈兵。

源码获取: 因外链限流问题,需要的朋友可以联系我获取完整代码(github)

脚手架目前的架构图如下:

image.png

第二部分:企业级技术要点

分享上述实战过程中,网上 Node.js 教程很少提及的企业级最佳实践。

优雅关闭(Graceful Shutdown)

无论部署在 Kubernetes、Docker Compose 还是物理机,都需要实现优雅关闭逻辑。

为什么重要?

当应用报错或升级时,容器编排系统会执行关闭流程:

应用故障/升级 → 容器启动关闭流程
  ↓
向 PID 1 进程发送 SIGTERM 信号
  ↓
开启倒计时(默认 10 秒)
  ↓
如果 10 秒后进程未退出,发送 SIGKILL(强制杀死)

问题场景

例:电商扣款业务
 
1. 扣除用户余额 ✅
2. Docker 信号来了,进程被强杀 🔥
3. 积分增加 ❌(未执行)
 
结果:用户钱扣了但没到账,投诉炸裂。

问题根源: Docker 强杀是瞬间的,Node.js 无法执行完事件循环中的剩余回调。

解决方案

优雅关闭可以让你在收到信号后,停止接收新请求,但把内存中已排队的写操作执行完。同时及时释放系统资源(如数据库连接),避免占满最大连接数。


链路追踪(TraceId)

TraceId 是请求在系统中的唯一标识符,从进入系统到返回响应,始终伴随整个生命周期。

为什么需要?

场景:前端用户报错
用户说:"我提交表单后,收到错误 ID:abc123def456"
 
后端排查:
❌ 日志有 1000 条,怎么找到那条错误?
✅ 按 traceId = abc123def456 过滤,立即定位问题

Node.js 的特殊性

与 Java/Go 等多线程模型相比,Node.js 的单进程模型在处理 TraceId 时有本质区别:

语言 模型 上下文隔离方案 难度
Java/Go 多线程/协程 ThreadLocal ⭐ 简单
Node.js 单进程 AsyncLocalStorage ⭐⭐⭐ 复杂

错误做法

方案 1:全局变量

let traceId;  // 全局变量
 
app.use((req, res, next) => {
  traceId = generateId();  // 请求 A 的 traceId
  next();
});
 
// 问题:请求 B 来了,traceId 被覆盖,日志全乱

方案 2:函数传参

// controller → service → dao,每层都要传 traceId
// 代码极其丑陋,难以维护
 
async function getUserOrder(traceId, userId) {
  const user = await getUser(traceId, userId);
  const order = await getOrder(traceId, user.id);
  return { user, order };
}

正确方案:AsyncLocalStorage

Node.js 官方在 async_hooks 基础上,封装了更高级、性能更优的 API:

import { AsyncLocalStorage } from 'async_hooks';
 
const traceIdStorage = new AsyncLocalStorage();
 
// 在请求中间件中创建隔离上下文
app.use((req, res, next) => {
  const traceId = generateId();
  
  // 在当前上下文中存储 traceId(自动隔离)
  traceIdStorage.run(traceId, () => {
    next();
  });
});
 
// 任何地方都可以取,不用传参
function getTraceId() {
  return traceIdStorage.getStore();
}
 
// 使用示例
async function getUserOrder(userId) {
  const traceId = getTraceId();  // 直接取,无需传参
  logger.info(`[${traceId}] Fetching user`, { userId });
  
  const user = await getUser(userId);
  logger.info(`[${traceId}] User fetched`, { userId: user.id });
  
  return user;
}

日志集成

const logger = createLogger((level, msg, meta) => {
  const traceId = getTraceId();
  const logEntry = {
    timestamp: new Date().toISOString(),
    level,
    traceId,    // 自动注入
    message: msg,
    ...meta,
  };
  console.log(JSON.stringify(logEntry));
});

TDD 测试驱动开发

TDD 是企业级后端项目的核心质量保障手段,在 AI 协作开发模式下更是确保代码质量的关键。

核心流程:红-绿-重构

  1. 红阶段 → 编写测试用例,预期会失败(功能未实现)
  2. 绿阶段 → 实现最小化代码,使测试通过
  3. 重构阶段 → 优化代码结构,保持测试通过

Hono.js 项目中的实践

采用 Hono 原生集成测试方案,结合 Vitest 测试框架:

// test/user.test.ts
import { describe, it, expect } from 'vitest';
import app from '../src/app';
 
describe('User API', () => {
  it('should return 404 for non-existent user', async () => {
    const res = await app.request('/api/users/9999', {
      method: 'GET'
    });
    
    expect(res.status).toBe(404);
    const data = await res.json();
    expect(data.code).toBe(0);
    expect(data.message).toBe('User not found');
  });
  
  it('should create a new user', async () => {
    const res = await app.request('/api/users', {
      method: 'POST',
      body: JSON.stringify({
        name: '测试用户',
        email: 'test@example.com',
        password: 'password123'
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.code).toBe(1);
    expect(data.data.name).toBe('测试用户');
  });
});

表格驱动测试

对于多分支逻辑和边界情况,采用表格驱动测试风格:

describe('User Validation', () => {
  const testCases = [
    {
      desc: '缺少必填字段',
      body: { name: '测试用户' },
      expectedStatus: 400,
      expectedMessage: 'Email is required'
    },
    {
      desc: '邮箱格式错误',
      body: { name: '测试用户', email: 'invalid-email' },
      expectedStatus: 400,
      expectedMessage: 'Invalid email format'
    },
    {
      desc: '密码长度不足',
      body: { name: '测试用户', email: 'test@example.com', password: '123' },
      expectedStatus: 400,
      expectedMessage: 'Password must be at least 6 characters'
    }
  ];
  
  test.each(testCases)('$desc', async ({ body, expectedStatus, expectedMessage }) => {
    const res = await app.request('/api/users', {
      method: 'POST',
      body: JSON.stringify(body),
      headers: { 'Content-Type': 'application/json' }
    });
    
    expect(res.status).toBe(expectedStatus);
    const data = await res.json();
    expect(data.message).toBe(expectedMessage);
  });
});

请求超时处理

请求超时处理是后端服务稳定性的重要保障,可以防止长时间运行的请求占用系统资源。

为什么需要?

  • 保护用户体验:与其让用户等待 30 秒,不如在 5 秒内返回"请求超时"
  • 防止系统雪崩:大量超时请求堆积会导致 CPU/内存被迅速耗尽

API 接口级超时

利用 Hono 自带的 timeout 中间件:

import { timeout } from 'hono/timeout'
 
// 1. 全局配置:所有请求默认 5 秒超时
app.use('/api/*', timeout(5000))
 
// 2. 局部配置:针对耗时操作,允许更长时间
app.get('/api/export', timeout(30000), async (c) => {
  // 执行耗时操作...
  return c.json({ success: true })
})
 
// 3. 自定义超时后的逻辑
const customTimeout = timeout(5000, {
  onTimeout: (c) => {
    return c.json({ code: 0, message: '服务器繁忙,请稍后再试' }, 408)
  }
})

数据库级超时

API 层超时只是"切断了回传给用户的路",但数据库内部的任务可能仍在运行。需要更细粒度的控制:

// Drizzle ORM 配置:通过底层驱动设置超时
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
 
const queryClient = postgres(process.env.DATABASE_URL, {
  timeout: 5,          // 建立连接超时 (秒)
  idle_timeout: 20,    // 空闲连接释放
  max_lifetime: 60 * 30 // 连接存活最大时间
})
 
// 在业务代码中手动控制单次查询超时
async function getSlowData() {
  return await db.select().from(users).execute();
}

全局错误处理

在复杂的后端系统中,错误可能来自业务逻辑、数据库约束、第三方 API 失败或语法错误。没有统一处理的话,返回给前端的可能是难看的堆栈信息。

设计原则

  1. 收口原则 → 业务代码通过 throw 抛出错误,由顶层中间件统一拦截处理
  2. 分类分级 → 区分"预期内错误"和"预期外错误"
  3. 安全性 → 生产环境下严禁将详细 Stack 返回给客户端

实现方案

步骤 1:定义标准错误类

// src/utils/errors.ts
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public code: number = 0 // 自定义业务状态码
  ) {
    super(message);
    this.name = 'AppError';
  }
}

步骤 2:配置全局捕获钩子

import { Hono } from 'hono';
import { AppError } from './utils/errors';
 
const app = new Hono();
 
app.onError((err, c) => {
  const traceId = c.get('traceId') || 'unknown';
  
  // 1. 处理已知业务异常
  if (err instanceof AppError) {
    return c.json({
      code: err.code,
      message: err.message,
      traceId
    }, err.statusCode as any);
  }
 
  // 2. 处理参数校验错误
  if (err.name === 'ZodError') {
    return c.json({
      code: 400,
      message: '参数验证失败',
      details: err,
      traceId
    }, 400);
  }
 
  // 3. 处理未知错误
  console.error(`[Fatal Error] [${traceId}]:`, err);
 
  return c.json({
    code: 500,
    message: process.env.NODE_ENV === 'production' 
      ? '服务器内部错误' 
      : err.message,
    traceId
  }, 500);
});

步骤 3:业务层使用

export async function deleteUser(id: string) {
  const user = await db.findUser(id);
  
  if (!user) {
    throw new AppError(404, '用户不存在', 10001);
  }
  
  return db.delete(id);
}

RBAC 权限控制

RBAC(基于角色的访问控制)是中后台系统最通用的权限模型。通过"用户-角色-权限"的关联,实现权限的解耦。

为什么不直接判断角色?

如果代码里写 if (user.role === 'admin'),当新增一个"超级编辑"角色也需要此权限时,得修改所有代码。判断权限点(Permission)而非角色名,才是系统扩展性的关键。

核心概念

  • 用户 (User) → 拥有一个或多个角色
  • 角色 (Role) → 如 Admin、Editor、Viewer
  • 权限 (Permission) → 如 user:create、order:delete

实现方案

步骤 1:定义数据模型

// 简化版 schema
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  role: text('role').default('viewer'),
});
 
// 权限映射表
const ROLE_PERMISSIONS = {
  admin: ['user:all', 'post:all'],
  editor: ['post:edit', 'post:create'],
  viewer: ['post:read'],
} as const;

步骤 2:实现 RBAC 中间件

// middleware/rbac.ts
import { createMiddleware } from 'hono/factory';
import { AppError } from '../utils/errors';
 
export const checkPermission = (requiredPermission: string) => {
  return createMiddleware(async (c, next) => {
    const user = c.get('user');
    
    if (!user) {
      throw new AppError(401, '未授权访问');
    }
 
    const userPermissions = ROLE_PERMISSIONS[user.role] || [];
    
    // 支持通配符或精确匹配
    const hasPermission = userPermissions.some(p => 
      p === requiredPermission || p === `${requiredPermission.split(':')[0]}:all`
    );
 
    if (!hasPermission) {
      throw new AppError(403, '权限不足,无法执行此操作');
    }
 
    await next();
  });
};

步骤 3:在路由层应用

const api = new Hono();
 
// 只有拥有 post:create 权限的角色才能访问
api.post('/posts', checkPermission('post:create'), async (c) => {
  return c.json({ message: '发布成功' });
});
 
// 管理员专属接口
api.get('/admin/stats', checkPermission('user:all'), async (c) => {
  return c.json({ stats: '...' });
});

日志轮转

在生产环境中,如果所有日志都无限制地写入同一个文件,最终会导致磁盘爆满和日志文件难以打开。

核心目的

  1. 防止单个文件过大(难以检索、占用磁盘空间)
  2. 自动化归档(按日期分类)
  3. 过期清理(例如只保留最近 14 天的日志)

实现方案:Winston + Daily Rotate File

import winston from 'winston';
import 'winston-daily-rotate-file';
 
const transport = new winston.transports.DailyRotateFile({
  filename: 'logs/application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,           // 历史日志压缩
  maxSize: '20m',                // 单个文件超过 20MB 也会切分
  maxFiles: '14d',               // 只保留最近 14 天的日志
  level: 'info',
});
 
const logger = winston.createLogger({
  transports: [
    transport,
    new winston.transports.Console()
  ]
});

应对 DDoS 攻击

DDoS 攻击的本质是发大量垃圾请求,导致带宽占满、CPU/内存耗尽、连接数耗尽。

现实: 普通企业很难防住大规模 DDoS,目的是提高攻击成本。

限流

在接入层(Nginx)—— 粗筛

性能极高,在流量进入 Node.js 之前就拦截:

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20;

在应用层(Middleware)—— 精滤

灵活度高,根据业务维度限流:

// 限制某个登录用户每分钟只能发 5 条评论
app.use(rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  keyGenerator: (c) => c.get('user').id
}));

限制请求体大小

防止内存溢出 (OOM):

// 攻击场景:发送 2GB 垃圾字符的 JSON POST 请求
// 后果:Node.js 进程尝试分配 2GB 内存,很快就 Out of Memory
 
// 解决:在 Nginx 层配置
client_max_body_size 1m;

Helmet 安全头

Helmet 通过设置各种 HTTP 响应头,自动防御常见的 Web 漏洞(XSS、点击劫持、MIME 类型嗅探等)。

性价比最高的安全加固方案。

Hono.js 官方支持 hono/helmet 中间件,在入口文件 src/app.ts 中引入即可:

import { helmet } from 'hono/helmet';
 
app.use(helmet());

告警机制

告警机制是"及时发现问题"的关键,通过监控关键指标,在异常情况下主动通知相关人员。

告警规则设计

根据应用的 SLA,定义不同严重等级:

export const alertRules = [
  {
    name: 'High Error Rate',
    condition: 'error_rate > 5%',
    severity: 'critical',
    duration: '5m',
    action: 'page_oncall',  // 立即电话/Slack 通知
  },
  {
    name: 'High Response Latency',
    condition: 'p95_latency > 1000ms',
    severity: 'warning',
    duration: '10m',
    action: 'send_to_slack',
  },
  {
    name: 'Database Connection Pool Exhausted',
    condition: 'db_connections > 90%',
    severity: 'critical',
    duration: '1m',
    action: 'page_oncall',
  }
];

与监控系统集成

使用 Prometheus + Alertmanager:

# prometheus.yml
global:
  scrape_interval: 15s
 
scrape_configs:
  - job_name: 'hono-app'
    static_configs:
      - targets: ['localhost:3000']
    metrics_path: '/metrics'
 
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['localhost:9093']

多渠道通知

export async function sendAlert(
  title: string,
  message: string,
  severity: 'critical' | 'warning' | 'info'
) {
  const timestamp = new Date().toISOString();
 
  // 1. Slack 通知
  if (severity === 'critical' || severity === 'warning') {
    await axios.post(process.env.SLACK_WEBHOOK_URL, {
      text: `[${severity.toUpperCase()}] ${title}`,
      attachments: [{
        color: severity === 'critical' ? 'danger' : 'warning',
        text: message,
        ts: Math.floor(new Date().getTime() / 1000),
      }],
    });
  }
 
  // 2. 邮件通知(仅限 critical)
  if (severity === 'critical') {
    await sendEmail({
      to: process.env.ALERT_EMAIL,
      subject: `🚨 CRITICAL: ${title}`,
      html: `<h2>${title}</h2><p>${message}</p><p>${timestamp}</p>`,
    });
  }
 
  // 3. 记录到数据库
  await db.insert(alerts).values({
    title,
    message,
    severity,
    createdAt: new Date(),
  });
}

性能测试

性能测试是确保应用在生产环境中稳定运行的最后一道防线。

基准测试(Benchmarking)

使用 Autocannon 进行简单的吞吐量和延迟测试:

# 安装 Autocannon
npm install -g autocannon
 
# 基准测试:100 并发,持续 30 秒
autocannon -c 100 -d 30 http://localhost:3000/api/users
 
# 输出示例
# Req/Sec: 1234
# Latency: { mean: 45.2, p50: 42, p95: 78, p99: 120 }

压力测试(Load Testing)

使用 K6 模拟真实用户行为:

// load-test.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
 
export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 100 },
    { duration: '2m', target: 200 },
    { duration: '5m', target: 200 },
    { duration: '2m', target: 0 },
  ],
};
 
export default function () {
  group('User API', () => {
    // 测试获取用户列表
    let listRes = http.get('http://localhost:3000/api/users');
    check(listRes, {
      'list status is 200': (r) => r.status === 200,
      'list response time < 100ms': (r) => r.timings.duration < 100,
    });
 
    // 测试创建用户
    let createRes = http.post('http://localhost:3000/api/users', {
      name: `user-${__VU}-${__ITER}`,
      email: `user-${__VU}-${__ITER}@example.com`,
      password: 'password123',
    });
    check(createRes, {
      'create status is 200': (r) => r.status === 200,
    });
 
    sleep(1);
  });
}

运行压力测试:

# 安装 K6
npm install -g k6
 
# 执行测试
k6 run load-test.js

数据库性能测试

// src/tests/db-performance.test.ts
import { describe, it, expect } from 'vitest';
import { db } from '../db';
 
describe('Database Performance', () => {
  it('should query 10k users in < 500ms', async () => {
    const start = performance.now();
    const users = await db.query.users.findMany({ limit: 10000 });
    const duration = performance.now() - start;
 
    expect(users.length).toBe(10000);
    expect(duration).toBeLessThan(500);
  });
 
  it('should create 1k users in batch < 2s', async () => {
    const data = Array.from({ length: 1000 }, (_, i) => ({
      name: `user-${i}`,
      email: `user-${i}@example.com`,
      password: 'hashed-password',
    }));
 
    const start = performance.now();
    await db.insert(users).values(data);
    const duration = performance.now() - start;
 
    expect(duration).toBeLessThan(2000);
  });
});

数据持久化与备份

数据持久化本质上解决的是:当系统崩溃、误操作、甚至被攻击时,数据还能不能恢复?

重要认知: 数据库 ≠ 数据安全。数据库只是"存储",而备份 + 恢复能力才是安全的核心。

备份脚本示例

#!/bin/bash
set -o pipefail  # 核心:捕获管道中任何一步的错误
 
DB_NAME="your_db"
BACKUP_FILE="/data/backups/db_$(date +%Y%m%d).sql.gz"
 
# 执行备份
pg_dump -U admin -d $DB_NAME | gzip -1 > $BACKUP_FILE
 
# 检查备份是否成功
if [ $? -ne 0 ]; then
    echo "❌ 备份失败!清理空文件..."
    rm -f $BACKUP_FILE
    # 调用告警机制
    # sendAlert "Database Backup Failed" "pg_dump connection error" "critical"
    exit 1
else
    echo "✅ 备份成功"
fi

可观测性(Observability)

可观测性与监控的区别:

  • 监控 → 告诉你"系统出了问题"(基于预定义的指标和阈值)
  • 可观测性 → 告诉你"系统为什么出了问题"(通过日志、指标、链路追踪)

可观测性的三大支柱

支柱 1:结构化日志

// src/utils/logger.ts
import winston from 'winston';
 
const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    // 自定义格式化,确保输出为结构化 JSON
    winston.format.printf(({ timestamp, level, message, ...meta }) => {
      return JSON.stringify({
        timestamp,
        level,
        traceId,
        message,
        ...meta,
      });
    })
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

支柱 2:指标收集(Metrics)

使用 Prometheus 收集性能指标:

// src/utils/metrics.ts
import promClient from 'prom-client';
 
// 创建指标
export const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request latency',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.5, 1, 2, 5],
});
 
export const dbQueryDuration = new promClient.Histogram({
  name: 'db_query_duration_seconds',
  help: 'Database query latency',
  labelNames: ['operation', 'table'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1],
});
 
// 暴露 Prometheus 指标端点
export function registerMetricsRoute(app: Hono) {
  app.get('/metrics', (c) => {
    return c.text(promClient.register.metrics());
  });
}

支柱 3:链路追踪(Traces)

已在前面的 TraceId 部分详细说明。


最后

这套工作流的核心理念就是 持续反馈、不断优化。感谢您的阅读,建议点赞收藏,我们下期再见!

Vue 项目必备:10 个高频实用自定义指令,直接复制即用(Vue2 / Vue3 通用)

作者 前端Hardy
2026年4月9日 16:43

在实际开发中,很多重复逻辑(权限控制、防抖点击、图片懒加载、文本复制等)用自定义指令来做最优雅,不污染组件、不写冗余代码、复用性极强。

今天整理了 10 个企业级最常用的 Vue 自定义指令,Vue2 / Vue3 都能跑,复制到项目里直接用,建议收藏进你的工具库。


1. v-permission 按钮权限控制(后台系统必用)

根据权限码控制按钮显隐,后端返回权限列表直接用。

// directives/permission.js
import { useUserStore } from '@/stores/user'

export default {
  mounted(el, binding) {
    const { permissions } = useUserStore()
    const value = binding.value
    if (!value) return
    // 无权限则移除元素
    if (!permissions.includes(value)) {
      el.parentNode?.removeChild(el)
    }
  }
}

使用:

<button v-permission="'user:add'">添加用户</button>

2. v-debounce 防抖点击(搜索/提交防重复)

// directives/debounce.js
export default {
  mounted(el, binding) {
    const { func, delay = 300 } = binding.value
    let timer = null
    el.addEventListener('click', () => {
      clearTimeout(timer)
      timer = setTimeout(() => func(), delay)
    })
  }
}

使用:

<button v-debounce="{ func: handleSearch, delay: 500 }">搜索</button>

3. v-throttle 节流指令(滚动/防狂点)

// directives/throttle.js
export default {
  mounted(el, binding) {
    const { func, delay = 500 } = binding.value
    let lastTime = 0
    el.addEventListener('click', () => {
      const now = Date.now()
      if (now - lastTime >= delay) {
        func()
        lastTime = now
      }
    })
  }
}

4. v-copy 一键复制文本

// directives/copy.js
export default {
  mounted(el, binding) {
    el.addEventListener('click', () => {
      const text = binding.value
      navigator.clipboard.writeText(text).then(() => {
        ElMessage.success('复制成功')
      })
    })
  }
}

使用:

<span v-copy="orderNo">复制订单号</span>

5. v-longpress 长按指令

// directives/longpress.js
export default {
  mounted(el, binding) {
    const { func, time = 1000 } = binding.value
    let timer = null
    el.addEventListener('mousedown', () => {
      timer = setTimeout(() => func(), time)
    })
    el.addEventListener('mouseup mouseleave', () => clearTimeout(timer))
  }
}

6. v-input-number 仅允许输入数字(支持小数)

// directives/number.js
export default {
  mounted(el) {
    const input = el.tagName === 'INPUT' ? el : el.querySelector('input')
    input.addEventListener('input', () => {
      input.value = input.value.replace(/[^\d.]/g, '')
      const arr = input.value.split('.')
      if (arr.length > 2) input.value = arr[0] + '.' + arr[1]
    })
  }
}

使用:

<el-input v-input-number v-model="num" />

7. v-lazy 图片懒加载(性能优化)

// directives/lazy.js
export default {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([{ isIntersecting }]) => {
      if (isIntersecting) {
        el.src = binding.value
        observer.unobserve(el)
      }
    })
    observer.observe(el)
  }
}

使用:

<img v-lazy="imgUrl" alt="" />

8. v-draggable 元素拖拽

// directives/drag.js
export default {
  mounted(el) {
    el.style.cssText += ';position:fixed;cursor:move;'
    el.addEventListener('mousedown', (e) => {
      const x = e.clientX - el.offsetLeft
      const y = e.clientY - el.offsetTop
      const move = (e) => {
        el.style.left = e.clientX - x + 'px'
        el.style.top = e.clientY - y + 'px'
      }
      document.addEventListener('mousemove', move)
      document.addEventListener('mouseup', () => {
        document.removeEventListener('mousemove', move)
      }, { once: true })
    })
  }
}

9. v-watermark 页面水印(防截图)

// directives/watermark.js
export default {
  mounted(el, binding) {
    const text = binding.value || '内部资料'
    const canvas = document.createElement('canvas')
    canvas.width = 200
    canvas.height = 150
    const ctx = canvas.getContext('2d')
    ctx.font = '14px Arial'
    ctx.fillStyle = 'rgba(0,0,0,0.1)'
    ctx.rotate(-0.2)
    ctx.fillText(text, 20, 50)
    el.style.background = `url(${canvas.toDataURL()}) repeat`
  }
}

10. v-auto-height 自适应高度(表格/弹窗常用)

自动计算高度,避免滚动条错乱

// directives/autoHeight.js
export default {
  mounted(el) {
    const resize = () => {
      const top = el.getBoundingClientRect().top
      el.style.height = window.innerHeight - top - 20 + 'px'
    }
    resize()
    window.addEventListener('resize', resize)
    el._resize = resize
  },
  unmounted(el) {
    window.removeEventListener('resize', el._resize)
  }
}

统一注册(Vue3)

directives/index.js 统一导出:

import permission from './permission'
import debounce from './debounce'
// ...其他

export default {
  install(app) {
    app.directive('permission', permission)
    app.directive('debounce', debounce)
  }
}

main.js 引入:

import directives from '@/directives'
app.use(directives)

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

前端开发效率翻倍:15个超级实用的工具函数,直接复制进项目(建议收藏)

作者 前端Hardy
2026年4月9日 16:42

大家好,今天给大家整理了一套前端日常开发高频用到的工具函数

没有复杂算法,也没有花里胡哨的封装,全是业务里真正常用的:时间格式化、手机号脱敏、防抖节流、深拷贝、URL参数解析……全部即插即用,复制到项目里就能跑,非常适合放进自己的 utils 工具库。


1. 时间格式化(最常用)

把时间戳 / Date 对象转成 YYYY-MM-DD HH:mm:ss

function formatDate(date, fmt = 'YYYY-MM-DD HH:mm:ss') {
  if (!date) return ''
  date = date instanceof Date ? date : new Date(date)

  const o = {
    'M+': date.getMonth() + 1,
    'D+': date.getDate(),
    'H+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  }

  if (/(Y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, date.getFullYear().toString().slice(4 - RegExp.$1.length))
  }

  for (const k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      fmt = fmt.replace(
        RegExp.$1,
        RegExp.$1.length === 1 ? o[k] : o[k].toString().padStart(2, '0')
      )
    }
  }
  return fmt
}

// 使用
formatDate(new Date()) // 2026-04-09 15:30:20

2. 防抖(输入搜索专用)

function debounce(fn, delay = 300) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

3. 节流(滚动/点击防重复)

function throttle(fn, interval = 500) {
  let last = 0
  return function (...args) {
    const now = Date.now()
    if (now - last >= interval) {
      last = now
      fn.apply(this, args)
    }
  }
}

4. 深拷贝(处理对象/数组)

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (hash.has(obj)) return hash.get(obj)

  const clone = Array.isArray(obj) ? [] : {}
  hash.set(obj, clone)

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash)
    }
  }
  return clone
}

5. 获取 URL 参数

function getQueryParams(url = location.href) {
  const params = {}
  new URL(url).searchParams.forEach((v, k) => (params[k] = v))
  return params
}

// 使用
getQueryParams('https://xxx.com?id=1&name=test') // { id: '1', name: 'test' }

6. 手机号脱敏

function maskPhone(phone) {
  if (!phone || phone.length !== 11) return phone
  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}

// 13812345678 → 138****5678

7. 姓名脱敏

function maskName(name) {
  if (!name) return ''
  if (name.length === 1) return name
  return name[0] + '*'.repeat(name.length - 1)
}

// 张三 → 张*
// 张三丰 → 张**

8. 数字千分位格式化

function formatMoney(num) {
  if (isNaN(num)) return '0'
  return Number(num).toLocaleString()
}

// 1234567 → 1,234,567

9. 存储操作(localStorage 封装)

const storage = {
  set(key, val) {
    localStorage.setItem(key, JSON.stringify(val))
  },
  get(key) {
    const val = localStorage.getItem(key)
    if (!val) return null
    try {
      return JSON.parse(val)
    } catch {
      return val
    }
  },
  remove(key) {
    localStorage.removeItem(key)
  },
  clear() {
    localStorage.clear()
  }
}

10. 判断数据类型

function getType(val) {
  return Object.prototype.toString.call(val).slice(8, -1).toLowerCase()
}

// getType([]) → 'array'
// getType({}) → 'object'
// getType(null) → 'null'

11. 数组去重

function uniqueArr(arr) {
  return [...new Set(arr)]
}

12. 数组扁平化

function flatten(arr) {
  return arr.flat(Infinity)
}

13. 生成随机字符串(ID)

function randomStr(len = 8) {
  const str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
  let res = ''
  for (let i = 0; i < len; i++) {
    res += str[Math.floor(Math.random() * str.length)]
  }
  return res
}

14. 防抖立即执行版(提交按钮专用)

function debounceImmediate(fn, delay = 500) {
  let timer = null
  return function (...args) {
    const first = !timer
    clearTimeout(timer)
    timer = setTimeout(() => (timer = null), delay)
    if (first) fn.apply(this, args)
  }
}

15. 滚动到顶部(平滑)

function scrollToTop() {
  window.scrollTo({ top: 0, behavior: 'smooth' })
}

最后

这 15 个工具函数基本覆盖了80% 前端业务场景,建议直接新建一个 utils.js 全部存起来,以后开发至少快一倍。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

前端面试通关指南:30个高频手写JS算法,吃透就能拿高薪(附完整代码)

作者 前端Hardy
2026年4月9日 16:18

聊前端面试,算法永远是绕不开的坎。很多小伙伴项目经验很丰富,框架用得溜,但一上面试场就被手写算法题卡住,直接导致面试失利。

我整理了一份30个高频手写JS算法清单,覆盖ES6语法、数组操作、链表、二叉树、动态规划等核心考点,从简单到进阶,每道题都附完整可复制代码+考点解析,跟着敲一遍,面试时直接手到擒来!

一、基础语法与数组变换(必拿分,入门级)

这类题考察基础语法功底,难度低、频率高,面试时先搞定这类题,稳拿基础分,给面试官留好第一印象。

1. 深度克隆(Deep Clone)

考察点:引用类型传址、循环引用、基本类型与引用类型区别

function deepClone(obj, map = new WeakMap()) {
  // 基本类型直接返回(null也是基本类型范畴)
  if (obj === null || typeof obj !== 'object') return obj;
  // 处理循环引用(避免无限递归)
  if (map.has(obj)) return map.get(obj);
  
  // 区分数组和对象,创建对应克隆容器
  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone); // 存入map,标记已克隆
  
  // 遍历对象/数组,递归克隆每一项
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) { // 只克隆自身属性,不克隆原型链属性
      clone[key] = deepClone(obj[key], map);
    }
  }
  return clone;
}

// 测试
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const cloneObj = deepClone(obj);
obj.b.c = 100;
console.log(cloneObj.b.c); // 2(克隆后互不影响)

2. 数组扁平化(Flatten)

考察点:递归、数组方法(forEach、concat)、ES6新特性

// 解法1:递归(兼容性好,易理解)
function flatten(arr) {
  let result = [];
  arr.forEach(item => {
    // 若当前项是数组,递归扁平化,否则直接加入结果
    result = result.concat(Array.isArray(item) ? flatten(item) : item);
  });
  return result;
}

// 解法2:ES6 flat方法(简洁,实际开发常用)
const flatten = arr => arr.flat(Infinity); // Infinity表示无限层级扁平化

// 解法3:reduce实现(更简洁,面试加分)
const flatten = arr => arr.reduce((prev, curr) => {
  return prev.concat(Array.isArray(curr) ? flatten(curr) : curr);
}, []);

// 测试
console.log(flatten([1, [2, [3, 4], 5]])); // [1,2,3,4,5]

3. 防抖(Debounce)

考察点:高频事件控制、定时器、this指向

// 核心:频繁触发时,只在最后一次触发后延迟执行
function debounce(fn, delay = 500) {
  let timer = null; // 定时器标识,闭包保存
  return function(...args) {
    clearTimeout(timer); // 每次触发,清除上一次定时器
    // 重新设置定时器,延迟执行目标函数
    timer = setTimeout(() => {
      fn.apply(this, args); // 绑定this和参数,适配实际场景
    }, delay);
  };
}

// 用法(搜索框输入示例)
const handleSearch = debounce((val) => {
  console.log('请求搜索接口:', val);
}, 500);

4. 节流(Throttle)

考察点:高频事件控制、时间戳/定时器、性能优化

// 解法1:时间戳版(触发时立即执行,之后固定时间内不执行)
function throttle(fn, interval = 1000) {
  let lastTime = 0; // 上一次执行时间
  return function(...args) {
    const nowTime = Date.now(); // 当前时间
    // 若当前时间 - 上一次执行时间 > 间隔,执行函数
    if (nowTime - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = nowTime; // 更新上一次执行时间
    }
  };
}

// 解法2:定时器版(触发后延迟执行,固定时间内只执行一次)
function throttle2(fn, interval = 1000) {
  let timer = null;
  return function(...args) {
    if (!timer) { // 若定时器不存在,执行函数
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行后清空定时器,允许下次执行
      }, interval);
    }
  };
}

// 用法(滚动加载示例)
window.addEventListener('scroll', throttle(() => {
  console.log('滚动加载更多');
}, 1000));

5. 数组去重(Unique)

考察点:数组方法、Set数据结构、兼容性

// 解法1:Set实现(最简洁,ES6+常用)
const unique = arr => [...new Set(arr)];

// 解法2:indexOf实现(兼容性好,适合旧项目)
function unique(arr) {
  const result = [];
  arr.forEach(item => {
    // 若结果数组中没有当前项,加入结果
    if (result.indexOf(item) === -1) {
      result.push(item);
    }
  });
  return result;
}

// 解法3:filter+indexOf(简洁,面试常用)
const unique = arr => arr.filter((item, index) => {
  // 只保留第一次出现的元素(indexOf返回第一个匹配的索引)
  return arr.indexOf(item) === index;
});

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

6. 数组排序(冒泡排序)

考察点:排序原理、循环逻辑、基础算法思维

// 冒泡排序:相邻元素对比,大的往后移,每次循环确定一个最大值
function bubbleSort(arr) {
  const len = arr.length;
  // 外层循环:控制排序轮次(共len-1轮)
  for (let i = 0; i < len - 1; i++) {
    let flag = false; // 优化:标记是否发生交换,若无则排序完成
    // 内层循环:对比相邻元素,交换位置
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换两个元素
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        flag = true;
      }
    }
    if (!flag) break; // 无交换,直接退出循环
  }
  return arr;
}

// 测试
console.log(bubbleSort([3, 1, 4, 1, 5, 9])); // [1,1,3,4,5,9]

7. 数组排序(快速排序)

考察点:分治思想、递归、时间复杂度优化(面试高频)

// 快速排序:分治思想,选一个基准值,将数组分成两部分,递归排序
function quickSort(arr) {
  // 终止条件:数组长度<=1,直接返回
  if (arr.length <= 1) return arr;
  // 选基准值(中间项,避免极端情况)
  const pivot = arr[Math.floor(arr.length / 2)];
  // 分治:小于基准值的放左边,等于的放中间,大于的放右边
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);
  // 递归排序左右两部分,拼接结果
  return [...quickSort(left), ...middle, ...quickSort(right)];
}

// 测试
console.log(quickSort([3, 1, 4, 1, 5, 9])); // [1,1,3,4,5,9]

8. 实现数组forEach方法

考察点:数组方法原理、this绑定、回调函数

// 模拟数组forEach,接收回调函数和this指向
Array.prototype.myForEach = function(callback, thisArg) {
  // 边界判断:回调必须是函数
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }
  // 遍历当前数组(this指向调用myForEach的数组)
  for (let i = 0; i < this.length; i++) {
    // 执行回调,传入三个参数:当前项、索引、原数组,绑定thisArg
    callback.call(thisArg, this[i], i, this);
  }
};

// 用法
[1, 2, 3].myForEach((item, index) => {
  console.log(item, index); // 1 0 | 2 1 | 3 2
});

9. 实现数组map方法

考察点:数组方法原理、返回值处理、回调函数

// 模拟数组map,返回新数组,新数组元素是回调函数的返回值
Array.prototype.myMap = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }
  const result = []; // 存储结果的新数组
  for (let i = 0; i < this.length; i++) {
    // 执行回调,将返回值加入结果数组
    result.push(callback.call(thisArg, this[i], i, this));
  }
  return result;
};

// 用法
const newArr = [1, 2, 3].myMap(item => item * 2);
console.log(newArr); // [2,4,6]

10. 实现数组filter方法

考察点:数组方法原理、条件判断、返回值处理

// 模拟数组filter,返回满足条件的元素组成的新数组
Array.prototype.myFilter = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }
  const result = [];
  for (let i = 0; i < this.length; i++) {
    // 回调返回true,将当前项加入结果数组
    if (callback.call(thisArg, this[i], i, this)) {
      result.push(this[i]);
    }
  }
  return result;
};

// 用法
const evenArr = [1, 2, 3, 4].myFilter(item => item % 2 === 0);
console.log(evenArr); // [2,4]

二、原型与作用域(进阶必问,中层前端考点)

这类题考察对JS底层原理的理解,是区分初级和中级前端的关键,面试时高频出现,必须吃透。

11. 实现new关键字

考察点:原型链、构造函数、this绑定、返回值判断

// myNew:模拟new关键字的作用
function myNew(fn, ...args) {
  // 1. 创建一个空对象,让其原型指向构造函数的prototype
  const obj = Object.create(fn.prototype);
  // 2. 执行构造函数,将this绑定到新创建的对象上
  const result = fn.apply(obj, args);
  // 3. 判断构造函数的返回值:若为对象(非null),则返回该对象;否则返回新创建的obj
  return result instanceof Object ? result : obj;
}

// 测试
function Person(name, age) {
  this.name = name;
  this.age = age;
}
const person = myNew(Person, '张三', 25);
console.log(person.name); // 张三
console.log(person instanceof Person); // true

12. 手写Promise(简易版,支持then链式调用)

考察点:异步编程、状态机、回调队列、链式调用

class MyPromise {
  constructor(exector) {
    // 初始化状态:pending(等待)、fulfilled(成功)、rejected(失败)
    this.status = 'pending';
    this.value = null; // 成功时的返回值
    this.reason = null; // 失败时的原因
    // 存储成功/失败的回调队列(支持多个then绑定)
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    // 成功回调:改变状态,保存值,执行所有成功回调
    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 执行所有缓存的成功回调
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    // 失败回调:改变状态,保存原因,执行所有失败回调
    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        // 执行所有缓存的失败回调
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    // 执行 executor,捕获异常,异常时调用reject
    try {
      exector(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  // then方法:支持链式调用,返回新的Promise
  then(onFulfilled, onRejected) {
    // 兼容:若then未传回调,默认透传值/原因
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

    // 返回新Promise,实现链式调用
    return new MyPromise((resolve, reject) => {
      // 状态为成功时,执行成功回调
      if (this.status === 'fulfilled') {
        try {
          const result = onFulfilled(this.value);
          // 回调返回值,传递给下一个then的成功回调
          resolve(result);
        } catch (err) {
          // 回调执行失败,传递给下一个then的失败回调
          reject(err);
        }
      }

      // 状态为失败时,执行失败回调
      if (this.status === 'rejected') {
        try {
          const result = onRejected(this.reason);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      }

      // 状态为等待时,缓存回调
      if (this.status === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          try {
            const result = onFulfilled(this.value);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
        this.onRejectedCallbacks.push(() => {
          try {
            const result = onRejected(this.reason);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
      }
    });
  }
}

// 测试
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功');
  }, 1000);
}).then(res => {
  console.log(res); // 成功
  return '下一个then';
}).then(res => {
  console.log(res); // 下一个then
});

13. 实现Promise.all方法

考察点:Promise并发控制、数组遍历、状态判断

// Promise.all:接收一个Promise数组,所有Promise成功才成功,有一个失败则失败
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    // 边界判断:若传入的不是数组,直接reject
    if (!Array.isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }

    const result = []; // 存储所有Promise的成功结果
    let count = 0; // 记录已完成的Promise数量

    // 若数组为空,直接resolve空数组
    if (promises.length === 0) return resolve(result);

    // 遍历每个Promise
    promises.forEach((promise, index) => {
      // 兼容非Promise值(直接视为成功)
      Promise.resolve(promise).then(res => {
        result[index] = res; // 按原顺序存储结果
        count++;
        // 所有Promise都完成,resolve结果数组
        if (count === promises.length) {
          resolve(result);
        }
      }).catch(err => {
        // 有一个失败,直接reject该错误
        reject(err);
      });
    });
  });
};

// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.myAll([p1, p2, p3]).then(res => {
  console.log(res); // [1,2,3]
});

14. 实现Promise.race方法

考察点:Promise并发控制、第一个完成的状态优先

// Promise.race:接收一个Promise数组,第一个完成(成功/失败)的结果作为最终结果
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }

    // 遍历每个Promise,第一个完成的直接改变状态
    promises.forEach(promise => {
      Promise.resolve(promise).then(res => {
        resolve(res); // 第一个成功,直接resolve
      }).catch(err => {
        reject(err); // 第一个失败,直接reject
      });
    });
  });
};

// 测试
const p1 = new Promise((resolve) => setTimeout(() => resolve(1), 1000));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('失败'), 500));
Promise.myRace([p1, p2]).catch(err => {
  console.log(err); // 失败(p2先完成,且失败)
});

15. 函数柯里化(Currying)

考察点:闭包、参数复用、函数式编程

// 柯里化:将多参数函数,转化为单参数函数的链式调用
function curry(fn) {
  // 闭包保存已传入的参数
  return function curried(...args) {
    // 若传入的参数数量 >= 原函数的参数数量,执行原函数
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      // 否则,返回一个新函数,接收剩余参数,递归调用curried
      return function(...args2) {
        return curried.apply(this, [...args, ...args2]);
      };
    }
  };
}

// 用法
const add = (a, b, c) => a + b + c; // 原函数,3个参数
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6(链式调用)
console.log(curriedAdd(1, 2)(3)); // 6(支持部分参数)
console.log(curriedAdd(1, 2, 3)); // 6(支持完整参数)

16. 实现call方法

考察点:this绑定、函数执行、参数传递

// 模拟Function.prototype.call:改变函数this指向,立即执行函数
Function.prototype.myCall = function(context, ...args) {
  // 边界判断:若context为null/undefined,指向window(浏览器环境)
  context = context || window;
  // 给context添加一个临时属性,指向当前函数(this就是调用myCall的函数)
  const fnKey = Symbol('tempFn'); // 用Symbol避免属性冲突
  context[fnKey] = this;
  // 执行函数,传入参数,获取返回值
  const result = context[fnKey](...args);
  // 删除临时属性,避免污染context
  delete context[fnKey];
  // 返回函数执行结果
  return result;
};

// 测试
const obj = { name: '张三' };
function sayHello(age) {
  console.log(`我是${this.name},年龄${age}`);
}
sayHello.myCall(obj, 25); // 我是张三,年龄25

17. 实现apply方法

考察点:this绑定、函数执行、参数传递(与call的区别:参数是数组)

// 模拟Function.prototype.apply:改变this指向,参数以数组形式传递,立即执行
Function.prototype.myApply = function(context, args = []) {
  context = context || window;
  const fnKey = Symbol('tempFn');
  context[fnKey] = this;
  // 执行函数,args是数组,用扩展运算符展开
  const result = context[fnKey](...args);
  delete context[fnKey];
  return result;
};

// 测试
const obj = { name: '李四' };
function sayHello(age, gender) {
  console.log(`我是${this.name},年龄${age},性别${gender}`);
}
sayHello.myApply(obj, [28, '男']); // 我是李四,年龄28,性别男

18. 实现bind方法

考察点:this绑定、闭包、函数柯里化、构造函数兼容

// 模拟Function.prototype.bind:改变this指向,返回一个新函数,不立即执行
Function.prototype.myBind = function(context, ...args1) {
  const fn = this; // 保存当前函数(this就是调用myBind的函数)
  // 返回新函数
  const boundFn = function(...args2) {
    // 兼容构造函数:若新函数被new调用,this指向实例,否则指向context
    const isNew = this instanceof boundFn;
    const targetContext = isNew ? this : context;
    // 合并参数,执行原函数
    return fn.apply(targetContext, [...args1, ...args2]);
  };
  // 继承原函数的原型,确保new调用时,实例能访问原函数原型上的属性
  boundFn.prototype = Object.create(fn.prototype);
  return boundFn;
};

// 测试1:普通调用
const obj = { name: '王五' };
function sayHello(age) {
  console.log(`我是${this.name},年龄${age}`);
}
const boundSay = sayHello.myBind(obj, 30);
boundSay(); // 我是王五,年龄30

// 测试2:new调用(构造函数兼容)
function Person(name, age) {
  this.name = name;
  this.age = age;
}
const BoundPerson = Person.myBind(null, '赵六');
const person = new BoundPerson(35);
console.log(person.name); // 赵六
console.log(person.age); // 35

19. 实现防抖+立即执行版

考察点:防抖原理、立即执行逻辑、定时器控制

// 立即执行版防抖:第一次触发立即执行,之后频繁触发不执行,延迟后可再次触发
function debounceImmediate(fn, delay = 500) {
  let timer = null;
  return function(...args) {
    // 若定时器存在,清除定时器(取消延迟执行)
    if (timer) clearTimeout(timer);
    // 判断是否是第一次触发(timer为null)
    const isImmediate = !timer;
    if (isImmediate) {
      fn.apply(this, args); // 立即执行
    }
    // 重置定时器,延迟后清空timer,允许下次立即执行
    timer = setTimeout(() => {
      timer = null;
    }, delay);
  };
}

// 用法(按钮提交示例,避免重复提交,第一次点击立即执行)
const handleSubmit = debounceImmediate(() => {
  console.log('提交表单');
}, 1000);

20. 实现节流+立即执行/延迟执行可选版

考察点:节流原理、参数配置、灵活性优化

// 可选版节流:可配置立即执行(leading)和延迟执行(trailing)
function throttleOpt(fn, interval = 1000, options = { leading: true, trailing: false }) {
  const { leading, trailing } = options;
  let lastTime = 0;
  let timer = null;

  return function(...args) {
    const nowTime = Date.now();
    // 若不允许立即执行,且是第一次触发,重置lastTime
    if (!leading && !lastTime) {
      lastTime = nowTime;
    }

    // 计算剩余时间
    const remainingTime = interval - (nowTime - lastTime);
    // 剩余时间<=0,执行函数
    if (remainingTime <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, args);
      lastTime = nowTime;
    } else if (trailing && !timer) {
      // 允许延迟执行,且无定时器,设置延迟执行
      timer = setTimeout(() => {
        timer = null;
        lastTime = Date.now();
        fn.apply(this, args);
      }, remainingTime);
    }
  };
}

// 用法
// 立即执行,不延迟执行(默认)
const throttle1 = throttleOpt(() => console.log('立即执行'), 1000);
// 不立即执行,延迟执行
const throttle2 = throttleOpt(() => console.log('延迟执行'), 1000, { leading: false, trailing: true });

三、数据结构与算法(大厂高频,高级前端考点)

这类题考察数据结构基础和算法思维,是大厂面试的重点,也是拉开薪资差距的关键,建议重点练习。

21. 两数之和(Two Sum)

考察点:哈希表、时间复杂度优化(从O(n²)优化到O(n))

// 题目:给定一个整数数组和一个目标值,找出数组中和为目标值的两个整数的索引
function twoSum(nums, target) {
  const map = new Map(); // 用Map存储{数值: 索引},快速查找
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i]; // 互补值
    // 若互补值存在于Map中,返回两个索引
    if (map.has(complement)) {
      return [map.get(complement), i];
    }
    // 否则,将当前数值和索引存入Map
    map.set(nums[i], i);
  }
  return []; // 无匹配项,返回空数组
}

// 测试
console.log(twoSum([2, 7, 11, 15], 9)); // [0,1]

22. LRU缓存机制

考察点:哈希表+双向链表、缓存淘汰策略(Vue3响应式缓存底层类似)

// LRU:最近最少使用,超出容量时,删除最久未使用的元素
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity; // 缓存容量
    this.cache = new Map(); // Map特性:插入顺序就是访问顺序,可快速获取最久未使用的元素
  }

  // 获取元素:访问后,将元素移到最近使用的位置
  get(key) {
    if (!this.cache.has(key)) return -1; // 无该元素,返回-1
    const value = this.cache.get(key);
    this.cache.delete(key); // 删除旧位置
    this.cache.set(key, value); // 重新插入,移到末尾(最近使用)
    return value;
  }

  // 存入元素:超出容量时,删除最久未使用的元素(Map的第一个键)
  put(key, value) {
    // 若元素已存在,先删除(避免覆盖后,位置不变)
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    // 超出容量,删除最久未使用的元素
    if (this.cache.size >= this.capacity) {
      const oldestKey = this.cache.keys().next().value; // Map的第一个键是最久未使用的
      this.cache.delete(oldestKey);
    }
    // 存入新元素,移到最近使用的位置
    this.cache.set(key, value);
  }
}

// 测试
const lru = new LRUCache(2);
lru.put(1, 1);
lru.put(2, 2);
console.log(lru.get(1)); // 1(访问后,1变为最近使用)
lru.put(3, 3); // 超出容量,删除最久未使用的2
console.log(lru.get(2)); // -1(已被删除)

23. 发布-订阅模式(EventBus)

考察点:设计模式、组件通信、回调函数管理(Vue EventBus底层原理)

// 发布-订阅模式:实现组件间通信,解耦
class EventEmitter {
  constructor() {
    this.events = new Map(); // 存储事件:{事件名: [回调函数数组]}
  }

  // 订阅事件:绑定回调函数
  on(event, callback) {
    if (!this.events.has(event)) {
      this.events.set(event, []); // 若事件不存在,初始化回调数组
    }
    this.events.get(event).push(callback); // 加入回调数组
  }

  // 发布事件:触发该事件的所有回调函数
  emit(event, ...args) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      // 执行所有回调,传入参数
      callbacks.forEach(cb => cb(...args));
    }
  }

  // 取消订阅:移除指定事件的指定回调
  off(event, callback) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      // 过滤掉要取消的回调,保留其他回调
      this.events.set(event, callbacks.filter(cb => cb !== callback));
      // 若回调数组为空,删除该事件
      if (this.events.get(event).length === 0) {
        this.events.delete(event);
      }
    }
  }

  // 一次性订阅:触发一次后,自动取消订阅
  once(event, callback) {
    // 包装回调,执行后取消订阅
    const wrapCallback = (...args) => {
      callback(...args);
      this.off(event, wrapCallback);
    };
    this.on(event, wrapCallback);
  }
}

// 测试
const bus = new EventEmitter();
const callback = (msg) => console.log('收到消息:', msg);

bus.on('message', callback);
bus.emit('message', 'Hello World'); // 收到消息:Hello World

bus.off('message', callback);
bus.emit('message', 'Hello Again'); // 无输出(已取消订阅)

bus.once('onceEvent', () => console.log('一次性事件'));
bus.emit('onceEvent'); // 一次性事件
bus.emit('onceEvent'); // 无输出(已自动取消)

24. 实现链表反转(单链表)

考察点:链表数据结构、指针操作、递归/迭代思维

// 1. 定义单链表节点
class ListNode {
  constructor(val = 0, next = null) {
    this.val = val;
    this.next = next;
  }
}

// 解法1:迭代法(推荐,空间复杂度O(1))
function reverseList(head) {
  let prev = null; // 前驱节点
  let curr = head; // 当前节点
  while (curr !== null) {
    const next = curr.next; // 保存下一个节点
    curr.next = prev; // 反转当前节点的指针
    prev = curr; // 前驱节点后移
    curr = next; // 当前节点后移
  }
  return prev; // 反转后,prev是新的头节点
}

// 解法2:递归法(易理解,空间复杂度O(n))
function reverseListRecursive(head) {
  // 终止条件:空节点或只有一个节点,直接返回
  if (head === null || head.next === null) return head;
  // 递归反转后续节点
  const newHead = reverseListRecursive(head.next);
  // 反转当前节点和下一个节点的指针
  head.next.next = head;
  head.next = null; // 避免循环
  return newHead;
}

// 测试
const head = new ListNode(1, new ListNode(2, new ListNode(3)));
const reversedHead = reverseList(head);
// 遍历反转后的链表:3 -> 2 -> 1
let curr = reversedHead;
while (curr) {
  console.log(curr.val); // 3 2 1
  curr = curr.next;
}

25. 判断回文链表

考察点:链表操作、双指针、回文判断

// 题目:判断一个单链表是否是回文(正读和反读一样)
// 步骤:1. 找到链表中点;2. 反转后半部分;3. 对比前半部分和反转后的后半部分
function isPalindrome(head) {
  if (head === null || head.next === null) return true; // 空链表或单个节点,是回文

  // 1. 找到链表中点(慢指针走1步,快指针走2步,快指针到末尾时,慢指针到中点)
  let slow = head;
  let fast = head;
  while (fast.next !== null && fast.next.next !== null) {
    slow = slow.next;
    fast = fast.next.next;
  }

  // 2. 反转后半部分链表(从slow.next开始)
  let prev = null;
  let curr = slow.next;
  while (curr !== null) {
    const next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
  }
  slow.next = prev; // 反转后的后半部分链表,头节点是prev

  // 3. 对比前半部分和反转后的后半部分
  let left = head;
  let right = prev;
  while (right !== null) {
    if (left.val !== right.val) return false; // 不相等,不是回文
    left = left.next;
    right = right.next;
  }
  return true; // 全部相等,是回文
}

// 测试
const head1 = new ListNode(1, new ListNode(2, new ListNode(1)));
console.log(isPalindrome(head1)); // true

const head2 = new ListNode(1, new ListNode(2, new ListNode(3)));
console.log(isPalindrome(head2)); // false

26. 二叉树的前序遍历(递归+迭代)

考察点:二叉树数据结构、遍历算法、递归/迭代思维

// 1. 定义二叉树节点
class TreeNode {
  constructor(val = 0, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

// 解法1:递归法(简洁,易理解)
function preorderTraversalRecursive(root, result = []) {
  if (root === null) return result;
  result.push(root.val); // 根节点
  preorderTraversalRecursive(root.left, result); // 左子树
  preorderTraversalRecursive(root.right, result); // 右子树
  return result;
}

// 解法2:迭代法(面试常考,避免递归栈溢出)
function preorderTraversalIterative(root) {
  const result = [];
  if (root === null) return result;
  const stack = [root]; // 用栈存储节点
  while (stack.length > 0) {
    const node = stack.pop(); // 弹出栈顶节点(根节点)
    result.push(node.val);
    // 注意:栈是先进后出,所以先压右子树,再压左子树
    if (node.right !== null) stack.push(node.right);
    if (node.left !== null) stack.push(node.left);
  }
  return result;
}

// 测试
const root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3)));
console.log(preorderTraversalRecursive(root)); // [1,2,3]
console.log(preorderTraversalIterative(root)); // [1,2,3]

27. 二叉树的中序遍历(递归+迭代)

考察点:二叉树遍历、栈的应用

// 解法1:递归法
function inorderTraversalRecursive(root, result = []) {
  if (root === null) return result;
  inorderTraversalRecursive(root.left, result); // 左子树
  result.push(root.val); // 根节点
  inorderTraversalRecursive(root.right, result); // 右子树
  return result;
}

// 解法2:迭代法
function inorderTraversalIterative(root) {
  const result = [];
  const stack = [];
  let curr = root;
  while (curr !== null || stack.length > 0) {
    // 先遍历左子树,所有左节点入栈
    while (curr !== null) {
      stack.push(curr);
      curr = curr.left;
    }
    // 弹出栈顶节点(左子树最底层节点),加入结果
    curr = stack.pop();
    result.push(curr.val);
    // 遍历右子树
    curr = curr.right;
  }
  return result;
}

// 测试
const root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3)));
console.log(inorderTraversalRecursive(root)); // [1,3,2]
console.log(inorderTraversalIterative(root)); // [1,3,2]

28. 斐波那契数列(递归+迭代+优化)

考察点:递归、动态规划、时间/空间复杂度优化

// 题目:求第n个斐波那契数(F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2))

// 解法1:递归法(简单但效率低,时间复杂度O(2ⁿ),有重复计算)
function fibRecursive(n) {
  if (n <= 1) return n;
  return fibRecursive(n - 1) + fibRecursive(n - 2);
}

// 解法2:迭代法(推荐,时间复杂度O(n),空间复杂度O(1))
function fibIterative(n) {
  if (n <= 1) return n;
  let prevPrev = 0; // F(n-2)
  let prev = 1; // F(n-1)
  let curr = 0;
  for (let i = 2; i <= n; i++) {
    curr = prevPrev + prev; // F(n) = F(n-2) + F(n-1)
    prevPrev = prev;
    prev = curr;
  }
  return curr;
}

// 解法3:动态规划(空间复杂度O(n),适合需要保存所有斐波那契数的场景)
function fibDP(n) {
  if (n <= 1) return n;
  const dp = new Array(n + 1);
  dp[0] = 0;
  dp[1] = 1;
  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n];
}

// 测试
console.log(fibIterative(10)); // 55

29. 最长公共前缀

考察点:字符串操作、遍历对比、边界处理

// 题目:编写一个函数,找出字符串数组中的最长公共前缀
function longestCommonPrefix(strs) {
  // 边界判断:数组为空,返回空字符串
  if (strs.length === 0) return '';
  // 以第一个字符串为基准,逐个字符对比
  let prefix = strs[0];
  for (let i = 1; i < strs.length; i++) {
    // 循环对比当前字符串和基准字符串,直到找到公共前缀
    while (strs[i].indexOf(prefix) !== 0) {
      // 若不匹配,缩短基准字符串(去掉最后一个字符)
      prefix = prefix.slice(0, prefix.length - 1);
      // 若基准字符串为空,说明没有公共前缀,直接返回
      if (prefix === '') return '';
    }
  }
  return prefix;
}

// 测试
console.log(longestCommonPrefix(["flower","flow","flight"])); // "fl"
console.log(longestCommonPrefix(["dog","racecar","car"])); // ""

30. 验证回文串

考察点:字符串处理、正则表达式、双指针

// 题目:验证一个字符串是否是回文串(只考虑字母和数字,忽略大小写)
function isPalindromeStr(s) {
  // 1. 过滤无效字符(只保留字母和数字),并转为小写
  const validStr = s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
  // 2. 双指针:左指针从开头,右指针从末尾,逐步对比
  let left = 0;
  let right = validStr.length - 1;
  while (left < right) {
    if (validStr[left] !== validStr[right]) {
      return false; // 不相等,不是回文串
    }
    left++;
    right--;
  }
  return true; // 全部相等,是回文串
}

// 测试
console.log(isPalindromeStr("A man, a plan, a canal: Panama")); // true
console.log(isPalindromeStr("race a car")); // false

写在最后

这30个手写JS算法,覆盖了前端面试从基础到进阶的所有高频考点——基础语法、数组操作、原型作用域、数据结构、算法思维,每道题都能直接复制到编辑器调试,吃透这30道题,面试时再遇到手写算法题,就能从容应对。

很多前端同学觉得算法难,其实是没找对方法:不用刷上千道题,重点吃透这些高频题,搞懂每道题的原理和考点,比盲目刷题更有效。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

前端性能优化实战:从3秒到1秒,我只做了这5件事(全网通用)

作者 前端Hardy
2026年4月9日 16:16

做前端的都知道,页面加载速度就是生命线

一个项目做完,代码写得再漂亮、框架选得再先进,只要打开慢个1-2秒,用户流失率直接翻倍。最近我接手了一个老项目,首屏加载要 3.5秒,通过一系列针对性优化,最终压到了 0.8秒

今天把这套通用型性能优化方案分享给大家,不限Vue、React、小程序,只要是前端项目,复制粘贴就能提升加载速度,收藏这一篇,面试、工作都能用!


一、现状复盘:为什么你的页面那么卡?

先别急着写代码,先搞清楚瓶颈在哪里。建议在开发环境打开 Chrome 开发者工具 -> Lighthouse 跑一次测评。

通常前端加载慢,无非逃不过这3点:

  1. 体积过大:打包后的JS/CSS体积太大,网络传输慢;
  2. 请求过多:首屏加载了无关的接口、图片,造成网络拥堵;
  3. 渲染阻塞:JS没加载完,页面就是一片空白,用户体验极差。

下面的5步优化,就是针对这三大痛点,由浅入深,解决80%的性能问题。

二、核心干货:5个必做优化步骤(直接复制)

1. 路由懒加载:只加载当前需要的代码

这是性价比最高的优化!默认情况下,Webpack会把所有路由打包成一个巨大的 app.js。首屏不管去哪个页面,都要把所有代码下载下来。

解决思路: 按路由拆分,只加载当前页面的代码。

Vue3 / Vite 写法

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    // 直接引入,首屏会加载
    component: () => import('@/views/Home.vue') 
  },
  {
    path: '/about',
    name: 'About',
    // 关键:使用箭头函数+import,实现路由懒加载
    // 访问该路由时才会加载对应的Chunk文件
    component: () => import('@/views/About.vue') 
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

React / React Router 写法

// 同样适用React,只需在路由配置处改一下
const About = React.lazy(() => import('@/views/About.vue')); 
// 配合Suspense显示加载中
import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}

2. 图片优化:去重+压缩,体积减半

图片通常是项目体积最大的资源。不要直接把UI给的原图丢上去。

3招搞定图片优化:

  1. 使用现代格式:把PNG/JPG转为 WebPAVIF,体积可缩小50%以上,兼容性极好。
  2. 压缩图片:使用 TinyPNG 或 Webpack 插件(如image-webpack-loader)自动压缩。
  3. 懒加载(Lazy Loading):给图片加上 loading="lazy" 属性,滚动到可视区域再加载,首屏请求瞬间减少。

原生写法(所有框架通用)

<!-- 只需添加这个属性,自动实现懒加载 -->
<img src="image.webp" loading="lazy" alt="优化后" />

Vue/React 中使用

在你的UI库(Element/Ant Design)中使用图片组件时,直接添加属性即可:

<el-image 
  src="image.webp" 
  loading="lazy" 
  fallback="fallback.png" <!-- 降级处理 -->
/>

3. 移除console和注释:清掉“垃圾代码”

打包发布时,千万别把 console.logdebugger 和大量注释打包进去。这些不仅增加体积,还会暴露前端代码,存在安全风险。

解决方案: 在Vite/Webpack配置中一键清除。

Vite 配置(vue.config.js 或 vite.config.js)

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    // 生产环境清除console
    minify: 'ter', // 使用terser进行压缩
    terserOptions: {
      compress: {
        drop_console: true, // 移除所有console
        drop_debugger: true // 移除debugger
      }
    }
  }
})

4. 资源压缩:Gzip / Brotli 终极武器

这一步是服务器端的,但只要是前端开发,必须跟后端同学沟通开启。

开启Gzip或Brotli压缩后,服务器会对传输的JS、CSS、HTML文件进行压缩,传输体积能减少60%-80%

如何配置:

  • Nginx:在配置文件中开启 gzip on;
  • Node.js (Express):使用 compression 中间件
  • 云服务:在阿里云/腾讯云CDN控制台直接开启Brotli压缩

效果对比: 原来100KB的JS文件,压缩后只剩20-30KB,加载速度快得惊人!

5. 核心JS降级与polyfill:告别老旧浏览器拖累

现在的ES6+语法很强大,但如果不转译,老旧浏览器(如IE11,甚至旧版Chrome)无法识别,会被迫重新加载大量polyfill补丁。

解决方案: 使用Babel或ESBuild进行转译。

Babel 配置(.babelrc)

{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage", // 按需引入polyfill
      "corejs": 3 // 指定core-js版本
    }]
  ]
}

作用:自动识别你代码中用到的ES6+语法,只引入必要的补丁,大幅减少打包体积。


三、避坑指南:这2件事千万别做

  1. 不要过度优化:比如把一个小工具库手动从项目中移除,得不偿失。优先优化首屏体积和网络请求,这才是最直观的体验提升。
  2. 不要忽略首屏空白:即使加载快了,如果页面是白屏直到JS加载完才显示,用户体验依然不好。可以在 index.html 中添加简单的骨架屏(Skeleton Screen)。

四、写在最后

性能优化不是一蹴而就的,它是一个持续迭代的过程。今天分享的这5个方法,是前端项目上线前的必做项

你会发现,很多时候不需要引入复杂的库,也不需要重写整个项目,只需要对现有配置做几处微调,性能就能有质的飞跃。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

npm 依赖管理:打包策略与依赖声明最佳实践

作者 JayCC1
2026年4月9日 16:12

目录

核心问题

问题场景

当一个包(pkgA)构建时将其依赖(pkgB)的代码打包进bundle后,如果pkgA的package.json中仍将pkgB声明为dependencies,那么用户安装pkgA时仍会下载安装pkgB,造成:

  1. 磁盘空间浪费(重复代码)
  2. 潜在的版本冲突
  3. 安装时间增加

依赖类型对比

依赖类型 谁安装 何时使用 示例场景
dependencies npm自动安装 包运行时必需,不期望用户提供 工具函数、纯JS库
peerDependencies 用户安装 需要与用户项目共享实例 React组件库、插件系统
devDependencies npm开发时安装 仅开发、测试、构建需要 TypeScript、测试框架
optionalDependencies 可选安装 功能增强,非必需 平台特定优化

打包策略

策略一:完全打包(自包含)

// package.json
{
  "name": "pkgA",
  "devDependencies": {
    "pkgB": "^1.0.0" // 仅开发需要
  }
  // 不声明 dependencies 或 peerDependencies
}

配置示例(Webpack):

// 不配置 externals,所有依赖都打包
module.exports = {
  // ... 无 externals 配置
};

适用场景:

  • 工具库、独立应用
  • 依赖体积小、API稳定
  • 希望用户安装简单

优点:

  • 用户安装简单:npm install pkgA
  • 无版本冲突风险
  • 自包含,无外部依赖

缺点:

  • bundle体积较大
  • 依赖无法单独更新

策略二:外部化 + peerDependencies

// package.json
{
  "name": "pkgA",
  "peerDependencies": {
    "pkgB": "^1.0.0" // 用户需要安装
  },
  "devDependencies": {
    "pkgB": "^1.0.0" // 开发测试用
  }
}

配置示例(Webpack):

module.exports = {
  externals: {
    pkgB: {
      commonjs: "pkgB",
      commonjs2: "pkgB",
      amd: "pkgB",
      root: "PkgB",
    },
  },
};

适用场景:

  • 插件、组件库
  • 依赖体积大、频繁更新
  • 需要与用户项目共享依赖实例

优点:

  • bundle体积小
  • 用户可控制依赖版本
  • 依赖可单独更新

缺点:

  • 用户需要额外安装
  • 可能版本不兼容

策略三:混合策略

// package.json
{
  "peerDependencies": {
    "react": "^18.0.0", // 外部化,用户安装
    "lodash": "^4.0.0" // 外部化,用户安装
  },
  "dependencies": {
    "tiny-utils": "^1.0.0" // 完全打包,不外部化
  }
}

peerDependencies详解

核心概念

peerDependencies表示:"我的包需要这些依赖,但我不自己安装,我希望使用我的人来安装这些依赖。"

实际示例

// React组件库示例
{
  "name": "my-react-components",
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  }
}

npm版本行为差异

npm版本 行为
npm 6及以下 只警告,不自动安装peerDependencies
npm 7+ 默认自动安装peerDependencies

强制指定为可选

{
  "peerDependencies": {
    "some-optional-dep": "^1.0.0"
  },
  "peerDependenciesMeta": {
    "some-optional-dep": {
      "optional": true
    }
  }
}

常见错误与解决方案

错误1:代码已打包,仍声明为dependencies

// ❌ 错误做法
{
  "dependencies": {
    "pkgB": "^1.0.0" // 代码已打包,但还声明依赖
  }
}

结果: 用户安装两份pkgB(一份打包,一份独立)

错误2:dependencies和peerDependencies重复声明

// ❌ 错误做法(npm 7+会报错)
{
  "dependencies": {
    "pkgB": "^1.0.0"
  },
  "peerDependencies": {
    "pkgB": "^2.0.0" // 版本冲突!
  }
}

解决方案:

// ✅ 正确做法
{
  "peerDependencies": {
    "pkgB": "^2.0.0" // 只保留一个
  },
  "devDependencies": {
    "pkgB": "^2.0.0" // 开发用
  }
}

错误3:忘记配置externals

// package.json
{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}
// webpack.config.js - ❌ 忘记配置externals
module.exports = {
  // 缺少 externals 配置,react仍会被打包
};

错误4:相同版本同时声明(反模式)

// ❌ 技术可行但逻辑混乱
{
  "dependencies": {
    "pkgB": "^1.0.0"
  },
  "peerDependencies": {
    "pkgB": "^1.0.0" // 完全相同版本
  }
}

问题:

  • dependencies 说"我硬依赖这个,已打包到代码中"
  • peerDependencies 说"我需要用户提供这个,与用户共享实例"
  • 两个声明自相矛盾,虽然技术上可行但不推荐

解决方案: 选择其中一种策略,不要混淆

npm依赖安装机制深度解析

npm 扁平化策略(npm 3+)

modern npm 采用扁平化安装策略来避免重复安装和过深的目录结构:

正常情况(扁平化):
node_modules/
  myPkg/
  pkgB/          ← 被提升到顶级,多个包可共享

版本冲突(嵌套):
node_modules/
  myPkg/
    node_modules/
      pkgB@1.0.0/  ← 无法共享,嵌套安装
  pkgB@2.0.0/      ← 用户要求的版本

各种配置组合的完整分析

组合1:仅 dependencies

{
  "dependencies": { "pkgB": "^1.0.0" }
}
场景 安装结果 位置 说明
用户项目不需要 pkgB ✅ 安装 node_modules/pkgB/ 扁平化,顶级安装
用户需要 pkgB@1.0.0 ✅ 安装 node_modules/pkgB/ 扁平化,共享一个副本
用户需要 pkgB@2.0.0 ✅ 安装(两个版本) node_modules/myPkg/node_modules/pkgB@1.0.0/ + node_modules/pkgB@2.0.0/ 版本冲突,嵌套安装

特点: 标准硬依赖,没有问题


组合2:仅 peerDependencies

{
  "peerDependencies": { "pkgB": "^1.0.0" }
}
场景 安装结果 位置 说明
用户项目不需要 pkgB ❌ 不安装 npm 7+ 会警告,用户使用时会报错
用户需要 pkgB@1.0.0 ✅ 安装 node_modules/pkgB/ 用户安装后,myPkg 可以访问
用户需要 pkgB@2.0.0 ❌ 版本不兼容 可能运行时报错

特点: 用户必须自己提供依赖,常用于插件系统


组合3:同版本的 dependencies + peerDependencies

{
  "dependencies": { "pkgB": "^1.0.0" },
  "peerDependencies": { "pkgB": "^1.0.0" }
}
场景 安装结果 位置 说明
用户项目不需要 pkgB ✅ 安装 node_modules/pkgB/ 扁平化,顶级安装
用户需要 pkgB@1.0.0 ✅ 安装 node_modules/pkgB/ 扁平化,共享一个副本
用户需要 pkgB@2.0.0 ⚠️ 版本冲突 node_modules/myPkg/node_modules/pkgB@1.0.0/ + node_modules/pkgB@2.0.0/ 虽然技术可行,但逻辑混乱

特点: 反模式,技术可行但不推荐(npm 7+ 允许但提示冗余)


组合4:不同版本的 dependencies + peerDependencies

{
  "dependencies": { "pkgB": "^1.0.0" },
  "peerDependencies": { "pkgB": "^2.0.0" }
}
npm 版本 安装结果 说明
npm 6 及以下 ⚠️ 警告但继续 允许但有警告,可能导致意外行为
npm 7+ ❌ 报错 直接拒绝,要求修复配置

特点: 配置冲突,绝对不能这样用


组合5:dependencies + peerDependencies + peerDependenciesMeta(optional)

{
  "dependencies": { "pkgB": "^1.0.0" },
  "peerDependencies": { "pkgB": "^1.0.0" },
  "peerDependenciesMeta": { "pkgB": { "optional": true } }
}
场景 安装结果 位置 说明
用户项目不需要 pkgB ✅ 安装 node_modules/pkgB/ 扁平化,dependencies 优先,optional 被忽视
用户需要 pkgB@1.0.0 ✅ 安装 node_modules/pkgB/ 扁平化,共享一个副本
用户需要 pkgB@2.0.0 ⚠️ dependencies 优先 node_modules/myPkg/node_modules/pkgB@1.0.0/ + node_modules/pkgB@2.0.0/ optional 标记在此情况下无效

特点: dependencies 硬依赖优先级更高,optional 标记被忽视


配置方案综合对比

配置方案 是否下载 安装位置(无冲突) 是否可共享 用户体验 推荐指数
仅 dependencies node_modules/pkgB/ 自动,简单 ⭐⭐⭐⭐⭐
仅 peerDependencies 需自己装 ⭐⭐⭐⭐
两者同版本 node_modules/pkgB/ 自动,简单 ⭐⭐ (反模式)
两者不同版本 npm 7+ 报错 配置冲突 ⭐ (禁用)
dependencies + optional node_modules/pkgB/ 自动,简单 ⭐ (反模式)

实际使用建议

  1. 优先使用「仅 dependencies」(大多数场景)

    • 简单清晰,无版本冲突
    • 用户零配置
    • npm 扁平化自动处理重复安装
  2. 选择「仅 peerDependencies」(特定场景)

    • 插件系统、中间件框架
    • React/Vue 组件库
    • 需要用户与包共享同一实例
  3. 避免所有混合配置

    • 如果需要同时声明,代表设计有问题
    • 重新评估是否真的需要同时配置两个

最佳实践总结

决策流程图

开始构建包
    ↓
评估依赖特点
    ├── 体积小、稳定、纯JS → 完全打包 + 不声明依赖
    ├── 体积大、频繁更新 → 外部化 + peerDependencies
    └── 混合情况 → 分类处理

具体指南

  1. 完全打包策略(适合工具库)

    • 不配置externals
    • 不声明dependencies(可在devDependencies声明)
    • 优点:用户安装简单
  2. 外部化策略(适合插件/组件库)

    • 配置externals
    • 声明peerDependencies
    • 在devDependencies声明开发版本
    • 优点:用户控制版本
  3. 绝对避免

    • 代码打包了,还声明为dependencies
    • 同时在dependencies和peerDependencies声明同一依赖
    • 忘记配置externals但使用peerDependencies

发布前检查清单

  • 是否所有打包的依赖都已从dependencies移除?
  • externals配置是否正确?
  • peerDependencies版本范围是否合理?
  • 是否在devDependencies中声明了开发版本?
  • README是否说明了安装要求?

工具与验证

检查命令

# 查看依赖树
npm list [package-name]

# 查看包大小
du -sh node_modules/[package-name]

# 查看重复包
npm ls --depth=10 | grep -E "dedupe|multiple"

# 检查实际发布内容
npx npm-packlist

验证脚本

// scripts/verify-deps.js
const fs = require("fs");
const pkg = require("./package.json");

console.log("🔍 检查依赖声明...\n");

// 检查重复声明
const deps = Object.keys(pkg.dependencies || {});
const peerDeps = Object.keys(pkg.peerDependencies || {});
const conflicts = deps.filter((dep) => peerDeps.includes(dep));

if (conflicts.length > 0) {
  console.error("❌ 发现重复声明:");
  conflicts.forEach((dep) => {
    console.error(
      `  ${dep}: dependencies=${pkg.dependencies[dep]}, peerDependencies=${pkg.peerDependencies[dep]}`,
    );
  });
  process.exit(1);
}

console.log("✅ 依赖声明检查通过");

测试安装

# 1. 链接本地包
cd /path/to/pkgA
npm link

# 2. 在测试项目中使用
cd /path/to/test-project
npm link pkgA
npm install

# 3. 检查安装结果
npm list pkgB

记住黄金法则:要么完全打包(不声明依赖),要么完全外部化(使用peerDependencies)。避免中间状态导致的重复安装和版本冲突。

JS炼化:手写一下promise——用一份外卖,看懂状态机+两个回调篓子

作者 忆往wu前
2026年4月9日 15:54

用一份外卖,看懂状态机+两个回调篓子

不少初学者看到完整版Promise手写源码就犯难,繁杂的边界处理和进阶优化让人望而生畏。其实抛开这些锦上添花的拓展逻辑,Promise的核心本质特别简单:一个状态机 + 两个回调篓子,所有功能都由此衍生。手写Promise从来不是盲目造轮子,核心目的就是拆开原生API的黑盒,告别只会调用不懂原理的陌生感,彻底吃透异步运行的底层逻辑。

大家或许对源码逻辑感到晦涩,但一定熟悉点外卖的日常。接下来我就用点外卖的生活化比喻,带你轻松弄懂状态机和回调篓子的核心运作逻辑。

先看逻辑思路

你点了一份外卖 = 发起一个异步任务

1. 状态机 = 外卖订单状态
  •  pending :商家正在做饭(任务进行中)
  •  fulfilled :外卖送到你手上(成功)
  •  rejected :商家没货/取消订单(失败)

状态机干了啥?

  • 告诉你现在能不能吃
  • 保证只会送一次,不会反复送
  • 饭做好了就一直是做好的状态,不会变回“正在做”
  • 结果会永久保存,你什么时候拿都有

(状态机 = 给异步任务定规矩: 只能走一次,走到哪就是哪,结果永久留着。)

2.回调篓子 = 你给外卖员留的“送达通知方式”

你还没拿到外卖时( pending ),你跟外卖员说:

  • 送到了给我打电话
  • 送不成给我发短信

这些“打电话、发短信”,就是你存在  then  里的回调。

回调篓子干了啥?

  • 饭还没好,先把你的要求存起来
  • 不催、不闹、不嵌套
  • 等饭一好,一次性按顺序执行

(回调篓子 = 暂存你的“后续操作”, 异步没跑完,先排队等通知。)

3. 两者合在一起,才是 Promise

流程是这样的:

1. 你下单 → Promise 新建

2. 状态立刻变成  pending → 商家正在做饭

3. 你调用  .then()  留下回调 → 把“打电话/发短信”放进篓子存好

4.饭做好了 →  resolve()

  • 状态变成  fulfilled 
  • 拿出成功篓子里的所有回调,挨个执行 → 挨个打电话通知你

5.饭做不成 →  reject()

  • 状态变成  rejected 
  • 拿出失败篓子里的回调,挨个执行 → 发短信告诉你取消了

再到后面链式调用“骑手”干了什么,“平台”有哪些补救措施

(今天我们就以点外卖的方式: 从0 开始,一小块一小块叠代码,先懂原理再落地实现,彻底撕开 Promise 的黑盒!)

 

第一阶段:逐块拆解手写「纯状态机基础版Promise」

比喻以注释的形式写进代码里方便理解

下单必有初始状态,Promise 创建瞬间也必须固定 pending 态,状态只能单向流转,这是一切的根基。我们从零一块块码

第1块:定义三大核心状态常量(杜绝硬编码写错)

// 对标外卖三种固定状态:做饭中 / 已送达 / 已取消
const STATUS_PENDING = "pending";    // 等待中-正在做饭
const STATUS_FULFILLED = "fulfilled";// 成功-外卖送到
const STATUS_REJECTED = "rejected";  // 失败-订单取消

为什么单独定义常量? 统一管理状态名,全程复用,避免手写字符串拼写错误。(可以理解为有报错提示,同时也能让大家看代码更清晰的行业规范)

第2块:搭建Promise空类骨架(相当于开通下单通道)

// 创建自定义Promise类,开始搭建订单系统外壳
class _Promise {
    // 构造函数:实例化瞬间触发 = 用户点击下单
    constructor(executor = () => {}) {

    }
}

executor 就是商家做饭的核心流程,默认给空函数防止报错。

第3块:构造器初始化核心属性(订单基础信息登记)

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        // 刚下单默认状态:商家正在做饭 pending
        this.status = STATUS_PENDING;
        // 预留位置:存放送到手的外卖成果(成功结果)
        this.value = undefined;
        // 预留位置:存放订单失败的原因(商家没货/超时)
        this.reason = undefined;
    }
}

核心规则:只要Promise一创建,天生固定 pending,绝不允许开局直接成功/失败。

第4块:内部定义resolve函数(外卖送达开关)

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;

        // 专属开关:调用就代表外卖顺利送达
        const resolve = (value) => {
            // 铁律校验:只有还在做饭中,才能改成已送达
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                // 把到手的外卖存起来
                this.value = value;
            }
        };
    }
}

状态不可逆核心体现:已经送达/取消的订单,再也不能二次修改状态。

第5块:内部追加reject函数(订单取消开关)

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;

        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
            }
        };

        // 专属开关:调用就代表订单作废取消
        const reject = (reason) => {
            // 同样铁律:只有做饭中才能取消订单
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                // 把取消原因记录下来
                this.reason = reason;
            }
        };
    }
}

第6块:立即执行executor做饭流程(下单立刻开工)

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;

        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
            }
        };

        const reject = (reason) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
            }
        };

        // 下单瞬间直接开火做饭!把两个开关交给外部掌控
        executor(resolve, reject);
    }
}

关键特性:Promise构造器里的执行器是同步立即执行,不会等待延迟。

第7块:追加基础then方法(外卖到了要干啥)

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;

        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
            }
        };

        const reject = (reason) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
            }
        };

        executor(resolve, reject);
    }

    // 定制收货操作:成功干啥、失败干啥
    then(onFulfilled, onRejected) {
        // 外卖已经送到,有成功回调就直接执行
        if (this.status === STATUS_FULFILLED && typeof onFulfilled === 'function') {
            onFulfilled(this.value);//这个onFulfilled传进来必须是函数,加判断为了防止报错崩程序
        }
        // 订单已经取消,有失败回调就直接执行
        if (this.status === STATUS_REJECTED && typeof onRejected === 'function') {
            onRejected(this.reason);//这里同理
        }
        // 注意:当前版本暂时处理不了还在做饭中的异步等待场景
    }
}

第一阶段整合完整版(逐块拼装最终成品)

// 1. 定义外卖三大状态常量
const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

// 2. 自定义基础Promise类
class _Promise {
    constructor(executor = () => {}) {
        // 3. 初始化订单默认状态和存储容器
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;

        // 4. 送达开关逻辑
        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
            }
        };

        // 5. 取消开关逻辑
        const reject = (reason) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
            }
        };

        // 6. 立刻启动做饭流程
        executor(resolve, reject);
    }

    // 7. 收货处理then方法
    then(onFulfilled, onRejected) {
        if (this.status === STATUS_FULFILLED && typeof onFulfilled === 'function') {
            onFulfilled(this.value);
        }
        if (this.status === STATUS_REJECTED && typeof onRejected === 'function') {
            onRejected(this.reason);
        }
    }
}

下一阶段我们就给这套基础状态机装上「回调篓子队列」,解决异步等待存任务的问题,完美适配真实延时外卖场景~

第二阶段:加装「回调篓子」完整版(衔接基础状态机,递进改造)

上一版只有状态机,处理不了异步延时——就像外卖要等一会才送到,你提前说了收货要做的事,总得先记下来,这两个数组  resolveQueue/rejectQueue  就是专门存事的「回调篓子」。

第2块:类骨架不变,构造器里新增两个回调篓子属性

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        // 原有基础状态不变
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;

        // ========== 全新加装:两个回调篓子 ==========
        // 成功篓子:存外卖没送到时,所有收货要做的事
        this.resolveQueue = [];
        // 失败篓子:存订单没取消前,所有失败兜底要做的事
        this.rejectQueue = [];
    }
}

改动说明:凭空新增两个数组,专门排队存待执行的回调函数,对应「先记下来,等送达再办」。

第3块:改造 resolve 函数——送达后自动清空执行成功篓子

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.resolveQueue = [];
        this.rejectQueue = [];

        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
                // ========== 新增逻辑:外卖送到,挨个执行篓子里所有寄存的事 ==========
                this.resolveQueue.forEach(fn => fn(this.value));
            }
        };
    }
}

第4块:同步改造 reject 函数——订单取消清空执行失败篓子

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.resolveQueue = [];
        this.rejectQueue = [];

        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
                this.resolveQueue.forEach(fn => fn(this.value));
            }
        };

        const reject = (reason) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
                // ========== 新增逻辑:订单取消,挨个执行失败篓子里寄存的事 ==========
                this.rejectQueue.forEach(fn => fn(this.reason));
            }
        };
    }
}

第6块:核心改造 then 方法——判断 pending,把回调塞进篓子

这是最关键改动:外卖还在做(pending),不执行,直接把事装篓子里排队

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.resolveQueue = [];
        this.rejectQueue = [];

        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
                this.resolveQueue.forEach(fn => fn(this.value));
            }
        };

        const reject = reason => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
                this.rejectQueue.forEach(fn => fn(this.reason));
            }
        };

        executor(resolve, reject);
    }

    then(onFulfilled, onRejected) {
        // 情况1:已经送到了,直接办收货的事
        if (this.status === STATUS_FULFILLED ) {
            onFulfilled(this.value);
        }
        // 情况2:已经取消了,直接办兜底的事
        else if (this.status === STATUS_REJECTED ) {
            onRejected(this.reason);
        }
        // ========== 全新核心逻辑:还在做饭 pending → 塞进对应篓子存起来 ==========
        else if (this.status === STATUS_PENDING) {
           this.resolveQueue.push(onFulfilled);
           this.rejectQueue.push(onRejected);
        }
    }
}

加装回调篓子·阶段完整汇总代码

// 外卖三态常量
const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        // 基础状态机属性
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;

        // 两个回调篓子队列
        this.resolveQueue = [];
        this.rejectQueue = [];

        // 送达开关 + 执行成功队列
        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
                this.resolveQueue.forEach(fn => fn(this.value));
            }
        };

        // 取消开关 + 执行失败队列
        const reject = (reason) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
                this.rejectQueue.forEach(fn => fn(this.reason));
            }
        };

        // 立刻执行做饭流程
        executor(resolve, reject);
    }

    // 智能分发:已完成直接执行,pending就装篓子
    then(onFulfilled, onRejected) {
        if (this.status === STATUS_FULFILLED ) {
            onFulfilled(this.value);
        } else if (this.status === STATUS_REJECTED ) {
            onRejected(this.reason);
        } else if (this.status === STATUS_PENDING) {
             this.resolveQueue.push(onFulfilled);
             this.rejectQueue.push(onRejected);
        }
    }
}

第三阶段:刚需进阶版 基础链式调用(无边界裸奔版,核心骨架)

加装核心链式调用真正解决回调函数嵌套地狱

const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        this.status = STATUS_PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.rejectReason = undefined;
        this.resolveQueue = [];
        this.rejectQueue = [];

        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
                this.resolveQueue.forEach(fn => fn(this.value));
            }
        };

        const reject = (reason) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
                this.rejectQueue.forEach(fn => fn(this.reason));
            }
        };

        executor(resolve, reject);
    }

    then(onFulfilled, onRejected) {
        // 核心刚需:返回新实例,实现链式接龙
        return new _Promise((nextResolve, nextReject) => {
            // 封装统一骑手任务:复用逻辑、可立即执行可入篓寄存、中转链式值
             // 封装统一处理函数handleSuccess原因:
            // 1.逻辑抽离复用,不用多处重复写判断/执行逻辑
            // 2.包装成独立函数,既能立即执行,也可直接塞进队列寄存
            // 3.中转结果交给下一个Promise,支撑链式流转
            const handleSuccess = () => {
                const res = onFulfilled(this.value);
                nextResolve(res);
            };
               // 同成功处理逻辑:统一封装复用、可入队列、中转链式结果
            const handleFail = () => {
                const err = onRejected(this.reason);
                nextResolve(err);
            };

            if (this.status === STATUS_FULFILLED) {
                handleSuccess();
            } else if (this.status === STATUS_REJECTED) {
                handleFail();
            } else if (this.status === STATUS_PENDING) {
                this.resolveQueue.push(handleSuccess);
                this.rejectQueue.push(handleFail);
            }
        });
    }
}

刚需裸奔版存在核心问题(对应骑手配送漏洞)

1. 执行器executor报错直接崩程序(后厨做饭出事直接瘫痪,无应急)

2. then乱传非函数、空传省略回调直接报错(招了不会干活的假骑手,配送直接翻车)

3. 无值透传,中间空then直接断链式(中途骑手离岗,外卖没人接力送,链路废掉)   4. then内部回调自己报错无捕获(骑手送餐中途出事,全程没人兜底救援)

分步针对性边界优化(只改对应位置)

优化1:构造器加try/catch 兜底executor全局报错

只改constructor最后一行执行代码,其余全不变:

// 原有执行代码删掉,替换成下面
try {
  executor(resolve, reject); // 正常执行商家出餐
} catch (err) {
  reject(err); // 改动注释:后厨做饭报错直接转订单失败兜底,不崩系统
}

  优化2:加函数校验+值透传 解决乱传/空传骑手断链问题

只改then里handle核心逻辑+队列存入判断:

const handleSuccess = () => {
  // 改动注释:判断是不是正经骑手函数,不是就原值透传接力,不废单
  const res = typeof onFulfilled === 'function' ? onFulfilled(this.value) : this.value;
  nextResolve(res);
};
const handleFail = () => {
  // 改动注释:失败回调同样校验+原因透传,保证坏单也能顺畅接力
  const err = typeof onRejected === 'function' ? onRejected(this.reason) : this.reason;
  nextResolve(err);
};

// 底部pending入队也改一行
if (this.status === STATUS_PENDING) {
  // 改动注释:只把正经骑手存进任务篓,假骑手直接拒收不占用队列
  typeof onFulfilled === 'function' && this.resolveQueue.push(handleSuccess);
  typeof onRejected === 'function' && this.rejectQueue.push(handleFail);
}
 

优化3:内层try/catch 兜底骑手送餐中途自身报错(最终闭环)

只再包一层内部捕获:

const handleSuccess = () => {
  // 改动注释:骑手干活中途出错即时救援,报错直接切失败单流转
  try {
    const res = typeof onFulfilled === 'function' ? onFulfilled(this.value) : this.value;
    nextResolve(res);
  } catch (err) {
    nextReject(err);
  }
};
const handleFail = () => {
  try {
    const err = typeof onRejected === 'function' ? onRejected(this.reason) : this.reason;
    nextResolve(err);
  } catch (err) { // 改动注释:失败处理出错同样兜底捕获,全链路无死角
    nextReject(err);
  }
};

 

最终完整完善版(直接阅读注释说明看到整个流程)

// 外卖订单三大固定状态:待接单/配送完成/订单拒收取消
const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

class _Promise {
    constructor(executor = () => {}) {
        // 初始化订单默认状态:全部处于待接单
        this.status = STATUS_PENDING;
        // 存放配送完成的餐品结果
        this.value = undefined;
        // 存放订单拒收的原因备注
        this.reason = undefined;

        // 骑手任务收纳篓:排队等候的配送任务/拒收善后任务
        this.resolveQueue = [];
        this.rejectQueue = [];

        // 配送放行开关:只有待接单能改成完成,批量执行所有等候配送骑手
        const resolve = (value) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_FULFILLED;
                this.value = value;
                this.resolveQueue.forEach(fn => fn(this.value));
            }
        };

        // 订单拒收开关:只有待接单能改成取消,批量执行善后骑手任务
        const reject = (reason) => {
            if (this.status === STATUS_PENDING) {
                this.status = STATUS_REJECTED;
                this.reason = reason;
                this.rejectQueue.forEach(fn => fn(this.reason));
            }
        };

        // 后厨出餐容错:做饭翻车直接判订单失败,不瘫痪整个店铺
        try {
            executor(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }

    then(onFulfilled, onRejected) {
        // 链式核心:每接一单就生成全新订单,实现骑手接力直送,完成异步时间扁平化
        return new _Promise((nextResolve, nextReject) => {
            // 统一封装正规配送骑手任务
            const handleSuccess = () => {
                // 骑手上岗核验+原值代送透传+送餐中途意外兜底
                try {
                    const res = typeof onFulfilled === 'function' ? onFulfilled(this.value) : this.value;
                    nextResolve(res);
                } catch (err) {
                    nextReject(err);
                }
            };
            // 统一封装订单善后退款骑手任务
            const handleFail = () => {
                // 坏单兜底核验+原因透传+善后出错应急保护
                try {
                    const err = typeof onRejected === 'function' ? onRejected(this.reason) : this.reason;
                    nextResolve(err);
                } catch (err) {
                    nextReject(err);
                }
            };

            // 根据订单当前状态调度骑手
            if (this.status === STATUS_FULFILLED) {
                handleSuccess(); // 已出餐直接派送
            } else if (this.status === STATUS_REJECTED) {
                handleFail(); // 已拒收直接善后
            } else if (this.status === STATUS_PENDING) {
                // 只收正经上岗骑手进任务篓,杂牌无效骑手直接拒收
                typeof onFulfilled === 'function' && this.resolveQueue.push(handleSuccess);
                typeof onRejected === 'function' && this.rejectQueue.push(handleFail);
            }
        });
    }
}

整个流程结尾

我们全程用外卖订单+骑手配送的思路走完了手写Promise的主要核心流程:

最初先搭建核心骨架,定好订单三态状态机,搭配存放任务的回调篓,实现了最基础的异步任务寄存能力;

接着核心打通链式调用逻辑,每调用一次then就生成一张全新外卖订单,让骑手接力顺路配送,把嵌套混乱的回调地狱改成线性直行的流程,完成了Promise最关键的异步扁平化;

最后一步步叠加全套边界优化,用try/catch兜底后厨出餐报错、骑手送餐中途异常,用函数筛查过滤不会干活的假骑手,搭配值透传规则让空岗骑手原样代送不丢单,保障整条配送链路永远顺畅不崩。

至此我们就从最简裸奔版,迭代打磨出了逻辑完整、健壮可用的完整版基础Promise。至于 Promise.all 、 Promise.race 这类静态工具方法,只是在这套核心完备的订单系统之上,额外封装的组合派单简易逻辑,属于锦上添花的拓展用法,不改动我们底层状态机、队列调度、链式流转的核心架构,这里就不再额外展开赘述了。

如有理解不当,欢迎大家指正,一起学习进步

Vite8 关于 vite build 命令构建过程

作者 米丘
2026年4月9日 15:54

在 Vite 8 中,vite build 命令已经从传统的 Rollup 打包,彻底转向了由 Rust 驱动的全新工具链。

Vite 8 最大的改变,是其构建流程的底层核心被完全重写,统一使用 Rust 生态的工具。

  • 单一打包器 Rolldown:此前,Vite 在开发环境使用 esbuild 追求速度,在生产环境使用 Rollup 追求能力,这导致了行为不一致。Vite 8 使用一个名为 Rolldown 的 Rust 打包器,统一了开发和生产环境的构建链路。它完全兼容 Rollup 的插件 API,使得绝大多数现有 Vite 插件无需修改即可在 Vite 8 中运行。
  • 高性能引擎 Oxc:Rolldown 本身构建于 Oxc(另一个用 Rust 编写的工具集)之上。Oxc 为 Rolldown 提供了极快的解析、转换能力,使其在处理 TypeScript 和 JSX 文件时性能大幅领先。

vite build 有哪些命令行参数?

// build
cli
  .command('build [root]', 'build for production')
  .option(
    '--target <target>',
    `[string] transpile target (default: 'baseline-widely-available')`,
  )
  .option('--outDir <dir>', `[string] output directory (default: dist)`)
  .option(
    '--assetsDir <dir>',
    `[string] directory under outDir to place assets in (default: assets)`,
  )
  .option(
    '--assetsInlineLimit <number>',
    `[number] static asset base64 inline threshold in bytes (default: 4096)`,
  )
  .option(
    '--ssr [entry]',
    `[string] build specified entry for server-side rendering`,
  )
  .option(
    '--sourcemap [output]',
    `[boolean | "inline" | "hidden"] output source maps for build (default: false)`,
  )
  .option(
    '--minify [minifier]',
    `[boolean | "terser" | "esbuild"] enable/disable minification, ` +
      `or specify minifier to use (default: esbuild)`,
  )
  .option('--manifest [name]', `[boolean | string] emit build manifest json`)
  .option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`)
  .option(
    '--emptyOutDir',
    `[boolean] force empty outDir when it's outside of root`,
  )
  .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
  .option('--app', `[boolean] same as \`builder: {}\``)

vite build 接收的 options 有哪些?

image.png

源码

createBuilder

/**
 * Creates a ViteBuilder to orchestrate building multiple environments.
 * 创建和配置 vite构建器
 * @experimental
 * params inlineConfig 行内配置
 * params useLegacyBuilder 是否使用旧版构建器
 */
export async function createBuilder(
  inlineConfig: InlineConfig = {},
  useLegacyBuilder: null | boolean = false,
): Promise<ViteBuilder> {

  // 处理旧版兼容
  const patchConfig = (resolved: ResolvedConfig) => {
    if (!(useLegacyBuilder ?? !resolved.builder)) return

    // Until the ecosystem updates to use `environment.config.build` instead of `config.build`,
    // we need to make override `config.build` for the current environment.
    // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later
    // remove the default values that shouldn't be used at all once the config is resolved
    const environmentName = resolved.build.ssr ? 'ssr' : 'client'
    ;(resolved.build as ResolvedBuildOptions) = {
      ...resolved.environments[environmentName].build,
    }
  }
  // 配置解析
  const config = await resolveConfigToBuild(inlineConfig, patchConfig)
  // 是否使用旧版构建器
  useLegacyBuilder ??= !config.builder
  // 构建器配置
  const configBuilder = config.builder ?? resolveBuilderOptions({})!

  const environments: Record<string, BuildEnvironment> = {}

  // 创建 ViteBuilder 对象
  const builder: ViteBuilder = {
    environments,
    config,
    /**
     * 构建整个应用
     */
    async buildApp() {
      // 创建插件上下文
      const pluginContext = new BasicMinimalPluginContext(
        { ...basePluginContextMeta, watchMode: false },
        config.logger,
      )

      // order 'pre' and 'normal' hooks are run first, then config.builder.buildApp, then 'post' hooks
      // 是否已调用配置构建器的 buildApp 方法
      let configBuilderBuildAppCalled = false

      // 执行插件的 buildApp 钩子
      for (const p of config.getSortedPlugins('buildApp')) {
        const hook = p.buildApp
        if (
          !configBuilderBuildAppCalled &&
          typeof hook === 'object' &&
          hook.order === 'post' // 只在 post 阶段调用
        ) {
          configBuilderBuildAppCalled = true
          await configBuilder.buildApp(builder)
        }
        const handler = getHookHandler(hook)
        await handler.call(pluginContext, builder)
      }
      // 如果未调用配置构建器的 buildApp 方法,调用默认 buildApp 方法
      if (!configBuilderBuildAppCalled) {
        await configBuilder.buildApp(builder)
      }
      // fallback to building all environments if no environments have been built
      // 检查是否有环境被构建
      if (
        Object.values(builder.environments).every(
          (environment) => !environment.isBuilt,
        )
      ) {
        for (const environment of Object.values(builder.environments)) {
          // 构建所有环境
          await builder.build(environment)
        }
      }
    },
    /**
     * 构建环境
     * @param environment 
     * @returns 
     */
    async build(
      environment: BuildEnvironment,
    ): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
      const output = await buildEnvironment(environment)
      environment.isBuilt = true
      return output
    },
    async runDevTools() {
      const devtoolsConfig = config.devtools
      if (devtoolsConfig.enabled) {
        try {
          const { start } = await import(`@vitejs/devtools/cli-commands`)
          await start(devtoolsConfig.config)
        } catch (e) {
          config.logger.error(
            colors.red(`Failed to run Vite DevTools: ${e.message || e.stack}`),
            { error: e },
          )
        }
      }
    },
  }

  /**
   * 环境设置函数
   */
  async function setupEnvironment(name: string, config: ResolvedConfig) {
    const environment = await config.build.createEnvironment(name, config)
    await environment.init()
    environments[name] = environment
  }

  // 环境初始化
  // 使用旧版构建器
  if (useLegacyBuilder) {
    await setupEnvironment(config.build.ssr ? 'ssr' : 'client', config)
  } else {
    // 新版构建器
    const environmentConfigs: [string, ResolvedConfig][] = []

    for (const environmentName of Object.keys(config.environments)) {
      // We need to resolve the config again so we can properly merge options
      // and get a new set of plugins for each build environment. The ecosystem
      // expects plugins to be run for the same environment once they are created
      // and to process a single bundle at a time (contrary to dev mode where
      // plugins are built to handle multiple environments concurrently).
      let environmentConfig = config
      if (!configBuilder.sharedConfigBuild) {
        const patchConfig = (resolved: ResolvedConfig) => {
          // Until the ecosystem updates to use `environment.config.build` instead of `config.build`,
          // we need to make override `config.build` for the current environment.
          // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later
          // remove the default values that shouldn't be used at all once the config is resolved
          ;(resolved.build as ResolvedBuildOptions) = {
            ...resolved.environments[environmentName].build,
          }
        }
        const patchPlugins = (resolvedPlugins: Plugin[]) => {
          // Force opt-in shared plugins
          let j = 0
          for (let i = 0; i < resolvedPlugins.length; i++) {
            const environmentPlugin = resolvedPlugins[i]
            if (
              configBuilder.sharedPlugins ||
              environmentPlugin.sharedDuringBuild
            ) {
              for (let k = j; k < config.plugins.length; k++) {
                if (environmentPlugin.name === config.plugins[k].name) {
                  resolvedPlugins[i] = config.plugins[k]
                  j = k + 1
                  break
                }
              }
            }
          }
        }
        // 为每个环境名称创建环境配置
        environmentConfig = await resolveConfigToBuild(
          inlineConfig,
          patchConfig,
          patchPlugins,
        )
      }
      
      environmentConfigs.push([environmentName, environmentConfig])
    }
    // 并行初始化所有环境
    await Promise.all(
      environmentConfigs.map(
        async ([environmentName, environmentConfig]) =>
          await setupEnvironment(environmentName, environmentConfig),
      ),
    )
  }

  return builder
}

image.png

image.png

buildEnvironment

buildEnvironment 函数是 Vite 8 中为单个环境(如 client 或 ssr)执行生产构建的核心函数:

  1. 首先解析 Rolldown 打包配置。
  2. 然后根据是否开启监听模式(options.watch)分别创建 Rolldown 的 watcher 以持续构建并监听文件变化,或一次性调用 Rolldown 完成打包。
  3. 构建过程中会收集每个输出 chunk 的元数据,支持多输出配置(如同时输出 ESM 和 CJS),并最终将产物写入磁盘或返回结果对象。
  4. 同时提供详细的日志输出和错误增强处理,在结束前确保关闭 Rolldown 实例以释放资源。

/**
 * Build an App environment, or a App library (if libraryOptions is provided)
 * Vite 8 中负责生产构建单个环境(如 client、ssr)的核心函数。
 * 基于 Rolldown(Rust 打包器)执行打包,支持普通构建和监听模式(watch)
 **/
async function buildEnvironment(
  environment: BuildEnvironment,
): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
  const { logger, config } = environment
  const { root, build: options } = config

  // 记录开始构建的日志
  logger.info(
    colors.cyan(
      `vite v${VERSION} ${colors.green(
        `building ${environment.name} environment for ${environment.config.mode}...`,
      )}`,
    ),
  )

  let bundle: RolldownBuild | undefined
  let startTime: number | undefined
  try {
    // 收集每个输出 chunk 的元数据(如模块 ID、文件大小等)
    const chunkMetadataMap = new ChunkMetadataMap()
    // 解析 Rolldown 选项
    const rollupOptions = resolveRolldownOptions(environment, chunkMetadataMap)

    // watch file changes with rollup
    // 监视文件变化
    if (options.watch) {
      logger.info(colors.cyan(`\nwatching for file changes...`))

      const resolvedOutDirs = getResolvedOutDirs(
        root,
        options.outDir,
        options.rollupOptions.output,
      )
      const emptyOutDir = resolveEmptyOutDir(
        options.emptyOutDir,
        root,
        resolvedOutDirs,
        logger,
      )
      const resolvedChokidarOptions = resolveChokidarOptions(
        {
          // @ts-expect-error chokidar option does not exist in rolldown but used for backward compat
          ...(rollupOptions.watch || {}).chokidar,
          // @ts-expect-error chokidar option does not exist in rolldown but used for backward compat
          ...options.watch.chokidar,
        },
        resolvedOutDirs,
        emptyOutDir,
        environment.config.cacheDir,
      )

      const { watch } = await import('rolldown')
      // 调用 rolldown.watch 创建监听器
      const watcher = watch({
        ...rollupOptions,
        watch: {
          ...rollupOptions.watch,
          ...options.watch,
          notify: convertToNotifyOptions(resolvedChokidarOptions),
        },
      })

      watcher.on('event', (event) => {
        if (event.code === 'BUNDLE_START') {
          logger.info(colors.cyan(`\nbuild started...`))
          chunkMetadataMap.clearResetChunks()
        } else if (event.code === 'BUNDLE_END') {
          event.result.close()
          logger.info(colors.cyan(`built in ${event.duration}ms.`))
        } else if (event.code === 'ERROR') {
          const e = event.error
          enhanceRollupError(e)
          clearLine()
          logger.error(e.message, { error: e })
        }
      })

      return watcher
    }

    // 普通构建
    // write or generate files with rolldown
    const { rolldown } = await import('rolldown')
    startTime = Date.now()
    // 创建 Rolldown 构建实例
    bundle = await rolldown(rollupOptions)

    // 多个输出配置
    const res: RolldownOutput[] = []

    for (const output of arraify(rollupOptions.output!)) {
      // bundle.write(outputOptions) 将产物写入磁盘
      // bundle.generate(outputOptions) 仅返回产物对象
      res.push(await bundle[options.write ? 'write' : 'generate'](output))
    }
    for (const output of res) {
      for (const chunk of output.output) {
        // 注入 chunk 元数据
        injectChunkMetadata(chunkMetadataMap, chunk)
      }
    }
    logger.info(
      `${colors.green(`✓ built in ${displayTime(Date.now() - startTime)}`)}`,
    )

    // 返回构建结果
    return Array.isArray(rollupOptions.output) ? res : res[0]
  } catch (e) {
    enhanceRollupError(e)
    clearLine()
    if (startTime) {
      logger.error(
        `${colors.red('✗')} Build failed in ${displayTime(Date.now() - startTime)}`,
      )
      startTime = undefined
    }
    throw e
  } finally {
    // 关闭 Rolldown 构建实例
    if (bundle) await bundle.close()
  }
}

image.png

image.png

image.png

Vite 8 的生产构建底层完全基于 Rolldown(Rust 打包器),支持两种构建模式:一次性打包(默认 vite build)和 监听打包vite build --watch)。

image.png

命令分析

"build": "run-p type-check \"build-only {@}\" --"
"build-only": "vite build",

run-p:来自 npm-run-all,表示并行执行后面的脚本

  • type-check:第一个要运行的脚本(通常用于 TypeScript 类型检查)。
  • "build-only {@}" :第二个要运行的脚本。
    • build-only 是另一个 npm 脚本(自定义,例如 vite build)。
    • {@} 是 npm-run-all 的特殊占位符,代表传递给当前 build 命令的所有原始参数

测试

{
    build: {
    emptyOutDir:true, // 清空目录
    copyPublicDir: true,
    reportCompressedSize: true,//启用/禁用 gzip 压缩大小报告
    chunkSizeWarningLimit:500,// 规定触发警告的 chunk 大小。(以 kB 为单位)。
    assetsInlineLimit:4096,// 4kb 小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求
    // baseline-widely-available 具体来说,它是 `['chrome111', 'edge111', 'firefox114', 'safari16.4']`
    // esnext —— 即假设有原生动态导入支持,并只执行最低限度的转译。
    target: 'baseline-widely-available',
    // 如果禁用,整个项目中的所有 CSS 将被提取到一个 CSS 文件中。
    cssCodeSplit: true,// 启用,在异步 chunk 中导入的 CSS 将内联到异步 chunk 本身,并在其被加载时一并获取。
    cssMinify: 'lightningcss',// Vite 默认使用 [Lightning CSS](https://lightningcss.dev/minification.html) 来压缩 CSS
    // true,将会创建一个独立的 source map 文件
    // inline,source map 将作为一个 data URI 附加在输出文件中
    sourcemap:false,
    license:true, // true,构建过程将生成一个 .vite/license.md文件,
    }
}

示例 build.outDir 、build.assetsDir

build.outDir默认值 dist,build.assetsDir默认值 assets

image.png

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
  },

image.png

image.png

示例 build.minify

1、默认情况。

minify 默认压缩,客户端构建默认为'oxc'

image.png

2、配置不压缩。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: false, // 不压缩
  },

image.png

3、配置 esbuild 。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: 'esbuild',
  },

提示 在 vite8 中 esbuild 已废弃。建议使用 oxc 。

image.png

4、 配置 terser。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: 'terser',
  },

image.png

当设置为 'esbuild' 或 'terser' 时,必须分别安装 esbuild 或 Terser。

npm add -D esbuild
npm add -D terser

示例 build.manifest / ssrManifest

manifest 设置为 true 时,路径将是 .vite/manifest.json
ssrManifest 设置为 true 时,路径将是 .vite/ssr-manifest.json

image.png

image.png

vue3-vite-cube/dist-cube/index.html

<!doctype html>
<html lang="">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App菜单</title>
    <script type="module" crossorigin src="/public/index-B9iM-AOo.js"></script>
    <link rel="modulepreload" crossorigin href="/public/runtime-core.esm-bundler-HXD8ebTp.js">
    <link rel="stylesheet" crossorigin href="/public/index-DuS5nk76.css">
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

最后

  1. rolldown 配置
  2. vite 配置

JavaScript 入门太难?看完这 3 个核心基础,我悟了!

作者 心连欣
2026年4月9日 15:33

前言:网页的“灵魂”

哈喽大家好,我是心连欣。如果说 HTML 是网页的骨架,CSS 是皮肤,那 JavaScript (JS) 就是网页的灵魂。没有 JS,网页就是静止的;有了 JS,网页才能和你互动(比如点击按钮、轮播图、弹窗)。

很多新手(包括我)刚开始学 JS 时,觉得语法乱七八糟。但今天整理完这几个核心知识点后,我发现 JS 其实很有逻辑。今天就把我总结的 “JS 三大基石” 分享出来,帮大家少走弯路。

一、 变量声明:var、let 还是 const?

这是新手遇到的第一个“”。在老代码里你经常能看到 var,但在现代开发(ES6 标准)中,我们**强烈建议弃用 var

代码示例: image.png

我的理解:

  • 想定义一个以后可能会变的值(比如计数器、用户输入),用 let
  • 想定义一个定死的值(比如配置项、URL),用 const
  • 最佳实践:默认全用 const,只有当程序报错告诉你不能赋值时,再改成 let。这样能减少很多意外修改导致的 Bug。

二、 数据类型:数字与字符串的“爱恨情仇”

JS 里的数据类型很简单,但有一个特别容易让人晕的地方:字符串拼接与数字相加

代码示例: image.png我的理解:
JS 很“聪明”也很“笨”,它会自动猜测你的意图。如果你想做数学题,千万别给数字加引号。如果你想拼接文字(比如 "第" + 1 + "名"),那就要用引号。

三、 比较运算符:== 和 === 的区别

这是面试必问,也是实际开发最容易出错的地方。

代码示例: image.png我的理解:
永远使用 ===!永远使用 ===!永远使用 ===!重要的事情说三遍。因为它不会自作主张地帮你转换类型,这样代码更安全,逻辑更清晰。

四、 函数:代码的“加工厂”

函数就是把一段代码打包起来,随时调用。现在最流行的是箭头函数,写起来特别帅。

代码示例: image.png

结语

JavaScript 的基础其实就这三板斧:变量、类型、函数。 刚开始写代码时,不要怕报错。比如遇到 Uncaught ReferenceError: xxx is not defined,通常就是你变量没定义或者拼写错了。多打开浏览器的控制台(按 F12),看着控制台写代码,进步会非常快! 今天的分析结束啦,我们下次再见!

油猴脚本实现生产环境加载本地qiankun子应用

作者 石小石Orz
2026年4月9日 15:26

大家好,我是石小石~


qiankun架构下的调试困境

如果你公司的前端架构基于 qiankun,你一定遇到过这样一个问题:由于子应用脱离主应用独立运行,在本地开发阶段,很多和主应用的操作联动、样式交互都无法直接验证,只能把子应用部署到开发或测试环境后,才能排查这类问题。

尤其是在一些不需要做 JS 沙箱隔离的业务场景里,主子应用需要通过 eventBus 这类方式实现交互,子应用不部署上线,调试起来就非常麻烦。

那有没有办法让生产环境直接加载本地子应用来实现代码调试?

方法肯定是有的,比如在主应用里写一套便于调试的逻辑。

import { registerMicroApps, start } from 'qiankun';

// ============== 核心:根据环境变量加载 本地/线上 子应用 ==============
const isDev = process.env.IS_DEV; // webpack 注入的环境变量

// 子应用配置列表
const microApps = [
  {
    name: 'subapp-vue', // 子应用唯一名称
    // 本地开发:加载 localhost 地址;生产:加载线上地址
    entry: isDev ? 'http://localhost:8080/gcshi-web-demo' : '/gcshi-web-demo',
    container: '#subapp-container', // 子应用挂载的容器 id
    activeRule: '/vue', // 路由匹配规则
  },
];

这种写法确实可以通过特定方式触发生产环境加载本地子应用,方便调试。但不可避免地需要修改主应用代码,如果没有主应用代码权限,那就很尴尬了。

其实,针对上面这个问题,用油猴脚本就能轻松解决!

油猴脚本简介

油猴(Tampermonkey)是一款浏览器插件,允许用户在网页加载时注入自定义的 JavaScript 脚本,来增强、修改或自动化网页行为

通俗地说,借助油猴,你可以将自己的 JavaScript 代码“植入”任意网页,实现自动登录、抢单、签到、数据爬取、广告屏蔽等各种“开挂级”功能,彻底掌控页面行为。

它和谷歌插件能实现的效果几乎一致,不过更加简单。如果你是前端开发,可以直接使用油猴,因为它本质就是针对网页写js。

如果你对油猴脚本感兴趣,可以看看: 《油猴脚本实战指南》

使用油猴脚本实现生产环境加载本地子应用

如图,我用 npm run dev 启动了一个本地子应用服务。

开启插件后,页面上会出现油猴脚本的调试工具。

点击【开启代理】,主应用会自动刷新,从而加载本地子应用,全程不需要做任何额外配置。

而且它完美支持热更新,这意味着你修改本地子应用代码后,生产环境页面会同步更新,调试非常方便。

核心原理

实现生产环境加载本地子应用其实很简单:

用油猴脚本在主应用加载时进行拦截,把原本要加载的线上子应用地址,替换成本地服务地址。

你可以这么理解:主应用原本要加载 http://baidu.com/gcshi-web-demo,被脚本替换成了 http://localhost:8080/gcshi-web-demo

重写fetch

qiankun 底层依赖 import-html-entry 这个库,核心流程是通过 fetch 加载子应用 HTML 模板,再解析 CSS、JS。 所以我们只需要在页面加载早期,拦截并重写 fetch 即可。

参考:juejin.cn/post/757214…

那么问题很好解决了, 我们只需要在页面加载早期,拦截并重写 fetch 即可。


const oldFetch = window.fetch;
window.fetch = (url, ...args) => {
  // 替换域名
  if (url === 'http://baidu.com/gcshi-web-demo') {
    url = 'http://localhost:8080/gcshi-web-demo';
  }
  return oldFetch(url, ...args);
};

保证脚本最早运行

重写 window.fetch 的前提,是脚本必须比页面其他逻辑更早执行,否则重写会失效。

在油猴脚本中,可以通过添加元信息实现:

// @run-at       document-start

参考:油猴脚本的运行生命周期

我在油猴脚本里的 fetch 重写逻辑如下:

import $ from "../../gmTool/index";
const { unsafeWindow } = $;

type FetchInterceptor = (url: RequestInfo | URL, options?: RequestInit) => [RequestInfo | URL, RequestInit?] | void | false;

const win = unsafeWindow;
const rawFetch = win.fetch.bind(win);

export function onFetch(interceptor: FetchInterceptor) {
  // 如果已经被代理过,先复用原来的
  if (!(win.fetch as any).rawFetch) {
    const proxyFetch: typeof fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
      let nextInput = input;
      let nextInit = init;
      // 执行 interceptor
      try {
        const result = interceptor(nextInput, nextInit);
        if (result === false) {
          console.warn("[winnex-web-proxy] 😭😭😭😭 fetch请求被用户阻止了===========================>", nextInput);
          return Promise.reject(new Error("[winnex-web-proxy] 😭😭😭😭 fetch请求被用户阻止了"));
        }
        if (result && Array.isArray(result)) {
          nextInput = result[0];
          nextInit = result[1];
        }
      } catch (err) {
        console.error("[fetch] interceptor error:", err);
      }
      // 处理 Request 对象情况
      if (nextInput instanceof Request && nextInit) {
        nextInput = new Request(nextInput, nextInit);
        nextInit = undefined;
      }
      return rawFetch(nextInput, nextInit);
    };

    (proxyFetch as any).rawFetch = rawFetch;
    win.fetch = proxyFetch;
  }

  // 返回取消方法
  return function unProxyFetch() {
    if ((win.fetch as any).rawFetch) {
      win.fetch = rawFetch;
    }
  };
}
  • 基础使用(替换接口地址)
// 注册拦截器
const unProxy = onFetch((url, options) => {
  const u = url.toString();
  // 匹配并替换地址
  if (u === 'http://baidu.com/gcshi-web-demo') {
    return ['http://localhost:8080/gcshi-web-demo', options];
  }
});
  • 阻止某个请求
onFetch((url) => {
  if (url.toString().includes('/black-api')) {
    return false; // 拦截并拒绝
  }
});

解决跨域问题

生产环境页面加载本地 localhost:8080 可能会出现跨域,导致子应用加载失败。解决方法很简单,在 vite 或 webpack 中添加响应头配置:

  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*'
    },
  },

解决热更新

默认情况下,生产环境加载子应用时,热更新会失效。原因是热更新相关的 XHR 请求前缀被替换成了主应用域名。只需要拦截 XHR 请求,修正热更新接口前缀即可。以 webpack 热更新为例,修复 sockjs-nodehot-update 两个接口就行。

使用 ajax-hook 实现 XHR 拦截,代码如下:


const appOrigin = "http://localhost:8080"
const fixHotUpdateUrl = (config: any) => {
  if (config.url.includes("sockjs-node") && appOrigin) {
    config.url = fixSockJsUrl(config.url, appOrigin);
  }
  if (config.url.includes(appName) && config.url.includes("hot-update")) {
    config.url = fixHotUpdate(config.url, appName, appOrigin);
    console.log(`[winnex-web-proxy] 热更新🚀🚀===============================> ${config.url}`);
  }
};

export const xhrProxy = (enable: boolean) => {
  if (!enable) return;
  // xhr拦截
  proxy(
    {
      //请求发起前进入
      onRequest: (config, handler) => {
        fixHotUpdateUrl(config);
        handler.next(config);
      },
      //请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
      onError: (err, handler) => {
        handler.next(err);
      },
      //请求成功后进入
      onResponse: (response, handler) => {
        handler.next(response);
      }
    },
    unsafeWindow
  );
};

总结

在 qiankun 微前端架构中,本地子应用想要直接在生产环境调试,不必修改主应用代码、不必申请权限,通过油猴脚本重写 fetch劫持子应用入口地址,配合跨域配置XHR 拦截修复热更新,就能实现线上环境加载本地子应用,并且支持热更新,极大提升微前端联调效率。整个方案轻量、无侵入、开箱即用,非常适合前端日常调试。

CocosCreator 游戏开发 - 多维度状态机架构设计与实现

2026年4月8日 23:05

背景与场景

在游戏开发中,实体对象(如角色、敌人、NPC)往往需要同时管理多个维度的状态。以一个典型的游戏敌人为例:

  • 行为维度:移动、停止、受击、击退、防御、死亡
  • 属性维度:正常、冰冻、燃烧、雷电、中毒

如果用单一状态机描述,需要维护 6 × 5 = 30 个组合状态。当维度增加时,状态数量呈指数级增长。

本文探讨的架构通过正交状态分离策略,将不同维度拆分到独立状态机中,只需维护 6 + 5 = 11 个状态类,通过共享宿主实现协作。

核心架构设计

三层职责划分

┌─────────────────────────────────────┐
│          宿主层 (Host)              │
│  - 持有多个状态机实例                │
│  - 提供状态切换入口                  │
│  - 管理跨维度协调                    │
│  - 处理生命周期                      │
└──────────┬──────────────────────────┘
           │
    ┌──────┴──────┐
    ▼             ▼
┌─────────┐  ┌─────────┐
│ 状态机A  │  │ 状态机B  │
│ - 状态缓存│  │ - 状态缓存│
│ - 状态分发│  │ - 状态分发│
└────┬────┘  └────┬────┘
     │            │
  ┌──┴──┐      ┌──┴──┐
  ▼  ▼  ▼      ▼  ▼  ▼
 状态类群A    状态类群B

维度协作机制

  1. 共享参数修正:属性状态修改 effectSpeed,行为状态读取 baseSpeed * effectSpeed
  2. 状态间触发:属性恢复时主动将行为切回默认状态
  3. 组合条件判断:暂停/恢复时同时读取两个维度的状态值

通用 FSM 框架实现

1. 状态基类

/**
 * 通用状态基类
 * @template THost 宿主类型
 * @template TStateEnum 状态枚举类型
 */
export abstract class BaseState<THost, TStateEnum> {
    public readonly value: TStateEnum;
    protected host: THost;

    constructor(host: THost, stateValue: TStateEnum) {
        this.host = host;
        this.value = stateValue;
    }

    /**
     * 状态进入时执行的逻辑
     * @param context 上下文信息
     */
    abstract execute(context?: any): void | Promise<void>;

    /**
     * 状态退出时的清理逻辑(可选)
     */
    onExit?(): void;
}

2. 通用状态机

/**
 * 通用状态机
 * @template THost 宿主类型
 * @template TStateEnum 状态枚举类型
 */
export class StateMachine<THost, TStateEnum> {
    private currentState: BaseState<THost, TStateEnum> | null = null;
    private stateMap: Map<TStateEnum, BaseState<THost, TStateEnum>> = new Map();
    private host: THost;

    constructor(host: THost) {
        this.host = host;
    }

    /**
     * 注册状态
     */
    registerState(state: BaseState<THost, TStateEnum>): void {
        this.stateMap.set(state.value, state);
    }

    /**
     * 批量注册状态
     */
    registerStates(states: BaseState<THost, TStateEnum>[]): void {
        states.forEach(state => this.registerState(state));
    }

    /**
     * 切换状态
     */
    switchState(stateValue: TStateEnum, context?: any): void {
        const nextState = this.stateMap.get(stateValue);

        if (!nextState) {
            console.warn(`State ${stateValue} not found`);
            return;
        }

        // 退出当前状态
        if (this.currentState?.onExit) {
            this.currentState.onExit();
        }

        // 切换到新状态
        this.currentState = nextState;
        this.currentState.execute(context);
    }

    /**
     * 获取当前状态值
     */
    get value(): TStateEnum | null {
        return this.currentState?.value ?? null;
    }

    /**
     * 获取当前状态实例
     */
    get current(): BaseState<THost, TStateEnum> | null {
        return this.currentState;
    }
}

3. 宿主接口

/**
 * 状态机宿主接口
 */
export interface IStateMachineHost {
    /**
     * 初始化状态机
     */
    initStateMachines(): void;

    /**
     * 暂停
     */
    pause?(): void;

    /**
     * 恢复
     */
    resume?(): void;

    /**
     * 销毁
     */
    destroy?(): void;
}

使用示例:实现游戏实体状态管理

1. 定义状态枚举

enum ActionState {
    MOVE = 'move',
    STOP = 'stop',
    HIT = 'hit',
    DEAD = 'dead',
}

enum AttributeState {
    NORMAL = 'normal',
    FROZEN = 'frozen',
    BURNING = 'burning',
}

2. 实现具体状态类

// 行为状态:移动
class MoveState extends BaseState<EntityController, ActionState> {
    constructor(host: EntityController) {
        super(host, ActionState.MOVE);
    }

    execute(): void {
        const speed = this.host.baseSpeed * this.host.effectSpeed;
        this.host.setVelocity(0, -speed);
    }
}

// 行为状态:死亡
class DeadState extends BaseState<EntityController, ActionState> {
    constructor(host: EntityController) {
        super(host, ActionState.DEAD);
    }

    execute(): void {
        this.host.stopPhysics();
        this.host.playDeathEffect();
        this.host.scheduleDestroy(0.5);
    }
}

// 属性状态:正常
class NormalAttributeState extends BaseState<EntityController, AttributeState> {
    constructor(host: EntityController) {
        super(host, AttributeState.NORMAL);
    }

    execute(): void {
        this.host.effectSpeed = 1;
        this.host.effectDamage = 0;
        this.host.resetColor();
    }
}

// 属性状态:冰冻
class FrozenAttributeState extends BaseState<EntityController, AttributeState> {
    constructor(host: EntityController) {
        super(host, AttributeState.FROZEN);
    }

    execute(): void {
        this.host.effectSpeed = 0.5;
        this.host.setColor(0, 255, 255);
    }
}

3. 实现宿主控制器

class EntityController implements IStateMachineHost {
    // 基础属性
    baseSpeed: number = 1;
    baseHealth: number = 100;

    // 状态修正属性
    effectSpeed: number = 1;
    effectDamage: number = 0;

    // 状态机实例
    private actionStateMachine: StateMachine<EntityController, ActionState>;
    private attributeStateMachine: StateMachine<EntityController, AttributeState>;

    // 调度器任务 ID
    private attributeTimerId: number | null = null;
    private dotTimerId: number | null = null;

    constructor() {
        this.initStateMachines();
    }

    initStateMachines(): void {
        // 初始化行为状态机
        this.actionStateMachine = new StateMachine(this);
        this.actionStateMachine.registerStates([
            new MoveState(this),
            new StopState(this),
            new HitState(this),
            new DeadState(this),
        ]);

        // 初始化属性状态机
        this.attributeStateMachine = new StateMachine(this);
        this.attributeStateMachine.registerStates([
            new NormalAttributeState(this),
            new FrozenAttributeState(this),
            new BurningAttributeState(this),
        ]);

        // 设置初始状态
        this.actionStateMachine.switchState(ActionState.MOVE);
        this.attributeStateMachine.switchState(AttributeState.NORMAL);
    }

    // 行为状态切换入口
    switchActionState(state: ActionState, context?: any): void {
        const currentState = this.actionStateMachine.value;

        // 防重入(MOVE 除外,允许重置)
        if (currentState === state && state !== ActionState.MOVE) {
            return;
        }

        this.actionStateMachine.switchState(state, context);
    }

    // 属性状态切换入口
    switchAttributeState(state: AttributeState, context?: any): void {
        const currentState = this.attributeStateMachine.value;

        // 已经是目标状态,跳过
        if (currentState === state) {
            return;
        }

        // 清理旧的调度任务
        this.clearAttributeTimers();

        // 切换到新状态
        this.attributeStateMachine.switchState(state, context);

        // 如果不是 NORMAL,设置定时恢复
        if (state !== AttributeState.NORMAL) {
            this.attributeTimerId = this.scheduleOnce(() => {
                this.switchAttributeState(AttributeState.NORMAL);
            }, 3.0);

            // 如果有持续伤害,设置周期性伤害
            if (this.effectDamage > 0) {
                this.dotTimerId = this.scheduleRepeat(() => {
                    this.takeDamage(this.effectDamage);
                }, 0.8);
            }
        }
    }

    // 清理属性相关定时器
    private clearAttributeTimers(): void {
        if (this.attributeTimerId !== null) {
            this.unschedule(this.attributeTimerId);
            this.attributeTimerId = null;
        }
        if (this.dotTimerId !== null) {
            this.unschedule(this.dotTimerId);
            this.dotTimerId = null;
        }
    }

    // 获取当前状态
    get actionState(): ActionState | null {
        return this.actionStateMachine.value;
    }

    get attributeState(): AttributeState | null {
        return this.attributeStateMachine.value;
    }

    // 暂停
    pause(): void {
        this.stopPhysics();
        // 根据当前状态暂停对应的动画/特效
    }

    // 恢复
    resume(): void {
        // 根据当前状态恢复对应的行为
        if (this.actionState === ActionState.MOVE) {
            const speed = this.baseSpeed * this.effectSpeed;
            this.setVelocity(0, -speed);
        }
    }

    // 销毁
    destroy(): void {
        this.clearAttributeTimers();
        // 其他清理逻辑
    }

    // 以下是宿主提供给状态类使用的方法
    setVelocity(x: number, y: number): void { /* ... */ }
    stopPhysics(): void { /* ... */ }
    playDeathEffect(): void { /* ... */ }
    setColor(r: number, g: number, b: number): void { /* ... */ }
    resetColor(): void { /* ... */ }
    takeDamage(damage: number): void { /* ... */ }
    scheduleOnce(callback: () => void, delay: number): number { /* ... */ return 0; }
    scheduleRepeat(callback: () => void, interval: number): number { /* ... */ return 0; }
    unschedule(timerId: number): void { /* ... */ }
    scheduleDestroy(delay: number): void { /* ... */ }
}

状态生命周期模式

瞬时状态

依赖外部事件自动回切,适合表现类状态:

class HitState extends BaseState<EntityController, ActionState> {
    execute(): void {
        this.host.playHitEffect();

        // 监听动画结束事件
        this.host.onEffectFinished(() => {
            this.host.switchActionState(ActionState.MOVE);
        });
    }
}

持续状态

由宿主管理生命周期,适合带持续效果的状态:

// 在宿主的 switchAttributeState 中统一管理
switchAttributeState(state: AttributeState): void {
    // 清理旧状态任务
    this.clearAttributeTimers();

    // 切换状态
    this.attributeStateMachine.switchState(state);

    // 注册新状态的定时任务
    if (state !== AttributeState.NORMAL) {
        this.scheduleOnce(() => {
            this.switchAttributeState(AttributeState.NORMAL);
        }, 3.0);
    }
}

终止状态

集中处理资源回收:

class DeadState extends BaseState<EntityController, ActionState> {
    execute(): void {
        // 停止物理
        this.host.stopPhysics();

        // 播放特效
        this.host.playDeathEffect();

        // 发放奖励
        this.host.grantRewards();

        // 记录统计
        this.host.recordKill();

        // 清理调度任务
        this.host.clearAllTimers();

        // 延迟回收
        this.host.scheduleDestroy(0.5);
    }
}

架构优势

状态空间可控

  • 单一状态机:N × M 个状态类
  • 多维度状态机:N + M 个状态类

职责清晰

  • 状态机:状态查找与分发,不关心业务逻辑
  • 状态类:具体行为执行,持有宿主引用
  • 宿主:协调多个状态机,管理生命周期

扩展性强

新增维度只需:

  1. 创建新的状态机实例
  2. 实现该维度的状态类
  3. 在宿主中添加切换入口

已有维度无需修改。

设计权衡

状态类与宿主的耦合度

当前方案:状态类直接持有宿主引用,可以访问宿主的所有方法和属性。

优点:开发效率高,状态实现直观
缺点:耦合度高,状态类依赖宿主的具体实现

替代方案:定义状态操作接口,状态类只能通过接口操作宿主。

interface IStateOperations {
    setVelocity(x: number, y: number): void;
    playEffect(effectName: string): void;
    // ...
}

class MoveState extends BaseState<IStateOperations, ActionState> {
    execute(): void {
        // 只能通过接口操作
        this.host.setVelocity(0, -1);
    }
}

生命周期管理位置

当前方案:持续状态的生命周期由宿主统一管理。

优点:调度任务集中管理,便于清理
缺点:状态逻辑被拆分到状态类和宿主两处

替代方案:状态类自己管理生命周期。

class BurningState extends BaseState<EntityController, AttributeState> {
    private timerId: number | null = null;

    execute(): void {
        this.host.setColor(255, 0, 0);

        // 状态自己注册定时器
        this.timerId = this.host.scheduleOnce(() => {
            this.host.switchAttributeState(AttributeState.NORMAL);
        }, 3.0);
    }

    onExit(): void {
        // 状态退出时清理
        if (this.timerId !== null) {
            this.host.unschedule(this.timerId);
        }
    }
}

适用场景

适合使用

  • 实体状态由多个正交维度组成
  • 状态切换频繁,需要高性能
  • 状态行为与表现资源紧密绑定
  • 需要清晰的生命周期管理

不适合使用

  • 状态维度单一且简单(直接用枚举 + switch 即可)
  • 状态转换有复杂的条件依赖(需要状态转换图)
  • 状态之间有严格的顺序约束

项目应用实例

场景描述

在一个塔防类游戏项目中,敌人系统需要同时管理:

行为维度(7 个状态):

  • MOVE:向下移动
  • STOP:停止
  • HIT:普通受击
  • CRITICAL_HIT:暴击受击
  • REPULSE:击退
  • DEFENSE:防御
  • DEAD:死亡

属性维度(5 个状态):

  • NORMAL:正常
  • FROZEN:冰冻(减速 + 视觉效果)
  • FIRE:燃烧(持续伤害 + 视觉效果)
  • THUNDER:雷电(定身 + 视觉效果)
  • POISON:中毒(持续伤害 + 视觉效果)

如果用单一状态机,需要维护 7 × 5 = 35 个组合状态。采用双状态机架构后,只需维护 7 + 5 = 12 个状态类。

核心实现

1. 行为状态机实现

// EnemyActionStateMachine.ts
export class EnemyActionStateMachine {
    private _state: BaseState;
    private _stateMap: Map<ENEMY_ACTION_STATE, BaseState> = new Map();

    constructor(node: Node, EnemyController: any, initialState: ENEMY_ACTION_STATE) {
        // 预创建所有状态实例
        this._initState(node, EnemyController, ENEMY_ACTION_STATE.MOVE);
        this._initState(node, EnemyController, ENEMY_ACTION_STATE.STOP);
        this._initState(node, EnemyController, ENEMY_ACTION_STATE.HIT);
        this._initState(node, EnemyController, ENEMY_ACTION_STATE.CRITICAL_HIT);
        this._initState(node, EnemyController, ENEMY_ACTION_STATE.REPULSE);
        this._initState(node, EnemyController, ENEMY_ACTION_STATE.DEFENSE);
        this._initState(node, EnemyController, ENEMY_ACTION_STATE.DEAD);

        this.switchState(initialState);
    }

    private async _initState(node: Node, Controller: any, state: ENEMY_ACTION_STATE) {
        let stateInstance: BaseState;
        switch (state) {
            case ENEMY_ACTION_STATE.MOVE:
                stateInstance = new MoveState(node, Controller);
                break;
            case ENEMY_ACTION_STATE.DEAD:
                stateInstance = new DeadState(node, Controller);
                break;
            // ... 其他状态
        }
        this._stateMap.set(state, stateInstance);
    }

    switchState(actionState: ENEMY_ACTION_STATE, info?: any) {
        let state = this._stateMap.get(actionState);
        if (!state) state = this._stateMap.get(ENEMY_ACTION_STATE.MOVE);
        this._state = state;
        return this._state.action(info);
    }

    get value(): ENEMY_ACTION_STATE {
        return this._state?.value;
    }
}

2. 关键状态类实现

移动状态:读取属性状态的速度修正

// MoveState.ts
export default class MoveState extends BaseState {
    async action(info: any) {
        // 速度 = 基础速度 × 属性效果修正
        const finalSpeed = this.EnemyController.baseSpeed
                         * this.EnemyController.effectSpeed;

        this.EnemyController.RigidBodyBox.linearVelocity = new Vec2(0, -finalSpeed);
    }
}

死亡状态:作为生命周期终点,集中处理清理逻辑

// DeadState.ts
export default class DeadState extends BaseState {
    async action(info: any) {
        // 1. 停止物理和动画
        this.EnemyController.Animation.pause();
        this.EnemyController.RigidBodyBox.enabled = false;
        this.EnemyController.RigidBodyBox.linearVelocity = new Vec2(0, 0);

        // 2. 播放死亡特效
        this.EnemyController.ExplosionEffect.active = true;
        this.EnemyController.ExplosionEffect.getComponent(Animation).play();

        // 3. 发放经验和记录统计
        PlayerDataManager.instance.setExp(this.EnemyController.baseExp);
        PlayDataManager.instance.killEnemiesMap.set(
            this.EnemyController.index,
            (PlayDataManager.instance.killEnemiesMap.get(this.EnemyController.index) || 0) + 1
        );

        // 4. 清空所有调度任务
        director.getScheduler().unscheduleAllForTarget(this.EnemyController);

        // 5. 延迟回收到对象池
        this.EnemyController.scheduleOnce(() => {
            this.EnemyNode.active = false;
            // 重置所有属性
            this.EnemyController.baseHp = this.EnemyController.originHp;
            this.EnemyController.effectSpeed = 1;
            this.EnemyController.effectDamage = 0;
            // 解绑事件
            this.EnemyController.ColliderBox.off(Contact2DType.BEGIN_CONTACT);
            this.EnemyController.ColliderBox.off(Contact2DType.END_CONTACT);
            // 放回对象池
            NodePoolManager.instance.putNodeToPool(
                `Enemy-${this.EnemyController.category}-${this.EnemyController.index}`,
                this.EnemyNode
            );
        }, 0.65);
    }
}

受击状态:瞬时状态,依赖动画事件自动回切

// HitState.ts
export default class HitState extends BaseState {
    async action(info: any) {
        // 激活受击特效
        this.EnemyController.HitEffect.active = true;
        this.EnemyController.HitEffect.getComponent(Animation).play();

        // 监听动画结束事件,自动切回移动状态
        this.EnemyController.HitEffect.getComponent(Animation).on(
            AnimationComponent.EventType.FINISHED,
            () => {
                this.EnemyController.HitEffect.active = false;
                this.EnemyController.switchActionState(ENEMY_ACTION_STATE.MOVE);
            }
        );
    }
}

3. 属性状态实现

正常状态:作为属性维度的重置模板

// NormalState.ts
export default class NormalState extends BaseState {
    async attribute(info: any) {
        // 重置所有属性修正
        this.EnemyController.effectSpeed = 1;
        this.EnemyController.effectDamage = 0;

        // 恢复默认视觉
        this.EnemyController.Sprite.color = new Color(255, 255, 255, 255);
        this.EnemyController.Animation.resume();

        // 恢复移动
        this.EnemyController.RigidBodyBox.linearVelocity = new Vec2(
            0,
            -1 * this.EnemyController.baseSpeed * this.EnemyController.effectSpeed
        );

        // 主动将行为状态拉回移动
        this.EnemyController.switchActionState(ENEMY_ACTION_STATE.MOVE);
    }
}

冰冻状态:修改速度修正参数

// FrozenState.ts
export default class FrozenState extends BaseState {
    async attribute(info: any) {
        // 修改视觉效果
        this.EnemyController.Sprite.color = new Color(255, 255, 0, 255);

        // 修改速度修正(不直接改速度,而是改修正系数)
        // 行为状态的 MoveState 会读取这个值
        this.EnemyController.effectSpeed = 0.5;
    }
}

燃烧状态:带持续伤害

// FireState.ts
export default class FireState extends BaseState {
    async attribute(info: any) {
        this.EnemyController.Sprite.color = new Color(65, 255, 65);

        // 设置持续伤害参数
        // 宿主会根据这个值注册周期性伤害回调
        this.EnemyController.effectDamage = info?.damage || 2;
    }
}

4. 宿主控制器实现

// EnemyController.ts
export class EnemyController extends Component {
    // 基础属性
    baseHp: number = 18;
    baseSpeed: number = 1.15;
    baseAttack: number = 4;

    // 状态修正属性(供状态类修改)
    effectSpeed: number = 1;
    effectDamage: number = 0;

    // 状态机实例
    enemyActionState: EnemyActionStateMachine;
    enemyAttributeState: EnemyAttributeStateMachine;

    initStateMachine() {
        this.enemyActionState = new EnemyActionStateMachine(
            this.node,
            EnemyController,
            ENEMY_ACTION_STATE.MOVE
        );
        this.enemyAttributeState = new EnemyAttributeStateMachine(
            this.node,
            EnemyController,
            ENEMY_ATTRIBUTE_STATE.NORMAL
        );
    }

    // 行为状态切换入口
    switchActionState(actionState: ENEMY_ACTION_STATE, info?: any) {
        // 防重入,但 MOVE 允许重复进入(用于重置)
        if ((this.actionState !== actionState) || (actionState === ENEMY_ACTION_STATE.MOVE)) {
            return this.enemyActionState.switchState(actionState, info);
        }
    }

    // 属性状态切换入口(带生命周期管理)
    switchAttributeState(attrState: ENEMY_ATTRIBUTE_STATE, info?: any) {
        // 某些行为状态下禁止切换属性
        if ([ENEMY_ACTION_STATE.STOP].includes(this.actionState)) return;
        if (this.attributeState === attrState) return;

        // 清理旧属性的调度任务
        this.unschedule(this.enemyAttributeState.switchState);
        this.unschedule(this.runAttributeHit);

        // 立即切换到新属性
        this.runSwitchAttributeState(attrState, info);

        // 3.2 秒后自动恢复到 NORMAL
        this.scheduleOnce(this.runSwitchAttributeState, 3.2);

        // 如果有持续伤害,注册周期性伤害回调
        if (this.effectDamage) {
            this.schedule(this.runAttributeHit, 0.88);
        }
    }

    runSwitchAttributeState(attrState: ENEMY_ATTRIBUTE_STATE = ENEMY_ATTRIBUTE_STATE.NORMAL, info?: any) {
        return this.enemyAttributeState.switchState(attrState, info);
    }

    runAttributeHit() {
        if (this.effectDamage) {
            handleHurtEvent(this, this.effectDamage);
        }
    }

    // 暂停/恢复时需要同时考虑两个维度的状态
    setResume() {
        director.getScheduler().resumeTarget(this);

        // 恢复移动速度(读取属性修正)
        this.RigidBodyBox.linearVelocity = new Vec2(
            0,
            -1 * this.baseSpeed * this.effectSpeed
        );

        // 根据属性状态决定是否恢复主动画
        if (this.attributeState !== ENEMY_ATTRIBUTE_STATE.THUNDER) {
            this.Animation.resume();
        }

        // 根据行为状态恢复对应特效
        if (this.actionState === ENEMY_ACTION_STATE.HIT) {
            this.HitEffect.getComponent(Animation).resume();
        }
        if (this.actionState === ENEMY_ACTION_STATE.DEAD) {
            this.ExplosionEffect.getComponent(Animation).resume();
        }
    }

    get actionState(): ENEMY_ACTION_STATE {
        return this.enemyActionState?.value || ENEMY_ACTION_STATE.MOVE;
    }

    get attributeState(): ENEMY_ATTRIBUTE_STATE {
        return this.enemyAttributeState?.value || ENEMY_ATTRIBUTE_STATE.NORMAL;
    }
}

实际运行效果

场景 1:敌人被冰冻后受击

// 1. 敌人初始状态
enemy.actionState === ENEMY_ACTION_STATE.MOVE
enemy.attributeState === ENEMY_ATTRIBUTE_STATE.NORMAL
enemy.effectSpeed === 1  // 正常速度

// 2. 玩家使用冰冻技能
enemy.switchAttributeState(ENEMY_ATTRIBUTE_STATE.FROZEN);
// → FrozenState.attribute() 执行
// → enemy.effectSpeed = 0.5
// → 颜色变为黄色
// → 3.2 秒后自动恢复 NORMAL

// 3. 此时敌人仍在移动,但速度变慢
// MoveState 读取 baseSpeed * effectSpeed = 1.15 * 0.5 = 0.575

// 4. 敌人被攻击
enemy.switchAttributeState(ENEMY_ACTION_STATE.HIT);
// → HitState.action() 执行
// → 播放受击特效
// → 动画结束后自动切回 MOVE

// 5. 冰冻效果持续,移动速度仍然是减速状态

场景 2:敌人中毒后死亡

// 1. 敌人中毒
enemy.switchAttributeState(ENEMY_ATTRIBUTE_STATE.POISON, { damage: 3 });
// → PoisonState.attribute() 执行
// → enemy.effectDamage = 3
// → 颜色变为紫色
// → 宿主注册周期性伤害回调(每 0.88 秒触发一次)

// 2. 持续伤害触发
// 每 0.88 秒执行一次 runAttributeHit()
// → handleHurtEvent(enemy, 3)
// → enemy.baseHp -= 3

// 3. 血量归零
if (enemy.baseHp <= 0) {
    enemy.switchActionState(ENEMY_ACTION_STATE.DEAD);
    // → DeadState.action() 执行
    // → 停止物理和动画
    // → 播放爆炸特效
    // → 发放经验
    // → 记录击杀统计
    // → 清空所有调度任务(包括中毒的持续伤害)
    // → 0.65 秒后回收到对象池
}

场景 3:游戏暂停/恢复

// 暂停时
gameManager.pauseGame();
// → 遍历所有敌人
// → 调用 enemy.setPause()
// → 停止物理、动画、特效

// 恢复时
gameManager.resumeGame();
// → 遍历所有敌人
// → 调用 enemy.setResume()
// → 根据 actionState 和 attributeState 的组合决定恢复行为

// 示例:敌人处于"移动 + 冰冻"状态
if (enemy.actionState === ENEMY_ACTION_STATE.MOVE) {
    // 恢复移动,速度 = baseSpeed * effectSpeed(冰冻修正)
    enemy.setVelocity(0, -1.15 * 0.5);
}
if (enemy.attributeState !== ENEMY_ATTRIBUTE_STATE.THUNDER) {
    // 非雷电状态才恢复主动画
    enemy.Animation.resume();
}

架构收益

通过这套双状态机架构,项目获得了:

  1. 状态数量可控:12 个状态类 vs 35 个组合状态
  2. 代码复用:4 种敌人类型(Tiny、Sub、Boss、Special)共享同一套状态机逻辑
  3. 易于扩展:新增属性效果(如"眩晕")只需添加一个属性状态类
  4. 清晰的生命周期:死亡状态集中处理所有清理逻辑,不会遗漏
  5. 表现驱动:受击、暴击等状态依赖动画事件自动回切,无需轮询

总结

多维度状态机架构通过正交分离策略,将复杂状态空间拆解为多个独立维度。核心价值在于:

  1. 控制状态爆炸:N + M 而非 N × M
  2. 职责清晰:状态机、状态类、宿主各司其职
  3. 易于扩展:新增维度不影响已有维度
  4. 表现驱动:状态切换由事件驱动,而非轮询判断

实现时需要权衡:

  • 状态类与宿主的耦合度
  • 生命周期管理的位置
  • 状态机数量与协调复杂度

通用框架提供了基础的状态机、状态基类和宿主接口,可以根据具体场景灵活扩展。

同样做缩略图,为什么别人又快又稳?踩过无数坑后,我总结出前端缩略图实战指南

作者 李剑一
2026年4月8日 16:02

最近有个项目需要用到上传图片,然后在列表页回显一下图片。

需求这边还想着要不要做一个瀑布图,但是做好以后图片量太多而且图片太大,导致展示效果并不好。

image.png

尤其是放在首屏上,长时间的白屏。

核心问题是:用户上传的图片过大,不仅导致页面加载缓慢、消耗过多带宽,而且还影响了服务器存储。

所以缩略图就成了最优解。

既不影响视觉展示,又能大幅降低资源消耗。

先明确结论:业界主流做法是什么?

很多人会陷入“非此即彼”的误区,纠结到底该前端还是后端生成缩略图。

但实际上,生产环境中最主流、最稳妥的架构是:前端做预览缩略图 + 后端/云存储做正式缩略图

两者分工配合,兼顾用户体验、性能和安全性。这也是目前大厂主流的实现方案。

简单来说:

  • 前端负责"":用户上传图片后,立即生成缩略图用于页面预览,提升交互体验;

  • 后端/云存储负责""和"生成":存储用户上传的原图,同时生成多尺寸正式缩略图,供页面正式展示。

前端缩略图实现(4种方案附代码)

前端生成缩略图的核心目的是"预览"和"减少上传流量",核心技术依赖 Canvas 绘图缩放createImageBitmap API

方案1:Canvas

这是前端生成缩略图的"标准方案",兼容所有浏览器(包括IE10+),零依赖,无需引入任何第三方库,是生产环境中最常用的方案。

核心原理

读取用户上传的图片文件 → 用Image对象加载图片 → 绘制到Canvas并按比例缩小 → 导出为缩略图Blob/Base64。

完整代码

/**
 * 生成图片缩略图
 * @param {File} file - 用户上传的图片文件(input[type="file"]获取)
 * @param {Number} maxWidth - 缩略图最大宽度(默认300px)
 * @param {Number} maxHeight - 缩略图最大高度(默认300px)
 * @param {Number} quality - 图片质量(0~1,1为最高质量,默认0.8)
 * @returns {Promise<Blob>} 缩略图文件(可直接上传或预览)
 */
async function createThumbnail(file, maxWidth = 300, maxHeight = 300, quality = 0.8) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.src = URL.createObjectURL(file);
    img.onload = () => {
      URL.revokeObjectURL(img.src);
      let { width, height } = img;
      // 如果原图尺寸超过设定的最大尺寸,进行等比缩小
      if (width > maxWidth || height > maxHeight) {
        const ratio = Math.min(maxWidth / width, maxHeight / height);
        width *= ratio;
        height *= ratio;
      }
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0, width, height);
      canvas.toBlob(
        (blob) => resolve(blob), // 成功回调,返回缩略图Blob
        file.type || 'image/jpeg', // 保持原图格式,无格式则默认jpeg
        quality // 图片质量
      );
    };
    img.onerror = () => reject(new Error('图片加载失败,请检查文件格式'));
  });
}

// 使用
document.querySelector('input[type="file"]').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return; // 未选择文件,直接返回

  try {
    // 生成300x300的缩略图(可根据需求调整尺寸和质量)
    const thumbBlob = await createThumbnail(file, 300, 300, 0.7);
    
    // 场景1:预览缩略图(页面展示)
    const thumbUrl = URL.createObjectURL(thumbBlob);
    document.querySelector('#preview').src = thumbUrl;

    // 场景2:将缩略图上传到服务器(搭配FormData)
    const formData = new FormData();
    // 第三个参数是缩略图文件名,可自定义
    formData.append('thumbnail', thumbBlob, `thumbnail_${Date.now()}.jpg`);
    // 发起上传请求(实际项目中替换为自己的接口地址)
    const response = await fetch('/api/upload/thumbnail', {
      method: 'POST',
      body: formData
    });
    const result = await response.json();
    console.log('缩略图上传成功:', result);
  } catch (error) {
    console.error('缩略图生成/上传失败:', error);
  }
});

方案2:createImageBitmap

如果项目不考虑兼容性问题,那么这个方案比Canvas原生方案更高效。

它支持直接解析File/Blob对象,无需创建Image对象,加载速度更快。

而且还能在Web Worker中使用(避免阻塞主线程),适合处理大尺寸图片。

完整代码

/**
 * 高性能缩略图生成(createImageBitmap方案)
 * @param {File} file - 用户上传的图片文件
 * @param {Number} maxWidth - 缩略图最大宽度(默认300px)
 * @param {Number} maxHeight - 缩略图最大高度(默认300px)
 * @returns {Promise<Blob>} 缩略图Blob文件
 */
async function createThumbnailFast(file, maxWidth = 300, maxHeight = 300) {
  try {
    // 直接解析File对象,生成ImageBitmap(比Image对象更快)
    const bitmap = await createImageBitmap(file);
    
    // 计算等比缩放尺寸(和Canvas方案逻辑一致)
    let { width, height } = bitmap;
    if (width > maxWidth || height > maxHeight) {
      const ratio = Math.min(maxWidth / width, maxHeight / height);
      width *= ratio;
      height *= ratio;
    }

    // 创建Canvas并绘制
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(bitmap, 0, 0, width, height);

    // 释放ImageBitmap内存(优化性能)
    bitmap.close();

    // 导出为Blob
    return new Promise((resolve) => {
      canvas.toBlob(resolve, file.type || 'image/jpeg', 0.8);
    });
  } catch (error) {
    console.error('高性能缩略图生成失败:', error);
    throw error;
  }
}

// 使用方式(和Canvas方案一致,直接替换函数名即可)
document.querySelector('input[type="file"]').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  const thumbBlob = await createThumbnailFast(file, 300, 300);
  // 预览/上传逻辑和上面一致,此处省略
});

方案3:browser-image-compression插件

如果懒得手写,可以使用browser-image-compression插件。Github地址: github.com/vitaly-z/br…

这是一个轻量级前端图片压缩库,自动处理图片缩放、压缩、格式转换,零配置即可使用,还能解决图片旋转(Exif orientation)等常见问题。

完整代码

// 安装依赖
// npm install browser-image-compression --save

// 导入
import imageCompression from 'browser-image-compression';

/**
 * 基于第三方库的缩略图生成
 * @param {File} file - 用户上传的图片文件
 * @returns {Promise<Blob>} 缩略图文件
 */
async function createThumbnailWithLib(file) {
  // 配置选项(灵活调整,无需手写逻辑)
  const options = {
    maxSizeMB: 0.1, // 缩略图最大体积(100KB,超过会自动压缩)
    maxWidthOrHeight: 300, // 缩略图最大尺寸(宽/高不超过300px)
    useWebWorker: true, // 使用Web Worker,避免阻塞主线程
    useWebp: true, // 导出为WebP格式(比JPG小30%+,质量无损失)
    initialQuality: 0.8 // 初始压缩质量
  };

  try {
    // 直接调用库方法,自动生成缩略图
    const thumbBlob = await imageCompression(file, options);
    console.log('库生成缩略图成功,大小:', thumbBlob.size);
    return thumbBlob;
  } catch (error) {
    console.error('库生成缩略图失败:', error);
    throw error;
  }
}

// 使用方式(和前面一致)
document.querySelector('input[type="file"]').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  const thumbBlob = await createThumbnailWithLib(file);
  // 预览/上传逻辑省略
});

当然,还有个偷懒的方法,直接给图片一个最大宽高,让他看起来像缩略图,不过仍然无法解决加载速度的问题。

后端/云存储方案

前面说过,前端生成的缩略图主要用于"预览",而"正式缩略图"(用于页面正式展示、多端适配),必须由后端或云存储生成——这是生产环境的标准做法,也是大厂通用架构。

为什么不能全靠前端生成正式缩略图?

很多兄弟会疑惑,既然前端能生成缩略图,为什么还要麻烦后端?

核心原因主要有4点:

  1. 可靠性不足:不同浏览器、不同设备(手机/PC)的Canvas渲染效果存在差异,可能导致缩略图模糊、变形,甚至生成失败。

  2. 安全性风险:前端传什么后端存什么,无法验证缩略图的真实性和合法性,可能存在恶意文件上传风险,甚至出现脚本。

  3. 多尺寸需求:一个项目通常需要多种尺寸的缩略图(如列表图300x300、头像图100x100、详情图1080x720),前端不可能生成所有尺寸,且维护成本极高。

  4. 性能与成本:云存储(如阿里云OSS、腾讯云COS、七牛云)的图片处理功能几乎免费,且速度极快,比自己写后端压缩代码更省资源、更稳定。

主流实现方式:云存储自动生成

目前大厂最常用的方式,是将用户上传的原图存储到云存储(如阿里云OSS)。

云存储会自动生成多种尺寸的缩略图,前端只需通过URL参数即可获取对应尺寸的缩略图,无需后端额外开发。

以阿里云OSS为例(实战示例)

  1. 用户上传原图到OSS,获得原图URL:https://xxx.oss-cn-hangzhou.aliyuncs.com/example.jpg

  2. 前端直接通过URL参数,获取不同尺寸的缩略图(无需后端干预):

  • 300x300缩略图:https://xxx.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,w_300,h_300

  • 100x100头像图:https://xxx.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,w_100,h_100,m_fixed

  • WebP格式缩略图(更小):https://xxx.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,w_300/format,wewebp

其中,x-oss-process=image/resize是OSS的图片缩放参数,还支持裁剪、旋转、加水印等功能,详细参数可参考阿里云OSS官方文档。

如果没有使用云存储,也可以通过后端代码生成缩略图(如Node.js、Java),核心逻辑和前端Canvas类似,都是"读取原图→缩放→保存"。

如果是Node的后端推荐尝试一下sharp库:sharp(originalPath).resize(width, height, {fit: 'cover',position: 'center'}).toFile(thumbnailPath);

总结

个人推荐:

前端负责“预览”,后端/云存储负责“正式生成”,用云存储自动生成多尺寸缩略图(生产首选)。

不过生产状况下有些事需要注意(个人踩过坑的):

  • 大图片上传建议提示用户压缩,同时建议仅支持上传jpg、png、webp等常见格式。
  • 推荐用户优先使用webp格式文件(图片小)。
  • 图片上传过程中的异常处理记得给足提示。
  • 多端适配记得覆盖全,尤其是支持移动端的项目。

观察 AIRI 源码:一个 Agent 系统如何处理入口、扩展与执行闭环

作者 奇舞精选
2026年4月8日 14:55

在 AI Agent 快速发展的这两年,很多项目都已经能做到“能聊、能演示、能截图。

但真正决定一个项目能走多远的,往往不是首屏效果,而是工程治理能力:请求怎么被接入、会话怎么被隔离、扩展怎么被约束、出错之后怎么继续稳定运行。

Project AIRI 值得借鉴的地方就在这里。 它不是把模型能力包一层 UI,而是在尝试把输入、推理、执行、反馈组织成一套可持续迭代的运行系统。

一、AIRI 是什么:一个“可运行”的 Agent 系统

一句话讲,AIRI 不是“给 LLM 套一层工具调用”的通用 Agent 平台,而是一个面向数字角色场景的运行系统。
它关注的不只是“任务能不能做完”,还关注“角色能不能持续存在、持续互动、跨端一致地存在”。 普通 Agent 平台通常把重点放在流程编排:输入任务、调用工具、返回结果。
AIRI 在这条链路之外,多做了三层事情:

  • 实时交互层:语音输入、语音输出、角色驱动(如 Live2D / VRM)要协同工作,体验目标不是一次性响应,而是“在场感”。
  • 多形态运行层:Web、桌面、移动端不是各做一套,而是围绕共享能力组织,确保角色能力跨端延续。
  • 长期运行层:会话、状态、能力配置、插件扩展都要可持续管理,项目目标是长期演进,不是短期 demo。 所以 AIRI 的核心不是“模型接得多”,而是“把模型、交互、执行、扩展放进同一套可运行系统里”。
    这也是它和常见 Agent 框架最容易被混淆、但本质上差异最大的地方。

从仓库结构可以看到它的职责分层:apps 承接入口,packages 沉淀复用能力,plugins 负责扩展,services 连接外部渠道。 这种分层背后有一个很现实的目标:
在保持产品迭代速度的同时,把“可复用能力”和“可扩展边界”稳定下来。否则功能一多,项目就会很快进入“每加一个能力都要改全局”的状态。

二、请求生命周期:先治理入口,再进入业务

AIRI 的服务入口不是“收到请求就直接执行业务”,而是先做基础治理:跨域策略、会话中间件、请求体限制、观测链路,然后才分发到聊天、Provider、角色等路由。
这一步的价值在于把稳定性问题前置,而不是留给业务代码兜底。

path:apps/server/src/app.ts

const app new Hono<HonoEnv>()
  .use(
    '/api/*',
    cors({
      origin: origin => getTrustedOrigin(origin),
      credentialstrue,
    }),
  )
  .use(honoLogger())
if (otel) {
  app.use('*'otelMiddleware(otel))
}
return app
  .use('*'sessionMiddleware(auth))
  .use('*'bodyLimit({ maxSize1024 * 1024 }))
  .route('/api/providers'createProviderRoutes(providerService))
  .route('/api/chats'createChatRoutes(chatService))

AIRI 把请求处理拆成了“治理层 + 业务层”,属于典型的系统化服务结构。

三、Provider 管理:不是配置项,而是系统资源(精修版)

AIRI 把模型 Provider 当成“用户可管理的资源”来做,而不是把 API Key 直接散落在前端配置里。 从路由实现可以看到两层约束:先通过 authGuard 作为权限入口;创建时用 CreateProviderConfigSchema 做结构化校验,并把 ownerId 绑定到当前用户;修改时走 patch 路由,先加载目标配置,再用 existing.ownerId !== user.id 校验归属,不属于当前用户的改动会被直接拒绝。

path:apps/server/src/routes/providers.ts

export function createProviderRoutes(providerService: ProviderService) {
  return new Hono<HonoEnv>()
    .use('*', authGuard)
    .post('/'async (c) => {
      const user = c.get('user')!
      const body = await c.req.json()
      const result = safeParse(CreateProviderConfigSchema, body)

      if (!result.success) {
        throw createBadRequestError('Invalid Request''INVALID_REQUEST', result.issues)
      }

      const provider = await providerService.createUserConfig({
        ...result.output,
        ownerId: user.id,
      })

      return c.json(provider, 201)
    })
    .patch('/:id'async (c) => {
      const user = c.get('user')!
      const id = c.req.param('id')
      const body = await c.req.json()
      const result = safeParse(UpdateProviderConfigSchema, body)

      if (!result.success) {
        throw createBadRequestError('Invalid Request''INVALID_REQUEST', result.issues)
      }

      const existing = await providerService.findUserConfigById(id)
      if (!existing) throw createNotFoundError()
      if (existing.ownerId !== user.id) throw createForbiddenError()

      const updated = await providerService.updateUserConfig(id, result.output)
      return c.json(updated)
    })
}

这类实现的工程意义在于:Provider 的修改边界由后端路由用 ownerId 校验强制固定下来,而不是依赖前端或约定维持。

四、插件机制:能扩展,也要可控

AIRI 的插件扩展不是“把能力塞进系统就算完成”,而是把插件当成需要长期协作的模块来管理。
插件宿主用状态机约束生命周期阶段:状态机覆盖这些阶段,再到需要配置、完成配置,最终进入就绪状态;失败会进入统一的失败阶段。
这样系统在任何时刻都能回答一个工程问题:插件现在处于什么阶段、下一步应该做什么、失败该怎么被观测与处理。 同时,宿主在插件调用能力前还会做权限断言:扩展能力可以被接入,但不会默认获得越权操作的能力边界。

path:packages/plugin-sdk/src/plugin-host/core.ts

const pluginLifecycleMachine createMachine({
  id'plugin-lifecycle',
  initial'loading',
  states: {
    loading: { on: { SESSION_LOADED'loaded', SESSION_FAILED'failed' } },
    loaded: { on: { START_AUTHENTICATION'authenticating', SESSION_FAILED'failed', STOP'stopped' } },
    authenticating: { on: { AUTHENTICATED'authenticated', SESSION_FAILED'failed' } },
    authenticated: { on: { ANNOUNCED'announced', SESSION_FAILED'failed' } },
    announced: { on: { START_PREPARING'preparing', CONFIGURATION_NEEDED'configuration-needed', STOP'stopped', SESSION_FAILED'failed' } },
    preparing: { on: { WAITING_DEPENDENCIES'waiting-deps', PREPARED'prepared', SESSION_FAILED'failed' } },
    prepared: { on: { CONFIGURATION_NEEDED'configuration-needed', CONFIGURED'configured', SESSION_FAILED'failed' } },
    configuration-needed: { on: { CONFIGURED'configured', SESSION_FAILED'failed' } },
    configured: { on: { READY'ready', SESSION_FAILED'failed' } },
    ready: { on: { REANNOUNCE'announced', CONFIGURATION_NEEDED'configuration-needed', STOP'stopped', SESSION_FAILED'failed' } },
    failed: { on: { STOP'stopped' } },
  },
})


private assertPermission(session: PluginHostSession, input: { area'apis'|'resources'|'capabilities'|'processors'|'pipelines'; actionstring; keystring; reason?: string }) {
  const allowed = this.permissions.isAllowed(this.getPermissionScopeKey(session), input.area, input.action, input.key)
  if (allowed) return
  const error new PermissionDeniedError({ area: input.area, action: input.action, key: input.key })
  session.channels.host.emit(errorPermission, { identity: session.identity, error: { area: input.area, action: input.action, key: input.key, recoverabletrue } })
  throw error
}

扩展增长可以持续,但增长要留在契约和权限边界内。

五、执行闭环:不止“会回复”,还要“会把事做完”

AIRI 在渠道服务(如 Telegram)里的实现,已经体现了典型 agent loop: 先根据上下文推断动作,再执行动作,必要时进入下一轮循环。

path:services/telegram-bot/src/bots/telegram/index.ts

const action = await imagineAnAction(
  ctx.bot.botInfo.id.toString(),
  currentController,
  chatCtx?.messages || [],
  chatCtx?.actions || [],
  { unreadMessages: ctx.unreadMessages, incomingMessages: [incomingMessage] },
)
return await dispatchAction(ctx, action, currentController, chatCtx)

while (typeof result === 'function') {
  result = await result()
}

六、如何理解这个仓库:一条更顺的阅读路径

如果要快速看懂 AIRI 的系统设计,可以按这个顺序:

apps/server:先看请求如何进入系统、入口如何治理 apps/:再看多端入口如何复用核心能力 packages/:看 Provider、Plugin、UI/runtime 的边界抽象 plugins 与 services:看扩展和外部渠道如何接入主链路 按这条路径阅读,会先建立系统主干,再进入扩展细节,不容易陷入局部代码。

结语

当一个 Agent 项目同时具备可治理的入口、可约束的扩展、可追踪的执行闭环,它才真正从“可演示”走向“可运行”,这也是 AIRI 最值得参考的地方,它把 AI 能力从功能展示,推进到了系统工程。

参考链接

Project AIRI:github.com/moeru-ai/ai…

第 30 课:综合实战 — 毕业项目

作者 王小酱
2026年4月8日 23:20

所属阶段:第六阶段「综合与创造」(第 28-30 课) 前置条件:全部 29 课 本课收获:一个为真实项目设计的完整 ECC 配置方案


一、本课概述

这是整个课程的最后一课。没有新知识点 — 本课的目标是把前 29 课学到的一切综合运用。

你将完成一个毕业项目:为一个真实项目设计和实施完整的 ECC 配置方案。这个项目分 8 个阶段,每个阶段对应之前课程的知识:

阶段 内容 对应课程
A 基础配置 第 1-5 课
B Agent/Skill 选型 第 6-9 课
C Hook 配置 第 10-11 课
D 全流程验证 第 12-14 课
E 上下文优化 第 15 课
F 多代理编排 第 16 课
G 安全加固 第 23-24 课
H 自定义组件 第 7/9/10 课

完成毕业项目后,你不仅拥有了一套可用的 ECC 配置,更重要的是验证了你对整个体系的理解。


二、阶段 A:基础配置(对应第 1-5 课)

2.1 选择安装 Profile

根据你的项目需求和机器配置,选择合适的 Profile:

你的项目类型是什么?
  ├── 个人项目 / 学习 → developer
  ├── 团队项目 / 生产 → security
  ├── AI/Agent 开发 → research
  └── 想体验全部功能 → full

2.2 编写 CLAUDE.md

为你的项目编写 CLAUDE.md。这是 ECC 最重要的配置文件之一 — 它告诉 AI 助手关于你项目的一切。

必须包含的部分

# CLAUDE.md

## Project Overview
[一句话描述项目]

## Running Tests
[测试命令]

## Architecture
[核心组件和目录结构]

## Key Commands
[常用的开发命令]

## Development Notes
[特殊约定、注意事项]

2.3 配置 Rules

选择并安装适合你项目语言的 Rules:

# 安装通用规则(必须)
cp -r rules/common ~/.claude/rules/common

# 安装语言特定规则(根据你的项目)
cp -r rules/typescript ~/.claude/rules/typescript
# 或
cp -r rules/python ~/.claude/rules/python
# 或
cp -r rules/golang ~/.claude/rules/golang

2.4 完成清单

  • 选择了安装 Profile
  • 编写了 CLAUDE.md
  • 安装了通用 Rules
  • 安装了语言特定 Rules

三、阶段 B:Agent/Skill 选型(对应第 6-9 课)

3.1 选择核心 Agent

根据你的开发工作流,选择需要的 Agent:

工作流 必要 Agent 可选 Agent
日常开发 planner, code-reviewer architect
TDD 开发 tdd-guide, code-reviewer e2e-runner
安全敏感 security-reviewer, code-reviewer
构建调试 build-error-resolver 语言特定 resolver
文档更新 doc-updater

3.2 选择核心 Skill

领域 推荐 Skill 用途
开发流程 tdd-workflow, verification-loop TDD 和验证
代码标准 coding-standards 编码规范
API 设计 api-design, backend-patterns API 和后端模式
语言专用 python-patterns / golang-patterns 语言最佳实践
框架专用 django-patterns / nestjs-patterns 框架最佳实践

3.3 完成清单

  • 确定了需要的 Agent 列表
  • 确定了需要的 Skill 列表
  • 验证 Agent 和 Skill 已可用

四、阶段 C:Hook 配置(对应第 10-11 课)

4.1 配置推荐的 Hook

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/hooks/security-guard.js"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/hooks/post-edit-format.js",
            "async": true,
            "timeout": 10000
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/hooks/session-end.js",
            "async": true,
            "timeout": 30000
          }
        ]
      }
    ]
  }
}

4.2 Hook 选择指南

Hook 类型 推荐配置 注意事项
PreToolUse (Bash) 安全检查,拦截危险命令 保持 <200ms
PostToolUse (Write/Edit) 自动格式化 用 async,设 timeout
Stop 会话总结/学习提取 用 async,timeout ≤30s

4.3 完成清单

  • 配置了 PreToolUse 安全 Hook
  • 配置了 PostToolUse 格式化 Hook(可选)
  • 配置了 Stop 会话总结 Hook(可选)
  • 使用 run-with-flags.js wrapper(如果使用 ECC Hook)

五、阶段 D:全流程验证(对应第 12-14 课)

5.1 运行完整的命令链

按顺序执行以下命令,验证配置是否正确:

步骤 1:规划
  /plan — 让 planner Agent 分析一个小任务

步骤 2:TDD
  /tdd — 用 TDD 流程实现该任务
  - RED:写测试(应该失败)
  - GREEN:写最小实现(应该通过)
  - IMPROVE:重构

步骤 3:代码审查
  /code-review — 让 code-reviewer 审查代码

步骤 4:验证
  /verify — 运行验证循环
  - 测试通过?
  - Lint 通过?
  - 类型检查通过?
  - 安全检查通过?

5.2 问题排查

问题 可能原因 解决方案
Agent 不响应 Agent 文件未安装 检查 ~/.claude/agents/
Skill 不加载 Skill 路径错误 检查 ~/.claude/skills/
Hook 不触发 settings.json 配置错误 检查 matcher 和路径
命令不存在 Command 未安装 检查 ~/.claude/commands/

5.3 完成清单

  • /plan 正常工作
  • /tdd 流程可以完成 RED-GREEN-IMPROVE
  • /code-review 能产出审查报告
  • /verify 验证循环可以通过

六、阶段 E:上下文优化(对应第 15 课)

6.1 检查上下文使用

评估你的配置是否会过度占用上下文窗口:

检查项 指标 优化方法
CLAUDE.md 长度 <200 行 精简,移除冗余
加载的 Skill 数量 <20 个 减少不必要的 Skill
Rule 文件数量 <15 个 只安装需要的语言
System Prompt 总量 检查 Token 使用 按需加载

6.2 完成清单

  • CLAUDE.md 精简到 200 行以内
  • 只安装了实际需要的 Skill
  • 只安装了项目语言的 Rules

七、阶段 F:多代理编排(对应第 16 课)

7.1 设计并行工作流

为你的项目设计一个多代理并行工作流:

任务到达
    ↓
planner Agent(规划拆分)
    ↓
    ├── Agent 1:核心功能开发(Sonnet)
    ├── Agent 2:单元测试编写(Haiku)
    └── Agent 3:文档更新(Haiku)
    ↓
汇合:code-reviewer Agent(审查所有变更)
    ↓
security-reviewer Agent(安全审查)
    ↓
完成

7.2 完成清单

  • 设计了并行工作流方案
  • 为每个 Agent 选择了合适的模型
  • 定义了汇合点和审查流程

八、阶段 G:安全加固(对应第 23-24 课)

8.1 安全检查

# 扫描硬编码密钥
rg -n 'sk-|AKIA|password\s*=\s*["\x27][^"\x27]+["\x27]' --type-not binary .

# 扫描隐藏 Unicode
rg -nP '[\x{200B}\x{200C}\x{200D}\x{2060}\x{FEFF}\x{202A}-\x{202E}]' .

# 检查 .claude/ 配置安全
rg -n 'curl|wget|nc|scp|ssh|ANTHROPIC_BASE_URL' .claude/ 2>/dev/null

8.2 配置安全 Hook

确保 PreToolUse Hook 拦截了危险命令(参考第 24 课)。

8.3 完成清单

  • 运行了密钥扫描,无硬编码密钥
  • 运行了 Unicode 扫描,无隐藏字符
  • 配置了安全 Hook
  • 检查了 .claude/ 配置安全

九、阶段 H:自定义组件(对应第 7/9/10 课)

9.1 创建一个自定义 Agent

为你的项目创建一个专用 Agent。格式:

---
name: my-project-reviewer
description: Review code changes specific to [your project] conventions
tools:
  - Read
  - Grep
  - Glob
model: sonnet
---

# [Your Project] Reviewer

Review code changes against project-specific conventions:

1. Check naming conventions match project style
2. Verify error handling follows project patterns
3. Ensure new code has appropriate test coverage
4. Check for project-specific anti-patterns

## Project Conventions
[列出你项目的具体约定]

9.2 创建一个自定义 Skill

为你项目的某个常见工作流创建 Skill:

---
name: my-project-deployment
description: Deployment workflow for [your project]
---

# [Your Project] Deployment

## When to Activate
- Deploying to staging or production
- Preparing a release

## How It Works
1. [部署步骤 1]
2. [部署步骤 2]
3. [部署步骤 3]

## Checklist
- [ ] All tests pass
- [ ] Security scan clean
- [ ] CHANGELOG updated
- [ ] Version bumped

9.3 创建一个自定义 Hook

为你的项目创建一个 PostToolUse Hook(如自动格式化、自动 lint)。

9.4 完成清单

  • 创建了 1 个自定义 Agent
  • 创建了 1 个自定义 Skill
  • 创建了 1 个自定义 Hook

十、参考模板

ECC 在 examples/ 目录中提供了多种项目模板,你可以参考:

模板文件 技术栈 适用场景
saas-nextjs-CLAUDE.md Next.js + TypeScript SaaS 全栈应用
django-api-CLAUDE.md Django + Python RESTful API 后端
go-microservice-CLAUDE.md Go 微服务
laravel-api-CLAUDE.md Laravel + PHP PHP API 后端
rust-api-CLAUDE.md Rust + Actix/Axum Rust API 后端

10.1 使用方式

# 查看与你项目最接近的模板
cat examples/saas-nextjs-CLAUDE.md

# 作为起点,复制并修改
cp examples/saas-nextjs-CLAUDE.md ./CLAUDE.md
# 然后根据你的项目实际情况修改

十一、交付物清单

毕业项目的完整交付物:

# 交付物 来源阶段 必须/可选
1 CLAUDE.md A 必须
2 hooks.json.claude/settings.json C 必须
3 1 个自定义 Agent H 必须
4 1 个自定义 Skill H 必须
5 1 个自定义 Hook 脚本 H 必须
6 配置说明文档(简要) 全部 必须
7 多代理编排方案 F 可选
8 安全扫描报告 G 可选

11.1 可选加分项

加分项 说明
提交 PR 将你的自定义组件提交到 ECC 仓库
运行 /harness-audit 对完整配置进行审计并修复问题
运行 Eval 为你的自定义 Agent 设计并运行一次 Eval
团队分享 将 Instinct 导出并分享给团队成员

十二、课程总结 — 30 课的完整学习路径

回顾整个课程的六个阶段:

第一阶段:认知建立(第 1-3 课)

第 1 课:设计哲学 — 理解了 ECC 的五大原则和存在意义
第 2 课:架构全景 — 掌握了六大组件的职责和协作关系
第 3 课:目录结构 — 完成了仓库浏览和首次安装

阶段成果:你知道 ECC 是什么、为什么存在、怎么组织的。

第二阶段:组件精讲(第 4-11 课)

第 4-5 课:Rules — 理解了规则分层和语言特定覆写
第 6-7 课:Agents — 掌握了 Agent 格式和设计原则
第 8-9 课:Skills — 理解了 Skill 结构和编写方法
第 10-11 课:Hooks & Scripts — 掌握了事件驱动自动化

阶段成果:你能读懂并创建每种核心组件。

第三阶段:工作流实战(第 12-16 课)

第 12 课:调用链 — 追踪了完整的命令执行链路
第 13 课:TDD 流程 — 完成了 RED-GREEN-IMPROVE 循环
第 14 课:验证循环 — 掌握了提交前的完整验证流程
第 15 课:上下文管理 — 学会了 Token 优化和动态上下文
第 16 课:多代理编排 — 设计了并行 Agent 工作流

阶段成果:你能在真实项目中跑通完整开发流程。

第四阶段:语言与框架(第 17-22 课)

第 17 课:后端语言 — 配置了主力后端语言的 ECC
第 18 课:前端框架 — 配置了前端框架的 ECC
第 19 课:移动开发 — 了解了 Swift/Dart 的 ECC 模式
第 20 课:数据库 — 完成了 Migration 的 ECC 辅助
第 21 课:API 设计 — 掌握了符合 ECC 规范的 API 设计
第 22 课:微服务 — 理解了微服务架构的 ECC 支持

阶段成果:你能针对具体技术栈深度使用 ECC。

第五阶段:进阶能力(第 23-27 课)

第 23 课:安全威胁 — 列出了 AI 代理特有的攻击向量
第 24 课:安全防御 — 完成了 AgentShield 扫描和安全 Hook
第 25 课:持续学习 — 从会话中提取了 Instinct
第 26 课:Eval 驱动 — 设计并运行了 Eval
第 27 课:Agent 工程 — 理解了 Harness 构建和成本优化

阶段成果:你能处理安全、性能、学习、评估等高级主题。

第六阶段:综合与创造(第 28-30 课)

第 28 课:跨平台 — 理解了 Plugin Manifest 和安装 Profile
第 29 课:ECC 2.0 — 体验了 Rust 控制面板
第 30 课:毕业项目 — 为真实项目设计了完整 ECC 方案 ← 你在这里

阶段成果:你能独立设计和贡献 ECC 配置方案。


十三、写在最后

13.1 你现在拥有的能力

完成 30 课后,你具备了以下能力:

能力 说明
理解 AI Harness 知道如何增强 AI 编程助手的能力
设计配置方案 能为任何项目设计 ECC 配置
创建自定义组件 能编写 Agent、Skill、Hook
安全意识 能识别和防御 AI 代理特有的安全威胁
评估 Agent 能设计 Eval 并用 pass@k 衡量 Agent 质量
成本控制 能设计成本感知的多 Agent 工作流
跨平台迁移 能将知识应用到不同的 AI 编程助手

13.2 继续学习的方向

方向 资源
深入安全 the-security-guide.md 完整阅读
深入 Agent 开发 agent-harness-construction + autonomous-agent-harness Skill
深入持续学习 持续使用 /learn/evolve,积累 Instinct
贡献 ECC 阅读 CONTRIBUTING.md,提交你的自定义组件
关注 ECC 2.0 构建并试用 ecc2/,关注后续更新

13.3 最后的提醒

ECC 的五大原则不仅适用于 AI 编程,也适用于所有软件工程:

先规划再执行,让专家做专家的事,用测试验证结果,把安全放在首位,保持状态可控。

这是你在本课程中学到的最重要的一句话。


十四、本课小结

你应该记住的 内容
毕业项目 8 阶段 A 基础 → B 选型 → C Hook → D 验证 → E 上下文 → F 编排 → G 安全 → H 自定义
交付物 CLAUDE.md + hooks 配置 + 1 Agent + 1 Skill + 1 Hook + 说明文档
参考模板 examples/ 目录下 5 种技术栈模板
课程回顾 6 个阶段、30 课、从认知到创造的完整路径
核心原则 Plan → Agent-First → Test-Driven → Security-First → Immutability

第 29 课:ECC 2.0 — Rust 控制面板与未来方向

作者 王小酱
2026年4月8日 23:19

所属阶段:第六阶段「综合与创造」(第 28-30 课) 前置条件:第 28 课(跨平台适配与插件机制) 本课收获:理解 ECC 2.0 架构,体验 Dashboard(如环境允许)


一、本课概述

ECC 1.x 是一个基于 Node.js 脚本、Markdown 文件和 JSON 配置的插件系统。它工作得很好,但随着规模增长,有些问题开始浮现:

  • 管理多个并行会话很困难
  • 没有可视化的全局视图
  • 会话状态不持久(关掉终端就丢了)
  • Node.js 脚本在大量 Hook 并发时性能有瓶颈

ECC 2.0 用 Rust 构建了一个控制面板(Control Plane),解决这些问题:

  1. ECC 2.0 架构 — 模块设计和代码结构
  2. 核心命令 — Dashboard、会话管理、守护进程
  3. 解决的问题 — 多会话、可视化、持久化、性能
  4. 当前状态 — Alpha 质量,可构建可测试
  5. 为什么选 Rust — 性能、安全、并发

二、ECC 2.0 架构

2.1 源码结构

ecc2/src/
├── main.rs              # CLI 入口,命令定义
├── config/
│   └── mod.rs           # 配置管理(Dashboard 布局等)
├── tui/
│   ├── mod.rs           # TUI 模块入口
│   ├── app.rs           # 应用主循环
│   ├── dashboard.rs     # Terminal UI 仪表盘
│   └── widgets.rs       # 自定义 UI 组件(Token 计量、预算状态)
├── session/
│   ├── mod.rs           # 会话模块入口
│   ├── manager.rs       # 会话生命周期管理
│   ├── runtime.rs       # 会话运行时
│   ├── output.rs        # 会话输出流处理
│   ├── store.rs         # SQLite 持久化存储
│   └── daemon.rs        # 后台守护进程
├── comms/
│   └── mod.rs           # 会话间通信
├── observability/
│   └── mod.rs           # 可观测性(日志、指标)
└── worktree/
    └── mod.rs           # Git Worktree 管理

2.2 技术栈

组件 技术 用途
CLI 框架 clap 命令行参数解析
TUI 框架 ratatui 终端用户界面
异步运行时 tokio 异步 IO 和并发
数据库 rusqlite (SQLite) 会话状态持久化
时间处理 chrono 时间戳和日期
错误处理 anyhow 统一错误类型
日志 tracing + tracing-subscriber 结构化日志

2.3 模块职责

┌──────────────────────────────────────────────────┐
│                  ECC 2.0 架构                     │
│                                                   │
│  main.rs (CLI)                                    │
│  ├── dashboard  → tui/dashboard.rs (TUI 渲染)    │
│  ├── start      → session/manager.rs (创建会话)  │
│  ├── delegate   → session/manager.rs (委派会话)  │
│  ├── assign     → session/manager.rs (分配任务)  │
│  ├── sessions   → session/store.rs (查询状态)    │
│  ├── status     → session/store.rs (会话详情)    │
│  ├── stop       → session/manager.rs (停止会话)  │
│  ├── resume     → session/runtime.rs (恢复会话)  │
│  └── daemon     → session/daemon.rs (后台服务)   │
│                                                   │
│  session/store.rs ←→ SQLite 数据库               │
│  comms/mod.rs ←→ 会话间消息传递                   │
│  worktree/mod.rs ←→ Git Worktree 隔离            │
│                                                   │
└──────────────────────────────────────────────────┘

三、核心命令

3.1 命令总览

命令 功能 说明
ecc2 dashboard 启动 TUI 仪表盘 可视化所有会话状态
ecc2 start 创建新会话 指定任务、Agent 类型、是否使用 Worktree
ecc2 delegate 委派子会话 从已有会话中派生子任务
ecc2 assign 分配任务 复用已有会话或创建新会话
ecc2 sessions 列出所有会话 查看活跃/完成/失败的会话
ecc2 status 查看会话详情 包括成本、Token、运行时间
ecc2 stop 停止会话 优雅地终止正在运行的会话
ecc2 resume 恢复会话 从持久化状态恢复已暂停的会话
ecc2 daemon 启动守护进程 后台运行,管理任务调度和恢复

3.2 启动会话

# 启动一个新的 Claude 会话
ecc2 start --task "Implement user authentication module" --agent claude

# 启动并使用独立的 Git Worktree
ecc2 start --task "Refactor database layer" --agent claude --worktree

# 从已有会话委派子任务
ecc2 delegate main-session --task "Write unit tests" --agent claude --worktree

3.3 Delegate vs Assign

操作 delegate assign
行为 总是创建新会话 优先复用已有会话
适用场景 明确需要新的工作流 有可能续用之前的会话
Worktree 默认创建 按需创建

3.4 Dashboard 界面

Dashboard 是一个终端界面(TUI),使用 ratatui 构建:

┌─ ECC 2.0 Dashboard ──────────────────────────────┐
│                                                    │
│  Sessions (3 active, 2 completed)                  │
│  ┌──────────┬─────────┬────────┬────────┬────────┐│
│  │ Session  │ Agent   │ Status │ Cost   │ Tokens ││
│  ├──────────┼─────────┼────────┼────────┼────────┤│
│  │ main-01  │ claude  │ ●      │ $1.23  │ 45.2K  ││
│  │ test-02  │ claude  │ ●      │ $0.45  │ 12.8K  ││
│  │ review-3 │ claude  │ ●      │ $0.30  │ 8.1K   ││
│  │ plan-04  │ claude  │ ✓      │ $0.12  │ 3.2K   ││
│  │ fix-05   │ claude  │ ✓      │ $0.67  │ 18.9K  ││
│  └──────────┴─────────┴────────┴────────┴────────┘│
│                                                    │
│  Selected: main-01                                 │
│  Task: Implement user authentication module        │
│  Runtime: 12m 34s                                  │
│  Budget: $2.00 remaining of $5.00                  │
│                                                    │
│  Output ────────────────────────────────────────── │
│  > Creating auth middleware...                      │
│  > Running tests: 12/15 passed                     │
│  > Fixing failing test: token_expiry_test          │
│                                                    │
│  [q] Quit  [↑↓] Select  [Enter] Details  [s] Stop │
└────────────────────────────────────────────────────┘

Dashboard 的关键特性:

特性 说明
实时状态 会话状态实时更新
成本追踪 每个会话的 Token 使用和费用
输出流 选中会话的实时输出
父子关系 显示会话的委派/被委派关系
未读消息 会话间的消息通知
风险评分 Daemon 活动和调度信息

四、解决的问题

4.1 多会话管理

ECC 1.x 的痛点

终端 1:Claude Code 在做主功能开发
终端 2:另一个 Claude Code 在写测试
终端 3:还有一个在做代码审查

问题:
- 三个终端之间没有关联
- 不知道总共花了多少钱
- 一个会话的结果无法自动流转到另一个

ECC 2.0 的解决

ecc2 dashboard  ← 一个界面看到所有会话

main-session (主开发)
    ├── delegate → test-session (测试)
    └── delegate → review-session (审查)

所有会话共享状态存储,可以互相通信

4.2 可视化仪表盘

ECC 1.x 没有全局视图。你需要在多个终端之间切换才能了解整体进度。

ECC 2.0 的 Dashboard 提供了一个单一窗口查看所有会话的状态、成本、输出。

4.3 持久化

ECC 1.x:关掉终端 = 丢失会话状态。

ECC 2.0:所有会话状态存储在 SQLite 中(session/store.rs)。

pub struct StateStore {
    conn: Connection,  // SQLite 连接
}

这意味着:

  • 关掉终端后可以恢复会话(ecc2 resume
  • 可以查看历史会话的统计信息
  • Daemon 可以在后台持续管理会话

4.4 性能提升

维度 Node.js (ECC 1.x) Rust (ECC 2.0)
启动时间 ~200ms ~5ms
内存占用 ~50MB ~3MB
并发会话 受 Event Loop 限制 原生多线程
Hook 执行 进程启动开销 原生函数调用

五、Daemon 守护进程

ecc2 daemon 启动一个后台守护进程,负责:

职责 说明
任务调度 自动将待处理任务分配给空闲会话
会话恢复 检测异常退出的会话,尝试恢复
负载均衡 在多个 Lead 会话间重新平衡任务
活动记录 记录调度、恢复、重平衡的活动日志
pub struct DaemonActivity {
    pub last_dispatch_at: Option<DateTime<Utc>>,
    pub last_dispatch_routed: usize,
    pub last_dispatch_deferred: usize,
    pub last_recovery_dispatch_at: Option<DateTime<Utc>>,
    pub last_rebalance_at: Option<DateTime<Utc>>,
    pub last_rebalance_rerouted: usize,
}

六、为什么选 Rust

6.1 三个核心理由

理由 说明
性能 零成本抽象,无 GC 暂停。控制面板需要低延迟响应
安全 所有权系统防止内存错误。控制面板管理敏感会话数据
并发 Tokio 异步运行时 + Send/Sync 保证。多会话并发是核心需求

6.2 与 Node.js 的互补关系

ECC 2.0 不是要替代 Node.js,而是互补

Node.js (ECC 1.x):
  ├── Skill/Agent/Command 定义(Markdown,不需要编译)
  ├── Hook 脚本(快速迭代,不需要编译)
  └── 安装脚本(跨平台兼容性好)

Rust (ECC 2.0):
  ├── 控制面板(性能关键路径)
  ├── 会话管理(并发和持久化)
  ├── Daemon(长时间运行的后台服务)
  └── TUI Dashboard(低延迟 UI)

七、当前状态与构建

7.1 当前状态

ECC 2.0 目前处于 Alpha 质量

方面 状态
核心功能 可用(dashboard、会话管理、store)
稳定性 Alpha(可能有 Bug)
文档 基础
测试 有单元测试
安装 需要从源码构建

7.2 构建方式

# 前置条件:安装 Rust 工具链
# https://rustup.rs/

# 进入 ecc2 目录
cd ecc2/

# 构建
cargo build

# 运行测试
cargo test

# 运行 Dashboard
cargo run -- dashboard

# 或者构建 Release 版本
cargo build --release
./target/release/ecc dashboard

7.3 开发环境要求

要求 最低版本
Rust 1.75+
Cargo 随 Rust 安装
SQLite 系统自带或由 rusqlite 编译
终端 支持 256 色的终端模拟器

八、本课练习

练习 1:阅读 main.rs(10 分钟)

cat ecc2/src/main.rs

回答问题:

  • ECC 2.0 定义了哪些子命令?
  • start 命令有哪些参数?
  • delegateassign 有什么区别?

练习 2:浏览源码结构(15 分钟)

浏览 ecc2/src/ 目录,画出模块依赖关系图:

  • 哪些模块依赖 session
  • tui 模块依赖哪些其他模块?
  • store.rs 被哪些模块使用?

练习 3:构建 ECC 2.0(20 分钟,需要 Rust 环境)

如果你的环境中已安装 Rust,尝试构建并运行:

cd ecc2/
cargo build
cargo test
cargo run -- dashboard

记录:

  • 构建是否成功?
  • 测试是否全部通过?
  • Dashboard 界面是什么样的?

如果没有 Rust 环境,阅读 ecc2/src/tui/dashboard.rs 的前 50 行,理解 Dashboard 的数据结构设计。

练习 4(选做):设计改进

基于你对 ECC 2.0 架构的理解,提出一个改进建议:

  • 你认为还缺少什么功能?
  • 有什么可以优化的地方?
  • 如何提升 Dashboard 的可用性?

九、本课小结

你应该记住的 内容
核心模块 main.rs(CLI)、tui/(Dashboard)、session/(会话管理)、store.rs(SQLite)
解决的问题 多会话管理、可视化仪表盘、状态持久化、性能提升
技术栈 Rust + clap + ratatui + tokio + rusqlite
当前状态 Alpha 质量,可构建可测试
与 1.x 关系 互补而非替代:Rust 做控制面板,Node.js 做内容定义

十、下节预告

第 30 课:综合实战 — 毕业项目

这是课程的最后一课。你将把前 29 课学到的所有知识综合运用,为一个真实项目设计完整的 ECC 配置方案:选择 Profile、编写 CLAUDE.md、配置 Agent/Skill/Hook、运行完整的开发流程验证。

预习建议:选择一个你正在开发的真实项目,思考它需要什么样的 ECC 配置。浏览 examples/ 目录中的参考模板。

❌
❌