ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

新项目启动如何快速搭建可运行原型?从线框到代码原型的落地路径

新项目启动如何快速搭建可运行原型?从线框到代码原型的落地路径 新项目启动会开了一轮又一轮PRD改了七八版代码却一行没写出来——这种场景我见了太多次。原因基本都一样大家都想让别人先动手都想在文档里把所有问题想清楚再开工。但真实的情况是想清楚这个状态永远等不来能把项目往前推动的恰恰是一个丑得不行但能跑的原型。今天这篇是新项目搭建系列的开篇核心话题就一个如何快速做出原型让一个模糊的想法变成团队能看懂、能讨论、能验证的东西。我会从原型设计的两种路线讲到OKR系统的代码原型实操再挖一下前端原型概念在真实项目里的用法最后聊聊原型阶段踩过的环境坑和向正式项目过渡的节奏。1. 新项目启动的正确姿势先让想法“落地”成原型一个项目从0到1最容易出问题的阶段往往不是写代码的时候而是什么都还没做的时候。需求方脑子里有个大概方向技术负责人心里有几个方案但大家互相之间的理解不一致又不好意思明说我其实也不太确定于是所有人都在等一个权威答案。原型就是用来打破这种僵局的。它不需要完美不需要覆盖所有边界只需要让一个想法可看见、可点击、可讨论。我把原型比作建筑里的第一版实体模型——你不会因为它丑就不盖楼但你一定希望在地基开工前就知道房间的布局是不是合理。1.1 原型的三种形态先搞清楚你要哪一种在动手之前先要搞清楚做原型这件事本身有几种做法因为很多人一开始就选错了方向低保真线框Wireframe本质是画图用方框和线条表达页面布局、信息层级、交互流程。工具可以是Figma、墨刀也可以是纸和笔。优点是快一个下午就能出整套草稿缺点是它不活没法真实操作。高保真交互原型Interactive Prototype在低保真的基础上加上真实配色、字体、细节样式并在工具里把页面之间的跳转关系连起来。它看起来几乎就像成品适合拿去给用户做可用性测试。缺点是制作成本不低容易让老板误以为产品已经做完了。可运行代码原型Code Prototype用真实技术栈写出来、能启动、能操作的前端页面数据先用写死的Mock数据撑着。它的价值在于提前验证技术路线打通数据流让后端、前端、产品在同一套代码上沟通。你可能注意到了我推荐的项目起步方式是先画线框再挑一小部分核心流程做成代码原型。画线框是为了快速达成方向共识代码原型是为了验证这个东西技术上到底能不能跑通。两者不是二选一而是先后关系。1.2 为什么推荐从代码原型入手解决大部分团队项目有人会问产品还没定稿写代码不是浪费吗问得好。这里的关键是你要控制代码原型的工作量。如果花两个月做一个完美的原型那叫浪费如果花两到三天做一个能演示核心流程的原型让项目从讨论模式切换到验证模式这比任何会议都高效。我经历过的、最成功的几次新项目启动都有一个共同特点在项目启动的头几天就有人拿出一个能跑的东西。哪怕它是硬编码的假列表哪怕样式只能看不能用它都让整个团队围绕一个真实存在的东西来讨论而不是围绕各自脑海里的想象来争论。2. 动手前的选择题明确范围别让原型变成失控的半成品确定要做可运行的代码原型之后第二个关键问题来了做多大做多细原型阶段最常见的失败不是做得太粗糙而是失控——开发者越写越顺手开始优化代码结构、做权限校验、做响应式适配、加单元测试。等抬头一看两个星期过去了原型还没跑起来。这里要先做一道针对原型的减法明确原型的原则是少而精。2.1 用一张页面清单圈定原型范围我建议动手前先列一张表把所有可能涉及的页面和功能写出来然后给每一项标记优先级。举个例子如果我们要做一个企业内部OKR系统页面清单可能是这样的页面/功能说明原型阶段是否要做登录/鉴权用户身份认证否原型阶段用假账号目标列表页展示当前周期的目标墙是目标详情页目标的KR拆解和进度明细是新建/编辑目标表单和字段校验是团队对齐视图跨部门目标对齐关系暂缓数据报表图表统计否等正式阶段个人中心头像、设置等否标记是的页面才是你接下来要做的核心范围。标记否的在原型里直接用静态占位页代替或者干脆不做。这张表看起来很朴素但它的价值在于防止两个常见问题一是做了太多不必要的东西二是团队内部对原型包含什么的预期不一致。2.2 代码原型的质量底线能跑、能演示、不怕丑做代码原型时我会给自己定三条质量标准第一能跑。也就是项目能一键启动不会有缺依赖、报错、白屏的问题。第二能演示。核心流程可以走通比如从创建目标到拆解KR再到更新进度这中间的数据跳动是真实可见的。第三不怕丑。样式的优先级非常低能用基础CSS保证阅读清晰就行别陷入调色调字距的兔子洞。在边界上还有一条重要原则原型阶段不做后端。所有数据都来自本地Mock所有状态都存在内存里或localStorage里。这样你不需要搭服务、设计数据库、处理接口鉴权可以把全部精力放在界面长得什么样、流程走得通走不通上。3. 以OKR系统为例从零搭出一个能跑的代码原型接下来进入实操环节。为了保证这条路线足够具体我以一个需求很典型的案例——企业OKR系统原型为例完整演示一遍。技术选型这里先说明一下这是一个比较常规的前端新项目场景我按当前社区最主流的组合来搭也就是Vite React TypeScript React Router。如果你团队用的是Vue或Next.js文章里的核心思路同样适用变的只是脚手架命令和文件写法。3.1 项目初始化用Vite快速拉起前端骨架先把目录建好。进入到你的工作目录执行pnpm create vite okr-prototype --template react-ts cd okr-prototype pnpm install这里我用了pnpm而不是npm主要原因是它的安装速度更快磁盘占用更小。如果你团队还没切换用npm的效果也完全一样——重点是把包管理器锁定下来别在一个项目里混用否则lock文件会互相冲突这在原型阶段最容易浪费无谓的时间。启动开发服务器确认一下pnpm dev看到Vite的默认页面后别急着往下写先把样板代码清理掉。打开项目里src目录删掉App.css、assets里的logo等内容把App.tsx精简成一个空壳import { BrowserRouter, Routes, Route } from react-router-dom; import ObjectiveListPage from ./pages/ObjectiveListPage; function App() { return ( BrowserRouter Routes Route path/ element{ObjectiveListPage /} / /Routes /BrowserRouter ); } export default App;同时装一下路由pnpm add react-router-dom dayjsdayjs是我做原型时的习惯依赖日期格式化时用它比手写Date方便太多装一次后面到处都用得上。3.2 数据模型先行把业务对象先画出来很多人做原型喜欢先写页面写到一半发现数据对不上又回头改。我个人的习惯是先把数据模型定义出来哪怕只是写在src/types里的几行TypeScript类型也能让后面的开发顺畅很多。OKR系统核心的对象就是这三个用户User、目标Objective、关键结果KeyResult。// src/types/index.ts export interface User { id: string; name: string; department: string; avatar?: string; } export interface KeyResult { id: string; title: string; objectiveId: string; targetValue: number; currentValue: number; unit: string; ownerId: string; } export interface Objective { id: string; userId: string; title: string; period: string; // 如 2025-Q1 status: ongoing | done | at_risk; krList: KeyResult[]; }这一步的价值不只是给后端或者数据库设计提供参考它更重要的是让项目里所有的对话有了共同语言。说到目标时大家知道它包含标题、周期和状态说到关键结果时大家知道它有目标值、当前值、负责人。代码原型到这一步基本就算定型了后续的正式项目大概率会沿用这套模型最多加一些字段。3.3 页面骨架从目标列表页开始有了数据模型就可以开始写页面了。原型阶段我喜欢按列表页 → 详情页 → 表单页的顺序推进因为列表页承担了系统最主要的入口功能先把入口做出来后面的页面就有了挂靠点。目标列表页的核心逻辑读取Mock数据渲染成卡片墙每个卡片显示标题、周期、进度并支持进入详情。// src/pages/ObjectiveListPage.tsx import { objectives } from ../mock/okrData; import ObjectiveCard from ../components/ObjectiveCard; function ObjectiveListPage() { return ( div classNameobjective-list header h12025 Q1 目标/h1 button新建目标/button /header div classNamecard-grid {objectives.map((obj) ( ObjectiveCard key{obj.id} objective{obj} / ))} /div /div ); } export default ObjectiveListPage;注意这里直接import了本地Mock数据而不是发一个HTTP请求。这是原型阶段最重要的决策之一用模块导入来代替API层。为什么要这样因为模块导入是同步的没有异步时序的问题没有跨域的问题刷新页面数据也不会丢。你写起来爽演示的时候点起来也爽。3.4 Mock数据的策略写死但别写进组件里刚才提到了Mock数据那具体应该放哪我的习惯是单独建一个src/mock/目录按照业务领域组织文件。// src/mock/okrData.ts import { Objective, User } from ../types; export const mockUsers: User[] [ { id: u1, name: 林悦, department: 产品部 }, { id: u2, name: 陈默, department: 技术部 }, ]; export const objectives: Objective[] [ { id: o1, userId: u1, title: Q1完成新版用户端改版, period: 2025-Q1, status: ongoing, krList: [ { id: kr1, title: 首页改版完成, objectiveId: o1, targetValue: 1, currentValue: 1, unit: 个, ownerId: u1, }, { id: kr2, title: 新用户注册转化率提升, objectiveId: o1, targetValue: 0.2, currentValue: 0.12, unit: %, ownerId: u2, }, ], }, // ... ];这里有一条我踩过坑的经验永远不要让组件直接从Mock文件里取数据中间必须过一层api目录。比如// src/api/objectiveApi.ts import { objectives } from ../mock/okrData; export const fetchObjectives () { return Promise.resolve(objectives); };页面里调用fetchObjectives()把它当成一个异步接口来用。这样做表面上看多此一举但它让原型到正式项目的过渡变得极其平滑——等后端接口真的就绪时你只需要修改api目录里的函数内部实现把Promise.resolve(objectives)换成fetch(/api/objectives)页面代码几乎一行都不用改。这就是小设计大节省的典型例子。3.5 建立一套全局状态让页面之间的数据联动原型往往不止一个页面。比如从列表页点进目标详情详情页还要读取目标的所有KR信息并且可能支持更新某个KR的进度。这时候如果你只在列表页拿到数据跳转时把整个对象通过useParams或URL参数传过去很快就乱套了。最轻量且好用的方案是用React自带的Context配合useReducer封装一个简单的Store。不需要引入Redux或Zustand原型阶段用框架自带的能力就够了。下面是一个简化的实现思路// src/store/OkrStore.tsx import { createContext, useContext, useReducer } from react; import { Objective } from ../types; type State { objectives: Objective[] }; type Action | { type: UPDATE_KR_PROGRESS; objectiveId: string; krId: string; value: number } | { type: CREATE_OBJECTIVE; objective: Objective }; const OkrContext createContextany(null); function reducer(state: State, action: Action): State { switch (action.type) { case UPDATE_KR_PROGRESS: return { objectives: state.objectives.map((obj) { if (obj.id ! action.objectiveId) return obj; return { ...obj, krList: obj.krList.map((kr) kr.id action.krId ? { ...kr, currentValue: action.value } : kr ), }; }), }; // ... } } export function OkrProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] useReducer(reducer, { objectives: [] }); return OkrContext.Provider value{{ state, dispatch }}{children}/OkrContext.Provider; } export const useOkrStore () useContext(OkrContext);这段代码最核心的意图是让你明白原型阶段也要保证数据修改会即时反映到界面上。如果只是做一个静态跳转那你验证的就不完整。当你在新建页提交一个新目标回到列表页能看到它这个闭环体验才是原型真正的价值所在。3.6 一次性跑通Demo验证最小闭环到这一步你应该能在本地跑通这样一个流程进入目标列表 → 看到现有目标 → 点击进入详情 → 看到KR列表 → 修改一条KR的当前值 → 返回列表列表里对应目标的进度条跟着变了。这个闭环跑通的意义非常重它说明前端页面、数据模型、状态管理、路由跳转这四层已经互相咬合。后面所有功能都是在这个闭环上做加法。项目启动阶段最重要的一次技术验证到这里就完成了。4. 前端里的“原型”不止是产品原型JS原型链与原型设计模式既然是聊新项目搭建很多前端项目跑起来之后很快会碰到另一个原型——JavaScript里的原型和原型链。这个词容易让人糊涂因为它和产品原型完全是两码事但在前端日常开发中同样绕不开。很多刚开始做项目的同学在公司代码里看到Object.create、prototype、new这些写法会莫名紧张这里我专门把它讲透。4.1 原型链机制为什么调试高频遇到它一句话解释JavaScript里每个对象都有一个隐藏的[[Prototype]]属性指向另一个对象。当你访问某个属性时引擎会先在对象本身找找不到再沿着这个隐藏指针去爸爸那里找再沿着爷爷找直到顶层。这条串起来的链叫原型链。const baseObjective { status: ongoing, getPeriod() { return this.period; }, }; const q1Goal Object.create(baseObjective); q1Goal.period 2025-Q1; console.log(q1Goal.getPeriod()); // 2025-Q1 console.log(q1Goal.status); // ongoing来自原型链q1Goal自身没有status和getPeriod但它们都能被访问到因为原型链上挂着baseObjective。这个机制的实际意义是共享行为和节省内存——多个实例不需要各自拷贝一份公共方法它们顺着链去同一个地方拿就行。很多新人调试时看到奇奇怪怪的对象属性冒出在console.log里就会困惑。比如打印一个数组会发现除了元素之外还有push、map这就是因为数组实例沿原型链继承了Array.prototype的方法。理解了原型链你对前端调试的恐惧能减少一半。4.2 原型链“补环境”一个真实的调试场景在较复杂的前端项目或工具链中有一个专门的说法叫补环境。我第一次遇到时还以为是运维词汇后来发现它是前端调试的日常操作。所谓补环境指的是在某个非标准浏览器环境里运行前端代码时因为某些全局API不存在导致报错需要手动模拟这些API。比如你用Node.js跑一段依赖浏览器全局对象的JS代码控制台直接飘红ReferenceError: window is not defined或者更隐蔽一点的某个类库内部检查了navigator.userAgent在Node环境里读取不到就抛异常。解决思路通常分两步先确认代码在哪个环节引用了什么全局API再在入口处手动挂一个环境模拟层。// 补环境示例让代码在Node下不报错 if (typeof window undefined) { global.window global; } if (typeof navigator undefined) { global.navigator { userAgent: Mozilla/5.0 (compatible; ProtoEnv/1.0) }; }这段代码做的事情就是补了一个栖息地。这其中的原理恰恰是原型链——你在对象上定义一个属性它的后代对象就能顺着链访问到。补环境本质上是往全局对象的原型链上添加白名单成员。为什么新项目搭建会遇到这个因为很多团队做原型时会引入一些工具库、脚本片段或者为了做自动化测试在Node里跑前端代码甚至为了快速验证某个库的功能直接在终端里跑浏览器代码。这时候补环境就成了绕不过去的一课。早点理解原型链你补环境的时候就不会一头雾水。4.3 原型设计模式用Object.create创建对象族设计模式里的原型模式核心思想是通过克隆已有对象来创建新对象而不是通过类构造。在JS里最直接的体现就是Object.create()。这个方法特别适合那些要创建一系列结构相同、但默认值略有差异的对象。在原型阶段它的物美价廉处在于快速造数据族。举个例子你做OKR系统时需要生成一批测试数据import { Objective } from ../types; const baseObjective: OmitObjective, id | title | period { userId: u1, status: ongoing, krList: [], }; function createObjective(overrides: Partialtypeof baseObjective PickObjective, title | period) { return { ...baseObjective, ...overrides, id: crypto.randomUUID() }; } const o1 createObjective({ title: 完成注册流程优化, period: 2025-Q1 }); const o2 createObjective({ title: 提升后端接口稳定性, period: 2025-Q2, status: at_risk });这里用对象展开模拟了原型式克隆的效果。虽然严格说这只是浅拷贝但原型阶段足够了。一旦你定义好基准对象派生出来的所有测试数据都遵循同一个结构。以后加字段只要改基准不用一个个去改。在Vue和React源码里也能看到原型模式的影子。比如Vue的组件配置对象、React的Fiber节点创建都不直接写构造器而是复用已有对象的结构。理解这个模式能帮你读框架源码时开挂不少。5. 原型阶段最容易翻车的几个环境细节代码原型做完业务逻辑之后真正的敌人出现在环境上。我见过太多原型项目代码写得挺好一跑就崩一崩就是环境问题。这里挑几个最典型的讲一讲它们为什么发生、怎么解决。5.1 Node版本不一致导致启动失败前端项目对Node版本是有要求的。你用Node 20做了一个项目中途新同事拉下来他机器上是Node 16跑pnpm install直接开始报错或者装完依赖之后启动服务器各种语法错误。最常见的处理办法是在package.json里声明可用的Node版本范围engines: { node: 18.0.0 }更稳妥的做法是项目里加.nvmrc文件内容就是一行版本号20.11.0安装nvmNode版本管理器之后进入项目目录执行nvm useNode会自动切换成项目要求的版本。这一步做不做影响巨大——不少原型项目是怎么黄的不是功能难写是新同事环境搭不起来一拖就是一两天士气直接掉一半。5.2 依赖安装慢和“幽灵版本”问题原型项目依赖不多但拉取慢的问题在部分网络环境下仍然存在。这种情况下可以考虑配置镜像源pnpm和npm都支持pnpm config set registry https://registry.npmmirror.com另一个更隐蔽的坑是幽灵版本——package.json里某个依赖写的是^1.2.3但实际装上来的可能是1.9.0两个版本之间API发生了变化。原型阶段为了省事许多人喜欢用最新版跑通了就不管了结果正式项目一拉发现某些用法在新版被废弃了。我的建议是原型项目也要提交lock文件。pnpm-lock.yaml或package-lock.json都要纳入版本管理这样团队所有人装出来的依赖都是同一套不会出现我本地好的你拉下来就是不行的扯皮。5.3 浏览器API差异localStorage、Cookie与跨域原型阶段如果直接打开浏览器控制台调试有个常见情况你的代码里用了localStorage存储数据本地跑得好好的打包部署到测试机之后发现白屏了。排查半天发现是部署域名带了HTTPS但在某些环境里被禁用了第三方Cookie或者存储空间被安全策略限制。还有更基础的——前后端联调时的跨域问题。原型阶段你可能临时起了一个json-server或者mock-server前端页面跑在5173端口接口在3000端口那就必须处理CORS。Vite里最省事的是用自带的proxy// vite.config.ts export default defineConfig({ server: { proxy: { /api: http://localhost:3000, }, }, });这样前端请求/api/objectives会自动代理到你本地的mock服务上页面上不用写任何CORS逻辑。这段配置在原型阶段可能只花五分钟但能省掉一上午的抓狂。5.4 一个真实案例原型联调时的“数据污染”排查分享一个我实际踩过的坑。当时做一个原型项目列表页从Mock数据源拉数据详情页修改进度后存进localStorage。逻辑看起来没问题但跑了几天后数据出现灵异现象——一会儿显示的是Mock里的原始值一会儿显示的又是修改过的值。排查链路是这样的先在详情页控制台打印当前加载的数据确认修改确实写入localStorage。2. 再到列表页打印数据源发现它优先用了localStorage缓存但缓存key写错了不同目标的KR共用了一个key导致互相覆盖。3. 进一步追查发现mock/okrData.ts里导出的数组是模块级常量详情页在代码里直接对它做了push操作意外改写了模块里的全局状态。这个问题就是典型的原型阶段没做数据隔离。Mock数据可以被读取但千万不能直接改。解决办法是在每次读取时做一次深拷贝或者在进入详情页时从Store重新获取快照。虽然伤痕累累但这一课让我认识到原型再小也要遵循单向数据流的基本原则——数据源统一、修改走Store、组件不直接改动全局数据。6. 原型跑通之后怎么向正式项目过渡最后聊聊原型阶段的收尾。很多团队的误区是原型验证完之后直接在原型代码上继续加功能试图顺便把它变成正式项目。这个做法在原型非常小、团队也小的时候勉强可行但一旦超过一个临界点整个项目就会陷入基础不牢、缝缝补补的泥潭。6.1 哪些可以直接复用哪些必须推倒重写我建议在过渡之前先把原型项目里的资产分类资产过渡建议原因数据模型TypeScript类型直接复制业务对象不会因为技术选型改变而变化UI组件简单列表、按钮、卡片视情况复用如果样式尚未系统化重写成本更低Mock数据不能直接用正式项目必须从真实API拉取状态管理Store重写原型Store没有错误处理、缓存、鉴权路由结构基本保留页面间关系已通过原型验证改动不大这个分类看起来简单但它背后有个核心思想原型验证的是业务逻辑对不对正式项目关心的是工程质量稳不稳。这两件事对应的代码形态是不同的。原型里可以全屏硬编码正式项目里要有错误边界、加载态、异常恢复原型里可以一进页面就拉数据正式项目里要处理token过期、重试、并发竞态。6.2 过渡的节奏先搭骨架再平移业务我的经验是过渡期不要追求一次性搬完而是按基础设施 → 业务模块 → 数据联通三步走第一步搭好正式项目的基础设施按团队规范建好目录、配好ESLint/Prettier、接入CI、确定组件库和样式方案。第二步把原型中已经验证好的页面逐一手动实现一遍业务逻辑照着原型的交互逻辑写但代码层面不以原型的写法为准。第三步联调真实接口替换Mock数据源跑通整个登录到操作的链路。为什么要手动实现而不是直接拷贝因为原型的代码往往是简化过的比如直接import本地数据、组件样式裸奔、没有做性能优化。直接拷贝相当于把一堆技术债原封不动带到了新项目。手动重构一遍相当于给团队一个机会把踩过的坑转化为更好的设计。6.3 过渡期最重要的心态别恋战原型做得好容易让人产生这已经是产品了的错觉。我的建议是原型项目的核心使命是验证想法、对齐预期、暴露风险一旦这三个目标达成就要果断进入正式项目的建设。你可以为自己做出了一个能跑的原型而自豪但千万别把修原型里的bug当成正式项目的开工仪式。让原型项目留在原地就像一个已经完成出图任务的建筑模型——它完成了自己的职责大楼的施工活该由施工队重新来干。我自己每次完成一个原型项目并顺利过渡最大的收获清单通常长这样一份经过验证的数据模型、一版被团队认可过的交互流程、一份踩过坑的技术记录。这些无形的资产比那几行能跑的原型代码值钱得多。
返回列表