技术0 阅读

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

作为工程师,我们天然喜欢“做完整”: 设计优雅的架构、抽象通用组件、配齐鉴权、CI/CD、一套完善的设计系统……结果上线时间一拖再拖。

MVP(Minimum Viable Product,最小可行产品)这套思维,本质上是帮你用更小的代价验证一个想法值不值得继续投

对开发者来说,它不是“写烂一点代码”,而是“在正确的时间做刚刚好的工程决策”。

先说清楚:什么是 MVP?

比较正式一点的定义是: MVP 是一个功能最小、但足以交付给真实用户使用,并用来收集反馈、验证假设的产品版本。

这里有三个关键信息:

简化成一句话:用最小的一坨代码,查清楚一件事——这个东西有没有人真正想用。

MVP 开发的核心思维

站在工程视角,可以把 MVP 思维拆成几步:

  1. 明确要验证的“一个问题”

    例如: “用户是否愿意用一个极简在线记事工具,而不是继续微信发自己、发邮箱。”

  2. 只保留验证这个问题必须的功能

    创建笔记、列表展示、删除一条——够了。

  3. 用你最快的路径实现

    不一定是“最潮技术栈”,而是你最熟的那一套。

  4. 在最小范围上线,观察真实使用

    先丢给身边的同事、朋友,再考虑公开发布。

  5. 基于行为数据迭代,而不是基于脑补加需求

    看大家是不是会回来继续用,而不是听他们说“不错不错”。

一个简单对比:工程师常见两条路线

非 MVP 路线(也就是我们经常走的那条):

MVP 路线

架构可以后面补回来,但如果没人愿意用,你就少搭了一整座“空城”。

实战:用 Next.js 做一个记事类 MVP

下面用一个“在线笔记 / 备忘录”作为示例,用 Next.js(App Router)走完一次 MVP 思路。

1. 先定义 MVP 范围

需求刻意压到很小:

暂时不做的东西:

这样一个范围,对任何熟悉 React 的开发者来说,1–2 天就可以搞出一个可用版本。

2. 项目脚手架与基本结构

用官方文档推荐方式创建 App Router 项目。

在终端中:

bash
npx create-next-app@latest note-mvp
# 或者选择 TypeScript、App Router 等默认选项
cd note-mvp
npm run dev

目录结构中,app/ 目录是核心。 [nextjs](https://nextjs.org/docs/app)

我们可以这样安排最小结构:

3. 用 App Router 写一个极简 API

app/api/notes/route.ts 中,先用内存数组模拟存储(真实环境你可以换成 SQLite / Supabase 等)。

ts
// 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 处理交互。

tsx
'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 思维理解成一种工程风险管理

对有经验的开发者来说,一个实用的做法是:

SHARE

分享

分享这篇文章。