[{"content":"","date":"2026-03-01T00:11:09+08:00","permalink":"https://impself.github.io/p/sql01-select-statement/","title":"SQL01-基础查询"},{"content":"引言 在关系型数据库中，数据通常被分散存储在多个表中，以遵循规范化设计原则。连接（JOIN） 是 SQL 中最强大的功能之一，它允许我们从多个表中组合相关数据。\n本文将系统讲解 SQL 连接的 12 种用法，帮助你从零掌握表连接的核心技巧。\n连接类型速览 连接类型 关键字 说明 图示 内连接 INNER JOIN 只返回匹配的行 A ∩ B 左外连接 LEFT JOIN 返回左表全部 + 匹配的右表 A 右外连接 RIGHT JOIN 返回右表全部 + 匹配的左表 B 交叉连接 CROSS JOIN 返回笛卡尔积 A × B 自连接 表别名 表与自身连接 — 一、内连接（INNER JOIN） 基本概念 内连接是最常用的连接类型，只返回两个表中满足连接条件的行。\ngraph LR A[表A] --- B[表B] C[内连接结果 = 交集] 语法示例 1 2 3 4 5 6 7 8 9 10 -- 查询订单明细及对应产品信息 SELECT oi.order_id, oi.product_id, p.name AS product_name, oi.quantity, oi.unit_price FROM order_items oi INNER JOIN products p ON p.product_id = oi.product_id; 执行逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ┌─────────────────┐ ┌─────────────────┐ │ order_items │ │ products │ ├─────────────────┤ ├─────────────────┤ │ order_id: 1 │────┐ │ product_id: 5 │ │ product_id: 5 │ ├────│ name: \u0026#39; Laptop\u0026#39; │ │ quantity: 2 │ │ │ price: 999.99 │ └─────────────────┘ │ └─────────────────┘ │ ▼ ┌──────────────────────────┐ │ 结果集 │ ├──────────────────────────┤ │ order_id: 1 │ │ product_name: \u0026#39;Laptop\u0026#39; │ │ quantity: 2 │ │ unit_price: 999.99 │ └──────────────────────────┘ 💡 注意：当 product_id 不匹配时，该行不会出现在结果中。\n二、跨数据库连接 使用场景 企业级应用中，数据可能分散在不同数据库中。SQL 允许跨数据库连接表。\n1 2 3 4 5 -- 连接当前数据库和 sql_inventory 数据库 SELECT * FROM order_items oi JOIN sql_inventory.products p ON oi.product_id = p.product_id; 语法格式 1 2 3 database_name.table_name.column_name │ │ │ 数据库名 表名 列名 最佳实践 建议 说明 ✅ 明确指定数据库 避免歧义，提高可读性 ✅ 使用同义词 为跨库表创建别名简化查询 ⚠️ 注意性能 跨库查询可能较慢 三、自连接（SELF JOIN） 基本概念 自连接是指表与自身进行连接，常用于处理层次结构数据。\n1 2 3 4 5 6 7 8 -- 查询员工及其经理信息 SELECT e.employee_id, e.first_name AS employee, m.first_name AS manager FROM employees e LEFT JOIN employees m ON e.reports_to = m.employee_id; 应用场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ┌─────────────────────────────────────────┐ │ employees 表 │ ├──────┬────────────┬──────────────┐ │ │ ID │ name │ reports_to │ │ ├──────┼────────────┼──────────────┤ │ │ 1 │ Alice │ NULL │ ← CEO│ │ 2 │ Bob │ 1 │ │ │ 3 │ Carol │ 1 │ │ │ 4 │ Dave │ 2 │ │ └──────┴────────────┴──────────────┘ │ ↓ │ 自连接查询结果： │ ┌──────┬────────────┬────────────┐ │ │ ID │ employee │ manager │ │ ├──────┼────────────┼────────────┤ │ │ 1 │ Alice │ NULL │ │ │ 2 │ Bob │ Alice │ │ │ 3 │ Carol │ Alice │ │ │ 4 │ Dave │ Bob │ │ └──────┴────────────┴────────────┘ │ └─────────────────────────────────────────┘ 常见用例 场景 示例 组织架构 员工 → 经理 分类层级 子分类 → 父分类 地区数据 城市 → 省份 → 国家 评论系统 评论 → 父评论 四、多表连接 语法示例 1 2 3 4 5 6 7 8 9 10 11 12 -- 查询支付详情（客户 + 支付方式） SELECT p.date, p.invoice_id, c.name AS client, c.phone, pm.name AS payment_method FROM payments p JOIN clients c ON p.client_id = c.client_id JOIN payment_methods pm ON p.payment_method = pm.payment_method_id; 连接顺序 graph TD A[payments 主表] --\u0026gt; B[clients 客户表] A --\u0026gt; C[payment_methods 支付方式表] B --\u0026gt; D[结果集] C --\u0026gt; D 性能建议 ⚠️ 注意：连接的表越多，查询性能影响越大。建议：\n限制连接数量（通常不超过 5 个表） 确保连接字段有索引 将小表作为驱动表 五、复合连接 基本概念 复合连接使用多个列作为连接条件，常见于复合主键的场景。\n1 2 3 4 5 6 -- 订单项和订单项备注通过订单ID和产品ID连接 SELECT * FROM order_items oi JOIN order_item_notes oin ON oi.order_id = oin.order_id AND oi.product_id = oin.product_id; 场景说明 1 2 3 4 5 6 7 8 9 10 11 ┌─────────────────────────┐ ┌─────────────────────────┐ │ order_items │ │ order_item_notes │ ├──────────┬──────────────┤ ├──────────┬──────────────┤ │ order_id │ product_id │ │ order_id │ product_id │ ├──────────┼──────────────┤ ├──────────┼──────────────┤ │ 1 │ 5 │─────►│ 1 │ 5 │ │ 1 │ 8 │ │ 1 │ 8 │ │ 2 │ 5 │ │ 2 │ 5 │ └──────────┴──────────────┘ └──────────┴──────────────┘ 复合主键 复合主键 (order_id, product_id) (order_id, product_id) 六、隐式连接 vs 显式连接 隐式连接（传统语法） 1 2 3 SELECT * FROM customers c, orders o WHERE c.customer_id = o.customer_id; 显式连接（推荐） 1 2 3 4 SELECT * FROM customers c INNER JOIN orders o ON c.customer_id = o.customer_id; 对比分析 特性 隐式连接 显式连接 可读性 ⭐⭐ ⭐⭐⭐⭐⭐ 连接条件分离 ❌ 容易与过滤条件混淆 ✅ ON 子句明确 外连接支持 ❌ 不支持 ✅ 完整支持 标准 SQL ⚠️ 旧语法 ✅ ANSI 标准 💡 最佳实践：始终使用显式连接语法（INNER JOIN/LEFT JOIN）\n七、外连接（OUTER JOIN） 类型对比 graph LR subgraph 左表 A A1[1] A2[2] A3[3] end subgraph 右表 B B1[2] B2[4] end 连接类型 结果 说明 INNER JOIN {2} 只保留匹配 LEFT JOIN {1,2,3} 保留左表全部 RIGHT JOIN {2,4} 保留右表全部 FULL JOIN {1,2,3,4} 保留全部（MySQL 不支持） 左外连接示例 1 2 3 4 5 6 7 8 -- 查询所有产品及其订单数量（包括无订单的产品） SELECT p.product_id, p.name, oi.quantity FROM products p LEFT JOIN order_items oi ON p.product_id = oi.product_id; 结果特征： 无订单的产品，quantity 显示为 NULL\n八、多表外连接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 查询订单完整信息（客户 + 运输商 + 状态） SELECT o.order_date, o.order_id, c.first_name AS customer, s.name AS shipper, os.name AS status FROM orders o LEFT JOIN shippers s ON o.shipper_id = s.shipper_id LEFT JOIN order_statuses os ON o.status = os.order_status_id LEFT JOIN customers c ON o.customer_id = c.customer_id ORDER BY os.name; 连接策略 1 2 3 4 5 6 7 8 9 10 ┌─────────────────────────────────┐ │ orders │ ← 主表 │ (所有订单都会保留) │ └─────────────┬───────────────────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │shippers │ │statuses │ │customers│ └─────────┘ └─────────┘ └─────────┘ 九、自外连接 1 2 3 4 5 6 7 8 -- 查询所有员工及经理（包括没有经理的CEO） SELECT e.employee_id, e.first_name, m.first_name AS manager FROM employees e LEFT JOIN employees m ON e.reports_to = m.employee_id; 结果特点： CEO 的 manager 字段为 NULL\n十、USING 子句 简化连接条件 当两个表的列名相同时，可以使用 USING 简化语法。\n1 2 3 4 5 6 7 8 9 10 11 -- 传统写法 SELECT * FROM payments p JOIN clients c ON p.client_id = c.client_id; -- USING 写法（更简洁） SELECT * FROM payments p JOIN clients c USING(client_id); 多列使用 1 2 3 4 5 -- 复合连接的 USING 写法 SELECT * FROM order_items oi JOIN order_item_notes oin USING(order_id, product_id); 语法对比 写法 适用场景 ON t1.id = t2.id 列名不同 USING(id) 列名相同 NATURAL JOIN 所有同名列（不推荐） ⚠️ 注意：USING 后的列名在结果集中只出现一次\n十一、交叉连接（CROSS JOIN） 基本概念 交叉连接返回笛卡尔积——两个表每一行的所有可能组合。\n1 2 3 4 5 6 -- 两种写法等价 SELECT * FROM shippers s, products p; SELECT * FROM shippers s CROSS JOIN products p; 结果示例 1 2 3 4 shippers 表有 3 行 products 表有 10 行 结果集：3 × 10 = 30 行 实际应用场景 场景 说明 生成所有组合 产品 × 颜色 × 尺寸 测试数据生成 快速构造测试集 日历生成 日期 × 时间段 ⚠️ 警告：交叉连接可能产生海量数据，使用前务必评估数据量！\n十二、联合（UNION） 基本概念 UNION 将多个 SELECT 语句的结果合并为一个结果集。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 -- 客户分级查询 SELECT customer_id, first_name, points, \u0026#39;Bronze\u0026#39; AS type FROM customers WHERE points \u0026lt; 2000 UNION SELECT customer_id, first_name, points, \u0026#39;Silver\u0026#39; AS type FROM customers WHERE points BETWEEN 2000 AND 3000 UNION SELECT customer_id, first_name, points, \u0026#39;Gold\u0026#39; AS type FROM customers WHERE points \u0026gt;= 3000 ORDER BY first_name; UNION vs UNION ALL 特性 UNION UNION ALL 去重 ✅ 自动去重 ❌ 保留全部 性能 较慢（需要排序去重） 较快 使用场景 需要去重 确定无重复/需保留 使用规则 1 2 3 4 ✓ 各 SELECT 语句的列数必须相同 ✓ 对应列的数据类型必须兼容 ✓ 结果集的列名取自第一个 SELECT ✓ ORDER BY 应放在最后一条 SELECT 之后 连接性能优化指南 1. 索引策略 1 2 3 -- 为连接条件创建索引 CREATE INDEX idx_order_items_product ON order_items(product_id); CREATE INDEX idx_products_id ON products(product_id); 2. 小表驱动大表 1 2 3 -- 推荐：小表在前 FROM small_table s JOIN large_table l ON s.id = l.small_id 3. 避免无效连接 1 2 3 4 5 6 7 8 9 -- ❌ 不推荐：在 WHERE 中过滤 FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE c.active = 1 -- ✅ 推荐：在 JOIN 前过滤 FROM orders o JOIN (SELECT * FROM customers WHERE active = 1) c ON o.customer_id = c.customer_id 4. 选择性原则 优先级 连接类型 原因 1 INNER JOIN 结果集最小 2 LEFT/RIGHT JOIN 单侧保留 3 CROSS JOIN 结果集最大 常见问题排查 问题 1：连接结果不符合预期 1 2 3 -- 检查连接条件是否正确 -- 检查是否有 NULL 值影响连接 SELECT COUNT(*) FROM table WHERE join_column IS NULL; 问题 2：性能慢 1 2 3 4 5 -- 使用 EXPLAIN 分析执行计划 EXPLAIN SELECT * FROM table1 JOIN table2 ON ...; -- 检查是否使用了索引 SHOW INDEX FROM table_name; 问题 3：列名冲突 1 2 3 4 5 6 7 8 -- 使用表别名或完整列名 SELECT o.order_id, c.order_id -- ❌ 歧义 FROM orders o JOIN customers c ON ... SELECT o.order_id, c.customer_id -- ✅ 明确 FROM orders o JOIN customers c ON ... 总结 连接类型决策树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 需要组合多表数据？ │ ├─ 需要所有匹配行？ │ └─ 使用 INNER JOIN │ ├─ 需要保留某个表全部行？ │ ├─ 左表 → LEFT JOIN │ └─ 右表 → RIGHT JOIN │ ├─ 表与自身关联？ │ └─ 使用 SELF JOIN（表别名） │ ├─ 需要所有组合？ │ └─ 使用 CROSS JOIN（谨慎！） │ └─ 需要合并结果集？ └─ 使用 UNION / UNION ALL 快速参考表 连接类型 关键字 行为 内连接 INNER JOIN 只返回匹配 左连接 LEFT JOIN 左表全 + 匹配右表 右连接 RIGHT JOIN 右表全 + 匹配左表 交叉连接 CROSS JOIN 笛卡尔积 自连接 表别名 表自身连接 自然连接 NATURAL JOIN 自动匹配同名列 联合 UNION 合并结果集 下一步学习 SQL01-基础查询 SQL03-聚合函数 SQL04-子查询 💬 有疑问？ 欢迎在评论区讨论，或提交 Issue 反馈问题。\n","date":"2026-03-01T00:09:09+08:00","permalink":"https://impself.github.io/p/sql02-join/","title":"SQL02-连接全解：从基础到实战"},{"content":"智能体记忆系统与RAG系统 - 问题详解 目录 情景记忆 vs 语义记忆评分机制对比 个人健康管理助手的记忆设计 工作记忆整合机制设计 基于语义边界的分块算法 MQE vs HyDE 检索策略对比 嵌入方案选型对比 智能遗忘策略实现 记忆归档机制设计 隐私数据彻底清除方案 RAG vs Memory 智能路由 智能学习报告生成器设计 多用户数据隔离方案 知识图谱质量评估 图检索 vs 向量检索对比 1. 情景记忆 vs 语义记忆评分机制对比 评分公式对比 情景记忆评分公式：\n1 score = 0.4 × recency + 0.3 × importance + 0.2 × access + 0.1 × embedding 语义记忆评分公式：\n1 score = 0.3 × graph_retrieval + 0.25 × embedding + 0.25 × access + 0.2 × importance 设计原因分析 维度 情景记忆 语义记忆 设计原因 时间权重 (recency) 0.4 0.0 情景记忆记录\u0026quot;我在何时何地做了什么\u0026quot;，新鲜度至关重要；语义记忆是持久知识，与时间无关 图检索权重 (graph_retrieval) 0.0 0.3 语义记忆基于知识图谱，概念间的关系推理是核心优势 嵌入权重 (embedding) 0.1 0.25 语义记忆更注重语义相似性匹配 重要性权重 (importance) 0.3 0.2 情景记忆中某些事件可能特别重要（如里程碑事件） 访问权重 (access) 0.2 0.25 两者都考虑访问频率，但语义记忆更重视知识的复用 核心设计理念 情景记忆 = \u0026ldquo;时间线上的故事\u0026rdquo;\n强调时间近因性：最近发生的事情更相关 类比人类的短期事件记忆 适用于：\u0026ldquo;我昨天看了什么电影？\u0026ldquo;这类查询 语义记忆 = \u0026ldquo;知识网络中的概念\u0026rdquo;\n强调关系推理：通过图结构发现关联 类比人类的知识体系 适用于：\u0026ldquo;什么是深度学习？\u0026ldquo;这类概念查询 2. 个人健康管理助手的记忆设计 整体架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ┌─────────────────────────────────────────────────────────────┐ │ 个人健康管理助手 │ ├─────────────────────────────────────────────────────────────┤ │ 工作记忆 情景记忆 语义记忆 │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │当前对话│ │饮食记录 │ │营养知识 │ │ │ │输入状态 │ │运动日志 │ │医学概念 │ │ │ │暂存数据 │ │睡眠数据 │ │健康图谱 │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ ↓ ↓ ↓ │ │ TTL=5分钟 重要性衰减 知识推理 │ │ ↑ │ │ 感知记忆 ────────────────────────────────────┘ │ │ ┌─────────┐ │ │ │设备数据 │ 实时同步：心率、步数、血氧、体温 │ │ │环境数据 │ 定期采样：天气、空气质量、季节 │ │ └─────────┘ │ └─────────────────────────────────────────────────────────────┘ 具体应用场景 记忆类型 应用场景 数据示例 检索示例 工作记忆 对话状态管理 用户当前输入的饮食内容 \u0026ldquo;我刚才说今天早餐吃了什么？\u0026rdquo; 情景记忆 健康行为追踪 \u0026ldquo;2024-01-15早餐：燕麦牛奶\u0026rdquo; \u0026ldquo;上周三的跑步记录是什么？\u0026rdquo; 语义记忆 健康知识推理 \u0026ldquo;缺乏维生素D → 需要晒太阳\u0026rdquo; \u0026ldquo;素食者容易缺乏什么营养素？\u0026rdquo; 感知记忆 实时数据流 手环心率：85 bpm \u0026ldquo;我当前心率是多少？\u0026rdquo; 数据流向示例 1 2 3 4 5 6 7 8 9 10 11 12 13 用户说：\u0026#34;我今天跑步了5公里\u0026#34; ↓ ┌─────────────────┐ │ 工作记忆（暂存） │ ← 存储原始输入 └─────────────────┘ ↓ (5分钟后未提及) ┌─────────────────┐ │ 整合到情景记忆 │ ← \u0026#34;2024-01-20 跑步5公里，耗时30分钟\u0026#34; └─────────────────┘ ↓ (与历史数据关联) ┌─────────────────┐ │ 更新语义记忆 │ ← \u0026#34;用户每周运动3次，属于活跃用户\u0026#34; └─────────────────┘ 3. 工作记忆整合机制设计 整合决策矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 ┌────────────────────────────────────────────────────────────┐ │ 工作记忆 → 长期记忆 整合触发条件 │ ├────────────────────────────────────────────────────────────┤ │ │ │ 触发信号 │ 整合目标 │ 延迟 │ │ ───────────── │ ───────── │ ──── │ │ 用户明确要求记住 │ → 情景记忆 │ 立即 │ │ 对话中反复提及(≥3次) │ → 语义记忆 │ 第3次触发 │ │ 与现有知识关联 │ → 语义记忆(关联) │ 匹配时 │ │ 情感标记(重要/紧急) │ → 情景记忆(高重要性)│ 检测时 │ │ 时间窗口持续(\u0026gt;1小时) │ → 模式识别 │ 到期触发 │ │ │ └─────────────────────────────────────────────────────────────┘ 自动整合算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class MemoryConsolidation: \u0026#34;\u0026#34;\u0026#34;工作记忆整合机制\u0026#34;\u0026#34;\u0026#34; def should_consolidate(self, work_item, context): \u0026#34;\u0026#34;\u0026#34;判断工作记忆是否需要整合\u0026#34;\u0026#34;\u0026#34; score = 0 reasons = [] # 1. 用户明确指示 (最高优先级) if work_item.get(\u0026#34;explicit_remember\u0026#34;): return {\u0026#34;action\u0026#34;: \u0026#34;immediate\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;episodic\u0026#34;} # 2. 提及频率检测 mention_count = work_item.get(\u0026#34;mention_count\u0026#34;, 0) if mention_count \u0026gt;= 3: score += 0.4 reasons.append(\u0026#34;频繁提及\u0026#34;) return {\u0026#34;action\u0026#34;: \u0026#34;consolidate\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;semantic\u0026#34;} # 3. 知识关联检测 if self._has_related_knowledge(work_item, context): score += 0.3 reasons.append(\u0026#34;存在关联知识\u0026#34;) # 4. 情感重要性检测 sentiment = work_item.get(\u0026#34;sentiment\u0026#34;) if sentiment in [\u0026#34;urgent\u0026#34;, \u0026#34;important\u0026#34;, \u0026#34;emotional\u0026#34;]: score += 0.5 reasons.append(\u0026#34;情感标记\u0026#34;) return {\u0026#34;action\u0026#34;: \u0026#34;immediate\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;episodic\u0026#34;} # 5. 时间阈值检测 age_seconds = time.time() - work_item.get(\u0026#34;created_at\u0026#34;, time.time()) if age_seconds \u0026gt; 3600: # 1小时 score += 0.2 reasons.append(\u0026#34;时间阈值\u0026#34;) # 综合判断 if score \u0026gt;= 0.5: return { \u0026#34;action\u0026#34;: \u0026#34;consolidate\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;semantic\u0026#34; if score \u0026gt; 0.6 else \u0026#34;episodic\u0026#34;, \u0026#34;reasons\u0026#34;: reasons } return {\u0026#34;action\u0026#34;: \u0026#34;keep\u0026#34;, \u0026#34;score\u0026#34;: score} def _has_related_knowledge(self, item, context): \u0026#34;\u0026#34;\u0026#34;检测是否与现有知识关联\u0026#34;\u0026#34;\u0026#34; # 检查实体重叠 item_entities = set(item.get(\u0026#34;entities\u0026#34;, [])) context_entities = set(context.get(\u0026#34;recent_entities\u0026#34;, [])) overlap = item_entities \u0026amp; context_entities return len(overlap) \u0026gt; 0 4. 基于语义边界的分块算法 问题背景 对于没有明确标题结构的文档（如小说、法律条文），传统的基于Markdown标题的分块策略失效，需要基于语义边界进行智能分块。\n语义边界检测算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import numpy as np from sentence_transformers import SentenceTransformer class SemanticChunker: \u0026#34;\u0026#34;\u0026#34;基于语义边界的智能分块\u0026#34;\u0026#34;\u0026#34; def __init__(self, model_name=\u0026#34;paraphrase-multilingual-MiniLM\u0026#34;): self.encoder = SentenceTransformer(model_name) def chunk(self, text, max_chunk_size=1000, min_chunk_size=200): \u0026#34;\u0026#34;\u0026#34; 主分块方法 Args: text: 输入文本 max_chunk_size: 最大块大小（字符数） min_chunk_size: 最小块大小（字符数） \u0026#34;\u0026#34;\u0026#34; # 1. 按句子分割 sentences = self._split_sentences(text) # 2. 计算句子嵌入 embeddings = self.encoder.encode(sentences) # 3. 检测语义边界 boundaries = self._detect_boundaries(embeddings) # 4. 在边界处创建chunks chunks = self._create_chunks( sentences, boundaries, max_chunk_size, min_chunk_size ) return chunks def _detect_boundaries(self, embeddings, window_size=3, threshold=0.5): \u0026#34;\u0026#34;\u0026#34; 检测语义边界 核心思想：如果相邻两个窗口内的语义相似度都显著高于跨边界的相似度， 则认为此处存在语义边界。 \u0026#34;\u0026#34;\u0026#34; boundaries = [0] # 第一个句子总是边界 for i in range(window_size, len(embeddings) - window_size): # 计算前窗口内的平均相似度 before_window = embeddings[i-window_size:i] before_similarity = self._avg_similarity(before_window) # 计算后窗口内的平均相似度 after_window = embeddings[i:i+window_size] after_similarity = self._avg_similarity(after_window) # 计算跨边界的相似度 cross_similarity = self._cosine_sim( before_window[-1], after_window[0] ) # 判断是否为边界：跨边界相似度显著低于窗口内相似度 avg_window_sim = (before_similarity + after_similarity) / 2 if cross_similarity \u0026lt; threshold * avg_window_sim: boundaries.append(i) boundaries.append(len(embeddings)) return boundaries def _avg_similarity(self, embeddings): \u0026#34;\u0026#34;\u0026#34;计算嵌入序列内的平均相似度\u0026#34;\u0026#34;\u0026#34; if len(embeddings) \u0026lt; 2: return 1.0 similarities = [ self._cosine_sim(embeddings[i], embeddings[i+1]) for i in range(len(embeddings) - 1) ] return np.mean(similarities) def _cosine_sim(self, a, b): \u0026#34;\u0026#34;\u0026#34;计算余弦相似度\u0026#34;\u0026#34;\u0026#34; return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) 不同文档类型的分块策略 文档类型 分块策略 实现要点 小说 章节边界 + 场景转换 检测\u0026quot;第X章\u0026quot;模式 + 对话/叙述交替 法律条文 条款边界 + 层级结构 检测\u0026quot;第X条\u0026quot;模式 + 缩进层级 学术论文 章节结构 + 段落完整性 Markdown标题 + 段落边界 技术文档 代码块独立 + API分组 识别代码块 + 按功能分组 新闻文章 段落语义连贯性 语义边界检测 5. MQE vs HyDE 检索策略对比 检索方法对比 检索方法 原理 查询示例 优缺点 基础检索 直接查询embedding \u0026ldquo;如何配置API密钥？\u0026rdquo; ✅ 简单快速 ❌ 召回率低 MQE 多查询扩展 → [\u0026ldquo;如何配置API密钥？\u0026rdquo;, \u0026ldquo;如何设置密钥？\u0026rdquo;, \u0026ldquo;在哪里配置凭证？\u0026rdquo;] ✅ 召回率高 ⚠️ 融合复杂 HyDE 假设文档嵌入 → 生成假设文档，用假设文档embedding检索 ✅ 精确度高 ❌ 依赖生成质量 场景选择矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 ┌─────────────────────────────────────────────────────────────┐ │ 检索策略选型指南 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 查询类型 │ 最佳策略 │ 原因 │ │ ────────── │ ───────── │ ──── │ │ 精确术语查询 │ 基础检索 │ 术语明确，无需扩展│ │ 模糊/口语化查询 │ MQE │ 多样化表达提高召回│ │ 复杂概念查询 │ HyDE │ 假设文档捕获语义 │ │ 多步骤问题 │ MQE + HyDE │ 组合拳效果最好 │ │ 领域专业查询 │ 基础 + 重排序 │ 减少幻觉风险 │ │ │ └─────────────────────────────────────────────────────────────┘ 实际场景效果对比 场景：技术文档问答\n查询：\u0026ldquo;如何配置API密钥？\u0026rdquo;\n检索方法 召回结果 效果评估 基础检索 找到3个相关片段 遗漏了使用\u0026quot;设置\u0026rdquo;、\u0026ldquo;凭证\u0026quot;等同义词的文档 MQE 找到8个相关片段 ✅ 召回率提升约150% HyDE 找到5个相关片段 精确度更高，匹配到配置步骤详解 6. 嵌入方案选型对比 三种方案全面对比 维度 百炼API 本地Transformer TF-IDF 准确性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ 速度 ⭐⭐⭐ (网络延迟50-200ms) ⭐⭐⭐⭐ (GPU加速，\u0026lt;50ms) ⭐⭐⭐⭐⭐ (\u0026lt;10ms) 成本 按调用付费 硬件+电费 免费 离线部署 ❌ ✅ ✅ 语言支持 中英双语优秀 需选择合适模型 统计方法，无语义理解 维护复杂度 低 中(模型更新) 低 可定制性 低 高 中 规模化成本 随调用量线性增长 固定成本 几乎无成本 选型决策树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 是否需要离线部署？ │ ├── 是 → 是否有GPU资源？ │ │ │ ├── 是 → 本地Transformer (推荐) │ └── 否 → TF-IDF (备用方案) │ └── 否 → 是否有预算？ │ ├── 是 → 百炼API (推荐，准确性最高) │ └── 否 → 考虑混合方案： • 热数据用API (保证质量) • 冷数据用本地 (降低成本) 混合方案建议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class HybridEmbedding: \u0026#34;\u0026#34;\u0026#34;混合嵌入方案：API + 本地兜底\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.api_client = BailianEmbedding() # 主力 self.local_model = LocalModel() # 备用 def encode(self, texts): \u0026#34;\u0026#34;\u0026#34;智能路由编码请求\u0026#34;\u0026#34;\u0026#34; try: # 正常情况下使用API return self.api_client.encode(texts) except Exception as e: # API失败时降级到本地模型 logger.warning(f\u0026#34;API failed, using local: {e}\u0026#34;) return self.local_model.encode(texts) 7. 智能遗忘策略实现 多因素加权遗忘算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 class IntelligentForgetting: \u0026#34;\u0026#34;\u0026#34;智能遗忘策略：综合多因素的遗忘决策\u0026#34;\u0026#34;\u0026#34; def __init__(self, weights=None): self.weights = weights or { \u0026#39;importance\u0026#39;: 0.30, # 重要性权重 \u0026#39;frequency\u0026#39;: 0.25, # 访问频率权重 \u0026#39;recency\u0026#39;: 0.25, # 时间权重 \u0026#39;connectivity\u0026#39;: 0.20 # 连接性权重 } def calculate_forgetting_score(self, memory): \u0026#34;\u0026#34;\u0026#34; 计算遗忘分数：分数越高越应该遗忘 返回: (forgetting_score, detail_scores) \u0026#34;\u0026#34;\u0026#34; scores = { \u0026#39;importance\u0026#39;: self._importance_score(memory), \u0026#39;frequency\u0026#39;: self._frequency_score(memory), \u0026#39;recency\u0026#39;: self._recency_score(memory), \u0026#39;connectivity\u0026#39;: self._connectivity_score(memory) } # 加权综合 forgetting_score = sum( scores[k] * self.weights[k] for k in scores ) return forgetting_score, scores def _importance_score(self, memory): \u0026#34;\u0026#34;\u0026#34; 重要性转换为遗忘倾向（反向） 重要性越高 → 遗忘倾向越低 \u0026#34;\u0026#34;\u0026#34; original = memory.get(\u0026#39;importance\u0026#39;, 0.5) return 1 - original def _frequency_score(self, memory): \u0026#34;\u0026#34;\u0026#34; 访问频率转换为遗忘倾向（反向） 访问越多 + 距离上次访问越短 → 遗忘倾向越低 \u0026#34;\u0026#34;\u0026#34; access_count = memory.get(\u0026#39;access_count\u0026#39;, 0) days_since_last = memory.get(\u0026#39;days_since_last_access\u0026#39;, 0) # 访问越多 → 遗忘倾向越低 frequency_factor = 1 / (1 + access_count * 0.1) # 距上次访问越久 → 遗忘倾向越高 recency_penalty = min(days_since_last * 0.02, 0.5) return frequency_factor + recency_penalty def _recency_score(self, memory): \u0026#34;\u0026#34;\u0026#34; 时间衰减：越旧越容易遗忘 使用S曲线：刚开始遗忘慢，中间加速，后期趋缓 \u0026#34;\u0026#34;\u0026#34; age_days = memory.get(\u0026#39;age_days\u0026#39;, 0) # Sigmoid函数 return 1 / (1 + np.exp(-0.05 * (age_days - 60))) def _connectivity_score(self, memory): \u0026#34;\u0026#34;\u0026#34; 连接性：与其他记忆关联少 → 更容易遗忘 在图数据库中查询相关节点数量 \u0026#34;\u0026#34;\u0026#34; related_count = self._get_related_memory_count(memory[\u0026#39;id\u0026#39;]) return 1 / (1 + related_count * 0.2) 遗忘决策阈值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 遗忘阈值设定 FORGETTING_THRESHOLDS = { \u0026#39;immediate\u0026#39;: 0.8, # 立即遗忘 \u0026#39;gradual\u0026#39;: 0.6, # 逐渐降低优先级 \u0026#39;archive\u0026#39;: 0.4, # 归档 \u0026#39;keep\u0026#39;: 0.4 # 保留 } def decide_forget_action(memory): \u0026#34;\u0026#34;\u0026#34;根据遗忘分数决定操作\u0026#34;\u0026#34;\u0026#34; score, _ = forgetting.calculate_forgetting_score(memory) if score \u0026gt;= FORGETTING_THRESHOLDS[\u0026#39;immediate\u0026#39;]: return \u0026#39;delete\u0026#39; elif score \u0026gt;= FORGETTING_THRESHOLDS[\u0026#39;gradual\u0026#39;]: return \u0026#39;deprioritize\u0026#39; elif score \u0026gt;= FORGETTING_THRESHOLDS[\u0026#39;archive\u0026#39;]: return \u0026#39;archive\u0026#39; else: return \u0026#39;keep\u0026#39; 8. 记忆归档机制设计 记忆生命周期管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ┌─────────────────────────────────────────────────────────────┐ │ 记忆生命周期管理 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ 迁移条件 ┌─────────┐ 迁移条件 ┌───────┐│ │ │ 活跃记忆 │ ──────────\u0026gt; │ 温记忆 │ ──────────\u0026gt; │ 冷存储 ││ │ │ Hot │ │ Warm │ │ Cold ││ │ └─────────┘ └─────────┘ └───────┘│ │ │ │ 特征： 特征： 特征： │ │ • 频繁访问 • 偶尔访问 • 长期未访问│ │ • 高重要性 • 中重要性 • 可能有价值│ │ • 最近创建 • 中等时间 • 历史记录 │ │ │ │ 迁移策略： │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 活跃 → 温： 7天无访问 + 重要性\u0026lt;0.7 │ │ │ │ 温 → 冷： 30天无访问 + 累计访问\u0026lt;3次 │ │ │ │ 冷 → 删除： 90天无访问 + 重要性\u0026lt;0.3 │ │ │ │ 冷 → 活跃： 被关联查询触发 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 存储优化： │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 活跃： Qdrant/Neo4j (热索引，毫秒级响应) │ │ │ │ 温： 压缩存储 + 定期重建索引 │ │ │ │ 冷： 对象存储(S3) + 元数据索引 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ 归档实现示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class MemoryArchiver: \u0026#34;\u0026#34;\u0026#34;记忆归档管理器\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.storage = { \u0026#39;hot\u0026#39;: HotStorage(), # Qdrant + Neo4j \u0026#39;warm\u0026#39;: WarmStorage(), # 压缩格式 \u0026#39;cold\u0026#39;: ColdStorage() # S3 + 元数据 } def evaluate_and_archive(self, memories): \u0026#34;\u0026#34;\u0026#34;评估并执行归档\u0026#34;\u0026#34;\u0026#34; for memory in memories: tier = self._evaluate_tier(memory) current_tier = memory.get(\u0026#39;storage_tier\u0026#39;, \u0026#39;hot\u0026#39;) if tier != current_tier: self._move_memory(memory, current_tier, tier) def _evaluate_tier(self, memory): \u0026#34;\u0026#34;\u0026#34;评估记忆应处于的存储层级\u0026#34;\u0026#34;\u0026#34; # 计算访问热度 access_score = self._calculate_access_score(memory) # 计算重要性 importance = memory.get(\u0026#39;importance\u0026#39;, 0.5) if access_score \u0026gt; 0.7 and importance \u0026gt; 0.7: return \u0026#39;hot\u0026#39; elif access_score \u0026gt; 0.3 or importance \u0026gt; 0.5: return \u0026#39;warm\u0026#39; else: return \u0026#39;cold\u0026#39; def _calculate_access_score(self, memory): \u0026#34;\u0026#34;\u0026#34;计算访问热度分数\u0026#34;\u0026#34;\u0026#34; # 最近访问权重高 now = time.time() last_access = memory.get(\u0026#39;last_access\u0026#39;, 0) days_since = (now - last_access) / 86400 # 指数衰减 time_factor = np.exp(-days_since / 7) # 7天半衰期 # 访问次数 count_factor = min(memory.get(\u0026#39;access_count\u0026#39;, 0) / 10, 1) return (time_factor * 0.7 + count_factor * 0.3) 9. 隐私数据彻底清除方案 多层验证清除机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 ┌─────────────────────────────────────────────────────────────┐ │ 隐私数据清除的多层验证机制 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 第1层：应用层删除 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ DELETE FROM memories │ │ │ │ WHERE id = ? AND user_id = ? │ │ │ │ 同时标记相关引用为\u0026#34;待删除\u0026#34; │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ 第2层：向量数据库清除 (Qdrant) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ qdrant_client.delete( │ │ │ │ collection_name=f\u0026#34;user_{user_id}\u0026#34;, │ │ │ │ points_selector=[memory_id] │ │ │ │ ) │ │ │ │ 验证：retrieve(memory_id) 应返回空 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ 第3层：图数据库清除 (Neo4j) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ MATCH (m:Memory {id: $id}) │ │ │ │ DETACH DELETE m, m-[r]-() │ │ │ │ 处理孤立节点 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ 第4层：备份区清除 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 从归档存储删除 │ │ │ │ 清除日志中的敏感信息 │ │ │ │ 清除缓存 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ 第5层：验证确认 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 全域搜索验证 │ │ │ │ 生成清除报告供审计 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 class PrivacyCleaner: \u0026#34;\u0026#34;\u0026#34;隐私数据彻底清除\u0026#34;\u0026#34;\u0026#34; def __init__(self, qdrant_client, neo4j_driver, storage): self.qdrant = qdrant_client self.neo4j = neo4j_driver self.storage = storage def delete_memory(self, user_id, memory_id): \u0026#34;\u0026#34;\u0026#34;彻底删除记忆\u0026#34;\u0026#34;\u0026#34; deletion_report = { \u0026#39;memory_id\u0026#39;: memory_id, \u0026#39;steps\u0026#39;: [], \u0026#39;success\u0026#39;: True } # 第1步：应用层删除 deletion_report[\u0026#39;steps\u0026#39;].append(self._delete_from_app(memory_id)) # 第2步：向量数据库删除 deletion_report[\u0026#39;steps\u0026#39;].append( self._delete_from_qdrant(user_id, memory_id) ) # 第3步：图数据库删除 deletion_report[\u0026#39;steps\u0026#39;].append( self._delete_from_neo4j(memory_id) ) # 第4步：存储删除 deletion_report[\u0026#39;steps\u0026#39;].append( self._delete_from_storage(memory_id) ) # 第5步：验证 deletion_report[\u0026#39;steps\u0026#39;].append( self._verify_deletion(memory_id) ) return deletion_report def _delete_from_qdrant(self, user_id, memory_id): \u0026#34;\u0026#34;\u0026#34;从Qdrant删除向量\u0026#34;\u0026#34;\u0026#34; collection = f\u0026#34;user_{user_id}_vectors\u0026#34; # 删除向量点 self.qdrant.delete( collection_name=collection, points_selector=[memory_id] ) # 验证删除 result = self.qdrant.retrieve( collection_name=collection, ids=[memory_id] ) if len(result) == 0: return {\u0026#39;step\u0026#39;: \u0026#39;qdrant\u0026#39;, \u0026#39;status\u0026#39;: \u0026#39;success\u0026#39;} else: return {\u0026#39;step\u0026#39;: \u0026#39;qdrant\u0026#39;, \u0026#39;status\u0026#39;: \u0026#39;failed\u0026#39;, \u0026#39;remains\u0026#39;: len(result)} 10. RAG vs Memory 智能路由 智能路由机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class RetrievalRouter: \u0026#34;\u0026#34;\u0026#34;智能路由：自动选择最佳检索方式\u0026#34;\u0026#34;\u0026#34; def route(self, query, context): \u0026#34;\u0026#34;\u0026#34;根据查询特征决定检索策略\u0026#34;\u0026#34;\u0026#34; # 1. 分析查询类型 query_type = self._classify_query(query) # 2. 评估上下文 context_score = self._evaluate_context(context) # 3. 路由决策 strategy = self._decide(query_type, context_score) return strategy def _classify_query(self, query): \u0026#34;\u0026#34;\u0026#34;查询分类\u0026#34;\u0026#34;\u0026#34; features = { \u0026#39;has_entity\u0026#39;: self._has_named_entity(query), \u0026#39;is_personal\u0026#39;: self._is_personal_reference(query), \u0026#39;is_factual\u0026#39;: self._is_factual_query(query), \u0026#39;temporal\u0026#39;: self._has_temporal_aspect(query) } # 决策树 if features[\u0026#39;is_personal\u0026#39;]: return \u0026#39;memory_primary\u0026#39; elif features[\u0026#39;has_entity\u0026#39;] and features[\u0026#39;is_factual\u0026#39;]: return \u0026#39;rag_primary\u0026#39; elif features[\u0026#39;temporal\u0026#39;]: return \u0026#39;memory_primary\u0026#39; else: return \u0026#39;hybrid\u0026#39; def _decide(self, query_type, context_score): \u0026#34;\u0026#34;\u0026#34;路由决策表\u0026#34;\u0026#34;\u0026#34; decision_matrix = { (\u0026#39;memory_primary\u0026#39;, \u0026#39;high\u0026#39;): [\u0026#39;memory\u0026#39;, \u0026#39;rag\u0026#39;], (\u0026#39;memory_primary\u0026#39;, \u0026#39;low\u0026#39;): [\u0026#39;memory\u0026#39;], (\u0026#39;rag_primary\u0026#39;, \u0026#39;any\u0026#39;): [\u0026#39;rag\u0026#39;], (\u0026#39;hybrid\u0026#39;, \u0026#39;high\u0026#39;): [\u0026#39;rag\u0026#39;, \u0026#39;memory\u0026#39;, \u0026#39;merge\u0026#39;], (\u0026#39;hybrid\u0026#39;, \u0026#39;low\u0026#39;): [\u0026#39;rag\u0026#39;, \u0026#39;memory\u0026#39;] } return decision_matrix.get((query_type, context_score), [\u0026#39;rag\u0026#39;]) 路由决策表 查询类型 上下文丰富度 推荐策略 理由 \u0026ldquo;我之前说过\u0026hellip;\u0026rdquo; 高 Memory → RAG 优先检索个人记忆 \u0026ldquo;什么是Transformer?\u0026rdquo; 任意 RAG 事实性查询 \u0026ldquo;上周讨论了什么?\u0026rdquo; 高 Memory 时间范围查询 \u0026ldquo;深度学习原理是什么?\u0026rdquo; 高 RAG + Memory 结合知识和记忆 11. 智能学习报告生成器设计 报告生成器架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 ┌─────────────────────────────────────────────────────────────┐ │ 智能学习报告生成器架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 输入：用户学习历史（问答记录、笔记、行为数据） │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 分析引擎模块 │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ 1. 学习轨迹分析 (情景记忆 + 时间序列) │ │ │ │ 2. 知识图谱构建 (语义记忆 + 图推理) │ │ │ │ 3. 兴趣偏好识别 (访问频率 + 停留时间) │ │ │ │ 4. 学习风格检测 (问题类型 + 笔记模式) │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 知识盲点识别 │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ • 未覆盖的知识领域 (文档覆盖 vs 学习覆盖) │ │ │ │ • 浅度学习的内容 (单次访问 vs 深度交互) │ │ │ │ • 遗忘的知识点 (学习后未复习) │ │ │ │ • 误解的概念 (矛盾问答记录) │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 推荐引擎 │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ • 复习优先级排序 (遗忘曲线 + 重要性) │ │ │ │ • 扩展学习建议 (知识图谱关联) │ │ │ │ • 个性化学习路径 (学习风格匹配) │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ 输出：结构化学习报告 + 可视化图表 + 行动建议 │ │ │ └─────────────────────────────────────────────────────────────┘ 报告结构示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 { \u0026#34;report_id\u0026#34;: \u0026#34;report_20240120_user123\u0026#34;, \u0026#34;generated_at\u0026#34;: \u0026#34;2024-01-20T10:30:00Z\u0026#34;, \u0026#34;summary\u0026#34;: { \u0026#34;total_study_time\u0026#34;: \u0026#34;5.2小时\u0026#34;, \u0026#34;documents_studied\u0026#34;: 3, \u0026#34;questions_asked\u0026#34;: 15, \u0026#34;notes_created\u0026#34;: 8 }, \u0026#34;learning_analysis\u0026#34;: { \u0026#34;strengths\u0026#34;: [\u0026#34;深度学习基础\u0026#34;, \u0026#34;Transformer架构\u0026#34;], \u0026#34;weaknesses\u0026#34;: [\u0026#34;注意力机制细节\u0026#34;, \u0026#34;训练技巧\u0026#34;], \u0026#34;knowledge_graph\u0026#34;: { \u0026#34;nodes\u0026#34;: 25, \u0026#34;edges\u0026#34;: 38, \u0026#34;central_concepts\u0026#34;: [\u0026#34;神经网络\u0026#34;, \u0026#34;反向传播\u0026#34;] } }, \u0026#34;knowledge_gaps\u0026#34;: [ { \u0026#34;topic\u0026#34;: \u0026#34;注意力机制\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;从未提问过相关问题\u0026#34;, \u0026#34;priority\u0026#34;: \u0026#34;high\u0026#34;, \u0026#34;recommendation\u0026#34;: \u0026#34;建议学习：Self-Attention、Multi-Head Attention\u0026#34; } ], \u0026#34;review_suggestions\u0026#34;: [ { \u0026#34;topic\u0026#34;: \u0026#34;Transformer架构\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;3天前学习，未复习\u0026#34;, \u0026#34;urgency\u0026#34;: \u0026#34;medium\u0026#34; } ], \u0026#34;next_steps\u0026#34;: [ \u0026#34;深入理解注意力机制\u0026#34;, \u0026#34;学习BERT和GPT的区别\u0026#34;, \u0026#34;实践：实现一个简单的Transformer\u0026#34; ] } 12. 多用户数据隔离方案 Qdrant 隔离方案 方案1：独立Collection（推荐）\n1 2 3 4 5 6 7 8 9 10 # 每个用户独立的collection collection_name = f\u0026#34;user_{user_id}_vectors\u0026#34; # 优点： # - 完全隔离，安全 # - 可独立删除 # - 性能互不影响 # 缺点： # - Collection数量多时管理复杂 方案2：共享Collection + Payload过滤\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 共享collection，通过payload过滤 collection_name = \u0026#34;global_vectors\u0026#34; # 插入时添加用户标识 point = { \u0026#34;id\u0026#34;: f\u0026#34;{user_id}_{doc_id}\u0026#34;, \u0026#34;vector\u0026#34;: embedding, \u0026#34;payload\u0026#34;: { \u0026#34;user_id\u0026#34;: user_id, \u0026#34;document\u0026#34;: doc_name } } # 查询时过滤 search_result = qdrant.search( collection_name=\u0026#34;global_vectors\u0026#34;, query_vector=embedding, query_filter={ \u0026#34;must\u0026#34;: [ {\u0026#34;key\u0026#34;: \u0026#34;user_id\u0026#34;, \u0026#34;match\u0026#34;: {\u0026#34;value\u0026#34;: user_id}} ] } ) Neo4j 隔离方案 1 2 3 4 5 6 7 8 9 10 11 -- 方案1：用户根节点（推荐） MATCH (u:User {id: $user_id})-[:HAS_MEMORY]-\u0026gt;(m:Memory) RETURN m -- 方案2：节点属性过滤 MATCH (m:Memory {user_id: $user_id}) RETURN m -- 方案3：标签隔离 MATCH (m:Memory:User_123) RETURN m 性能优化策略 策略 实现方式 效果 索引优化 CREATE INDEX ON :Memory(user_id) 查询速度提升10x 分片策略 按user_id哈希分片 负载均衡 连接池 限制每用户并发数 防止资源耗尽 缓存层 Redis缓存热点用户session 减少数据库压力 13. 知识图谱质量评估 质量评估维度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class KnowledgeGraphQuality: \u0026#34;\u0026#34;\u0026#34;知识图谱质量评估器\u0026#34;\u0026#34;\u0026#34; def evaluate(self, graph): \u0026#34;\u0026#34;\u0026#34;综合质量评分\u0026#34;\u0026#34;\u0026#34; return { \u0026#39;completeness\u0026#39;: self._check_completeness(graph), \u0026#39;accuracy\u0026#39;: self._check_accuracy(graph), \u0026#39;consistency\u0026#39;: self._check_consistency(graph), \u0026#39;coverage\u0026#39;: self._check_coverage(graph), \u0026#39;overall\u0026#39;: self._calculate_overall(graph) } def _check_completeness(self, graph): \u0026#34;\u0026#34;\u0026#34;完整性检查\u0026#34;\u0026#34;\u0026#34; total_nodes = graph.count_nodes() isolated_nodes = graph.count_isolated_nodes() total_relationships = graph.count_relationships() # 完整性指标 isolation_ratio = isolated_nodes / total_nodes if total_nodes \u0026gt; 0 else 0 relationship_density = total_relationships / max(total_nodes * (total_nodes - 1) / 2, 1) return { \u0026#39;isolation_ratio\u0026#39;: isolation_ratio, # 越低越好 \u0026#39;density\u0026#39;: relationship_density, # 适度最佳 \u0026#39;score\u0026#39;: 1 - isolation_ratio * 0.5 # 转换为分数 } def _check_accuracy(self, graph): \u0026#34;\u0026#34;\u0026#34;准确性检查（需要样本验证）\u0026#34;\u0026#34;\u0026#34; # 实体类型一致性 entities = graph.get_all_entities() type_conflicts = 0 for entity in entities: if self._has_type_conflict(entity): type_conflicts += 1 # 关系合理性 relationships = graph.get_all_relationships() invalid_rels = 0 for rel in relationships: if not self._is_valid_relation(rel): invalid_rels += 1 return { \u0026#39;entity_consistency\u0026#39;: 1 - type_conflicts / len(entities), \u0026#39;relation_validity\u0026#39;: 1 - invalid_rels / len(relationships) } 常见错误类型 错误类型 检测方法 修正策略 实体边界错误 统计共现频率 重新分词 关系方向错误 传递性检查 反转关系 类型冲突 类型一致性验证 多类型标签 冗余关系 重复检测 关系合并 虚假关联 置信度阈值 过滤低置信度 14. 图检索 vs 向量检索对比 查询类型对比表 查询类型 向量检索 图检索 推荐方案 \u0026ldquo;机器学习的定义\u0026rdquo; ✅ 语义相似 ❌ 无优势 向量 \u0026ldquo;与Transformer相关的所有技术\u0026rdquo; ⚠️ 召回有限 ✅ 多跳关联 图 \u0026ldquo;A如何影响B\u0026rdquo; ❌ 无法判断 ✅ 路径查找 图 \u0026ldquo;与X类似的概念\u0026rdquo; ✅ 语义相似 ⚠️ 需要同类型 向量 \u0026ldquo;X在知识体系中的位置\u0026rdquo; ❌ 无结构信息 ✅ 层级结构 图 \u0026ldquo;我学过哪些相关内容\u0026rdquo; ⚠️ 语义匹配 ✅ 关联遍历 图 混合检索示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 查询：找出所有与\u0026#34;Transformer\u0026#34;相关的技术，并按关联强度排序 MATCH path = (t:Concept {name: \u0026#34;Transformer\u0026#34;})-[:RELATED_TO*1..3]-(related:Concept) WHERE related.category = \u0026#34;技术\u0026#34; // 同时考虑向量相似度（通过预计算） WITH related, apoc.cypher.run( \u0026#39;CALL db.index.vector.queryNodes(\u0026#34;embedding_index\u0026#34;, 10, $embedding) YIELD node, score RETURN node.name AS name, score\u0026#39;, {embedding: $query_embedding} ) AS vector_result // 合并图检索和向量检索分数 MATCH (related) WHERE related.name IN vector_result.name RETURN related.name, sum(r.weight) as graph_score, vector_result.score as vector_score, (graph_score * 0.6 + vector_score * 0.4) as combined_score ORDER BY combined_score DESC LIMIT 10 图检索优势场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ┌─────────────────────────────────────────────────────────────┐ │ 图检索优势场景示例 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 场景1：关系推理 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 问题：\u0026#34;A如何影响B？\u0026#34; │ │ │ │ 图检索：FIND PATH A-\u0026gt;...-\u0026gt;B │ │ │ │ 向量检索：无法理解\u0026#34;影响\u0026#34;关系 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 场景2：层级遍历 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 问题：\u0026#34;列出所有机器学习的子领域\u0026#34; │ │ │ │ 图检索：MATCH (p:Concept)-[:SUBTYPE*]-\u0026gt;(c) │ │ │ │ 向量检索：只能找到语义相似的概念 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 场景3：社区发现 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 问题：\u0026#34;找出所有相互关联的概念群\u0026#34; │ │ │ │ 图检索：社区检测算法 │ │ │ │ 向量检索：需要先聚类再分析 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ 总结 本解答涵盖了智能体记忆系统和RAG系统的核心设计理念：\n记忆类型设计需要理解每种记忆的本质特征 RAG系统优化需要在召回率、精确度、效率之间权衡 工程实践需要考虑隐私、性能、可扩展性 多用户场景需要仔细设计数据隔离方案 质量保证需要建立评估和验证机制 这些设计原则不仅适用于学习助手，也适用于其他智能体应用场景，如个人助手、知识管理、教育培训等。\n","date":"2026-02-26T21:42:09+08:00","permalink":"https://impself.github.io/p/agent-08-memory-and-retrieval/","title":"Agent 08-记忆与检索"},{"content":"当前，智能体与 LLM 应用的低代码平台市场呈现出百花齐放的态势，每个平台都有其独特的定位和优势。选择哪个平台，往往取决于你的核心需求、技术背景以及项目的最终目标。在本章的后续内容中，我们将重点介绍并实操三个各具代表性的平台：Coze、Dify和 n8n。在此之前，我们先对它们进行一个概要性的介绍。\nCoze\n核心定位：由字节跳动推出的 Coze[1]，主打零代码/低代码的 Agent 的构建体验，让不具备编程背景的用户也能轻松创造。 特点分析：Coze 拥有极其友好的可视化界面，用户可以像搭建乐高积木一样，通过拖拽插件、配置知识库和设定工作流来创建智能体。其内置了极为丰富的插件库，并支持一键发布到抖音、飞书、微信公众号等多个主流平台，极大地简化了分发流程。 适用人群：AI 应用的入门用户、产品经理、运营人员，以及希望快速将创意变为可交互产品的个人创作者。 Dify\n核心定位：Dify 是一个开源的、功能全面的 LLM 应用开发与运营平台[2]，旨在为开发者提供从原型构建到生产部署的一站式解决方案。 特点分析：它融合了后端服务和模型运营的理念，支持 Agent 工作流、RAG Pipeline、数据标注与微调等多种能力。对于追求专业、稳定、可扩展的企业级应用而言，Dify 提供了坚实的基础。 适用人群：有一定技术背景的开发者、需要构建可扩展的企业级 AI 应用的团队。 n8n\n核心定位：n8n 本质上是一个开源工作流自动化工具[3]，而非纯粹的 LLM 平台。近年来，它积极集成了 AI 能力。\n特点分析：n8n 的强项在于“连接”。它拥有数百个预置的节点，可以轻松地将各类 SaaS 服务、数据库、API 连接成复杂的自动化业务流程。你可以在这个流程中嵌入 LLM 节点，使其成为整个自动化链路中的一环。虽然在 LLM 功能的专一度上不如前两者，但其通用自动化能力是独一无二的。不过，其学习曲线也相对陡峭。\n适用人群：需要将 AI 能力深度整合进现有业务流程、实现高度定制化自动化的开发者和企业 1：Coze 工作流： 关卡通关路线图 对话流：NPC 对话通关 插件：角色技能卡 知识库：游戏百科全书 卡片：快捷道具栏 提示词：角色的移动键 数据库：“云存档” 发布管理：关卡审核员 模型管理：游戏角色库或者叫捏脸系统 效果评测：闯关评分系统\nCoze 的优势与局限性分析 优势:\n强大的插件生态系统: Coze 平台的核心优势在于其丰富的插件库，这使得智能体能够轻松接入外部服务与数据源，从而实现功能的高度扩展性。 直观的可视化编排: 平台提供了一个低门槛的可视化工作流编排界面，用户无需深厚的编程知识，即可通过“拖拽”方式构建复杂的工作流，大大降低了开发难度。 灵活的提示词控制: 通过精确的角色设定与提示词编写，用户可以对智能体的行为和内容生成进行细粒度的控制，实现高度定制化的输出。而且还支持提示词管理和模板，极大的方便开发者进行智能体的开发。 便捷的多平台部署: 支持将同一智能体发布到不同的应用平台，实现了跨平台的无缝集成与应用。而且扣子还在不断的整合新平台加入他的生态圈，越来越多的手机厂商和硬件厂商都在陆续支持扣子智能体的发布。 局限性:\n不支持MCP: 我觉得这是最致命的，尽管扣子的插件市场极其丰富，也极其有吸引力。但是不支持mcp可能会成为限制其发展的枷锁，如果放开那将是又一杀手锏。 部分插件配置的复杂度高: 对于需要 API Key 或其他高级参数的插件，用户可能需要具备一定的技术背景才能完成正确的配置。复杂的工作流编排也不仅仅是零基础就可以掌握的，需要一定的js或者python的基础。 无法导入编排json文件: 之前扣子是没有导出导入功能的，但是现在付费版是可以导出导入的，但是导出导入的不是像dify,n8n一样的json文件，而是一个zip。也就是说你只能在扣子导出然后扣子导入这个zip。不过你取巧的话也可以选择复制编排，在编排界面ctrl+a选中全部ctrl+c复制编排，然后到另一个空白的工作流或者其他工作流粘贴编排。\n平台二：Dify 5.3.3 Dify 的优势与局限性分析 Dify 作为一款领先的 AI 应用开发平台，在多个方面展现出显著优势：​\n核心优势​ 全栈式开发体验：Dify 将 RAG 管道、AI 工作流、模型管理等功能整合到一个平台中，提供一站式的开发体验​ 低代码与高扩展性的平衡：Dify 在低代码开发的便利性和专业开发的灵活性之间取得了良好平衡​​ 企业级安全与合规：Dify 提供 AES-256 加密、RBAC 权限控制和审计日志等功能，满足严格的安全和合规要求​​ 丰富的工具集成能力：Dify 支持 9000 + 工具和 API 扩展，提供了广泛的功能扩展性​ 活跃的开源社区：Dify 拥有活跃的开源社区，提供了丰富的学习资源和支持​ 主要局限​ 学习曲线较陡：对于完全没有技术背景的用户，仍然存在一定的学习曲线​​ 性能瓶颈：在高并发场景下可能面临性能挑战，需要进行适当的优化​。Dify 系统的核心服务端组件由 Python 语言实现，与 C++、Golang、Rust 等语言相比，性能表现相对较差​ 多模态支持不足：当前主要以文本处理为主，对图像、视频、HTML等的支持有限​​ 企业版成本较高：Dify 的企业版定价相对较高，可能超出小型团队的预算​​ API 兼容性问题：Dify 的 API 格式不兼容 OpenAI，可能限制与某些第三方系统的集成​\n","date":"2026-02-18T16:05:09+08:00","permalink":"https://impself.github.io/p/agent-05-low-code-agent/","title":"Agent 05-基于低代码平台构建智能体"},{"content":"引言 在前三章的学习中，我们已经掌握了关于大语言模型的基础知识。本章将进入实践环节，通过\u0026quot;亲手造轮子\u0026quot;的方式，从零开始编码实现三种业界经典的智能体构建范式：\nReAct — 推理与行动结合 Plan-and-Solve — 规划与执行分离 Reflection — 自我反思与迭代优化 我们将深入探索它们的核心工作原理，并通过具体的实战案例，分析各自的优势、局限与适用场景。\n核心知识点概览 1. ReAct：思考-行动的动态循环 ReAct 范式构建了一个能与外部世界交互的智能体。其核心机制是 \u0026ldquo;思考(Thought) → 行动(Action) → 观察(Observation)\u0026rdquo; 的动态循环。\n核心优势：\n环境适应性强，能够利用外部工具 具备动态纠错能力 适合处理探索性、需要实时信息的任务 典型应用场景：\n需要搜索引擎辅助的问答 实时数据查询 API 调用与交互式任务 2. Plan-and-Solve：先规划，后执行 Plan-and-Solve 范式将复杂问题分解为清晰的步骤，然后逐一执行。\n执行流程：\n1 输入问题 → 规划阶段（生成步骤列表） → 执行阶段（逐步完成） 核心优势：\n结构性强，逻辑清晰 稳定性高，推理路径确定 适合逻辑路径固定、内部推理密集的任务 典型应用场景：\n多步数学推理 复杂问题分解 流程化任务处理 3. Reflection：执行-反思-优化 Reflection 范式引入了自我优化能力，通过 \u0026ldquo;执行 → 反思 → 优化\u0026rdquo; 的迭代循环提升解决方案质量。\n迭代流程：\n1 初始方案 → 执行与评估 → 反思反馈 → 优化改进 → (循环) → 最终方案 核心价值：\n显著提升输出质量 自动发现并修正错误 适用于对准确性和可靠性要求极高的场景 典型应用场景：\n代码生成与优化 学术写作辅助 决策支持系统 三种范式的对比分析 本章探讨的三种范式，代表了智能体解决问题的三种不同策略，如下表所示：\n范式 思考模式 核心优势 适用场景 ReAct 思考-行动一体，动态循环 环境适应、动态纠错 探索性任务、工具调用 Plan-and-Solve 先规划，后执行 结构稳定、逻辑清晰 多步推理、流程化任务 Reflection 执行-反思-优化迭代 质量提升、自我修正 高精度要求、复杂输出 💡 选择建议：在实际应用中，选择哪种范式取决于任务的核心需求。某些复杂场景下，也可以将多种范式组合使用。\n架构组合示例 当面临复杂任务时，可以考虑 混合范式架构：\nPlan-and-Solve 进行高层次任务规划 ReAct 在执行过程中处理具体的操作和环境交互 Reflection 对整个过程进行自我优化和改进 适用场景： 智能客服系统、自动驾驶、复杂工作流自动化等。\n小结与展望 至此，我们已经掌握了构建单个智能体的核心技术体系：\n✅ 理解了 ReAct 的动态交互机制 ✅ 掌握了 Plan-and-Solve 的结构化规划方法 ✅ 学会了 Reflection 的自我优化技巧 下一站预告： 为了更好地将理论付诸实践，下一章我们将探索不同低代码平台的使用方式，以及轻代码构建 Agent的实战方案。\n思考与练习 ⚠️ 提示：部分习题没有标准答案，重点在于培养对智能体范式设计的综合理解和实践能力。\n练习一：范式理解与选择 问题 1.1：ReAct、Plan-and-Solve 和 Reflection 这三种范式在\u0026quot;思考\u0026quot;与\u0026quot;行动\u0026quot;的组织方式上有什么本质区别？\n点击查看参考答案 范式 组织方式 ReAct 思考-行动一体，动态循环；思考指导行动，行动结果反哺思考 Plan-and-Solve 先规划，后执行；分阶段处理，一次规划多步执行 Reflection 执行-反思-优化；通过迭代循环持续提升质量 问题 1.2：如果要设计一个\u0026quot;智能家居控制助手\u0026quot;（需要控制灯光、空调、窗帘等多个设备，并根据用户习惯自动调节），你会选择哪种范式作为基础架构？为什么？\n点击查看参考答案 推荐选择：ReAct\n理由：\n智能家居控制需要实时响应用户指令和环境变化 ReAct 的动态循环能够更好地适应这种需求 其环境适应性和动态纠错能力非常适合处理各种突发情况 问题 1.3：是否可以将这三种范式进行组合使用？请设计一个混合范式的智能体架构。\n点击查看参考答案 混合架构设计：\n1 2 3 4 5 6 7 8 9 输入任务 ↓ [Plan-and-Solve] 高层次任务规划 ↓ [ReAct] 动态执行与环境交互（每一步骤） ↓ [Reflection] 全过程自我优化与改进 ↓ 输出结果 适用场景： 智能客服系统、自动驾驶、复杂项目管理等需要同时具备规划能力、实时交互和持续优化的场景。\n练习二：输出解析的鲁棒性 问题 2.1：在 ReAct 实现中，我们使用正则表达式解析大语言模型的输出。当前的解析方法存在哪些潜在脆弱性？\n点击查看参考答案 潜在问题：\n格式依赖性强：如果模型输出格式变化（如标签被替换或省略），解析会失败 额外文本干扰：输出中包含额外文本时可能导致匹配错误 边界情况处理：特殊字符、多行输出等边界情况容易引发问题 问题 2.2：除了正则表达式，还有哪些更鲁棒的输出解析方案？\n点击查看参考答案 改进方案：\n结构化输出：使用 JSON、XML 等格式规范模型输出 自然语言理解：使用 NLU 技术识别意图和实体 专用解析器：针对特定格式设计专用解析逻辑 模型约束：使用支持结构化输出的模型（如 Function Calling） 练习三：工具调用与扩展 问题 3.1：为 ReAct 智能体添加一个\u0026quot;计算器\u0026quot;工具，使其能够处理复杂的数学计算问题。\n点击查看代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 def calculator(expression: str) -\u0026gt; float: \u0026#34;\u0026#34;\u0026#34;安全地计算数学表达式\u0026#34;\u0026#34;\u0026#34; import ast import operator as op operators = { ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, } def eval_node(node): if isinstance(node, ast.Num): return node.n elif isinstance(node, ast.BinOp): left = eval_node(node.left) right = eval_node(node.right) return operators[type(node.op)](left, right) raise ValueError(f\u0026#34;不支持的节点类型: {type(node)}\u0026#34;) try: return eval_node(ast.parse(expression, mode=\u0026#39;eval\u0026#39;).body) except Exception as e: return f\u0026#34;计算错误: {e}\u0026#34; # 注册工具 tools.register(\u0026#34;calculator\u0026#34;, calculator, \u0026#34;计算数学表达式，如 \u0026#39;(123 + 456) * 789 / 12\u0026#39;\u0026#34;) 问题 3.2：当可调用工具数量增加到 50-100 个时，如何优化工具的组织和检索机制？\n点击查看参考答案 优化策略：\n问题 解决方案 上下文占用大 分层组织，按需加载工具描述 相似工具干扰 工具聚类 + 语义检索 选择错误率高 约束执行 + 置信度过滤 工程实现：\n分层索引：建立工具分类目录（类目 → 子类 → 工具） 语义检索：用向量相似度找到候选工具子集 工具路由：训练轻量级路由模型，直接映射到目标工具 缓存优化：缓存常用工具的调用结果 练习四：动态规划机制 问题 4.1：Plan-and-Solve 的计划是\u0026quot;静态\u0026quot;的，如何设计一个\u0026quot;动态重规划\u0026quot;机制？\n点击查看参考答案 动态重规划机制设计：\ngraph TD A[执行步骤] --\u0026gt; B{结果校验} B --\u0026gt;|成功| C[继续下一步] B --\u0026gt;|失败| D{重试次数 \u0026lt; 阈值?} D --\u0026gt;|是| E[局部重规划] D --\u0026gt;|否| F[全局重规划] E --\u0026gt; A F --\u0026gt; A 关键组件：\n结果校验模块：判断是否满足成功标准 局部重规划：更换工具/调整参数/插入补充步骤 全局重规划：重新生成后续步骤 安全退出：设置重试和重规划次数上限 问题 4.2：对比 Plan-and-Solve 与 ReAct，在处理\u0026quot;预订商务旅行\u0026quot;任务时哪种更合适？\n点击查看参考答案 场景分析：\n| 范式 | 优势 | 劣势 | |:\u0026mdash;|:\u0026mdash;| | Plan-and-Solve | 适合处理有关联的多步骤预订 | 难以应对实时变化（如售罄、价格跳变） | | ReAct | 能根据实时结果灵活调整 | 可能缺乏整体规划 |\n最佳方案： 组合使用\n用 Plan-and-Solve 做高层次规划（确定预订顺序和约束） 用 ReAct 处理每个子步骤的执行与纠错（如换日期、换机场） 问题 4.3：设计一个\u0026quot;分层规划\u0026quot;系统及其优势。\n点击查看参考答案 分层规划架构：\n1 2 3 4 5 6 高层抽象计划 (3-8 个里程碑) ├── 阶段 1 │ └── 细化子计划 (具体工具、参数、检查点) ├── 阶段 2 │ └── 细化子计划 └── ... 核心优势：\n优势 说明 可扩展 高层计划稳定，细节按需生成 更稳健 局部失败只需重规划子计划 工具选择 明确的意图收窄候选工具范围 可观测性 高层里程碑便于审计和监控 可复用 高层模板可跨任务复用 练习五：Reflection 机制深入 问题 5.1：使用两个不同的模型（一个更强大做反思，一个更快做执行）会带来什么影响？\n点击查看参考答案 双模型架构分析：\n收益 代价 用\u0026quot;高推理能力\u0026quot;提升正确性 增加系统复杂度 用\u0026quot;高吞吐低成本\u0026quot;承担重复劳动 需要模型对齐与调试 成本效益更优 增加调用链路延迟 结论： 对于工程化长期运行的系统（如自动代码代理），双模型架构通常比单模型更稳定、更经济。\n问题 5.2：如何设计更智能的 Reflection 终止条件？\n点击查看参考答案 改进的终止条件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def should_terminate(iteration, history): # 1. 质量评分阈值 if latest_score \u0026gt;= 0.95: return True, \u0026#34;质量达标\u0026#34; # 2. 改进幅度检测 if abs(latest_score - previous_score) \u0026lt; 0.01: return True, \u0026#34;改进饱和\u0026#34; # 3. 外部评估通过 if all_tests_passed(): return True, \u0026#34;测试通过\u0026#34; # 4. 迭代次数上限 if iteration \u0026gt;= max_iterations: return True, \u0026#34;达到最大迭代次数\u0026#34; return False, \u0026#34;继续优化\u0026#34; 多维度判断： 质量评分、改进幅度、外部测试、计算预算\n问题 5.3：设计一个\u0026quot;学术论文写作助手\u0026quot;的多维度 Reflection 机制。\n点击查看参考答案 多维度评估体系：\ngraph TD A[论文初稿] --\u0026gt; B[段落逻辑性评估] A --\u0026gt; C[方法创新性评估] A --\u0026gt; D[语言表达评估] A --\u0026gt; E[引用规范评估] B --\u0026gt; F[综合反馈报告] C --\u0026gt; F D --\u0026gt; F E --\u0026gt; F F --\u0026gt; G[针对性优化] G --\u0026gt; H{终止条件满足?} H --\u0026gt;|否| A H --\u0026gt;|是| I[最终论文] 评估维度：\n段落逻辑性：连贯性与结构合理性 方法创新性：科学贡献与技术亮点 语言表达：语法、用词、流畅度 引用规范：格式正确性与引用完整性 练习六：提示词工程 问题 6.1：ReAct 和 Plan-and-Solve 的提示词结构设计有何不同？\n点击查看参考答案 范式 提示词设计重点 ReAct 强调\u0026quot;Thought-Action-Observation\u0026quot;循环，明确三者角色定义 Plan-and-Solve 强调分阶段处理，明确规划阶段和执行阶段的边界 核心差异：提示词结构设计直接服务于各自范式的执行逻辑。\n问题 6.2：角色设定对智能体行为的影响。\n点击查看参考答案 角色设定对比：\n角色 输出倾向 \u0026ldquo;严格的代码评审专家\u0026rdquo; 关注正确性、性能、安全性 \u0026ldquo;注重可读性的开源维护者\u0026rdquo; 关注清晰度、注释、结构 \u0026ldquo;注重效率的工程经理\u0026rdquo; 关注开发速度、可维护性 结论： 角色设定是引导模型行为的重要手段，应根据目标场景精心设计。\n问题 6.3：Few-shot 示例的效果分析。\n点击查看参考答案 方案 优点 缺点 适用场景 无 Few-shot 灵活、节省 token 格式不稳定 创意性任务 有 Few-shot 稳定性高、格式遵循好 token 消耗大 结构化输出任务 建议： 对于需要结构化输出的智能体（如 Reflection、Planner），Few-shot 是必要而非可选的优化。\n练习七：综合实战 场景： 某电商公司希望搭建\u0026quot;客服智能体\u0026quot;，需要以下功能：\n理解用户的退款申请理由 查询用户的订单信息和物流状态 根据公司政策智能判断是否批准退款 生成得体的回复邮件并发送 争议情况下进行自我反思 问题 7.1：选择何种范式作为核心架构？\n点击查看参考答案 推荐架构：ReAct + Reflection\n组件 职责 ReAct 动态调用工具查询订单、物流，做出初步判断 Reflection 对争议决策进行反思，提供更审慎的建议 问题 7.2：系统需要哪些核心工具？\n点击查看参考答案 工具 功能描述 订单查询工具 根据用户信息查询订单详情和物流状态 政策判断工具 根据退款政策评估申请是否符合条件 邮件生成工具 生成得体回复并发送至用户邮箱 情感分析工具 分析用户情绪，调整回复语气 问题 7.3：如何设计提示词以平衡公司利益与用户体验？\n点击查看参考答案 提示词设计策略：\n1 2 3 4 5 6 7 8 9 10 11 12 # 角色设定 你是一位专业且友好的客服代表，代表公司处理退款申请。 # 决策原则 1. 严格遵守公司退款政策 2. 在保护公司利益的前提下，尽可能满足用户需求 3. 对用户表达理解和关心 # 回复要求 - 语气友好、专业 - 清晰说明决策理由 - 提供替代方案（如适用） 问题 7.4：产品上线可能面临哪些风险？如何降低？\n点击查看参考答案 风险类型 风险描述 缓解措施 错误决策 误批/误拒退款申请 多模型评估 + 人工审核兜底 隐私泄露 用户数据暴露 数据加密 + 访问控制 + 审计 体验下降 回复不当导致用户不满 持续优化提示词 + 反馈机制 系统稳定性 服务中断影响用户 冗余架构 + 监控告警 + 定期维护 结语 本章通过三种经典范式的实践，建立了构建智能体完整技术栈的认知。下一章，我们将走进低代码平台的世界，探索更高效的 Agent 构建方式。\n敬请期待！\n","date":"2026-02-18T13:24:09+08:00","permalink":"https://impself.github.io/p/agent-04-agent-classic-paradigm/","title":"Agent 04-智能体经典范式构建"},{"content":"1.自然语言处理中，语言模型经历了从统计到神经网络的模型演进。\n请使用本章提供的迷你语料库（datawhale agent learns, datawhale agent works），计算句子 agent works 在Bigram模型下的概率 N-gram模型的核心假设是马尔可夫假设。请解释这个假设的含义，以及N-gram模型存在哪些根本性局限？ 神经网络语言模型（RNN/LSTM）和Transformer分别是如何克服N-gram模型局限的？它们各自的优势是什么？ 2.Transformer架构[4]是现代大语言模型的基础。其中：\n提示：可以结合本章3.1.2节的代码实现来辅助理解\n自注意力机制（Self-Attention）的核心思想是什么？ 自注意力 (Self-Attention) 机制就是对这种现象的数学建模。它允许模型在处理序列中的每一个词时，都能兼顾句子中的所有其他词，并为这些词分配不同的“注意力权重”。权重越高的词，代表其与当前词的关联性越强，其信息也应该在当前词的表示中占据更大的比重。\n为什么Transformer能够并行处理序列，而RNN必须串行处理？位置编码（Positional Encoding）在其中起什么作用？ 它完全抛弃了循环结构，转而使用自注意力机制来捕捉序列中的依赖关系。这使得Transformer能够同时处理序列中的所有位置，从而实现并行计算。位置编码则为模型提供了序列中各个词的位置信息，使得模型能够理解词与词之间的顺序关系。 RNN这种循环的计算方式也带来了新的瓶颈：它必须按顺序处理数据。第 t 个时间步的计算，必须等待第 t−1 个时间步完成后才能开始。\nDecoder-Only架构与完整的Encoder-Decoder架构有什么区别？为什么现在主流的大语言模型都采用Decoder-Only架构？ 编码器负责深入理解输入的整个句子，形成一个包含全局信息的上下文记忆，然后解码器基于这份记忆来生成翻译 它完全抛弃了编码器，只保留了解码器部分 训练目标统一：模型的唯一任务就是“预测下一个词”，这个简单的目标非常适合在海量的无标注文本数据上进行预训练。 结构简单，易于扩展：更少的组件意味着更容易进行规模化扩展。今天的 GPT-4、Llama 等拥有数千亿甚至万亿参数的巨型模型，都是基于这种简洁的架构。 天然适合生成任务：其自回归的工作模式与所有生成式任务（对话、写作、代码生成等）完美契合，这也是它能成为构建通用智能体基础的核心原因。\n3.文本子词分词算法是大语言模型的一项关键技术，负责将文本转换为模型可处理的 token 序列。那为什么不能直接以\u0026quot;字符\u0026quot;或\u0026quot;单词\u0026quot;作为模型的输入单元？BPE（Byte Pair Encoding）算法解决了什么问题？ 统一输入单元：字符级模型过于细粒度，导致序列过长；单词级模型则过于粗粒度，无法处理未登录词（OOV）。 BPE通过子词分割，提供了一个平衡的输入单元。\n4.本章3.2.3节介绍了如何本地部署开源大语言模型。请完成以下实践和分析：\n提示：这是一道动手实践题，建议实际操作\n按照本章的指导，在本地部署一个轻量级的开源模型（推荐Qwen3-0.6B），并尝试调整采样参数并观察其对输出的影响 选择一个具体任务（如文本分类、信息抽取、代码生成等），设计并对比以下不同的提示策略（如Zero-shot、Few-shot、Chain-of-Thought）对输出结果的效果差异 从性能、成本、可控性、隐私等维度比较闭源模型和开源模型 如果你要构建一个企业级的客服智能体，你会选择哪种类型的模型？需要考虑哪些因素？ 5.模型幻觉（Hallucination）[11]是大语言模型当前存在的关键局限性之一。本章介绍了缓解幻觉的方法（如检索增强生成、多步推理、外部工具调用）\n请选择其中一种，说明其工作原理和适用场景 调研前沿的研究和论文，是否还有其他的缓解模型幻觉的方法，他们又有哪些改进和优势？ 6.假设你要设计一个论文辅助阅读智能体，它能够帮助研究人员快速阅读并理解学术论文，包括：总结论文研究的核心内容、回答关于论文的问题、提取关键信息、比较多篇不同论文的观点等。请回答：\n你会选择哪个模型作为智能体设计时的基座模型？选择时需要考虑哪些因素？ 如何设计提示词来引导模型更好地理解学术论文？学术论文通常很长，可能超过模型的上下文窗口限制，你会如何解决这个问题？ 学术研究是严谨的，这意味着我们需要确保智能体生成的信息是准确客观忠于原文的。你认为系统中加入哪些设计能够更好的实现这一需求？\n","date":"2026-02-15T18:37:25+08:00","permalink":"https://impself.github.io/p/agent-03-agent-language-basics/","title":"Agent 03-大模型语言基础"},{"content":"本文聚焦智能体从符号主义到大模型驱动范式的关键演进路径。\n2.1 基于符号与逻辑的早期智能体 物理符号系统假说 论断 含义 充分性 任何物理符号系统都具备产生通用智能行为的充分手段 必要性 任何展现通用智能行为的系统，本质必然是物理符号系统 核心思想：智能的本质是符号的计算与处理。\n专家系统 由 知识库（IF-THEN规则）和 推理机（正向链/反向链）组成。\nMYCIN：血液感染诊断专家系统，约600条规则。创新之处是引入 置信因子（CF） 处理不确定性。\n符号主义的局限：\n常识知识瓶颈 框架问题与系统脆弱性 SHRDLU SHRDLU是积木世界中的自然语言交互智能体，具备：\n自然语言理解（解析复杂指令） 指代消解（理解\u0026quot;你手中拿的\u0026quot;） 上下文记忆 规划与行动（规划动作序列） 记忆与问答 2.2 基于规则的聊天机器人 ELIZA 的设计 核心机制：模式匹配 + 文本替换\n1 2 3 4 5 6 def respond(user_input): # 1. 关键词识别 # 2. 分解（捕获通配符*匹配的内容） # 3. 代词转换（I ↔ you） # 4. 重组生成回应 pass 局限：\n无语义理解（不理解否定词） 无上下文记忆 规则扩展导致组合爆炸 2.3 马文·明斯基的\u0026quot;心智社会\u0026quot; 核心观点：智能源于大量简单智能体的协作，而非单一完美系统。\n概念 含义 智能体（Agent） 简单、专门、无心智 机构（Agency） 协作的智能体集合 对多智能体系统（MAS）的启发：\n去中心化控制 涌现式计算 智能体社会性 2.4 学习范式的演进 从符号到联结主义 范式 核心思想 符号主义 知识显式编码为规则 联结主义 知识以连接权重分布式存储 强化学习框架 五元组 (S, A, P, R, γ)\n组件 说明 Agent 学习者和决策者 Environment 智能体外部的一切 State (S) 环境在某一时刻的描述 Action (A) 智能体可采取的操作 Reward (R) 评价行动好坏的标量信号 学习循环：观察 → 选择行动 → 执行 → 获得奖励 → 调整策略\n预训练-微调范式 1 2 3 海量文本 → 自监督学习 → 基础模型 ↓ 特定任务数据微调 → 下游任务 大语言模型驱动的智能体 核心组件：\n1 2 3 感知模块 → 思考模块(Planning + LLM) → 执行模块 → 工具调用 ↑ ↓ └──────────── 观察与记忆更新 ←──────────────────┘ 阶段 功能 感知 接收用户指令、环境状态 思考 规划分解、推理决策、工具调用 行动 执行工具、与环境交互 观察 获取工具结果、更新记忆 发展时间线 时期 代表技术 1950s-1980s 符号主义：专家系统、SHRDLU 1980s-2010s 联结主义：神经网络复苏、深度学习 2010s-至今 强化学习 + 预训练-微调 + LLM Agent 2.5 习题解析 题 1：物理符号系统假说 Q1：充分性论断和必要性论断分别是什么？\n论断 含义 充分性 任何物理符号系统都具备产生通用智能行为的充分手段 必要性 任何展现通用智能行为的系统，本质必然是物理符号系统 Q2：符号主义实践中的哪些问题挑战了\u0026quot;充分性\u0026quot;？\n常识问题：依赖庞大的背景知识 框架问题：规则静态，无法适应变化 Q3：LLM驱动的智能体是否符合该假说？\n不完全符合。LLM通过分布式权重隐式编码知识，而非显式符号规则。\n题 2：专家系统MYCIN Q1：为何MYCIN未大规模应用于临床？\n维度 原因 技术 知识库难以覆盖全部医学知识 伦理 诊断错误责任归属不明 用户接受度 医患对非人类系统缺乏信任 Q2：现代医疗诊断智能体如何改进？\n动态知识更新（结合最新研究） 个体化诊断（考虑患者差异） 可解释性（清晰展示诊断依据） Q3：哪些垂直领域仍适合基于规则的系统？\n法律咨询（规则明确） 工业控制（稳定可预测） 药物配置（避免相互作用冲突） 题 3：ELIZA 扩展实践 核心扩展：添加 上下文记忆 功能，记住姓名、年龄、职业等信息。\n与ChatGPT的本质差异：\n维度 ELIZA ChatGPT 知识边界 固定规则集 预训练知识+推理 上下文 无状态 多轮对话连贯 开放能力 预设场景 开放域处理 组合爆炸问题：假设n个规则、每个规则匹配m种输入，组合数为m^n（指数级增长）。\n题 4：\u0026ldquo;心智社会\u0026quot;理论 Q1：GRASP智能体失效会怎样？\n系统失去抓取积木能力，但其他智能体可继续工作。优势是鲁棒性，劣势是整体性能下降。\nQ2：与现代多智能体系统的关联？\n关联：都体现协作思想、去中心化控制 不同：现代系统有更先进的通信协议和学习算法 Q3：LLM时代\u0026quot;心智社会\u0026quot;是否仍适用？\n适用。智能的涌现和协作思想仍是理解多智能体系统的重要框架。\n题 5：强化学习与监督学习 Q1：AlphaGo的\u0026quot;试错学习\u0026quot;机制？\n通过自我对弈不断尝试策略，根据胜负结果调整，优化长期收益。\nQ2：为何强化学习适合序贯决策？\n强化学习 监督学习 关注长期奖励 关注单次预测正确 与环境交互收集数据 需要预标注数据集 适合动态环境 适合静态模式识别 Q3：RLHF在LLM训练中的作用？\n通过人类反馈强化学习，使模型输出更符合人类期望和偏好。\n题 6：预训练-微调范式 Q1：预训练如何解决\u0026quot;知识获取瓶颈\u0026rdquo;？\n符号主义：专家手工编码规则 预训练：从数据中自动学习，分布式表示知识 Q2：互联网数据带来的问题及缓解？\n问题 缓解措施 错误信息 数据清洗 偏见 偏见检测与修正 知识过时 持续更新 题 7：智能代码审查助手 三个时代的设计对比：\n时代 方案 困难 符号主义 手工编码规则 知识获取瓶颈、脆弱性 深度学习(2015) 特征工程+分类器 特征设计难、泛化能力弱 LLM时代 感知+思考+执行模块 架构复杂度 演进意义：从\u0026quot;几乎不可能\u0026quot;到\u0026quot;可行\u0026quot;，核心是 理解和推理能力 的质变。\n小结 智能体发展史是一场关于\u0026quot;如何定义智能、获取知识、进行决策\u0026quot;的思想变革：\n符号主义：显式规则 → 知识获取瓶颈 联结主义：分布式表示 → 感知能力突破 强化学习：试错交互 → 决策能力突破 LLM Agent：感知-思考-行动闭环 → 通用智能雏形 ","date":"2026-02-15T12:40:47+08:00","permalink":"https://impself.github.io/p/agent-02-agent-develop/","title":"Agent 02-智能体发展史"},{"content":"智能体（Agent）不是\u0026quot;会算就行\u0026quot;的程序，而是一个能够在环境中感知、决策、行动并持续改进的主体。本文先梳理核心概念，再给出习题的标准化解析，确保内容可阅读、可复盘、可用于后续实战。\n1. 智能体基础概念 1.1 四个核心要素 一个完整智能体通常包含以下要素：\n传感器（Sensors）：获取环境信息。 环境（Environment）：智能体所处并交互的外部世界。 执行器（Actuators）：对环境施加影响的手段。 行动（Action）：基于决策实际执行的动作。 1.2 从简单到复杂的智能体类型 类型 特点 局限 简单反射智能体 只基于当前观测做规则匹配，不保留历史 无法应对需要理解上下文的复杂任务 基于模型的反射智能体 维护内部状态/世界模型，弥补\u0026quot;看不全\u0026quot;的问题 模型可能不准确，需要持续校准 基于目标的智能体 以\u0026quot;达到目标状态\u0026quot;为导向做规划 规划开销大，实时性较差 基于效用的智能体 比较不同结果\u0026quot;好到什么程度\u0026quot;，最大化期望效用 效用函数设计困难 学习型智能体 通过反馈不断更新策略 需要大量训练数据和反馈信号 强化学习（Reinforcement Learning, RL）是实现学习型智能体最具代表性的路径之一。一个学习型智能体包含一个性能元件（执行决策）和一个学习元件（通过观察结果不断修正策略）。\n1.3 常见分类维度 按反应方式分类 反应式智能体：接收到信息后立即行动，追求速度。 规划式智能体：经过深思熟虑的规划再行动，追求最优解。 混合式智能体：结合规划（Reasoning）与反应（Acting \u0026amp; Observing），在速度与质量之间取得平衡。 按知识表示分类 类型 特点 适用场景 符号主义 AI 基于符号和规则，强调逻辑推理和明确的知识表示 结构化问题 亚符号主义 AI 基于连接主义和分布式表示（如神经网络） 复杂、非结构化数据 神经符号主义 AI 结合符号推理与神经网络的学习能力 需要同时处理逻辑与模式识别 按学习能力分类 固定策略智能体：行为规则预先设定，不会随经验改变。 可学习策略智能体：能够从反馈中学习，持续优化决策。 1.4 系统 1 / 系统 2 与神经符号主义类比 卡尼曼的\u0026quot;双系统理论\u0026quot;为理解 AI 架构提供了很好的类比：\n系统 1：快速、直觉、并行、模式识别驱动（更接近亚符号主义方法）。 系统 2：缓慢、审慎、基于逻辑、可解释推理驱动（更接近符号主义方法）。 工程上常见的有效做法是\u0026quot;系统 1 快速筛查 + 系统 2 重点复核\u0026quot;，既保证效率，又不失准确性。\n2. 智能体任务环境建模：PEAS PEAS 用于精确定义任务环境，是设计智能体的第一步：\n维度 问题 示例（自动驾驶） P（Performance） 如何评价做得好不好 安全性、到达时间、乘车舒适度 E（Environment） 智能体面对什么世界 道路、其他车辆、行人、交通信号、天气 A（Actuators） 它能做什么动作 加速、刹车、转向、转向灯、鸣笛 S（Sensors） 它能看到什么信号 摄像头、雷达、激光雷达、速度传感器、GPS 如果 PEAS 不清晰，后续算法、工具链和评估标准都会失焦。\n3. 习题完整解析 题 1：判断主体是否为智能体及其类型 题目：请分析以下四个 case 中的主体是否属于智能体，如果是，属于哪种类型的智能体（可以从多个分类维度进行分析），并说明理由。\nCase A：超级计算机（2 EFlop） 结论：通常不直接算智能体本体。\n理由：一台符合冯·诺依曼结构的超级计算机，无论算力多强，本质上仍是高性能计算平台。它本身不具备自主目标、感知-决策-行动闭环；真正的智能体是其上运行的软件系统。智能体的关键在于\u0026quot;自主性\u0026quot;和\u0026quot;与环境的交互\u0026quot;，而非计算能力。\nCase B：高速场景下的自动驾驶紧急决策 特斯拉自动驾驶系统在高速公路上行驶时，突然检测到前方有障碍物，需要在毫秒级做出刹车或变道决策。\n结论：是智能体，核心是反应式，同时具备模型与目标约束。\n理由：\n毫秒级避障强调实时反应，符合反应式智能体特征。 但车道保持、速度控制、障碍物轨迹预测等又依赖内部状态估计（基于模型的反射智能体）。 同时还有导航目的地这一目标约束（基于目标的智能体）。 因此实际是混合式智能体，兼顾实时反应与规划能力。 Case C：AlphaGo 对弈 AlphaGo在与人类棋手对弈时，需要评估当前局面并规划未来数十步的最优策略。\n结论：是智能体，属于规划式 + 学习型智能体。\n理由：\n通过蒙特卡洛树搜索与价值评估规划后续多步（规划式）。 策略网络和价值网络由大量对弈数据训练而成（学习型）。 通过自我对弈持续改进策略（强化学习）。 Case D：ChatGPT 智能客服 ChatGPT 扮演的智能客服在处理用户投诉时，需要查询订单信息、分析问题原因、提供解决方案并安抚用户情绪。\n结论：是智能体系统（或可演化为智能体系统），属于混合式。\n理由：\n需要多步工具调用（查订单、检索政策、查询库存）。 需要对话推理能力理解用户意图。 需要根据用户反馈调整策略（如安抚情绪）。 单纯的语言模型不是完整智能体，但结合工具调用后可视为智能体系统。 题 2：智能健身教练的 PEAS 与环境特性 题目：假设你需要为一个\u0026quot;智能健身教练\u0026quot;设计任务环境。这个智能体能够：\n通过可穿戴设备监测用户的心率、运动强度等生理数据 根据用户的健身目标（减脂/增肌/提升耐力）动态调整训练计划 在用户运动过程中提供实时语音指导和动作纠正 评估训练效果并给出饮食建议 请使用 PEAS 模型完整描述这个智能体的任务环境，并分析该环境具有哪些特性（如部分可观察、随机性、动态性等）。\nPEAS 描述 维度 内容 Performance（性能度量） - 训练目标达成率（减脂/增肌/耐力提升）\n- 安全性（异常心率、动作风险触发率）\n- 用户依从性（计划完成率、持续打卡率）\n- 用户体验（满意度、建议采纳率） Environment（环境） - 用户身体状态（疲劳度、伤病情况）\n- 家庭/健身房场景、可用器械\n- 时间段、天气（影响户外运动）\n- 饮食与睡眠状况\n- 网络与设备状态 Actuators（执行器） - 语音指导输出\n- 动作纠正提示推送\n- 训练计划调整指令\n- 推送提醒\n- 饮食建议生成 Sensors（传感器） - 心率带/手表数据\n- 动作传感器（如加速度计、陀螺仪）\n- 摄像头姿态估计\n- 历史训练日志\n- 用户主观反馈（如问卷、语音） 环境特性分析 特性 说明 示例 部分可观察 无法直接观测某些关键状态 疲劳、疼痛、真实饮食执行情况难以直接测量 随机性 存在不可预测的扰动 生理状态波动、外部干扰、设备噪声 动态性 环境状态随时间快速变化 运动中心率、速度、体能状态实时变化 连续性 状态变量连续变化 心率、速度、负重等是连续量 多目标约束 需要平衡多个目标 效果、安全、体验之间存在权衡 题 3：Workflow vs Agent 在退款审核中的选择 题目：某电商公司正在考虑两种方案来处理售后退款申请：\n方案 A（Workflow）：设计一套固定流程，例如：\nA.1 对于一般商品且在 7 天之内，金额 \u0026lt; 100RMB 自动通过；100-500RMB 由客服审核；\u0026gt;500RMB 需主管审批；而特殊商品（如定制品）一律拒绝退款 A.2 对于超过 7 天的商品，无论金额，只能由客服审核或主管审批 方案 B（Agent）：搭建一个智能体系统，让它理解退款政策、分析用户历史行为、评估商品状况，并自主决策是否批准退款\n请分析：\n这两种方案各自的优缺点是什么？ 在什么情况下 Workflow 更合适？什么情况下 Agent 更有优势？ 如果你是该电商公司的负责人，你更倾向于采用哪种方案？ 是否存在一个方案 C，能够结合两种方案，达到扬长避短的效果？ 方案 A（Workflow） 优点 缺点 - 可控、可审计、合规实现简单\n- 决策一致性高，成本和响应时间可预测\n- 规则透明，易于向用户解释 - 对边界案例不灵活\n- 规则维护成本随业务复杂度上升\n- 无法处理规则之外的情况 方案 B（Agent） 优点 缺点 - 能处理复杂情境与个体差异\n- 可融合更多信号（历史行为、商品状态、风险分）\n- 适应性强，新场景下可快速调整 - 决策稳定性与可解释性挑战更大\n- 需要监控、回放、评估与人工兜底体系\n- 错误决策可能造成批量损失 什么时候选谁 Workflow 更合适：规则清晰、合规强约束、低风险高并发场景（如标准化商品的小额退款） Agent 更有优势：案例复杂、需个性化、规则难以穷举场景（如大额、定制商品、VIP 用户） 方案 C：分层混合架构 如果是负责人，推荐采用方案 C（分层混合）：\n1 2 3 4 5 第 1 层：Workflow 处理高频标准单 ↓ (路由规则) 第 2 层：高风险或疑难单路由给 Agent 评估 ↓ (不确定或人工复核阈值) 第 3 层：人工复核最终兜底 这个组合通常比\u0026quot;纯 A\u0026quot;或\u0026quot;纯 B\u0026quot;更稳健：\n标准case快速处理，降低成本 复杂case由Agent个性化处理 关键决策有人工兜底，控制风险 题 4：在旅行助手中扩展记忆、备选、反思、知识库 题目：在智能旅行助手的基础上，请思考如何添加以下功能（可以只描述设计思路，也可以进一步尝试代码实现）：\n提示：思考如何修改 Thought-Action-Observation 循环来实现这些功能。\n添加一个\u0026quot;记忆\u0026quot;功能，让智能体记住用户的偏好（如喜欢历史文化景点、预算范围等） 当推荐的景点门票已售罄时，智能体能够自动推荐备选方案 如果用户连续拒绝了 3 个推荐，智能体能够反思并调整推荐策略 当用户接受推荐的景点时，把景点和相关信息加入智能体的\u0026quot;知识库\u0026quot;，以便未来推荐时参考 核心思路 把 Thought-Action-Observation 循环升级为\u0026quot;带状态的闭环\u0026quot;：\n组件 功能 State（状态） user_preferences：用户偏好\nrejection_count：连续拒绝计数\nknowledge_base：已接受景点的历史记录 Thought（思考） 融合用户偏好、历史接受记录与当前约束生成候选 Action（行动） 查询库存/票务、输出推荐、必要时切换备选 Observation（观察） 记录接受/拒绝反馈；若连续拒绝达到阈值触发策略反思 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class TravelAgent: def __init__(self): self.user_preferences = {} # 用户偏好 self.rejection_count = 0 # 连续拒绝计数 self.knowledge_base = [] # 已接受景点的知识库 def recommend(self, user_input): \u0026#34;\u0026#34;\u0026#34;生成推荐，若售罄则生成备选\u0026#34;\u0026#34;\u0026#34; rec = self.generate_recommendation(user_input) if self.is_sold_out(rec): rec = self.generate_alternative(user_input) return rec def receive_feedback(self, feedback, accepted_item=None): \u0026#34;\u0026#34;\u0026#34;接收用户反馈，更新状态\u0026#34;\u0026#34;\u0026#34; if feedback == \u0026#34;reject\u0026#34;: self.rejection_count += 1 if self.rejection_count \u0026gt;= 3: self.reflect_and_adjust() self.rejection_count = 0 # 重置计数 elif feedback == \u0026#34;accept\u0026#34; and accepted_item is not None: self.rejection_count = 0 self.update_knowledge_base(accepted_item) def generate_recommendation(self, user_input): \u0026#34;\u0026#34;\u0026#34;结合偏好 + 历史行为生成推荐\u0026#34;\u0026#34;\u0026#34; # 实现逻辑：根据 user_preferences 和 knowledge_base 生成候选 raise NotImplementedError def is_sold_out(self, recommendation): \u0026#34;\u0026#34;\u0026#34;调用票务接口检查库存\u0026#34;\u0026#34;\u0026#34; raise NotImplementedError def generate_alternative(self, user_input): \u0026#34;\u0026#34;\u0026#34;在约束不变时返回备选方案\u0026#34;\u0026#34;\u0026#34; # 实现逻辑：寻找与原推荐相似但仍有库存的景点 raise NotImplementedError def reflect_and_adjust(self): \u0026#34;\u0026#34;\u0026#34;分析连续拒绝原因，动态调整推荐权重\u0026#34;\u0026#34;\u0026#34; # 实现逻辑：分析历史拒绝记录，调整推荐策略 raise NotImplementedError def update_knowledge_base(self, accepted_item): \u0026#34;\u0026#34;\u0026#34;记录\u0026#34;被接受方案\u0026#34;的上下文信息\u0026#34;\u0026#34;\u0026#34; self.knowledge_base.append({ \u0026#34;item\u0026#34;: accepted_item, \u0026#34;timestamp\u0026#34;: time.now(), # 可以附加更多信息，如用户反馈等 }) 题 5：系统 1 / 系统 2 在医疗诊断助手中的分工 题目：卡尼曼的\u0026quot;系统 1\u0026quot;（快速直觉）和\u0026quot;系统 2\u0026quot;（慢速推理）理论为神经符号主义 AI 提供了很好的类比。请首先构思一个具体的智能体的落地应用场景，然后说明场景中的：\n哪些任务应该由\u0026quot;系统 1\u0026quot;处理？ 哪些任务应该由\u0026quot;系统 2\u0026quot;处理？ 这两个系统如何协同工作以达成最终目标？ 提示：医疗诊断助手、法律咨询机器人、金融风控系统等都是常见的应用场景。\n场景：门诊分诊与辅助诊断助手 系统 1 负责（快速直觉处理） 任务 说明 症状快速归类 根据主诉快速判断科室分类 高危信号初筛 识别胸痛、呼吸困难等紧急症状 候选疾病粗排 基于症状模式匹配给出候选疾病列表 系统 2 负责（慢速推理处理） 任务 说明 结合病史、检验、指南做因果推理 综合多源信息进行诊断 处理冲突证据 当不同检验结果矛盾时做出判断 输出可解释诊断建议与下一步检查计划 给出明确的诊断依据和建议 协同方式 1 2 3 4 5 6 7 患者输入 → 系统1快速筛查 → 低风险/常见病 → 直接输出建议 ↓ 高风险/不确定 ↓ 系统2深度分析 → 诊断建议 + 检查计划 ↓ 反哺系统1（持续校准筛查规则） 目标是在效率、准确性与可解释性之间取得平衡：\n系统 1 保证高召回率（不漏掉高危患者） 系统 2 对关键样本投入更多算力 系统 2 的结论持续反哺系统 1，形成闭环学习 题 6：智能体局限与评估 题目：尽管大语言模型驱动的智能体系统展现出了强大的能力，但它们仍然存在诸多局限。请分析以下问题：\n为什么智能体或智能体系统有时会产生\u0026quot;幻觉\u0026quot;（生成看似合理但实际错误的信息）？ 在智能体案例中，我们设置了最大循环次数为 5 次。如果没有这个限制，智能体可能会陷入什么问题？ 如何评估一个智能体的\u0026quot;智能\u0026quot;程度？仅使用准确率指标是否足够？ 为什么会出现\u0026quot;幻觉\u0026quot; 原因 说明 训练数据噪声与知识过时 模型学习到的是训练时的知识，可能已经过时或包含错误 检索增强阶段召回错误证据 RAG 召回了\u0026quot;相似但不正确\u0026quot;的证据（向量相似度 ≠ 语义等价） 工具调用失败或返回不完整信息 外部工具异常时，模型可能\u0026quot;编造\u0026quot;答案来补全 长链路推理中的误差累积 多步推理中，每一步的小误差会被放大 奖励目标与真实业务目标不一致 RLHF 的奖励模型可能鼓励某些表面行为而非真实正确性 为什么要限制最大循环次数 若不限制最大循环次数，可能出现：\n风险 说明 死循环或无效反复调用工具 智能体可能在无效路径上反复尝试 Token 与时延成本失控 无限增长导致成本和响应时间不可控 上下文被噪声挤占 过长历史导致关键信息被稀释，决策质量下降 陷入局部最优 缺乏\u0026quot;及时止损\u0026quot;机制，无法跳出无效策略 如何评估智能体\u0026quot;智能程度\u0026quot; 仅看准确率不够，应至少加入以下维度：\n维度 指标示例 任务成功率 端到端目标是否完成（如客户问题是否解决） 成本与时延 Token 消耗、调用次数、响应时间 稳定性 多次运行结果一致性 鲁棒性 异常输入与边界条件下的表现 安全与合规 是否输出有害内容、是否越权访问 可解释性与可审计性 能否追溯关键决策依据 4. 小结 智能体工程的关键不在\u0026quot;模型参数更大\u0026quot;，而在于：\n任务定义是否清晰（PEAS 是第一步） 决策结构是否匹配场景（反应式/规划式/混合式各有所长） 评估与兜底是否完善（准确率之外的多维指标 + 人工兜底） 从工程落地看，混合架构通常是最实用路线：规则保底、智能增益、人工兜底。这既保证了系统的可控性，又能在复杂场景下发挥智能体的优势。\n","date":"2026-02-13T13:12:43+08:00","permalink":"https://impself.github.io/p/agent-01-initial-agent/","title":"Agent 01-初识智能体"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\n组件的生命周期 在组件创建、到加载到页面上运行、以及组件被销毁的过程中，总是伴随着各种各样的事件，这些在组件特定时期，触发的事件统称为组件的生命周期。\n生命周期的阶段 组件生命周期分为三个阶段，下面分别来讲解。\n1、组件创建阶段 组件创建阶段的生命周期函数，有一个显著的特点：创建阶段的生命周期函数，在组件的一辈子中，只执行一次。\ngetDefaultProps 初始化 props 属性默认值。\ngetInitialState 初始化组件的私有数据。因为 state 是定义在组件的 constructor 构造器当中的，只要new 了 class类，必然会调用 constructor构造器。\ncomponentWillMount() 组件将要被挂载。此时还没有开始渲染虚拟DOM。\n在这个阶段，不能去操作DOM元素，但可以操作属性、状态、function。相当于 Vue 中的Create()函数。\nrender() 第一次开始渲染真正的虚拟DOM。当render执行完，内存中就有了完整的虚拟DOM了。\n意思是，此时，虚拟DOM在内存中创建好了，但是还没有挂在到页面上。\n在这个函数内部，不能去操作DOM元素，因为还没return之前，虚拟DOM还没有创建；当return执行完毕后，虚拟DOM就创建好了，但是还没有挂在到页面上。\ncomponentDidMount() 当组件（虚拟DOM）挂载到页面之后，会进入这个生命周期函数。\n只要进入到这个生命周期函数，则必然说明，页面上已经有可见的DOM元素了。此时，组件已经显示到了页面上，state上的数据、内存中的虚拟DOM、以及浏览器中的页面，已经完全保持一致了。\n当这个方法执行完，组件就进入都了 运行中 的状态。所以说，componentDidMount 是创建阶段的最后一个函数。\n在这个函数中，我们可以放心的去 操作 页面上你需要使用的 DOM 元素了。如果我们想操作DOM元素，最早只能在 componentDidMount 中进行。相当于 Vue 中的 mounted() 函数\n2、组件运行阶段 有一个显著的特点，根据组件的state和props的改变，有选择性的触发0次或多次。\ncomponentWillReceiveProps() 组件将要接收新属性。只有当父组件中，通过某些事件，重新修改了 传递给 子组件的 props 数据之后，才会触发这个钩子函数。\nshouldComponentUpdate() 判断组件是否需要被更新。此时，组件尚未被更新，但是，state 和 props 肯定是最新的。\ncomponentWillUpdate() 组件将要被更新。此时，组件还没有被更新，在进入到这个生命周期函数的时候，内存中的虚拟DOM还是旧的，页面上的 DOM 元素也是旧的。（也就是说，此时操作的是旧的 DOM元素）\nrender 此时，又要根据最新的 state 和 props，重新渲染一棵内存中的 虚拟DOM树。当 render 调用完毕，内存中的旧DOM树，已经被新DOM树替换了！此时，虚拟DOM树已经和组件的 state 保持一致了，都是最新的；但是页面还是旧的。\ncomponentDidUpdate 此时，组件完成更新，页面被重新渲染。此时，state、虚拟DOM 和 页面已经完全保持同步。\n3、组件销毁阶段 一辈子只执行一次。\ncomponentWillUnmount: 组件将要被卸载。此时组件还可以正常使用。 React 生命周期的截图如下：\n生命周期对比：\nvue中的生命周期图\nReact Native 中组件的生命周期\n组件生命周期的执行顺序 1、Mounting：\nconstructor()\ncomponentWillMount()\nrender()\ncomponentDidMount()\n2、Updating：\ncomponentWillReceiveProps(nextProps)：接收父组件传递过来的属性\nshouldComponentUpdate(nextProps, nextState)：一旦调用 setState，就会触发这个方法。方法默认 return true；如果 return false，后续的方法就不会走了。\ncomponentWillUpdate(nextProps, nextState)\nrender()\ncomponentDidUpdate(prevProps, prevState)\n3、Unmounting：\ncomponentWillUnmount() ","date":"2026-02-11T17:27:27+08:00","permalink":"https://impself.github.io/p/react-component-lifecycle/","title":"React 03-组件（一）：生命周期"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\n前言 我们先来看下面这段代码：\ncomponents/MyComponent.jsx\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React from \u0026#34;react\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是 MyComponent 组件 默认的msg\u0026#34; }; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;绑定This并传参\u0026lt;/h1\u0026gt; \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;绑定this并传参\u0026#34; onClick={this.changeMsg} /\u0026gt; \u0026lt;h3\u0026gt;{this.state.msg}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } changeMsg() { // 注意：这里的changeMsg()只是一个普通方法。因此，在触发的时候，这里的 this 是 undefined console.log(this); // 打印结果：undefined this.setState({ msg: \u0026#34;设置 msg 为新的值\u0026#34; }); } } 上面的代码中，点击按钮，执行 changeMsg() 方法，尝试修改 this.state.msg 的值。但是，这个方法执行的时候，是会报错的：\n1 Uncaught TypeError: Cannot read property \u0026#39;setState\u0026#39; of null 而且，打印this的结果也是 undefined。这是为啥呢？因为这里的 this 并不是指向 MyComponent 组件本身。\n那如何让 changeMsg() 方法里面的 this，指向MyComponent 组件呢？办法总是有的，比如说，将changeMsg() 修改为箭头函数：\n1 2 3 4 5 6 changeMsg = () =\u0026gt; { console.log(this); // 打印结果：MyComponent 组件 this.setState({ msg: \u0026#34;设置 msg 为新的值\u0026#34; }); }; 那么，除了箭头函数可以 绑定 this，还有没有其他的方式呢？我们接下来讲一讲。\n绑定 this 的方式一：bind() 代码举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React from \u0026#34;react\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是 MyComponent 组件 默认的msg\u0026#34; }; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;绑定This并传参\u0026lt;/h1\u0026gt; {/* bind 的作用：为前面的函数，修改函数内部的 this 指向。让 函数内部的this，指向 bind 参数列表中的 第一个参数 */} \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;绑定this并传参\u0026#34; onClick={this.changeMsg1.bind(this)} /\u0026gt; \u0026lt;h3\u0026gt;{this.state.msg}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } changeMsg1() { this.setState({ msg: \u0026#34;设置 msg 为新的值\u0026#34; }); } } 上方代码中，我们为什么用 bind()，而不是用 call/apply 呢？因为 bind() 并不会立即调用，正是我们需要的。\n注意：bind 中的第一个参数，是用来修改 this 指向的。第一个参数后面的所有参数，都将作为函数的参数传递进去。\n我们来看看通过 bind() 是怎么传参的。\n通过 bind() 绑定this，并给函数传参：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React from \u0026#34;react\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是 MyComponent 组件 默认的msg\u0026#34; }; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;绑定This并传参\u0026lt;/h1\u0026gt; {/* bind 的作用：为前面的函数，修改函数内部的 this 指向。让 函数内部的this，指向 bind 参数列表中的 第一个参数 */} \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;绑定this并传参\u0026#34; onClick={this.changeMsg1.bind(this, \u0026#34;千古啊\u0026#34;, \u0026#34;壹号啊\u0026#34;)} /\u0026gt; \u0026lt;h3\u0026gt;{this.state.msg}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } changeMsg1(arg1, arg2) { this.setState({ msg: \u0026#34;设置 msg 为新的值\u0026#34; + arg1 + arg2 }); } } 绑定 this 并给函数传参 的方式二：构造函数里设置 bind() 我们知道，构造函数中的 this 本身就是指向组件的实例的，所以，我们可以在这里做一些事情。\n代码举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React from \u0026#34;react\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是 MyComponent 组件 默认的msg\u0026#34; }; // 绑定 this 并给函数传参的方式2: 在构造函数中绑定并传参 // 注意：当一个函数调用 bind 改变了this指向后，bind 函数调用的结果，有一个【返回值】，这个值，就是被改变this指向后的函数的引用。 // 也就是说： bind 不会修改 原函数的 this 指向，而是改变了 “函数拷贝”的this指向。 this.changeMsg2 = this.changeMsg2.bind(this, \u0026#34;千古恩\u0026#34;, \u0026#34;壹号恩\u0026#34;); } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;绑定This并传参\u0026lt;/h1\u0026gt; \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;绑定this并传参\u0026#34; onClick={this.changeMsg2} /\u0026gt; \u0026lt;h3\u0026gt;{this.state.msg}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } changeMsg2(arg1, arg2) { this.setState({ msg: \u0026#34;设置 msg 为新的值\u0026#34; + arg1 + arg2 }); } } 上方代码中，需要注意的是：当一个函数调用 bind 改变了this指向后，bind 函数调用的结果，有一个【返回值】，这个值，就是被改变this指向后的函数的引用。也就是说： bind 不会修改 原函数的 this 指向，而是改变了 “函数拷贝”的this指向。\n绑定 this 并给函数传参 的方式三：箭头函数【荐】 第三种方式用得最多。\n代码举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import React from \u0026#34;react\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是 MyComponent 组件 默认的msg\u0026#34; }; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;绑定This并传参\u0026lt;/h1\u0026gt; \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;绑定this并传参\u0026#34; onClick={() =\u0026gt; { this.changeMsg3(\u0026#34;千古3\u0026#34;, \u0026#34;壹号3\u0026#34;); }} /\u0026gt; \u0026lt;h3\u0026gt;{this.state.msg}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } changeMsg3 = (arg1, arg2) =\u0026gt; { // console.log(this); // 注意：这里的方式，是一个普通方法，因此，在触发的时候，这里的 this 是 undefined this.setState({ msg: \u0026#34;绑定this并传参的方式3：\u0026#34; + arg1 + arg2 }); }; } ","date":"2026-02-11T17:00:00+08:00","permalink":"https://impself.github.io/p/react-bind-this-and-pass-params/","title":"React 05-绑定 this 与函数传参"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\ndefaultProps 和 prop-types 使用 defaultProps 设置组件的默认值 React 中，使用静态的 defaultProps 属性，来设置组件的默认属性值。\n格式举例：\n1 2 3 4 // 在 React 中，使用静态的 defaultProps 属性，来设置组件的默认属性值 static defaultProps = { initcount: 0 // 如果外界没有传递 initcount，那么，自己初始化一个数值（比如0） }; 使用prop-types进行props数据类型的校验 在组件中，可以通过 prop-types 把外界传递过来的属性，做类型校验。如果类型不匹配，控制台会弹出告警。\n注意：如果要为 传递过来的属性做类型校验，必须安装 React 提供的 第三方包，叫做 prop-types。\n格式举例：\n1 2 3 static propTypes = { initcount: ReactTypes.number // 使用 prop-types 包，来定义 initcount 为 number 类型 }; 下方代码中，在引用组件的时候，如果类型不匹配：\n1 2 3 4 5 6 7 8 // 使用 render 函数渲染 虚拟DOM ReactDOM.render( \u0026lt;div\u0026gt; {/* 规定，每个用户在使用 组件的时候，必须传递一个 默认的 数值，作为 组件初始化的 数据 */} \u0026lt;Counter initcount=\u0026#34;我是string类型\u0026#34;\u0026gt;\u0026lt;/Counter\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); 控制台告警如下：\n代码举例 我们把 defaultProps 和 prop-types 来举个例子。\n（1）index.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;ie=edge\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 容器，通过 React 渲染得到的 虚拟DOM，会呈现到这个位置 --\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; （2）main.js:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // JS打包入口文件 // 1. 导入包 import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom\u0026#34;; // 导入计数器组件 import Counter from \u0026#34;./components/Counter.jsx\u0026#34;; // 使用 render 函数渲染 虚拟DOM ReactDOM.render( \u0026lt;div\u0026gt; {/* 规定，每个用户在使用 组件的时候，必须传递一个 默认的 数值，作为 组件初始化的 数据 */} \u0026lt;Counter initcount={0}\u0026gt;\u0026lt;/Counter\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); （3）/components/Counter.jsx：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React from \u0026#34;react\u0026#34;; // 注意： prop-types 包中职能跟单一，只提供了 一些常见的 数据类型，用于做类型校验 import ReactTypes from \u0026#34;prop-types\u0026#34;; export default class Counter extends React.Component { constructor(props) { super(props); // 初始化组件，保存的是组件的私有数据 this.state = { msg: \u0026#34;ok\u0026#34;, count: props.initcount // 把 父组件传递过来的 initcount 赋值给子组件 state 中的 count值。这样的话，就把 count 值改成了可读可写的 state 属性。因此，以后就能实现“点击 按钮 ，count 值 + 1”的需求了 }; } // 在 React 中，使用静态的 defaultProps 属性，来设置组件的默认属性值 static defaultProps = { initcount: 0 // 如果外界没有传递 initcount，那么，自己初始化一个 数值，为0 }; render() { return ( \u0026lt;div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是 Counter 计数器组件 \u0026lt;/h3\u0026gt; \u0026lt;p\u0026gt;当前的计数是：{this.state.count}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); // 当 return 执行完毕后， 虚拟DOM创建好了，但是，还没有挂载到真正的页面中 } } 运行效果：\n事件绑定 案例：点击按钮后，计数器 +1。\n原生js做事件绑定 代码举例：\n（1）index.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;ie=edge\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 容器，通过 React 渲染得到的 虚拟DOM，会呈现到这个位置 --\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; （2）main.js:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // JS打包入口文件 // 1. 导入包 import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom\u0026#34;; // 导入计数器组件 import Counter from \u0026#34;./components/Counter.jsx\u0026#34;; // 使用 render 函数渲染 虚拟DOM ReactDOM.render( \u0026lt;div\u0026gt; {/* 规定，每个用户在使用 组件的时候，必须传递一个 默认的 数值，作为 组件初始化的 数据 */} \u0026lt;Counter initcount={0}\u0026gt;\u0026lt;/Counter\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); （3）/components/Counter.jsx：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import React from \u0026#34;react\u0026#34;; // 注意： prop-types 包的职能跟单一，只提供了 一些常见的 数据类型，用于做类型校验 import ReactTypes from \u0026#34;prop-types\u0026#34;; export default class Counter extends React.Component { constructor(props) { super(props); // 初始化组件，保存的是组件的私有数据 this.state = { msg: \u0026#34;ok\u0026#34;, count: props.initcount // 把 父组件传递过来的 initcount 赋值给子组件 state 中的 count值。这样的话，就把 count 值改成了可读可写的 state 属性。因此，以后就能实现“点击 按钮 ，count 值 + 1”的需求了 }; } // 在 React 中，使用静态的 defaultProps 属性，来设置组件的默认属性值 static defaultProps = { initcount: 0 // 如果外界没有传递 initcount，那么，自己初始化一个数值（比如0） }; // 这是创建一个 静态的 propTypes 对象，在这个对象中，可以把 外界传递过来的属性，做类型校验 static propTypes = { initcount: ReactTypes.number // 使用 prop-types 包，来定义 initcount 为 number 类型 }; render() { return ( \u0026lt;div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是 Counter 计数器组件 \u0026lt;/h3\u0026gt; \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;+1\u0026#34; id=\u0026#34;btn\u0026#34; /\u0026gt; \u0026lt;p\u0026gt;当前的计数是：{this.state.count}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); // 当 return 执行完毕后， 虚拟DOM创建好了，但是，还没有挂载到真正的页面中 } // 当组件挂载到页面上之后，会进入这个生命周期函数，只要进入这个生命周期函数了，必然说明，页面上，已经有可见的DOM元素了 componentDidMount() { // 在这个函数中，我们可以放心的去 操作 页面上你需要使用的 DOM 元素了。 // 也就是说，如果我们想操作DOM元素，最早，只能在 componentDidMount 中进行。 document.getElementById(\u0026#34;btn\u0026#34;).onclick = () =\u0026gt; { this.setState({ count: this.state.count + 1 }); }; } } 使用 React 提供的方法，做事件绑定 代码举例：\n（1）index.html和 （2）main.js 的代码不变，和上一小段中的代码一致。\n（3）/components/Counter.jsx：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import React from \u0026#34;react\u0026#34;; // 注意： prop-types 包的职能跟单一，只提供了 一些常见的 数据类型，用于做类型校验 import ReactTypes from \u0026#34;prop-types\u0026#34;; export default class Counter extends React.Component { constructor(props) { super(props); // 初始化组件，保存的是组件的私有数据 this.state = { msg: \u0026#34;ok\u0026#34;, count: props.initcount // 把 父组件传递过来的 initcount 赋值给子组件 state 中的 count值。这样的话，就把 count 值改成了可读可写的 state 属性。因此，以后就能实现“点击 按钮 ，count 值 + 1”的需求了 }; } // 在 React 中，使用静态的 defaultProps 属性，来设置组件的默认属性值 static defaultProps = { initcount: 0 // 如果外界没有传递 initcount，那么，自己初始化一个数值（比如0） }; // 这是创建一个 静态的 propTypes 对象，在这个对象中，可以把 外界传递过来的属性，做类型校验 static propTypes = { initcount: ReactTypes.number // 使用 prop-types 包，来定义 initcount 为 number 类型 }; render() { return ( \u0026lt;div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是 Counter 计数器组件 \u0026lt;/h3\u0026gt; {/* 这里的 this 指向的是 Counter 组件的实例 */} \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;+1\u0026#34; id=\u0026#34;btn\u0026#34; onClick={this.myMethod} /\u0026gt; \u0026lt;p\u0026gt;当前的计数是：{this.state.count}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); // 当 return 执行完毕后， 虚拟DOM创建好了，但是，还没有挂载到真正的页面中 } // 点击事件的方法定义 myMethod = () =\u0026gt; { // 修改组件的state里面的值 this.setState({ count: this.state.count + 1 }); }; } 生命周期函数：shouldComponentUpdate() 在 shouldComponentUpdate() 函数中，必须要求返回一个布尔值。\n需要注意的是：如果返回的值是 false，则不会继续执行后续的生命周期函数，而是直接退回到了 运行中 的状态。因为此时，后续的 render 函数并没有被调用，因此页面不会被更新，但是组件的 state 状态，却被修改了。这种情况，我们也可以这样理解：如果返回值为 false，此时只是更新了 state 里面的数值，但是并没有渲染到 DOM节点上。\n利用上面这个特性，我们可以来举个例子。\n举例：实现 Counter 计数器只在偶数情况下更新。\n实现思路：在 shouldComponentUpdate() 函数中，如果 state 中 的count 的值为奇数，就 return false；否则就 return true。\n代码实现：（我们在上面的Counter.jsx代码基础之上，做添加）\n1 2 3 4 5 6 7 8 9 10 11 12 // 判断组件是否需要更新 shouldComponentUpdate(nextProps, nextState) { // 经过打印测试发现：在 shouldComponentUpdate 中，通过 this.state.count 拿到的值，是上一次的旧数据，并不是当前最新的； // 解决办法：通过 shouldComponentUpdate 函数的第二个参数 nextState，可以拿到 最新的 state 数据。 console.log(this.state.count + \u0026#34; ---- \u0026#34; + nextState.count); // 需求： 如果 state 中的 count 值是偶数，则 更新页面；如果 count 值 是奇数，则不更新页面。最终实现的的页面效果：2，4，6，8，10，12.... // return this.state.count % 2 === 0 ? true : false return nextState.count % 2 === 0 ? true : false; } 上面这部分的代码，和 render() 方法是并列的。我们需要注意里面的注释，关注 nextState 参数的用法。\n在js代码中获取html标签的属性 比如说，如果想获取 html标签的 innerHTML 属性，做法如下：\n通过原生 js 获取：\n1 document.getElementById(\u0026#39;myh3\u0026#39;).innerHTML 也可以通过 React 提供的 refs 获取：\n1 this.refs.h3.innerHTML 代码举例：\n（3）/components/Counter.jsx：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import React from \u0026#34;react\u0026#34;; // 注意： prop-types 包的职能跟单一，只提供了 一些常见的 数据类型，用于做类型校验 import ReactTypes from \u0026#34;prop-types\u0026#34;; export default class Counter extends React.Component { constructor(props) { super(props); // 初始化组件，保存的是组件的私有数据 this.state = { msg: \u0026#34;ok\u0026#34;, count: props.initcount // 把 父组件传递过来的 initcount 赋值给子组件 state 中的 count值。这样的话，就把 count 值改成了可读可写的 state 属性。因此，以后就能实现“点击 按钮 ，count 值 + 1”的需求了 }; } // 在 React 中，使用静态的 defaultProps 属性，来设置组件的默认属性值 static defaultProps = { initcount: 0 // 如果外界没有传递 initcount，那么，自己初始化一个数值（比如0） }; // 这是创建一个 静态的 propTypes 对象，在这个对象中，可以把 外界传递过来的属性，做类型校验 static propTypes = { initcount: ReactTypes.number // 使用 prop-types 包，来定义 initcount 为 number 类型 }; render() { return ( \u0026lt;div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是 Counter 计数器组件 \u0026lt;/h3\u0026gt; {/* 这里的 this 指向的是 Counter 组件的实例 */} \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;+1\u0026#34; id=\u0026#34;btn\u0026#34; onClick={this.myMethod} /\u0026gt; \u0026lt;h3 id=\u0026#34;myh3\u0026#34; ref=\u0026#34;mymyh3\u0026#34;\u0026gt; 当前的计数是：{this.state.count} \u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); // 当 return 执行完毕后， 虚拟DOM创建好了，但是，还没有挂载到真正的页面中 } // 点击事件的方法定义 myMethod = () =\u0026gt; { // 修改组件的state里面的值 this.setState({ count: this.state.count + 1 }); }; // 判断组件是否需要更新 shouldComponentUpdate(nextProps, nextState) { // 需求： 如果 state 中的 count 值是偶数，则 更新页面；如果 count 值 是奇数，则不更新页面。最终实现的的页面效果：2，4，6，8，10，12.... // 经过打印测试发现：在 shouldComponentUpdate 中，通过 this.state.count 拿到的值，是上一次的旧数据，并不是当前最新的； // 解决办法：通过 shouldComponentUpdate 函数的第二个参数 nextState，可以拿到 最新的 state 数据。 console.log(this.state.count + \u0026#34; ---- \u0026#34; + nextState.count); // return this.state.count % 2 === 0 ? true : false // return nextState.count % 2 === 0 ? true : false; return true; } // 组件将要更新。此时尚未更新，在进入这个 生命周期函数的时候，内存中的虚拟DOM是旧的，页面上的 DOM 元素 也是旧的 componentWillUpdate() { // 经过打印分析发现：此时页面上的 DOM 节点，都是旧的，应该慎重操作，因为你可能操作的是旧DOM // console.log(document.getElementById(\u0026#39;myh3\u0026#39;).innerHTML) console.log(this.refs.mymyh3.innerHTML); } // 组件完成了更新。此时，state 中的数据、虚拟DOM、页面上的DOM，都是最新的，此时，你可以放心大胆的去操作页面了 componentDidUpdate() { console.log(this.refs.mymyh3.innerHTML); } } 上方代码中，componentWillUpdate() 和 componentDidUpdate() 方法里的代码，就是我们这一段要举的例子。\n需要注意的是，\u0026lt;h3 id=\u0026quot;myh3\u0026quot; ref=\u0026quot;mymyh3\u0026quot;\u0026gt;这部分代码中，属性名只能小写，不能大写。\n工程文件：\n2019-02-12-ReactDemo.zip 生命周期函数：componentWillReceiveProps() 当子组件第一次被渲染到页面上的时候，不会触发这个 函数。\n只有当父组件中，通过 某些 事件，重新修改了 传递给 子组件的 props 数据之后，才会触发 componentWillReceiveProps。\n代码举例：\n（1）index.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;ie=edge\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 容器，通过 React 渲染得到的 虚拟DOM，会呈现到这个位置 --\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; （2）main.js:（引入组件）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // JS打包入口文件 // 1. 导入包 import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom\u0026#34;; import MyParent from \u0026#34;./components/TestReceiveProps.jsx\u0026#34;; // 使用 render 函数渲染 虚拟DOM ReactDOM.render( \u0026lt;div\u0026gt; \u0026lt;MyParent\u0026gt;\u0026lt;/MyParent\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); （3）TestReceiveProps.jsx：（组件的定义）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import React from \u0026#34;react\u0026#34;; // 父组件 export default class Parent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是父组件中的 msg 消息\u0026#34; }; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;这是父组件\u0026lt;/h1\u0026gt; \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;点击修改父组件的 MSG\u0026#34; onClick={this.changeMsg} /\u0026gt; \u0026lt;hr /\u0026gt; {/* 在父组件 Parent 中引用子组件 Son */} \u0026lt;Son pmsg={this.state.msg} /\u0026gt; \u0026lt;/div\u0026gt; ); } changeMsg = () =\u0026gt; { this.setState({ msg: \u0026#34;修改组件的msg为新的值\u0026#34; }); }; } // 子组件 class Son extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是子组件 --- {this.props.pmsg}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } // 组件将要接收外界传递过来的新的 props 属性值 // 当子组件第一次被渲染到页面上的时候，不会触发这个 函数； // 只有当 父组件中，通过 某些 事件，重新修改了 传递给 子组件的 props 数据之后，才会触发 componentWillReceiveProps componentWillReceiveProps(nextProps) { // console.log(\u0026#39;被触发了！\u0026#39;); // 注意： 在 componentWillReceiveProps 被触发的时候，如果我们使用 this.props 来获取属性值，这个属性值，不是最新的，是上一次的旧属性值 // 如果想要获取最新的属性值，需要通过 componentWillReceiveProps 的参数列表来获取 console.log(this.props.pmsg + \u0026#34; ---- \u0026#34; + nextProps.pmsg); } } 上方代码中，我们在组件 Parent 中引入了子组件 Son。重点注意 componentWillReceiveProps()函数 的注释部分。\n","date":"2026-02-11T16:00:00+08:00","permalink":"https://impself.github.io/p/react-component-common-props-functions/","title":"React 04-组件（二）：常见属性和函数"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\n搭建开发环境 官方文档：https://reactnative.cn/docs/getting-started.html\n安装Node、homebrew、Watchman 安装 homebrew：\n安装 watchman：\n1 brew install watchman Watchman则是由 Facebook 提供的监视文件系统变更的工具。安装此工具可以提高开发时的性能（packager 可以快速捕捉文件的变化从而实现实时刷新）。\n安装 React Native 的命令行工具（react-native-cli） 安装 react-native-cli：\n1 npm install -g react-native-cli React Native 的命令行工具用于执行创建、初始化、更新项目、运行打包服务（packager）等任务。\n创建新项目 1 react-native init MyApp --version 0.44.3 编译并运行 React Native 应用 在 ios 模拟器上运行：\n1 react-native run-ios 调试 官网文档：https://reactnative.cn/docs/debugging.html\n访问 App 内的开发菜单 如果是在 iOS 模拟器中运行，还可以按下Command + D快捷键，Android 模拟器对应的则是Command⌘ + M（windows 上可能是 F1 或者 F2），或是直接在命令行中运行adb shell input keyevent 82来发送菜单键命令。\n","date":"2026-02-11T16:00:00+08:00","permalink":"https://impself.github.io/p/react-native-intro/","title":"React 11-React Native 初识"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\nJSX介绍 JSX的引入 如果直接让用户通过 JS 代码手动创建DOM元素，肯定是非常麻烦的。\n于是，React 官方就提出了一套 JSX 语法规范，能够让我们在 JS 文件中，书写类似于 HTML 那样的代码，快速定义虚拟DOM结构。\nJSX的全称 JSX：JavaScript XML，一种类似于XML的JS扩展语法。也可以理解成：符合 XML 规范的 JS 语法。\n需要注意的是，哪怕你在 JS 中写的是 JSX 语法（即JSX这样的标签），但是，JSX内部在运行的时候，并不是直接把 我们的 HTML 标签渲染到页面上；而是先把 类似于HTML 这样的标签代码，转换成 React.createElement 这样的JS代码，再渲染到页面中。\n从这一点我们可以看出，JSX是一个对程序员友好的语法糖。\nJSX语法的本质：以 React.createElement 的形式来实现的，并没有直接把 用户写的 HTML代码，渲染到页面上。\nbabel转换工具 如果要直接使用 JSX 语法，需要先安装相关的 语法转换工具：\n1 运行 cnpm i babel-preset-react -D 这个babel包的作用是：将 JSX语法 转换为 JS语法。\n安装完成后，就可以开始使用JSX语法了。\n完整代码举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 注意，这一行的 type 是写 \u0026#34;text/babel\u0026#34;，而不是 \u0026#34;text/javascript\u0026#34; --\u0026gt; \u0026lt;script type=\u0026#34;text/babel\u0026#34;\u0026gt; //页面中的真实容器元素 var containDiv = document.getElementById(\u0026#34;app\u0026#34;); //1、使用JSX语法 创建虚拟DOM对象 var vDom = ( \u0026lt;div\u0026gt; Hello, React! \u0026lt;h2\u0026gt;这是标题\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; ); //2、渲染虚拟DOM对象（将虚拟DOM对象渲染到页面元素中） ReactDOM.render(vDom, containDiv); // 参数1：虚拟DOM对象；参数2：页面中的容器 \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; JSX的基本语法 （1）在 JSX内部 写 JS代码：如果要在 JSX 语法内部，书写 JS 代码，那么，所有的JS代码必须写到 {} 的内部。在{}内部，可以写任何符合JS规范的代码。\n例如：\n1 2 3 4 5 6 7 8 9 var myTitle = \u0026#39;这是使用变量定义的 tilte 值\u0026#39; // 使用JSX语法 创建虚拟DOM对象 var vDom = ( \u0026lt;div\u0026gt; Hello, React! \u0026lt;h2 title={myTitle + \u0026#39;vae\u0026#39;}\u0026gt;这是标题\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; ); （2）当编译引擎在编译JSX代码的时候，如果遇到了\u0026lt;，会把它当作 HTML代码 去编译；如果遇到了 {}， 会把方括号里面的代码当作 普通JS代码 去编译。\n（3）在JSX中，如果要为元素添加class属性，则必须写成className，因为 class在ES6中是一个关键字；和class类似，label标签的 for 属性需要替换为 htmlFor。\n代码举例：\n1 2 3 4 5 6 7 8 // 使用JSX语法 创建虚拟DOM对象 var vDom = ( \u0026lt;div\u0026gt; Hello, React! \u0026lt;p className=\u0026#34;qianguyihao\u0026#34;\u0026gt;千古壹号\u0026lt;/p\u0026gt; \u0026lt;label htmlFor=\u0026#34;\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; ); （4）在JSX创建DOM的时候，所有的节点，必须有唯一的根元素进行包裹。\n（5）如果要写注释，注释必须放到 {} 内部。例如：\n1 2 3 4 5 6 7 8 9 // 使用JSX语法 创建虚拟DOM对象 var vDom = ( // 这一行是注释 \u0026lt;div\u0026gt; Hello, React! \u0026lt;p className=\u0026#34;qianguyihao\u0026#34;\u0026gt;千古壹号\u0026lt;/p\u0026gt; {/*这一行也是注释 */} \u0026lt;/div\u0026gt; ); 最后，再举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 注意，这一行的 type 是写 \u0026#34;text/babel\u0026#34;，而不是 \u0026#34;text/javascript\u0026#34; --\u0026gt; \u0026lt;script type=\u0026#34;text/babel\u0026#34;\u0026gt; //页面中的真实容器元素 var containDiv = document.getElementById(\u0026#34;app\u0026#34;); var arr = [] for (var i = 0; i \u0026lt; 6; i++) { var p = \u0026lt;p className=\u0026#34;myp\u0026#34; key={i}\u0026gt;这个是p标签\u0026lt;/p\u0026gt; // 注意这个地方的写法： key = {i} arr.push(p) } //1、使用JSX语法 创建虚拟DOM对象 var vDom = ( \u0026lt;div\u0026gt; Hello, React! {arr} \u0026lt;/div\u0026gt; ); //2、渲染虚拟DOM对象 ReactDOM.render(vDom, containDiv); // 参数1：虚拟DOM对象；参数2：页面中的容器 \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 运行结果：\n创建组件的第一种方式 创建组件 在React中，构造函数就是一个最基本的组件。\n如果想要把组件放到页面中，可以把构造函数的名称当作组件的名称，以 HTML标签形式引入页面中即可。\n举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 注意，这一行的 type 是写 \u0026#34;text/babel\u0026#34;，而不是 \u0026#34;text/javascript\u0026#34; --\u0026gt; \u0026lt;script type=\u0026#34;text/babel\u0026#34;\u0026gt; // 这个构造函数，就相当于一个 组件 function Hello() { return ( \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是 Hello组件 中定义的元素\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } ReactDOM.render( \u0026lt;div\u0026gt; \u0026lt;Hello\u0026gt; \u0026lt;/Hello\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 运行结果：\n需要注意的是：\nReact在解析所有标签的时候，是以标签的首字母来区分的：如果标签的首字母是小写，就按照普通的 HTML 标签来解析；如果首字母是大写，则按照 组件的形式来解析。\n比如上方代码中，如果把大写的 Hello 改成小写的 hello，运行会报错，无法看到预期的结果。\n结论：组件的首字母必须大写。\n父组件传值给子组件 代码举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 注意，这一行的 type 是写 \u0026#34;text/babel\u0026#34;，而不是 \u0026#34;text/javascript\u0026#34; --\u0026gt; \u0026lt;script type=\u0026#34;text/babel\u0026#34;\u0026gt; // 父组件中的数据 var person = { name: \u0026#34;qianguyihao\u0026#34;, age: 27, gender: \u0026#34;男\u0026#34;, address: \u0026#34;深圳\u0026#34; }; // 在子组件中，如果想要使用外部传递过来的数据，必须显示的在 构造函数参数列表中，定义 props 属性来接收 // 通过 props 得到的任何数据都是只读的，不能重新赋值 function Hello(props) { return ( \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是 Hello子组件 中定义的元素： {props.name}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } ReactDOM.render( \u0026lt;!-- 注意：这里的 ...Obj 语法，是 ES6中的属性扩散，表示：把这个对象上的所有属性，展开了，放到这个位置 --\u0026gt; \u0026lt;div\u0026gt; \u0026lt;Hello {...person}\u0026gt; \u0026lt;/Hello\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 上方代码中，我们是想把整个person对象传递给子组件，所以采用了...Obj 语法语法。传递给子组件后，子组件获取的数据仅仅只是可读的。\nclass 关键字的介绍 面向对象语言的三个特性：封装、继承、多态。多态 和 接口、虚拟方法有关。\nclass的基本用法：使用class创建对象 myclass.js:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // 以前学习的：使用构造函数创建对象 function Person(name, age) { this.name = name; this.age = age; } Person.prototype.say = function() { console.log(\u0026#34;呵呵哒\u0026#34;); }; Person.info = 123; var p1 = new Person(\u0026#34;zs\u0026#34;, 20); // 本次需要学习的：class 后面跟上类名，类名后面，不需要加 () ，直接上 {} class Per { // 在每个class类内部，都有一个 constructor 构造器， 如果没有显示定义 构造器，那么类内部默认都有个看不见的 constructor // constructor 的作用，就好比 咱们之前的 function Person(){ } // 每当，使用 new 关键字创建 class 类实例的时候，必然会优先调用 constructor 构造器 // constructor(){} constructor(name, age) { this.name = name; this.age = age; } // 这是实例方法，必须通过 new 出来的对象调用 say() { console.log(\u0026#34;ok a \u0026#34;); } static info = 123; static sayHello() { console.log(\u0026#34;这是静态方法\u0026#34;); } } var p2 = new Per(\u0026#34;壹号\u0026#34;, 26); console.log(p2); console.log(Per.info); console.log(Per.sayHello()); 使用 class 实现 JS 中的继承 myclass2.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Person { constructor(name, age) { console.log(3); this.name = name; this.age = age; } say() { console.log(\u0026#34;这是 Person中的 say 方法\u0026#34;); } static info = 123; } // 使用 extends 实现继承，extends的前面的是子类，后面的是父类 class Chinese extends Person { constructor(name, age, color, language) { console.log(1); // 注意： 当使用 extends 关键字实现了继承， 子类的 constructor 构造函数中，必须显示调用 super() 方法，这个 super 表示父类中 constructor 的引用 super(name, age); this.color = color; this.language = language; console.log(2); } } var c1 = new Chinese(\u0026#34;张三\u0026#34;, 22, \u0026#34;yellow\u0026#34;, \u0026#34;汉语\u0026#34;); console.log(c1); // 父类中任何东西，子类都能继承到 c1.say(); 注意上方 constructor处的注释：当使用 extends 关键字实现了继承， 子类的 constructor 构造函数中，必须显示调用 super() 方法，这个 super 表示父类中 constructor 的引用。也就是说，在子类当中，要么不写 constructor，如果写了 constructor，就一定要把 super()也加上。\n为啥我们要引入 class这个功能？就是因为， class里，永远都存在着一个 constructor。我们可以利用 constructor做很多事情。\n创建组件的第二种方式：使用 class 关键字 使用 class 创建的类，通过 extends 关键字，继承 React.Component 之后，这个类，就是一个组件的模板了。如果想要引用这个组件，可以把类的名称以标签的形式，导入到 JSX 中使用。\n在 class 实现的组件内部，必须定义一个 render 函数。在 render 函数中，还必须 return 一个东西，如果没有什么需要被return 的，则需要 return null。\n代码举例：\nindex.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 注意，这一行的 type 是写 \u0026#34;text/babel\u0026#34;，而不是 \u0026#34;text/javascript\u0026#34; --\u0026gt; \u0026lt;script type=\u0026#34;text/babel\u0026#34;\u0026gt; // 使用 class 创建的类，通过 extends 关键字，继承 `React.Component` 之后，这个类，就是一个组件的模板了。 // 如果想要引用这个组件，可以把类的名称以**标签的形式**，导入到 JSX 中使用。 class Hello2 extends React.Component { // 在 class 实现的组件内部，必须定义一个 render 函数 render() { // 在 render 函数中，还必须 return 一个东西，如果没有什么需要被return 的，则需要 return null return ( \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是使用 class 类创建的组件 \u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } } ReactDOM.render( \u0026lt;div\u0026gt; \u0026lt;Hello2\u0026gt; \u0026lt;/Hello2\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 父组件传值给子组件 代码举例：\nindex.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 注意，这一行的 type 是写 \u0026#34;text/babel\u0026#34;，而不是 \u0026#34;text/javascript\u0026#34; --\u0026gt; \u0026lt;script type=\u0026#34;text/babel\u0026#34;\u0026gt; // 使用 class 创建的类，通过 extends 关键字，继承 `React.Component` 之后，这个类，就是一个组件的模板了。 // 如果想要引用这个组件，可以把类的名称以**标签的形式**，导入到 JSX 中使用。 class Hello2 extends React.Component { constructor(props) { super(props); console.log(props.name); // 注意：`this.state` 是固定写法，表示当前组件实例的私有数据对象，就好比 vue 中，组件实例身上的 data(){ return {} } 函数 // 如果想要使用 组件中 state 上的数据，直接通过 this.state.*** 来访问即可 this.state = { msg: \u0026#34;这是 Hello2 组件的私有msg数据\u0026#34;, info: \u0026#34;永不止步\u0026#34; }; } // 在 class 实现的组件内部，必须定义一个 render 函数 render() { // 在 render 函数中，还必须 return 一个东西，如果没有什么需要被return 的，则需要 return null return ( \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;这是使用 class 类创建的组件 \u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } } ReactDOM.render( \u0026lt;div\u0026gt; \u0026lt;Hello2 name=\u0026#34;qianguyihao\u0026#34;\u0026gt; \u0026lt;/Hello2\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 方式一和方式二的对比 上面的内容里，我们使用了两种方式创建组件。这两种方式，有着本质的区别，我们来对比一下。\n对比：\n方式一：通过 function构造函数 创建组件。内部没有 state 私有数据，只有 一个 props 来接收外界传递过来的数据。\n方式二：通过 class 创建子组件。内部除了有 this.props 这个只读属性之外，还有一个专门用于 存放自己私有数据的 this.state 属性，这个 state 是可读可写的。\n基于上面的区别，我们可以为这两种创建组件的方式下定义： 使用 function 创建的组件，叫做【无状态组件】；使用 class 创建的组件，叫做【有状态组件】。\n本质区别：\n有状态组件和无状态组件，最本质的区别，就是有无 state 属性。同时， class 创建的组件，有自己的生命周期函数，但是，function 创建的 组件，没有自己的生命周期函数。\n什么时候使用 有状态组件，什么时候使用无状态组件：\n（1）如果一个组件需要存放自己的私有数据，或者需要在组件的不同阶段执行不同的业务逻辑，此时，非常适合用 class 创建出来的有状态组件。\n（2）如果一个组件，只需要根据外界传递过来的 props，渲染固定的页面结构即可的话，此时，非常适合使用 function 创建出来的无状态组件。（使用无状态组件的小小好处： 由于剔除了组件的生命周期，所以，运行速度会相对快一点点）。\n","date":"2026-02-11T15:59:37+08:00","permalink":"https://impself.github.io/p/react-jsx-introduction/","title":"React 02-JSX语法介绍"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。 本次做后台管理系统，采用的是 AntD 框架。涉及到图片的上传，用的是AntD的 upload 组件。\n我在上一篇文章《前端AntD框架的upload组件上传图片时遇到的一些坑》中讲到：AntD 的 upload 组件有很多坑，引起了很多人的关注。折腾过的人，自然明白其中的苦楚。\n今天这篇文章，我们继续来研究 AntD 的 upload 组件的另一个坑。\n备注：本文写于2020-06-11，使用的 antd 版本是 3.13.6。\n使用 AntD 的 upload 组件做图片的上传，效果演示 因为需要上传多张图片，所以采用的是照片墙的形式。上传成功后的界面如下：\n代码实现 首先，你需要让后台同学提供好图片上传的接口。上一篇文章中，我们是把接口调用直接写在了 \u0026lt;Upload\u0026gt; 标签的 action 属性当中。但如果你在调接口的时候，动作很复杂（比如根据业务要求，需要连续调两个接口才能上传图片，或者在调接口时还要做其他的事情），这个 action 方法就无法满足需求了。那该怎么做呢？\n好在 AntD 的 upload 组件给我们提供了 customRequest这个方法：\n关于customRequest 这个方法， AntD 官方并没有给出示例，他们只是在 GitHub 上给出了这样一个简短的介绍：\n但这个方法怎么用呢？用的时候，会遇到什么问题呢？AntD 官方没有说。我在网上搜了半天，也没看到比较完整的、切实可行的 Demo。我天朝地大物博，网络资料浩如烟海，AntD 可是口口声声被人们号称是天朝最好用的管理后台的样式框架。可如今，却面临这样的局面。我看着你们，满怀羡慕。\n既然如此，那我就自己研究吧。折腾了一天，总算是把 customRequest 的坑踩得差不多了。\n啥也不说了，直接上代码。\n采用 AntD框架的 upload 组件的 customRequest 方法，自定义上传行为。核心代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 import React, { PureComponent } from \u0026#39;react\u0026#39;; import { Button, Card, Form, message, Upload, Icon, Modal, Row, Col } from \u0026#39;antd\u0026#39;; import { connect } from \u0026#39;dva\u0026#39;; import { queryMyData, submitData } from \u0026#39;../api\u0026#39;; import { uploadImage } from \u0026#39;../../utils/wq.img.upload\u0026#39;; import styles from \u0026#39;../../utils/form.less\u0026#39;; const FormItem = Form.Item; @Form.create() export default class PicturesWall extends PureComponent { constructor(props) { super(props); const { id } = this.props.match.params; this.state = { id, img: undefined, // 从接口拿到的图片字段 imgList: [], // 展示在 antd图片组件上的数据 previewVisible: false, previewImage: \u0026#39;\u0026#39;, }; } componentDidMount() { const { id } = this.state; id \u0026amp;\u0026amp; this.queryData(); } // 调接口，查询已有的数据 queryData() { const { id } = this.state; queryMyData({ id, }) .then(({ ret, data }) =\u0026gt; { if (ret == 0 \u0026amp;\u0026amp; data \u0026amp;\u0026amp; data.list \u0026amp;\u0026amp; data.list.length) { const item = data.list[0]; const img = data.img; const imgList = item.img ? [ { uid: \u0026#39;1\u0026#39;, // 注意，这个uid一定不能少，否则展示失败 name: \u0026#39;hehe.png\u0026#39;, status: \u0026#39;done\u0026#39;, url: img, }, ] : []; this.setState({ img, imgList, }); } else { return Promise.reject(); } }) .catch(() =\u0026gt; { message.error(\u0026#39;查询出错，请重试\u0026#39;); }); } handleCancel = () =\u0026gt; this.setState({ previewVisible: false }); // 方法：图片预览 handlePreview = (file) =\u0026gt; { console.log(\u0026#39;smyhvae handlePreview:\u0026#39; + JSON.stringify(file)); this.setState({ previewImage: file.url || file.thumbUrl, previewVisible: true, }); }; // 参考链接：https://www.jianshu.com/p/f356f050b3c9 handleBeforeUpload = (file) =\u0026gt; { console.log(\u0026#39;smyhvae handleBeforeUpload file:\u0026#39; + JSON.stringify(file)); console.log(\u0026#39;smyhvae handleBeforeUpload file.file:\u0026#39; + JSON.stringify(file.file)); console.log(\u0026#39;smyhvae handleBeforeUpload file type:\u0026#39; + JSON.stringify(file.type)); //限制图片 格式、size、分辨率 const isJPG = file.type === \u0026#39;image/jpeg\u0026#39;; const isJPEG = file.type === \u0026#39;image/jpeg\u0026#39;; const isGIF = file.type === \u0026#39;image/gif\u0026#39;; const isPNG = file.type === \u0026#39;image/png\u0026#39;; const isLt2M = file.size / 1024 / 1024 \u0026lt; 1; if (!(isJPG || isJPEG || isPNG)) { Modal.error({ title: \u0026#39;只能上传JPG、JPEG、PNG格式的图片~\u0026#39;, }); } else if (!isLt2M) { Modal.error({ title: \u0026#39;图片超过1M限制，不允许上传~\u0026#39;, }); } return (isJPG || isJPEG || isPNG) \u0026amp;\u0026amp; isLt2M; }; // checkImageWH 返回一个promise 检测通过返回resolve 失败返回reject阻止图片上传 checkImageWH(file) { return new Promise(function (resolve, reject) { let filereader = new FileReader(); filereader.onload = (e) =\u0026gt; { let src = e.target.result; const image = new Image(); image.onload = function () { // 获取图片的宽高 file.width = this.width; file.height = this.height; resolve(); }; image.onerror = reject; image.src = src; }; filereader.readAsDataURL(file); }); } // 图片上传 doImgUpload = (options) =\u0026gt; { const { onSuccess, onError, file, onProgress } = options; // start：进度条相关 // 伪装成 handleChange里面的图片上传状态 const imgItem = { uid: \u0026#39;1\u0026#39;, // 注意，这个uid一定不能少，否则上传失败 name: \u0026#39;hehe.png\u0026#39;, status: \u0026#39;uploading\u0026#39;, url: \u0026#39;\u0026#39;, percent: 99, // 注意不要写100。100表示上传完成 }; this.setState({ imgList: [imgItem], }); // 更新 imgList // end：进度条相关 const reader = new FileReader(); reader.readAsDataURL(file); // 读取图片文件 reader.onload = (file) =\u0026gt; { const params = { myBase64: file.target.result, // 把 本地图片的base64编码传给后台，调接口，生成图片的url }; // 上传图片的base64编码，调接口后，返回 imageId uploadImage(params) .then((res) =\u0026gt; { console.log(\u0026#39;smyhvae doImgUpload:\u0026#39; + JSON.stringify(res)); console.log(\u0026#39;smyhvae 图片上传成功：\u0026#39; + res.imageUrl); const imgItem = { uid: \u0026#39;1\u0026#39;, // 注意，这个uid一定不能少，否则上传失败 name: \u0026#39;hehe.png\u0026#39;, status: \u0026#39;done\u0026#39;, url: res.imageUrl, // url 是展示在页面上的绝对链接 imgUrl: res.imageUrl, // imgUrl 是存到 db 里的相对链接 // response: \u0026#39;{\u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;}\u0026#39;, }; this.setState({ imgList: [imgItem], }); // 更新 imgList }) .catch((e) =\u0026gt; { console.log(\u0026#39;smyhvae 图片上传失败:\u0026#39; + JSON.stringify(e || \u0026#39;\u0026#39;)); message.error(\u0026#39;图片上传失败，请重试\u0026#39;); }); }; }; handleChange = ({ file, fileList }) =\u0026gt; { console.log(\u0026#39;smyhvae handleChange file:\u0026#39; + JSON.stringify(file)); console.log(\u0026#39;smyhvae handleChange fileList:\u0026#39; + JSON.stringify(fileList)); if (file.status == \u0026#39;removed\u0026#39;) { this.setState({ imgList: [], }); } }; submit = (e) =\u0026gt; { e.preventDefault(); this.props.form.validateFields((err, fieldsValue) =\u0026gt; { if (err) { return; } const { id, imgList } = this.state; const tempImgList = imgList.filter((item) =\u0026gt; item.status == \u0026#39;done\u0026#39;); // 筛选出 status = done 的图片 const imgArr = []; tempImgList.forEach((item) =\u0026gt; { imgArr.push(item.imgUrl); // imgArr.push(item.url); }); submitData({ id, img: imgArr[0] || \u0026#39;\u0026#39;, // 1、暂时只传一张图片给后台。如果传多张图片，那么，upload组件需要进一步完善，比较麻烦，以后有需求再优化。2、如果图片字段是选填，那就用空字符串兜底 }) .then((res) =\u0026gt; { if (res.ret == 0) { message.success(`${id ? \u0026#39;修改\u0026#39; : \u0026#39;新增\u0026#39;}成功，自动跳转中...`); } else if (res.ret == 201 || res.ret == 202 || res.ret == 203 || res.ret == 6) { return Promise.reject(res.msg); } else { return Promise.reject(); } }) .catch((e) =\u0026gt; { message.error(e || \u0026#39;提交失败，请重试\u0026#39;); }); }); }; render() { const { id, imgList } = this.state; console.log(\u0026#39;smyhvae render imgList:\u0026#39; + JSON.stringify(imgList)); const { getFieldDecorator } = this.props.form; const formItemLayout = { labelCol: { span: 3 }, wrapperCol: { span: 10 }, }; const buttonItemLayout = { wrapperCol: { span: 10, offset: 3 }, }; const uploadButton = ( \u0026lt;div\u0026gt; \u0026lt;Icon type=\u0026#34;plus\u0026#34; /\u0026gt; \u0026lt;div className=\u0026#34;ant-upload-text\u0026#34;\u0026gt;Upload\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); return ( \u0026lt;Card title={id ? \u0026#39;修改信息\u0026#39; : \u0026#39;新增信息\u0026#39;}\u0026gt; \u0026lt;Form onSubmit={this.submit} layout=\u0026#34;horizontal\u0026#34;\u0026gt; {/* 新建图片、编辑图片 */} \u0026lt;FormItem label=\u0026#34;图片\u0026#34; {...formItemLayout}\u0026gt; {getFieldDecorator(\u0026#39;img\u0026#39;, { rules: [{ required: false, message: \u0026#39;请上传图片\u0026#39; }], })( \u0026lt;Upload action=\u0026#34;2\u0026#34; customRequest={this.doImgUpload} listType=\u0026#34;picture-card\u0026#34; fileList={imgList} onPreview={this.handlePreview} beforeUpload={this.handleBeforeUpload} onChange={this.handleChange} \u0026gt; {imgList.length \u0026gt;= 1 ? null : uploadButton} \u0026lt;/Upload\u0026gt; )} \u0026lt;/FormItem\u0026gt; \u0026lt;Row\u0026gt; \u0026lt;Col span={3} /\u0026gt; \u0026lt;Col span={18} className={styles.graytext}\u0026gt; 注：图片支持JPG、JPEG、PNG格式，小于1M，最多上传1张 \u0026lt;/Col\u0026gt; \u0026lt;/Row\u0026gt; \u0026lt;FormItem {...buttonItemLayout}\u0026gt; \u0026lt;Button type=\u0026#34;primary\u0026#34; htmlType=\u0026#34;submit\u0026#34;\u0026gt; 提交 \u0026lt;/Button\u0026gt; \u0026lt;/FormItem\u0026gt; \u0026lt;/Form\u0026gt; {/* 图片点开预览 */} \u0026lt;Modal visible={this.state.previewVisible} footer={null} onCancel={this.handleCancel}\u0026gt; \u0026lt;img alt=\u0026#34;example\u0026#34; style={{ width: \u0026#39;100%\u0026#39; }} src={this.state.previewImage} /\u0026gt; \u0026lt;/Modal\u0026gt; \u0026lt;/Card\u0026gt; ); } } 参考链接 注意file的格式：https://www.lmonkey.com/t/oREQA5XE1\nDemo在线演示：\nhttps://stackoverflow.com/questions/58128062/using-customrequest-in-ant-design-file-upload\nhttps://stackblitz.com/edit/so-58128062-upload-progress\nfileList 格式在线演示：\nhttps://stackoverflow.com/questions/51514757/action-function-is-required-with-antd-upload-control-but-i-dont-need-it\nhttps://codesandbox.io/s/rl7ooo544q\nant design Upload组件的使用总结：https://www.jianshu.com/p/0aa4612af987\nantd上传功能的CustomRequest：https://mlog.club/article/3832743\n","date":"2026-02-11T15:00:00+08:00","permalink":"https://impself.github.io/p/antd-upload-customrequest/","title":"React 10-AntD Upload customRequest 实战"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\n前言 本次做后台管理系统，采用的是 AntD 框架。涉及到图片的上传，用的是AntD的 upload 组件。\n前端做文件上传这个功能，是很有技术难度的。既然框架给我们提供好了，那就直接用呗。结果用的时候，发现 upload 组件的很多bug。下面来列举几个。\n备注：本文写于2019-03-02，使用的 antd 版本是 3.13.6。\n使用 AntD 的 upload 组件做图片的上传 因为需要上传多张图片，所以采用的是照片墙的形式。上传成功后的界面如下：\n按照官方提供的实例，特此整理出项目开发中的完整写法，亲测有效。代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 /* eslint-disable */ import { Upload, Icon, Modal, Form } from \u0026#39;antd\u0026#39;; const FormItem = Form.Item; class PicturesWall extends PureComponent { state = { previewVisible: false, previewImage: \u0026#39;\u0026#39;, imgList: [], }; handleChange = ({ file, fileList }) =\u0026gt; { console.log(JSON.stringify(file)); // file 是当前正在上传的 单个 img console.log(JSON.stringify(fileList)); // fileList 是已上传的全部 img 列表 this.setState({ imgList: fileList, }); }; handleCancel = () =\u0026gt; this.setState({ previewVisible: false }); handlePreview = file =\u0026gt; { this.setState({ previewImage: file.url || file.thumbUrl, previewVisible: true, }); }; // 参考链接：https://www.jianshu.com/p/f356f050b3c9 handleBeforeUpload = file =\u0026gt; { //限制图片 格式、size、分辨率 const isJPG = file.type === \u0026#39;image/jpeg\u0026#39;; const isJPEG = file.type === \u0026#39;image/jpeg\u0026#39;; const isGIF = file.type === \u0026#39;image/gif\u0026#39;; const isPNG = file.type === \u0026#39;image/png\u0026#39;; if (!(isJPG || isJPEG || isGIF || isPNG)) { Modal.error({ title: \u0026#39;只能上传JPG 、JPEG 、GIF、 PNG格式的图片~\u0026#39;, }); return; } const isLt2M = file.size / 1024 / 1024 \u0026lt; 2; if (!isLt2M) { Modal.error({ title: \u0026#39;超过2M限制，不允许上传~\u0026#39;, }); return; } return (isJPG || isJPEG || isGIF || isPNG) \u0026amp;\u0026amp; isLt2M \u0026amp;\u0026amp; this.checkImageWH(file); }; //返回一个 promise：检测通过则返回resolve；失败则返回reject，并阻止图片上传 checkImageWH(file) { let self = this; return new Promise(function(resolve, reject) { let filereader = new FileReader(); filereader.onload = e =\u0026gt; { let src = e.target.result; const image = new Image(); image.onload = function() { // 获取图片的宽高，并存放到file对象中 console.log(\u0026#39;file width :\u0026#39; + this.width); console.log(\u0026#39;file height :\u0026#39; + this.height); file.width = this.width; file.height = this.height; resolve(); }; image.onerror = reject; image.src = src; }; filereader.readAsDataURL(file); }); } handleSubmit = e =\u0026gt; { const { dispatch, form } = this.props; e.preventDefault(); form.validateFieldsAndScroll((err, values) =\u0026gt; {// values 是form表单里的参数 // 点击按钮后，将表单提交给后台 dispatch({ type: \u0026#39;mymodel/submitFormData\u0026#39;, payload: values, }); }); }; render() { const { previewVisible, previewImage, imgList } = this.state; // 从 state 中拿数据 const uploadButton = ( \u0026lt;div\u0026gt; \u0026lt;Icon type=\u0026#34;plus\u0026#34; /\u0026gt; \u0026lt;div className=\u0026#34;ant-upload-text\u0026#34;\u0026gt;Upload\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); return ( \u0026lt;div className=\u0026#34;clearfix\u0026#34;\u0026gt; \u0026lt;Form onSubmit={this.handleSubmit} hideRequiredMark style={{ marginTop: 8 }}\u0026gt; \u0026lt;FormItem label=\u0026#34;图片图片\u0026#34; {...formItemLayout}\u0026gt; {getFieldDecorator(\u0026#39;myImg\u0026#39;)( \u0026lt;Upload action=\u0026#34;//jsonplaceholder.typicode.com/posts/\u0026#34; // 这个是图片上传的接口请求，实际开发中，要替换成你自己的业务接口 data={file =\u0026gt; ({ // data里存放的是接口的请求参数 param1: myParam1, param2: myParam2, photoCotent: file, // file 是当前正在上传的图片 photoWidth: file.height, // 通过 handleBeforeUpload 获取 图片的宽高 photoHeight: file.width, })} listType=\u0026#34;picture-card\u0026#34; fileList={this.state.imgList} onPreview={this.handlePreview} // 点击图片缩略图，进行预览 beforeUpload={this.handleBeforeUpload} // 上传之前，对图片的格式做校验，并获取图片的宽高 onChange={this.handleChange} // 每次上传图片时，都会触发这个方法 \u0026gt; {this.state.imgList.length \u0026gt;= 9 ? null : uploadButton} \u0026lt;/Upload\u0026gt; )} \u0026lt;/FormItem\u0026gt; \u0026lt;/Form\u0026gt; \u0026lt;Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}\u0026gt; \u0026lt;img alt=\u0026#34;example\u0026#34; style={{ width: \u0026#39;100%\u0026#39; }} src={previewImage} /\u0026gt; \u0026lt;/Modal\u0026gt; \u0026lt;/div\u0026gt; ); } } export default PicturesWall; 上传后，点击图片预览，浏览器卡死的问题 依据上方的代码，通过 Antd 的 upload 组件将图片上传成功后，点击图片的缩略图，理应可以在当前页面弹出 Modal，预览图片。但实际的结果是，浏览器一定会卡死。\n定位问题发现，原因竟然是：图片上传成功后， upload 会将其转为 base64编码。base64这个字符串太大了，点击图片预览的时候，浏览器在解析一大串字符串，然后就卡死了。详细过程描述如下。\n上方代码中，我们可以把 handleChange(file, fileList)方法中的 file、以及 fileList打印出来看看。 file指的是当前正在上传的 单个 img，fileList是已上传的全部 img 列表。 当我上传完 两张图片后， 打印结果如下：\nfile的打印的结果如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { \u0026#34;uid\u0026#34;: \u0026#34;rc-upload-1551084269812-5\u0026#34;, \u0026#34;width\u0026#34;: 600, \u0026#34;height\u0026#34;: 354, \u0026#34;lastModified\u0026#34;: 1546701318000, \u0026#34;lastModifiedDate\u0026#34;: \u0026#34;2019-01-05T15:15:18.000Z\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;e30e7b9680634b2c888c8bb513cc595d.jpg\u0026#34;, \u0026#34;size\u0026#34;: 31731, \u0026#34;type\u0026#34;: \u0026#34;image/jpeg\u0026#34;, \u0026#34;percent\u0026#34;: 100, \u0026#34;originFileObj\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;rc-upload-1551084269812-5\u0026#34;, \u0026#34;width\u0026#34;: 600, \u0026#34;height\u0026#34;: 354 }, \u0026#34;status\u0026#34;: \u0026#34;done\u0026#34;, \u0026#34;thumbUrl\u0026#34;: \u0026#34;data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAHQ9qKKlbimcXrIH9o2vH/AC2T+ddPj98v+9RRWsuhnHdk0ar9qb5R0Pb6VPB/qh9aKKiRr0Irnt/vUDr+NFFJCRqWxJik5Pb+dLJ938aKK06mYSdKKKKBH//Z\u0026#34;, \u0026#34;response\u0026#34;: { \u0026#34;retCode\u0026#34;: 0, \u0026#34;imgUrl\u0026#34;: \u0026#34;http://qianguyihao.com/opfewfwj098902kpkpkkj976fe.jpg\u0026#34;, \u0026#34;photoid\u0026#34;: 271850 } } fileList 的打印结果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 [ { \u0026#34;uid\u0026#34;: \u0026#34;rc-upload-1551084269812-3\u0026#34;, \u0026#34;width\u0026#34;: 1000, \u0026#34;height\u0026#34;: 667, \u0026#34;lastModified\u0026#34;: 1501414799000, \u0026#34;lastModifiedDate\u0026#34;: \u0026#34;2017-07-30T11:39:59.000Z\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;29381f30e924b89914e91b33.jpg\u0026#34;, \u0026#34;size\u0026#34;: 135204, \u0026#34;type\u0026#34;: \u0026#34;image/jpeg\u0026#34;, \u0026#34;percent\u0026#34;: 100, \u0026#34;originFileObj\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;rc-upload-1551084269812-3\u0026#34;, \u0026#34;width\u0026#34;: 1000, \u0026#34;height\u0026#34;: 667 }, \u0026#34;status\u0026#34;: \u0026#34;done\u0026#34;, \u0026#34;thumbUrl\u0026#34;: \u0026#34;data:image/jpeg;base64,/E3ju1tlaK1fzJOnHQU3LsLV7HO6Zrk11MZJ7luT0A4FZuRagi9quvzQQ4iuEJ7ZpqTG4djDsPFl2Lg733f8C4q+YhQ8zoYfGSqoMmfwo5huLL0HjiyPDSYPvxRdC1XQvxeLrB8fvl/OnoLmL9vrdvvYS3NGFVe2YsASOh71JfQyrqV2mXLHOcccVSIYEnDyZO9XXB9KYH//Z\u0026#34;, \u0026#34;response\u0026#34;: { \u0026#34;retCode\u0026#34;: 0, \u0026#34;msg\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;imgUrl\u0026#34;: \u0026#34;http://qianguyihao.com/hfwpjouiurewnmbhepr689.jpg\u0026#34;, } }, { \u0026#34;uid\u0026#34;: \u0026#34;rc-upload-1551084269812-5\u0026#34;, \u0026#34;width\u0026#34;: 600, \u0026#34;height\u0026#34;: 354, \u0026#34;lastModified\u0026#34;: 1546701318000, \u0026#34;lastModifiedDate\u0026#34;: \u0026#34;2019-01-05T15:15:18.000Z\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;e30e7b9680634b2c888c8bb513cc595d.jpg\u0026#34;, \u0026#34;size\u0026#34;: 31731, \u0026#34;type\u0026#34;: \u0026#34;image/jpeg\u0026#34;, \u0026#34;percent\u0026#34;: 100, \u0026#34;originFileObj\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;rc-upload-1551084269812-5\u0026#34;, \u0026#34;width\u0026#34;: 600, \u0026#34;height\u0026#34;: 354 }, \u0026#34;status\u0026#34;: \u0026#34;done\u0026#34;, \u0026#34;thumbUrl\u0026#34;: \u0026#34;data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAHQ9qKKlbimcXrIH9o2vH/AC2T+ddPj98v+9RRWsuhnHdk0ar9qb5R0Pb6VPB/qh9aKKiRr0Irnt/vUDr+NFFJCRqWxJik5Pb+dLJ938aKK06mYSdKKKKBH//Z\u0026#34;, \u0026#34;response\u0026#34;: { \u0026#34;retCode\u0026#34;: 0, \u0026#34;imgUrl\u0026#34;: \u0026#34;http://qianguyihao.com/opfewfwj098902kpkpkkj976fe.jpg\u0026#34;, \u0026#34;photoid\u0026#34;: 271850 } } ] 上方的json数据中，需要做几点解释：\n（1）response 字段里面的数据，就是请求接口后，后台返回给前端的数据，里面包含了图片的url链接。\n（2）status 字段里存放的是图片上传的实时状态，包括上传中、上传完成、上传失败。\n（3）thumbUrl字段里面存放的是图片的base64编码。\n这个base64编码非常非常长。当点击图片预览的时候，其实就是加载的 thumbUrl 这个字段里的资源，难怪浏览器会卡死。\n解决办法：在 handleChange方法里，图片上传成功后，将 thumbUrl 字段里面的 base64 编码改为真实的图片url。代码实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 handleChange = ({ file, fileList }) =\u0026gt; { console.log(JSON.stringify(file)); // file 是当前正在上传的 单个 img console.log(JSON.stringify(fileList)); // fileList 是已上传的全部 img 列表 // 【重要】将 图片的base64替换为图片的url。 这一行一定不会能少。 // 图片上传成功后，fileList数组中的 thumbUrl 中保存的是图片的base64字符串，这种情况，导致的问题是：图片上传成功后，点击图片缩略图，浏览器会会卡死。而下面这行代码，可以解决该bug。 fileList.forEach(imgItem =\u0026gt; { if (imgItem \u0026amp;\u0026amp; imgItem.status == \u0026#39;done\u0026#39; \u0026amp;\u0026amp; imgItem.response \u0026amp;\u0026amp; imgItem.response.imgUrl) { imgItem.thumbUrl = imgItem.response.imgUrl; } }); this.setState({ imgList: fileList, }); }; 新需求：编辑现有页面 上面一段的代码中，我们是在新建的页面中，从零开始上传图片。\n现在有个新的需求：如何编辑现有的页面呢？也就是说，现有的页面在初始化时，是默认有几张图片的。当我编辑这个页面时，可以对现有的图片做增删，也能增加新的图片。而且要保证：新建页面和编辑现有页面，是共用一套代码。\n我看到upload 组件有提供 defaultFileList 的属性。我试了下，这个defaultFileList 的属性根本没法儿用。\n那就只有手动实现了。我的model层代码，是用 redux 写的。整体的实现思路如下：（这个也是在真正在实战中用到的代码）\n（1）PicturesWall.js：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 /* eslint-disable */ import { Upload, Icon, Modal, Form } from \u0026#39;antd\u0026#39;; const FormItem = Form.Item; class PicturesWall extends PureComponent { state = { previewVisible: false, previewImage: \u0026#39;\u0026#39;, }; // 页面初始化的时候，从接口拉取默认的图片数据 componentDidMount() { const { dispatch } = this.props; dispatch({ type: \u0026#39;mymodel/getAllInfo\u0026#39;, payload: { params: xxx }, }); } handleChange = ({ file, fileList }) =\u0026gt; { const { dispatch } = this.props; // 【重要】将 图片的base64替换为图片的url。 这一行一定不会能少。 // 图片上传成功后，fileList数组中的 thumbUrl 中保存的是图片的base64字符串，这种情况，导致的问题是：图片上传成功后，点击图片缩略图，浏览器会会卡死。而下面这行代码，可以解决该bug。 fileList.forEach(imgItem =\u0026gt; { if (imgItem \u0026amp;\u0026amp; imgItem.status == \u0026#39;done\u0026#39; \u0026amp;\u0026amp; imgItem.response \u0026amp;\u0026amp; imgItem.response.imgUrl) { imgItem.thumbUrl = imgItem.response.imgUrl; } }); dispatch({ type: \u0026#39;mymodel/setImgList\u0026#39;, payload: fileList, }); }; handleCancel = () =\u0026gt; this.setState({ previewVisible: false }); handlePreview = file =\u0026gt; { this.setState({ previewImage: file.url || file.thumbUrl, previewVisible: true, }); }; // 参考链接：https://www.jianshu.com/p/f356f050b3c9 handleBeforeUpload = file =\u0026gt; { //限制图片 格式、size、分辨率 const isJPG = file.type === \u0026#39;image/jpeg\u0026#39;; const isJPEG = file.type === \u0026#39;image/jpeg\u0026#39;; const isGIF = file.type === \u0026#39;image/gif\u0026#39;; const isPNG = file.type === \u0026#39;image/png\u0026#39;; const isLt2M = file.size / 1024 / 1024 \u0026lt; 2; if (!(isJPG || isJPEG || isGIF || isPNG)) { Modal.error({ title: \u0026#39;只能上传JPG 、JPEG 、GIF、 PNG格式的图片~\u0026#39;, }); } else if (!isLt2M) { Modal.error({ title: \u0026#39;超过2M限制，不允许上传~\u0026#39;, }); } } // 参考链接：https://github.com/ant-design/ant-design/issues/8779 return new Promise((resolve, reject) =\u0026gt; { if (!(isJPG || isJPEG || isGIF || isPNG)) { reject(file); } else { resolve(file \u0026amp;\u0026amp; this.checkImageWH(file)); } }); }; //返回一个 promise：检测通过则返回resolve；失败则返回reject，并阻止图片上传 checkImageWH(file) { let self = this; return new Promise(function(resolve, reject) { let filereader = new FileReader(); filereader.onload = e =\u0026gt; { let src = e.target.result; const image = new Image(); image.onload = function() { // 获取图片的宽高，并存放到file对象中 console.log(\u0026#39;file width :\u0026#39; + this.width); console.log(\u0026#39;file height :\u0026#39; + this.height); file.width = this.width; file.height = this.height; resolve(); }; image.onerror = reject; image.src = src; }; filereader.readAsDataURL(file); }); } handleSubmit = e =\u0026gt; { const { dispatch, form } = this.props; e.preventDefault(); const { mymodel: { imgList }, // 从props中拿默认的图片数据 } = this.props; form.validateFieldsAndScroll((err, values) =\u0026gt; { // values 是form表单里的参数 // 点击按钮后，将表单提交给后台 // start 问题描述：当编辑现有页面时，如果针对已经存在的默认图片不做修改，则不会触发 upload 的 onChange方法。此时提交表单，表单里的 myImg 字段是空的。 // 解决办法：如果发现存在默认图片，则追加到表单中 if (!values.myImg) { values.myImg = { fileList: [] }; values.myImg.fileList = imgList; } // end dispatch({ type: \u0026#39;mymodel/submitFormData\u0026#39;, payload: values, }); }); }; render() { const { previewVisible, previewImage } = this.state; // 从 state 中拿数据 const { mymodel: { imgList }, // 从props中拿到的图片数据 } = this.props; const uploadButton = ( \u0026lt;div\u0026gt; \u0026lt;Icon type=\u0026#34;plus\u0026#34; /\u0026gt; \u0026lt;div className=\u0026#34;ant-upload-text\u0026#34;\u0026gt;Upload\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ); return ( \u0026lt;div className=\u0026#34;clearfix\u0026#34;\u0026gt; \u0026lt;Form onSubmit={this.handleSubmit} hideRequiredMark style={{ marginTop: 8 }}\u0026gt; \u0026lt;FormItem label=\u0026#34;图片上传\u0026#34; {...formItemLayout}\u0026gt; {getFieldDecorator(\u0026#39;myImg\u0026#39;)( \u0026lt;Upload action=\u0026#34;//jsonplaceholder.typicode.com/posts/\u0026#34; // 这个是图片上传的接口请求，实际开发中，要替换成你自己的业务接口 data={file =\u0026gt; ({ // data里存放的是接口的请求参数 param1: myParam1, param2: myParam2, photoCotent: file, // file 是当前正在上传的图片 photoWidth: file.height, // 通过 handleBeforeUpload 获取 图片的宽高 photoHeight: file.width, })} listType=\u0026#34;picture-card\u0026#34; fileList={imgList} // 改为从 props 里拿图片数据，而不是从 state onPreview={this.handlePreview} // 点击图片缩略图，进行预览 beforeUpload={this.handleBeforeUpload} // 上传之前，对图片的格式做校验，并获取图片的宽高 onChange={this.handleChange} // 每次上传图片时，都会触发这个方法 \u0026gt; {this.state.imgList.length \u0026gt;= 9 ? null : uploadButton} \u0026lt;/Upload\u0026gt; )} \u0026lt;/FormItem\u0026gt; \u0026lt;/Form\u0026gt; \u0026lt;Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}\u0026gt; \u0026lt;img alt=\u0026#34;example\u0026#34; style={{ width: \u0026#39;100%\u0026#39; }} src={previewImage} /\u0026gt; \u0026lt;/Modal\u0026gt; \u0026lt;/div\u0026gt; ); } } export default PicturesWall; （2）mymodel.js:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 /* eslint-disable */ import { routerRedux } from \u0026#39;dva/router\u0026#39;; import { message, Modal } from \u0026#39;antd\u0026#39;; import { getGoodsInfo, getAllGoods, } from \u0026#39;../services/api\u0026#39;; import { trim, getCookie } from \u0026#39;../utils/utils\u0026#39;; export default { namespace: \u0026#39;mymodel\u0026#39;, state: { form: {}, list: [], listDetail: [], goodsList: [], goodsListDetail: [], pagination: { pageSize: 10, total: 0, current: 1, }, imgList: [], //图片 }, subscriptions: { setup({ dispatch, history }) { history.listen(location =\u0026gt; { if (location.pathname !== \u0026#39;/xx/xxx\u0026#39;) return; if (!location.state || !location.state.xxxId) return; dispatch({ type: \u0026#39;fetch\u0026#39;, payload: location.state, }); }); }, }, effects: { // 接口。获取所有工厂店的列表 (步骤02) *getAllInfo({ payload }, { select, call, put }) { yield put({ type: \u0026#39;form\u0026#39;, payload, }); console.log(\u0026#39;params:\u0026#39; + JSON.stringify(payload)); let params = {}; params = payload; const response = yield call(getGoodsInfo, params); console.log(\u0026#39;smyhvae response:\u0026#39; + JSON.stringify(response)); if (response.error) return; yield put({ type: \u0026#39;allInfo\u0026#39;, payload: (response.data \u0026amp;\u0026amp; response.data.map(item =\u0026gt; ({ xx1: item.yy1, xx2: item.yy2, }))) || [], }); // response 里包含了接口返回给前端的默认图片数据 if (response \u0026amp;\u0026amp; response.data \u0026amp;\u0026amp; response.data[0] \u0026amp;\u0026amp; response.data[0].my_jpg) { let tempImgList = response.data[0].my_jpg.split(\u0026#39;,\u0026#39;); let imgList = []; if (tempImgList.length \u0026gt; 0) { tempImgList.forEach(item =\u0026gt; { imgList.push({ uid: item, name: \u0026#39;xxx.png\u0026#39;, status: \u0026#39;done\u0026#39;, thumbUrl: item, }); }); } // 通过 redux的方式 将 默认图片 传给 imgList console.log(\u0026#39;smyhvae payload imgList:\u0026#39; + JSON.stringify(imgList)); yield put({ type: \u0026#39;setImgList\u0026#39;, payload: imgList, }); } }, *setImgList({ payload }, { call, put }) { console.log(\u0026#39;model setImgList\u0026#39;); yield put({ type: \u0026#39;getImgList\u0026#39;, payload, }); }, }, reducers: { allInfo(state, action) { return { ...state, list: action.payload, }; }, getImgList(state, action) { return { ...state, imgList: action.payload, }; }, }, }; 上面的代码，可以规避 upload 组件的一些bug；而且可以在上传前，通过校验图片的尺寸、大小等，如果不满足条件，则弹出modal弹窗，阻止上传。\n大功告成。本文感谢 ld 同学的支持。\n更多内容，可以看本人的另外一篇文章：\nAntD框架的upload组件上传图片时使用customRequest方法自定义上传行为 其他问题 beforeUpload返回false后，文件仍然为上传中的状态 最后一段 有人说，前端开发，连卖菜的都会。可如果真的遇到技术难题，还是得找个靠谱的前端同学才行。这不，来看看前端码农日常：\n","date":"2026-02-11T14:00:00+08:00","permalink":"https://impself.github.io/p/antd-upload-pitfalls/","title":"React 09-AntD Upload 踩坑记录"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\nandt 的介绍 Ant Design 是基于 React 实现，开发和服务于企业级后台产品。\n支持环境 现代浏览器和 IE9 及以上（需要 polyfills）。\n支持服务端渲染。\nElectron\nElectron（原名为Atom Shell）是GitHub开发的一个开源框架。 它允许使用Node.js（作为后端）和Chromium（作为前端）完成桌面GUI应用程序的开发。\n很多客户端软件都是基于 Electron 开发的。比如 VS Code。我们打开 VS Code 菜单栏的 “帮助 \u0026ndash;\u0026gt; 切换开发人员工具”，就会看到类似于 chrome的调试工具。\n相关链接 官方文档：https://ant.design/docs/react/introduce-cn andt 的使用 环境安装 1 npm install antd --save 代码示例 我们需要什么组件，就导入该组件即可。\n（1）index.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;ie=edge\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 容器，通过 React 渲染得到的 虚拟DOM，会呈现到这个位置 --\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; （2）main.js:\n1 2 3 4 5 6 7 8 9 // JS打包入口文件 // 1. 导入包 import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom\u0026#34;; import MyComponent from \u0026#34;./components/MyComponent.jsx\u0026#34;; // 使用 render 函数渲染 虚拟DOM ReactDOM.render(\u0026lt;MyComponent\u0026gt;\u0026lt;/MyComponent\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;)); (3)MyComponent.jsx:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React from \u0026#34;react\u0026#34;; // 导入 日期选择组件 import { DatePicker } from \u0026#34;antd\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;在组件中引入 andt\u0026lt;/h3\u0026gt; \u0026lt;DatePicker /\u0026gt; \u0026lt;/div\u0026gt; ); } } 代码运行效果：\nAntD组件 表格 pagination属性可以用来分页。\nloading框 需求：在数据显示之前，展示 loading；在数据显示之后，关闭loading。\n相关问题的链接 AntD pro，跳转到详情页，携带参数 ant design列表页，转跳到详情页，携带参数\nant design pro商品页带参数转到详情页\nAntD pro ，必填项前面，显示星号 表单必填项label上的红色*号是怎么出现的 其他问题 面包屑层级显示问题：https://github.com/ant-design/ant-design-pro/issues/1584\nfrom验证input框只能输入数字：https://blog.csdn.net/zr15829039341/article/details/82745239\n","date":"2026-02-11T13:00:00+08:00","permalink":"https://impself.github.io/p/react-ant-design-basics/","title":"React 08-Ant Design 基本使用"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\nReact路由的使用 使用React路由之前，我们需要先安装 react-router-dom这个包。比如：\n1 yarn add react-router-dom 代码举例：\n（1）index.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;ie=edge\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 容器，通过 React 渲染得到的 虚拟DOM，会呈现到这个位置 --\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; （2）main.js：\n1 2 3 4 5 6 7 8 9 // JS打包入口文件 // 1. 导入包 import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom\u0026#34;; import App from \u0026#34;./App.jsx\u0026#34;; // 使用 render 函数渲染 虚拟DOM ReactDOM.render(\u0026lt;App /\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;)); （3）app.jsx:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import React from \u0026#34;react\u0026#34;; // 如果要使用 路由模块，第一步，运行 yarn add react-router-dom // 第二步，导入 路由模块 // HashRouter 表示一个路由的跟容器，将来，所有的路由相关的东西，都要包裹在 HashRouter 里面，而且，一个网站中，只需要使用一次 HashRouter 就好了； // Route 表示一个路由规则， 在 Route 上，有两个比较重要的属性， path component // Link 表示一个路由的链接 ，就好比 vue 中的 \u0026lt;router-link to=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/router-link\u0026gt; import { HashRouter, Route, Link } from \u0026#34;react-router-dom\u0026#34;; import Home from \u0026#34;./components/Home.jsx\u0026#34;; import Movie from \u0026#34;./components/Movie.jsx\u0026#34;; import About from \u0026#34;./components/About.jsx\u0026#34;; export default class App extends React.Component { constructor(props) { super(props); this.state = {}; } render() { // 当 使用 HashRouter 把 App 根组件的元素包裹起来之后，网站就已经启用路由了 // 在一个 HashRouter 中，只能有唯一的一个根元素 // 在一个网站中，只需要使用 唯一的一次 \u0026lt;HashRouter\u0026gt;\u0026lt;/HashRouter\u0026gt; 即可 return ( \u0026lt;HashRouter\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;这是网站的APP根组件\u0026lt;/h1\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;Link to=\u0026#34;/home\u0026#34;\u0026gt;首页\u0026lt;/Link\u0026gt;\u0026amp;nbsp;\u0026amp;nbsp; \u0026lt;Link to=\u0026#34;/movie\u0026#34;\u0026gt;电影\u0026lt;/Link\u0026gt;\u0026amp;nbsp;\u0026amp;nbsp; \u0026lt;Link to=\u0026#34;/about\u0026#34;\u0026gt;关于\u0026lt;/Link\u0026gt; \u0026lt;hr /\u0026gt; {/* Route 创建的标签，就是路由规则，其中 path 表示要匹配的路由，component 表示要展示的组件 */} {/* 在 vue 中有个 router-view 的路由标签，专门用来放置，匹配到的路由组件的，但是，在 react-router 中，并没有类似于这样的标签，而是 ，直接把 Route 标签，当作的 坑（占位符） */} {/* Route 具有两种身份：1. 它是一个路由匹配规则； 2. 它是 一个占位符，表示将来匹配到的组件都放到这个位置 */} \u0026lt;Route path=\u0026#34;/home\u0026#34; component={Home} /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;Route path=\u0026#34;/movie\u0026#34; component={Movie} /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;Route path=\u0026#34;/about\u0026#34; component={About} /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/HashRouter\u0026gt; ); } } （4）ReactDemo/src/components/Home.jsx\n1 2 3 4 5 6 7 8 9 10 11 12 import React from \u0026#34;react\u0026#34;; export default class Home extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return \u0026lt;div\u0026gt;Home组件\u0026lt;/div\u0026gt;; } } （5）ReactDemo/src/components/Movie.jsx\n1 2 3 4 5 6 7 8 9 10 11 12 13 import React from \u0026#34;react\u0026#34;; export default class Movie extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return \u0026lt;div\u0026gt;Movie组件\u0026lt;/div\u0026gt;; } } （6）ReactDemo/src/components/About.jsx\n1 2 3 4 5 6 7 8 9 10 11 12 import React from \u0026#34;react\u0026#34;; export default class About extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return \u0026lt;div\u0026gt;About组件\u0026lt;/div\u0026gt;; } } 运行结果：\n匹配路由参数 模糊匹配与精准匹配 我们在上面的代码中，进一步修改。假设 Movie 这个组件修改成这种路由匹配方式：\n1 2 3 \u0026lt;Link to=\u0026#34;/movie/top250\u0026#34;\u0026gt;电影\u0026lt;/Link\u0026gt; \u0026lt;Route path=\u0026#34;/movie\u0026#34; component={Movie} /\u0026gt; 上面这种匹配方式，也是可以成功匹配到的。这是为啥呢？\n这是因为：默认情况下，路由中的匹配规则，是模糊匹配的。如果 路由可以部分匹配成功，就会展示这个路由对应的组件。\n如果想让路由规则，进行精确匹配，可以为Route添加 exact 属性。比如下面这种写法，因为是开启了精准匹配，所以是匹配不到的：（无法匹配）\n1 2 3 \u0026lt;Link to=\u0026#34;/movie/top250/20\u0026#34;\u0026gt;电影\u0026lt;/Link\u0026gt; \u0026lt;Route path=\u0026#34;/movie/\u0026#34; component={Movie} exact/\u0026gt; 另外，如果要匹配参数，可以在匹配规则中，使用 : 修饰符，表示这个位置匹配到的是参数。举例如下：（匹配正常）\n1 2 3 \u0026lt;Link to=\u0026#34;/movie/top250/20\u0026#34;\u0026gt;电影\u0026lt;/Link\u0026gt;\u0026amp;nbsp;\u0026amp;nbsp; \u0026lt;Route path=\u0026#34;/movie/:type/:id\u0026#34; component={Movie} exact/\u0026gt; 获取路由参数 继续修改上面的代码。如果我想在 Movie 组件中显示路由中的参数，怎么做呢？\n我们可以通过 props.match.params获取路由中的参数。举例做法如下：\napp.jsx中的匹配规则如下：\n1 2 3 \u0026lt;Link to=\u0026#34;/movie/top100/5\u0026#34;\u0026gt;电影\u0026lt;/Link\u0026gt;\u0026amp;nbsp;\u0026amp;nbsp; \u0026lt;Route path=\u0026#34;/movie/:type/:id\u0026#34; component={Movie} exact/\u0026gt; Moivie 组件的写法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React from \u0026#34;react\u0026#34;; export default class Movie extends React.Component { constructor(props) { super(props); this.state = { routeParams: props.match.params // 把路由中的参数保存到 state 中 }; } render() { console.log(this); // 如果想要从路由规则中，提取匹配到的参数，进行使用，可以使用 this.props.match.params.*** 来访问 return ( \u0026lt;div\u0026gt; {/* Movie --- {this.props.match.params.type} --- {this.props.match.params.id} */} Movie --- {this.state.routeParams.type} --- {this.state.routeParams.id} \u0026lt;/div\u0026gt; ); } } 打印结果如下：\n工程文件：2019-02-14-ReactDemo.zip\n","date":"2026-02-11T12:00:00+08:00","permalink":"https://impself.github.io/p/react-router-usage/","title":"React 07-路由使用"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\n单项数据绑定 在 Vue 中，可以通过 v-model 指令来实现双向数据绑定。但是，在 React 中并没有指令的概念，而且 React 默认不支持 双向数据绑定。\nReact 只支持，把数据从 state 上传输到 页面，但是，无法自动实现数据从 页面 传输到 state 中 进行保存。\nReact中，只支持单项数据绑定，不支持双向数据绑定。不信的话，我们来看下面这个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React from \u0026#34;react\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是 MyComponent 组件 默认的msg\u0026#34; }; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h3\u0026gt;呵呵哒\u0026lt;/h3\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; value={this.state.msg} /\u0026gt; \u0026lt;/div\u0026gt; ); } } 上方代码中，我们尝试在 input文本框中读取 state.msg 的值，运行结果中，却弹出了警告：\n1 Warning: Failed prop type: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`. 通过onChange方法，实现双向数据绑定 如果针对 表单元素做 value 属性绑定，那么，必须同时为 表单元素 绑定 readOnly, 或者提供 onChange 事件：\n如果是绑定readOnly，表示这个元素只读，不能被修改。此时，控制台就不会弹出警告了。\n如果是绑定onChange，表示这个元素的值可以被修改，但是，要自己定义修改的逻辑。\n绑定readOnly的举例如下：（表示value中的数据是只读的）\n1 \u0026lt;input type=\u0026#34;text\u0026#34; value={this.state.msg} readOnly /\u0026gt; 绑定 onChange 的举例如下：（通过onChange方法，实现双向数据绑定）\n(1)index.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;ie=edge\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 容器，通过 React 渲染得到的 虚拟DOM，会呈现到这个位置 --\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; （2）main.js:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // JS打包入口文件 // 1. 导入包 import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom\u0026#34;; // 导入组件 import MyComponent from \u0026#34;./components/MyComponent.jsx\u0026#34;; // 使用 render 函数渲染 虚拟DOM ReactDOM.render( \u0026lt;div\u0026gt; \u0026lt;MyComponent\u0026gt;\u0026lt;/MyComponent\u0026gt; \u0026lt;/div\u0026gt;, document.getElementById(\u0026#34;app\u0026#34;) ); （3）components/MyComponent.jsx\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import React from \u0026#34;react\u0026#34;; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: \u0026#34;这是组件 默认的msg\u0026#34; }; } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;呵呵哒\u0026lt;/h1\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; value={this.state.msg} onChange={this.txtChanged} ref=\u0026#34;txt\u0026#34; /\u0026gt; \u0026lt;h3\u0026gt;{\u0026#34;实时显示msg中的内容：\u0026#34; + this.state.msg}\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; ); } // 为 文本框 绑定 txtChanged 事件 txtChanged = (e) =\u0026gt; { // 获取 \u0026lt;input\u0026gt; 文本框中 文本的3种方式： // 方式一：使用 document.getElementById // 方式二：使用 ref // console.log(this.refs.txt.value); // 方式三：使用 事件对象的 参数 e 来拿 // 此时，e.target 就表示触发 这个事件的 事件源对象，得到的是一个原生的JS DOM 对象。在这个案例里，e.target就是指文本框 // console.log(e.target.value); this.setState({ msg: e.target.value }); }; } 工程文件：2019-02-13-ReactDemo.zip\n","date":"2026-02-11T11:00:00+08:00","permalink":"https://impself.github.io/p/react-one-way-data-binding/","title":"React 06-单向数据绑定"},{"content":"\n本文导读 这篇文章按照「先看场景、再跑代码、最后总结坑点」的顺序来写。建议你边读边敲，遇到概念先不死记，先看它在代码里解决了什么问题。\n虚拟DOM和diff算法 在学习 React 之前，我们需要先了解两个概念：虚拟DOM、diff算法。\n虚拟DOM 问题描述：\n假设我们的数据发生一点点的变化，也会被强制重建整颗DOM树，这么做，会涉及到很多元素的重绘和重排，导致性能浪费严重。\n解决上述问题的思路：\n实现按需更新页面上的元素即可。也就是说，把需要修改的元素，所对应的 DOM 元素重新构建；其他没有变化的数据，所对应的 DOM 节点不需要被强制更新。\n具体实现方案：（如何按需更新页面上的元素）\n只需要拿到 页面更新前的 内存中的DOM树，同时再拿到 页面更新前的 新渲染出来的内存DOM树；然后，对比这两颗新旧DOM树，找到那些需要被重新创建和修改的元素即可。这样就能实现 DOM 的按需更新。\n如何拿到这两棵DOM树：（即：如何从浏览器的内存住哪个获取到 浏览器私有的那两颗DOM树？）\n如果要拿到浏览器私有的DOM树，那我们必须调用浏览器提供的相关JS的API才行。但是问题来了，浏览器并没有提供这样的API。既然如此，那我们可以自己模拟这两颗 新旧DOM树。\n如何自己模拟这两颗 新旧DOM树：（即：如何自己模拟一个DOM节点？）\n这里涉及到手动模拟DOM树的原理：使用 JS 创建一个对象，用和这个对象来模拟每一个DOM节点；然后在每个DOM节点中，又提供了类似于 children 这样的属性来描述当前DOM的子节点。这样的话，当DOM节点形成了嵌套关系，就模拟出了一颗 DOM 树。\n总结：\n虚拟DOM的本质：使用 JS 对象模拟DOM树。\n虚拟DOM的目的：为了实现 DOM 节点的高效更新。\nReact内部已经帮我们实现了虚拟DOM，初学者掌握如何调用即可。\ndiff算法 怎么实现 两颗新旧DOM树的对比 呢？这里就涉及到了 diff算法。常见的 diff算法如下：\ntree diff：新旧DOM树，逐层对比的方式，就叫做 tree diff。每当我们从前到后，把所有层的节点对比完后，必然能够找到那些 需要被更新的元素。\ncomponent diff：在对比每一层的时候，组件之间的对比，叫做 component diff。当对比组件的时候，如果两个组件的类型相同，则暂时认为这个组件不需要被更新，如果组件的类型不同，则立即将旧组件移除，新建一个组件，替换到被移除的位置。\nelement diff：在组件中，每个元素之间也要进行对比，那么，元素级别的对比，叫做 element diff。\nkey：key这个属性，可以把 页面上的 DOM节点 和 虚拟DOM中的对象，做一层关联关系。\nReact 介绍 React 是什么 Facebook 开源的一个JS库。\n一个用于动态构建用户界面的JS库。\nReact 的特点 Declarative（声明式编码）\nComponent-Based（组件化编码）\nLearn Once, Write Anywhere（支持客户端、服务器端渲染）\n高效的DOM Diff算法，最小化页面重绘\n单向数据流\nReact高效的原因 虚拟(virtual)DOM，不总是直接操作DOM\n高效的DOM Diff算法，最小化页面重绘（即“局部渲染”）。\n虚拟DOM指的是：在真实DOM的上一层映射一层虚拟DOM。我们操作的是映射关系，而不是真实的DOM。假设页面的样式做了修改（比如新增了一个标签），此时修改的是虚拟DOM的样式，真实的DOM并未发生变化。那什么时候，真实的DOM会发生变化呢？ 当我把所有的内容操作完之后，转化为真实的DOM，此时要打包统一的渲染页面，于是真实的DOM发生变化，然后渲染一次。 这样做的话，可以减少页面的渲染次数。\n相关网址 官网：https://reactjs.org/\nGitHub 地址：https://github.com/facebook/react 截至2019-02-08，React项目已经有 121k 的star。\n有一个特性是“Learn Once, Write Anywhere”。这里的 “Anywhere” 其实指的是两个地方：一个是浏览器端，一个是服务器端。后者指的是，React支持在服务器端渲染页面。\n生态介绍 Vue生态：Vue + Vue-Router + Vuex + Axios + Babel + Webpack\nReact生态：React + React-Router + Redux + Axios + Babel + Webpack\nReact 模块化、组件化 模块 理解：向外提供特定功能的js程序, 一般就是一个js文件\n理由：js代码更多更复杂\n作用：简化js的编写，阅读，提高运行效率\n组件 理解：用来实现特定功能效果的代码集合(html/css/js)\n理由：一个界面的功能更复杂\n作用：复用，简化项目编码，提高运行效率\n模块化与组件化 模块化：当应用的js都以模块来编写的, 这个应用就是一个模块化的应用\n组件化：当应用是以多组件的方式实现功能, 这上应用就是一个组件化的应用\n面相对象与面向过程的区别 面向对象编程：\n重点是对象\n更加关心的是干活的人\n面向过程编程：\n更加关心的是干活的过程\n谁去干活儿不关心\nReact 环境搭建：写第一个Hello World react.js 和 react-dom.js 为了通过 React 写一个Hello World程序，我们需要先安装几个包：\nreact.js: React的核心库。这个包，是专门用来创建React组件、组件生命周期等。\nreact-dom.js: 操作DOM的扩展库。这个包，主要封装了和 DOM 操作相关的包（比如，把组件渲染到页面上）。\nbabel.min.js: 将 JSX语法 解析为 纯JS语法代码。\n方式一：本地引入相关的js库 入门的时候，我们建议采取方式一。\n如果是本地引入的话，可以这样写：\n1 2 3 4 \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 如果是通过CDN的方式引入的话，可以使用网站 https://www.bootcdn.cn/ 提供的CDN链接。\n完整代码举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;meta /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- 引入React相关的js库 --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/react-dom.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;./libs/babel.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;myContainer\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 注意，这一行的 type 是写 \u0026#34;text/babel\u0026#34;，而不是 \u0026#34;text/javascript\u0026#34; --\u0026gt; \u0026lt;script type=\u0026#34;text/babel\u0026#34;\u0026gt; //页面中的真实容器元素 var containDiv = document.getElementById(\u0026#34;myContainer\u0026#34;); //1、创建虚拟DOM对象 var vDom = \u0026lt;div\u0026gt;Hello, React!\u0026lt;/div\u0026gt;; // 不是字符串, 不能加引号 //2、渲染虚拟DOM对象（将虚拟DOM对象渲染到页面元素中） ReactDOM.render(vDom, containDiv); // 参数1：虚拟DOM对象；参数2：页面中的容器 \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 代码运行后，页面上的DOM结构如下：\n1 2 3 \u0026lt;div id=\u0026#34;myContainer\u0026#34;\u0026gt; \u0026lt;div\u0026gt;Hello, React!\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 代码解释：\nrender的中文含义是“渲染”。render 方法的语法如下：\n1 ReactDOM.render(要渲染的虚拟DOM对象, 容器 container：要渲染到页面上的哪个位置); 方式二：npm install 实际开发中，我们一般都是通过 npm install 的方式来安装 react 相关的包。\n首先，新建一个空的文件夹2019-02-08-ReactDemo，作为项目的根目录。然后在根目录下执行如下命令，进行项目初始化：\n1 npm init --yes 上方命令执行完成后，会生成package.json文件。\n然后继续执行如下命令，安装 react.js 和 react-dom.js 这两个包：\n1 npm i react react-dom 完整代码举例：\nindex.html:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;React Demo\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 打包工具输出的入口脚本（例如 webpack 打包到 dist/bundle.js） --\u0026gt; \u0026lt;script src=\u0026#34;./dist/bundle.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; main.js:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // JS打包入口文件 import React from \u0026#39;react\u0026#39; import ReactDOM from \u0026#39;react-dom\u0026#39; // 在 react 中，如要要创建 DOM 元素，只能使用 React 提供的 JS API 来创建，不能【直接】像 Vue 中那样，手写 HTML 元素 // React.createElement() 方法，用于创建 虚拟DOM 对象，它接收 3个及以上的参数 // 参数1： 是个字符串类型的参数，表示要创建的元素类型 // 参数2： 是一个属性对象，表示 创建的这个元素上，有哪些属性 // 参数3： 从第三个参数的位置开始，后面可以放好多的虚拟DOM对象，这写参数，表示当前元素的子节点 // \u0026lt;div title=\u0026#34;this is a div\u0026#34; id=\u0026#34;mydiv\u0026#34;\u0026gt;这是一个div\u0026lt;/div\u0026gt; var myDiv = React.createElement(\u0026#39;div\u0026#39;, { title: \u0026#39;this is a div\u0026#39;, id: \u0026#39;mydiv\u0026#39; }, \u0026#39;这是一个div\u0026#39;); // ReactDOM.render(\u0026#39;要渲染的虚拟DOM元素\u0026#39;, \u0026#39;要渲染到页面上的哪个位置\u0026#39;); ReactDOM.render(myDiv, document.getElementById(\u0026#39;app\u0026#39;)); 上方代码中，createElement()方法介绍如下：\n1 React.createElement(需要创建的元素类型, 有哪些属性, 子节点) ","date":"2026-02-11T00:00:00Z","permalink":"https://impself.github.io/p/react-introduction/","title":"React 01-React介绍"}]