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 思維理解成一種工程風險管理:
在用戶還沒證明「值得你為他蓋城堡」之前,用一頂帳篷就夠了
你完全可以用 Next.js 這種現代框架搭建出「帳篷」,而不是直接修一個城堡
對有經驗的開發者來說,一個實用的做法是:
先用你最熟的棧(比如 Next.js + 一個簡單資料庫)做出一個「能跑的骨架」
找到 3–10 個真實用戶,觀察他們一個月的行為
只為「被真實行為證明重要的東西」做架構升級
在 Google 上持續關注
把 HeyBinyang 加入 Google 首選來源
如果你希望之後在 Google 上更容易看到我的更新,可以把這個站點加入 preferred source,讓它在相關閱讀情境裡更容易被找到。
SHARE
分享
分享這篇文章。