Skip to content

3.7.4 实战演练:综合练习

本节提供四个综合练习,帮助你内化本章所学技能。建议认真完成每个练习,然后对照参考答案自评。

练习一:提示词改写

任务说明

下面有三个「差提示词」,请用 S.C.A.F.F. 框架改写成「好提示词」。

差提示词 1

帮我做一个登录页面

你的改写

(请先自己尝试改写,再看参考答案)

点击查看参考答案
markdown
【Situation - 情境】
我正在开发一个个人博客的管理后台。
技术栈:Next.js 14 + TypeScript + Tailwind CSS
当前进度:首页和文章列表页已完成,现在需要添加登录功能
设计风格:极简风格,参考 Notion 的登录页面

【Challenge - 挑战】
实现管理员登录页面:
- 表单包含:邮箱输入框、密码输入框、登录按钮
- 点击登录后,验证格式(邮箱格式、密码不为空)
- 格式错误时,在对应输入框下方显示红色提示
- 暂时不需要真正的后端验证,先做前端界面和交互

【Audience - 受众】
- 用户:只有我一个人(博客管理员)
- 代码维护者:我自己,React 初学者
- 请在关键逻辑处添加注释

【Format - 格式】
请提供:
1. 完整的登录页面组件代码(单文件)
2. 包含 TypeScript 类型定义
3. 样式使用 Tailwind CSS
4. 文件命名:LoginPage.tsx

【Foundations - 约束】
- 不使用任何 UI 组件库
- 不使用第三方表单库
- 状态管理用 React 原生的 useState
- 响应式设计:移动端和桌面端都要适配

评分标准

  • ✅ 有项目背景和技术栈(2分)
  • ✅ 任务描述具体,有输入输出(2分)
  • ✅ 说明了受众和技术水平(1分)
  • ✅ 指定了输出格式(2分)
  • ✅ 有明确的约束条件(2分)
  • ✅ 使用了框架结构(1分)

满分 10 分,8 分以上为优秀。

差提示词 2

帮我分析一下这段代码有什么问题

你的改写

点击查看参考答案
markdown
【当前代码】
```typescript
const fetchUsers = async () => {
  const response = await fetch('/api/users');
  const data = response.json();
  setUsers(data);
}

【问题现象】 调用这个函数后,users 状态是一个 Promise 对象而不是数组,导致渲染时报错。

【期望行为】 users 应该是从 API 获取的用户数组

【请帮我】

  1. 指出代码中的问题
  2. 解释为什么会出现这个问题
  3. 给出修正后的代码
  4. 说明如何避免类似错误

【关于我】 我是 JavaScript 初学者,对 async/await 还不太熟悉


**评分标准**:
- ✅ 提供了具体的代码(2分)
- ✅ 描述了问题现象(2分)
- ✅ 说明了期望行为(2分)
- ✅ 明确了需要什么帮助(2分)
- ✅ 说明了自己的技术水平(2分)

满分 10 分,8 分以上为优秀。

</details>


### 差提示词 3

我想做个记账软件


**你的改写**:

<details>
<summary>点击查看参考答案</summary>

```markdown
【Situation - 情境】
我想做一个个人记账网页应用,帮助自己追踪日常开销。
技术栈:React + TypeScript + Tailwind CSS
目标用户:我自己(月光族,想知道钱花哪了)
设计原则:极简,记一笔账不超过 5 秒

【Challenge - 挑战】
第一版(MVP)实现以下功能:
1. 快速记账:选择分类 + 输入金额 + 可选备注
2. 查看账单:按日期查看当天的所有记录
3. 月度汇总:显示当月总支出和各分类占比

【Audience - 受众】
- 用户:我自己,需要简单快捷
- 代码维护者:我自己,有一点 React 基础

【Format - 格式】
这是第一次对话,请先帮我:
1. 确认这个 MVP 范围是否合理
2. 建议项目的文件结构
3. 列出需要实现的组件清单

【Foundations - 约束】
- 数据先存在 localStorage,不需要后端
- 分类固定为:餐饮、交通、购物、娱乐、其他
- 只记支出,不记收入(简化)
- 不需要用户登录

【不做清单】
- 不做多账本
- 不做预算功能
- 不做数据导出
- 不做图表可视化(第二版考虑)

评分标准

  • ✅ 有完整的项目背景(2分)
  • ✅ MVP 功能范围明确(2分)
  • ✅ 有「不做清单」(2分)
  • ✅ 第一次对话请求合理(规划而非直接写代码)(2分)
  • ✅ 约束条件具体(2分)

满分 10 分,8 分以上为优秀。

练习二:PRD 编写实战

任务说明

假设你想做一个「读书笔记」工具,帮助自己记录读书心得。

请整合第二章的思维工具,完成以下内容:

  1. 用 JTBD 格式描述用户任务
  2. 用减法思维确定 MVP 范围(P0/P1/不做)
  3. 回答灵魂三问
  4. 写出一份精简版 PRD

你的答案

(请先自己完成,再看参考答案)

点击查看参考答案

1. JTBD 任务描述

当我读完一本书的某个章节时,我想快速记录关键观点和我的思考,这样我在需要回顾时能够快速找到这些笔记,而不是重新翻书。

2. MVP 范围

优先级功能原因
P0创建笔记(书名 + 章节 + 内容)核心功能
P0查看所有笔记列表核心功能
P0按书名筛选笔记找笔记必须快
P1编辑已有笔记经常需要修改补充
P1删除笔记清理无用笔记
不做笔记标签/分类增加复杂度,MVP 不需要
不做笔记分享个人使用,不需要
不做Markdown 编辑器简单文本足够
不做书籍信息自动获取手动输入书名即可

3. 灵魂三问

问题回答
用户是谁?我自己——喜欢读书但经常忘记书中内容的人
痛点在哪?读过的书记不住,需要时找不到当时的思考
为什么做这个?现有工具(Notion、备忘录)要么太重,要么不好按书检索

4. 精简版 PRD

markdown
# 读书笔记工具 PRD

## 项目背景

### 问题
我喜欢读书,但经常读完就忘。需要回顾时,要么重新翻书,要么在各种 app 里找当时的笔记,很费时间。

### JTBD
当我读完一本书的某个章节时,我想快速记录关键观点和我的思考,这样我在需要回顾时能够快速找到这些笔记。

## 目标用户

- 身份:喜欢读书的个人用户
- 场景:读完一章后记录、需要引用时检索
- 核心需求:快速记录 + 快速检索

## P0 功能

### 功能1:创建笔记
- 必填:书名、笔记内容
- 选填:章节名、页码
- 交互:打开页面直接可以开始写

### 功能2:查看笔记列表
- 按创建时间倒序显示
- 每条显示:书名、章节、内容摘要、时间
- 点击可查看完整内容

### 功能3:按书名筛选
- 页面顶部有书名筛选器
- 选择后只显示该书的笔记

## 技术方案

- 前端:React + TypeScript + Tailwind CSS
- 数据:localStorage(个人使用,不需要后端)
- 响应式:支持手机和电脑

## 不做清单

- 不做笔记标签/分类
- 不做笔记分享功能
- 不做 Markdown 编辑器
- 不做书籍封面/信息自动获取
- 不做多端同步

## 成功标准

自己使用 2 周,记录至少 10 条笔记,并成功检索到之前的笔记。

自评标准

  • ✅ JTBD 描述完整(场景 + 任务 + 价值)
  • ✅ MVP 范围有优先级,有「不做清单」
  • ✅ 灵魂三问都能回答
  • ✅ PRD 结构完整,AI 能理解执行

练习三:迭代对话模拟

任务说明

假设你让 AI 帮你实现「读书笔记」的创建功能,AI 给了你以下代码,但有问题。

请模拟 3 轮迭代对话,逐步修正问题。

AI 第一次输出

typescript
const CreateNote = () => {
  const [bookName, setBookName] = useState('');
  const [content, setContent] = useState('');

  const handleSubmit = () => {
    const note = { bookName, content };
    const notes = JSON.parse(localStorage.getItem('notes'));
    notes.push(note);
    localStorage.setItem('notes', JSON.stringify(notes));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={bookName} onChange={e => setBookName(e.target.value)} />
      <textarea value={content} onChange={e => setContent(e.target.value)} />
      <button type="submit">保存</button>
    </form>
  );
}

问题识别

请找出这段代码的问题,然后写出你的反馈。

点击查看问题分析和迭代示例

问题清单

  1. localStorage.getItem 可能返回 null:第一次使用时会报错
  2. 没有生成 note id:后续编辑/删除需要 id
  3. 没有记录创建时间:列表需要按时间排序
  4. 表单提交后没有清空输入:用户体验差
  5. 没有表单验证:空内容也能提交
  6. handleSubmit 没有阻止默认行为:表单会刷新页面

第一轮反馈

markdown
【问题】
1. localStorage.getItem('notes') 第一次会返回 null,导致 notes.push 报错
2. handleSubmit 没有 e.preventDefault(),表单会刷新页面

【原因】
localStorage 第一次使用时没有数据,JSON.parse(null) 会出错

【期望】
1. 处理 localStorage 为空的情况,初始化为空数组
2. 阻止表单默认提交行为

AI 第二次输出

typescript
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  const note = { bookName, content };
  const existingNotes = localStorage.getItem('notes');
  const notes = existingNotes ? JSON.parse(existingNotes) : [];
  notes.push(note);
  localStorage.setItem('notes', JSON.stringify(notes));
}

第二轮反馈

markdown
【问题】
1. 笔记没有 id,后续无法编辑或删除特定笔记
2. 没有创建时间,列表无法按时间排序

【期望】
每条笔记需要包含:
- id:使用 Date.now() 或 uuid
- createdAt:创建时间戳
- bookName:书名
- content:内容

AI 第三次输出

typescript
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  const note = {
    id: Date.now().toString(),
    bookName,
    content,
    createdAt: new Date().toISOString()
  };
  const existingNotes = localStorage.getItem('notes');
  const notes = existingNotes ? JSON.parse(existingNotes) : [];
  notes.push(note);
  localStorage.setItem('notes', JSON.stringify(notes));
}

第三轮反馈

markdown
【问题】
1. 保存成功后没有清空输入框
2. 没有表单验证,书名和内容为空也能提交
3. 没有保存成功的反馈提示

【期望】
1. 保存成功后清空 bookName 和 content
2. 书名和内容都不能为空(去除空格后判断)
3. 保存成功后显示简短提示(如 toast)

【示例】
保存成功后:
- 输入框清空
- 显示「笔记已保存」提示 2 秒后消失

最终代码

typescript
const CreateNote = () => {
  const [bookName, setBookName] = useState('');
  const [content, setContent] = useState('');
  const [showToast, setShowToast] = useState(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // 表单验证
    if (!bookName.trim() || !content.trim()) {
      return;
    }

    const note = {
      id: Date.now().toString(),
      bookName: bookName.trim(),
      content: content.trim(),
      createdAt: new Date().toISOString()
    };
    
    const existingNotes = localStorage.getItem('notes');
    const notes = existingNotes ? JSON.parse(existingNotes) : [];
    notes.push(note);
    localStorage.setItem('notes', JSON.stringify(notes));
    
    // 清空输入并显示提示
    setBookName('');
    setContent('');
    setShowToast(true);
    setTimeout(() => setShowToast(false), 2000);
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input 
          value={bookName} 
          onChange={e => setBookName(e.target.value)}
          placeholder="书名"
        />
        <textarea 
          value={content} 
          onChange={e => setContent(e.target.value)}
          placeholder="笔记内容"
        />
        <button type="submit">保存</button>
      </form>
      {showToast && <div className="toast">笔记已保存</div>}
    </>
  );
}

迭代要点

  • 每轮只反馈 2-3 个问题,不要一次性列出所有问题
  • 反馈格式:问题 + 原因 + 期望
  • 重要问题(如报错)优先处理

练习四:幻觉识别与修正

任务说明

假设 AI 给了你以下代码片段。请识别其中的幻觉问题,并说明如何验证和修正。

AI 输出

typescript
// AI 推荐使用 super-easy-storage 库来简化 localStorage 操作
import { createStore, useStore } from 'super-easy-storage';

const notesStore = createStore('notes', []);

const CreateNote = () => {
  const [notes, setNotes] = useStore(notesStore);
  
  // 使用 array.findLast() 获取最后一条笔记
  const lastNote = notes.findLast(note => note.bookName === currentBook);
  
  // 使用 React 18 的 useTransition 处理大列表
  const [isPending, startTransition] = useTransition();
  
  const handleSubmit = () => {
    startTransition(() => {
      setNotes([...notes, newNote]);
    });
  }
}

你的答案

请识别上述代码中的幻觉问题,并说明验证方法。

点击查看参考答案

幻觉识别

代码位置问题类型问题描述验证方法
super-easy-storage🔴 包幻觉这个包可能不存在npm search super-easy-storage 或访问 npmjs.com
array.findLast()🟡 API 幻觉ES2023 新 API,旧环境不支持查 MDN 文档确认兼容性
useTransition✅ 正确React 18 确实有这个 Hook但需确认项目用的是 React 18

验证步骤

1. 验证 super-easy-storage 包

bash
npm search super-easy-storage

结果:没有找到这个包。这是一个包幻觉

修正方案

  • 使用真实存在的库,如 zustandjotai
  • 或者自己封装一个简单的 localStorage hook

2. 验证 findLast() API

访问 MDN - Array.findLast()

结果:

  • ES2023 引入
  • Chrome 97+、Firefox 104+、Safari 15.4+
  • Node.js 18+

修正方案

typescript
// 兼容写法
const lastNote = [...notes].reverse().find(note => note.bookName === currentBook);

// 或添加 polyfill

3. 验证 useTransition

访问 React 文档 - useTransition

结果:React 18 确实有这个 Hook。

需要确认

  • 项目的 React 版本是否 >= 18
  • 当前场景是否真的需要 useTransition(这里可能是过度优化)

修正后的代码

typescript
// 使用 zustand 替代不存在的 super-easy-storage
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface NotesState {
  notes: Note[];
  addNote: (note: Note) => void;
}

const useNotesStore = create<NotesState>()(
  persist(
    (set) => ({
      notes: [],
      addNote: (note) => set((state) => ({ 
        notes: [...state.notes, note] 
      })),
    }),
    { name: 'notes-storage' }
  )
);

const CreateNote = () => {
  const { notes, addNote } = useNotesStore();
  
  // 使用兼容写法替代 findLast
  const lastNote = [...notes]
    .reverse()
    .find(note => note.bookName === currentBook);
  
  const handleSubmit = () => {
    addNote(newNote);
  }
}

验证习惯总结

  1. 遇到不熟悉的包:先 npm search 验证存在性
  2. 遇到不熟悉的 API:查阅 MDN 或官方文档
  3. 遇到新语法:确认项目环境是否支持
  4. 遇到「高级用法」:先验证是否真的需要

练习自评表

完成四个练习后,用这个表格自评:

练习完成情况主要收获需要加强的地方
练习一:提示词改写□ 完成 □ 部分完成 □ 未完成
练习二:PRD 编写□ 完成 □ 部分完成 □ 未完成
练习三:迭代对话□ 完成 □ 部分完成 □ 未完成
练习四:幻觉识别□ 完成 □ 部分完成 □ 未完成

本节要点

提示词改写:用 S.C.A.F.F. 框架把模糊需求转化为结构化提示词

PRD 编写:整合第二章思考成果,形成 AI 能理解的任务书

迭代对话:每轮反馈 2-3 个问题,格式为「问题 + 原因 + 期望」

幻觉识别:对不熟悉的包和 API,先验证再使用

恭喜你完成了第三章的所有学习内容!下一节我们将盘点你学到的技能,并为第四章的实战开发做准备。