MVP 不是将就,是一种更聪明的开发顺序

作为工程师,我们天然喜欢“做完整”: 设计优雅的架构、抽象通用组件、配齐鉴权、CI/CD、一套完善的设计系统……结果上线时间一拖再拖。
MVP(Minimum Viable Product,最小可行产品)这套思维,本质上是帮你用更小的代价验证一个想法值不值得继续投。
对开发者来说,它不是“写烂一点代码”,而是“在正确的时间做刚刚好的工程决策”。
先说清楚:什么是 MVP?
比较正式一点的定义是: MVP 是一个功能最小、但足以交付给真实用户使用,并用来收集反馈、验证假设的产品版本。
这里有三个关键信息:
它是真正可用的产品,而不是原型图或 PPT。
目标是“学习”和“验证”,不是追求一次性做成完美成品。
它应该以最小的实现成本,获得尽可能多的用户行为反馈。
简化成一句话:用最小的一坨代码,查清楚一件事——这个东西有没有人真正想用。
MVP 开发的核心思维
站在工程视角,可以把 MVP 思维拆成几步:
明确要验证的“一个问题”
例如: “用户是否愿意用一个极简在线记事工具,而不是继续微信发自己、发邮箱。”
只保留验证这个问题必须的功能
创建笔记、列表展示、删除一条——够了。
用你最快的路径实现
不一定是“最潮技术栈”,而是你最熟的那一套。
在最小范围上线,观察真实使用
先丢给身边的同事、朋友,再考虑公开发布。
基于行为数据迭代,而不是基于脑补加需求
看大家是不是会回来继续用,而不是听他们说“不错不错”。
一个简单对比:工程师常见两条路线
非 MVP 路线(也就是我们经常走的那条):
上来设计完整数据模型和复杂关系
规划多角色、多权限体系
搭完整鉴权、支付、通知、日志、监控
做完 UI 组件库和主题系统
半年后才准备打开 /signup 页面给别人看
MVP 路线:
数据模型只保留核心表
先不做注册登录,用简单方式区分用户或干脆单用户
一个列表页 + 一个创建页
一周内丢给 5–10 个真实用户用用看
架构可以后面补回来,但如果没人愿意用,你就少搭了一整座“空城”。
实战:用 Next.js 做一个记事类 MVP
下面用一个“在线笔记 / 备忘录”作为示例,用 Next.js(App Router)走完一次 MVP 思路。
1. 先定义 MVP 范围
需求刻意压到很小:
用户可以在浏览器里输入一条文本笔记
点击保存后,页面上能看到这条笔记
可以删除某条笔记
数据存储先用内存或简单 JSON 文件(甚至浏览器 localStorage 也行)
暂时不做的东西:
用户注册 / 登录
标签、搜索、富文本、排序
多端同步、移动端适配
精美 UI
这样一个范围,对任何熟悉 React 的开发者来说,1–2 天就可以搞出一个可用版本。
2. 项目脚手架与基本结构
用官方文档推荐方式创建 App Router 项目。
在终端中:
npx create-next-app@latest note-mvp
# 或者选择 TypeScript、App Router 等默认选项
cd note-mvp
npm run dev目录结构中,app/ 目录是核心。 [nextjs](https://nextjs.org/docs/app)
我们可以这样安排最小结构:
app/page.tsx:首页,显示笔记列表和创建表单app/api/notes/route.ts:一个简单 API,用于创建 / 获取 / 删除笔记(使用内存或简易存储) [nextjs](https://nextjs.org/docs/app)
3. 用 App Router 写一个极简 API
在 app/api/notes/route.ts 中,先用内存数组模拟存储(真实环境你可以换成 SQLite / Supabase 等)。
// app/api/notes/route.ts
import { NextResponse } from 'next/server'
type Note = {
id: string
content: string
createdAt: string
}
let notes: Note[] = []
export async function GET() {
return NextResponse.json(notes)
}
export async function POST(request: Request) {
const { content } = await request.json()
if (!content || typeof content !== 'string') {
return new NextResponse('Invalid content', { status: 400 })
}
const note: Note = {
id: crypto.randomUUID(),
content,
createdAt: new Date().toISOString(),
}
notes.unshift(note)
return NextResponse.json(note, { status: 201 })
}
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) {
return new NextResponse('Missing id', { status: 400 })
}
notes = notes.filter((n) => n.id !== id)
return new NextResponse(null, { status: 204 })
}这段代码有很多“可以优化”的点,比如:
数据只在内存里,进程重启就没了
没有并发安全,也没有鉴权
但在 MVP 阶段,它已经满足了“验证:有人愿意写点什么并回来看”的目标。
4. 写一个简单的页面
在 app/page.tsx 中,用 Client Component 处理交互。
'use client'
import { useEffect, useState } from 'react'
type Note = {
id: string
content: string
createdAt: string
}
export default function Home() {
const [notes, setNotes] = useState<Note[]>([])
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
fetch('/api/notes')
.then((res) => res.json())
.then((data) => setNotes(data))
}, [])
async function handleAdd(e: React.FormEvent) {
e.preventDefault()
if (!input.trim()) return
setLoading(true)
try {
const res = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: input.trim() }),
})
const note: Note = await res.json()
setNotes((prev) => [note, ...prev])
setInput('')
} finally {
setLoading(false)
}
}
async function handleDelete(id: string) {
await fetch(`/api/notes?id=${id}`, { method: 'DELETE' })
setNotes((prev) => prev.filter((n) => n.id !== id))
}
return (
<main style={{ maxWidth: 600, margin: '2rem auto', padding: '0 1rem' }}>
<h1>Minimal Notes MVP</h1>
<form onSubmit={handleAdd} style={{ marginBottom: '1rem' }}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
rows={3}
style={{ width: '100%', marginBottom: '0.5rem' }}
placeholder="写点什么..."
/>
<button type="submit" disabled={loading}>
{loading ? '保存中...' : '保存'}
</button>
</form>
<section>
{notes.length === 0 && <p>还没有任何笔记。</p>}
{notes.map((note) => (
<article
key={note.id}
style={{
border: '1px solid #ddd',
padding: '0.75rem',
marginBottom: '0.75rem',
}}
>
<p style={{ whiteSpace: 'pre-wrap' }}>{note.content}</p>
<small style={{ color: '#666' }}>
{new Date(note.createdAt).toLocaleString()}
</small>
<div>
<button
onClick={() => handleDelete(note.id)}
style={{ marginTop: '0.25rem' }}
>
删除
</button>
</div>
</article>
))}
</section>
</main>
)
}这个页面非常“朴素”:一个 textarea,一个按钮,一个列表。
但它已经完整打通了一个闭环:
用户能创建一条笔记
能看到自己的历史笔记
能删除不想要的
从 MVP 角度看,这已经足以拿给 5–10 个测试用户去试用,看看他们的真实行为。
这个 Next.js MVP 示例体现了哪些 MVP 思维?
围绕刚才这段实现,我们可以反过来看:
技术栈足够简单
没有额外引入状态管理库、UI 框架、复杂后端服务。
架构允许未来扩展
虽然现在用的是内存存储,但 API 路径已经固定,将来换成数据库只要改 route 里的实现即可。
功能只够用来验证一个核心问题
“有人会愿意在浏览器里写一条简短笔记,并在同一入口回来查看 / 删除吗?”
成本可控
以一个熟悉 Next.js 的工程师水平,这个 MVP 级实现一天内可以完成并部署到 Vercel。
从这个起点开始,你可以按用户行为来决定后续路线,例如:
如果用户大量反馈“想搜索”,再考虑加入搜索
如果大家抱怨“换设备就看不到”,再加持久化存储和登录
如果发现根本没人回来第二次,那可能需要回头审视需求本身
常见误区:把“工程完美”当成目标
在实际项目中,很多工程师会把这类 MVP 当成“临时玩具”,心里过不去:
“我不想写这种简陋实现,以后还要重构两次”
“不做鉴权上线太不专业了”
“不用某某最佳实践框架感觉不安心”
这里有两个现实考虑:
如果验证下来“没人用”,你已经省下了所有后续重构成本
如果验证下来“有人真用”,那你至少有一份真实的需求和行为数据,可以支持你去做工程级优化
MVP 并不是否定工程质量,而是把“高质量工程”放在需求被证明有效之后再做。
写在最后:给工程师的 MVP 心态
可以把 MVP 思维理解成一种工程风险管理:
在用户还没证明“值得你为 TA 搭城堡”之前,用一顶帐篷就够
你完全可以用 Next.js 这种现代框架搭出“帐篷”,而不是直接修一个城堡
对有经验的开发者来说,一个实用的做法是:
先用你最熟的栈(比如 Next.js + 一个简单数据库)做出一个“能跑的骨架”
找到 3–10 个真实用户,观察他们一个月的行为
只为“被真实行为证明重要的东西”做架构升级
在 Google 上继续关注
把 HeyBinyang 添加为 Google 首选来源
如果你愿意继续在 Google 里读到我的更新,可以把这个站点添加为 preferred source,之后更容易在相关内容场景里看到它。
SHARE
分享
分享这篇文章。