网页原型设计
前言
想检查一个网页在手机、平板和电脑上的效果,不一定要先登录账号、连接后端,再准备一批真实数据。
如果项目已经有页面和交互,可以直接复用这些代码,把网络请求和外部操作替换成本地演示数据。这样得到的原型不仅能看布局,还能操作搜索、菜单、弹窗和列表,发现的问题也能直接修回项目。
这篇讲的是已有前端项目的离线交互原型,不是从零画设计稿,也不是证明真实后端已经可用。
页面不重写,只替换外部依赖
原型和正式版使用同一套路由、布局、组件、样式与状态逻辑。区别放在请求和平台能力的边界:
路由、页面、组件、状态管理
│
service / platform
│
┌─────┴─────┐
正常构建 演示构建
真实服务 本地数据与替代实现比如一个收藏按钮,点击后的状态变化、确认框和提示仍走原来的代码;只有请求不再发往真实服务器。
不要另写一套 HTML 模仿页面,再给演示页单独补样式。否则原型修好了,正式页面还要再改一次,两边也容易逐渐不同。
先确定实现边界
先找出页面依赖哪些请求、存储和外部能力,再决定替换位置。以下内容需要从代码和构建配置中确认,不是统一的目录约定。
| 需要确认 | 查看位置与处理方式 |
|---|---|
| 页面入口和共享组件 | 查路由、布局与状态管理;演示构建复用它们,不另建一套路由树 |
| 请求入口 | 查 service、Axios 实例或 fetch 封装;在统一传输层接入本地响应 |
| 数据格式 | 查页面取值、类型和错误判断;返回结构与原接口一致,包括分页和错误格式 |
| 平台能力 | 查文件、媒体、外链、登录与同步调用;在对应接口提供演示实现 |
| 构建开关与输出 | 查现有脚本和构建器;演示开关默认关闭,输出到独立目录 |
| 存储位置 | 查 localStorage、IndexedDB 等命名;演示使用独立命名空间,提供重置入口 |
| 启动方式 | 查资源路径和路由模式;需要静态服务时选空闲端口,不沿用其他服务的监听端口 |
固定的是共享页面、隔离副作用和验证顺序;请求路径、响应字段、存储键名、构建命令要按实际代码确定。缺少某一层就不必为了套结构额外创建服务。
用构建开关切换演示模式
用单独的构建命令明确选择演示模式,不根据域名、网络是否断开或登录失败临时猜测。
演示构建应同时完成:
- 启用本地请求和平台适配。
- 注入演示初始数据,禁用真实账号读取。
- 不注入统计脚本,不启动同步、日志上传等常驻任务。
- 将输出写入独立目录,不覆盖正式产物。
开关集中定义,例如 OFFLINE_DEMO,由所用构建器转换成编译期常量;名称只是示例。正常构建必须显式保持关闭,不能因为上一次构建残留的环境变量误用演示数据。
只有一个开关还不够:分别构建正常版和演示版,检查两者实际选择了哪套适配器。
在请求层提供演示数据
页面仍然调用原来的 service,由请求层返回 fixture,也就是预先准备的本地数据。
使用 Axios 的项目可以在演示模式替换自定义 adapter;使用 fetch 封装的项目在对应传输入口替换。不要在每个页面里写一遍判断。Axios 的返回约定可参考 自定义请求适配配置。
本地分发至少要识别 HTTP 方法、路径、查询参数和请求体。下面是一个演示接口处理函数,返回业务响应体,不是可直接赋给 Axios 的完整 adapter:
const initialItems = [{ id: 'demo-1', title: '示例项目', liked: false }]
let items = initialItems.map(item => ({ ...item }))
export async function demoResponse({ method = 'GET', url }) {
// 示例基址仅用于解析相对 URL,不发起网络请求
const parsed = new URL(url, 'https://demo.invalid')
const verb = method.toUpperCase()
if (verb === 'GET' && parsed.pathname === '/api/items') {
const query = parsed.searchParams.get('q') || ''
return {
code: 0,
data: items.filter(item => item.title.includes(query))
.map(item => ({ ...item })),
}
}
if (verb === 'POST' && parsed.pathname === '/api/items/demo-1/like') {
items = items.map(item => item.id === 'demo-1'
? { ...item, liked: true } : item)
return { code: 0, data: { liked: true } }
}
throw new Error(`未覆盖的演示接口:${verb} ${parsed.pathname}`)
}
export function resetDemo() {
items = initialItems.map(item => ({ ...item }))
}接入时按请求库约定封装响应,合并查询参数、解析请求体,并保留项目需要的取消和错误语义。示例中的路径、字段和成功码都应换成真实接口约定。
未覆盖的接口明确报错,不回退到真实网络。 这样新增页面漏了 fixture 时能及时发现,而不是悄悄请求正式服务。
写操作要前后一致
只让 POST 返回成功,可能出现“收藏成功,切回来却没收藏”的情况。
如果真实 store 的乐观更新已经维持了整个演示流程,接口可以只作本地确认;如果页面会重新查询,就要像示例一样更新本地模型,让后续读取反映修改。PUT、PATCH、DELETE 也按各自业务处理,不能把所有变更统一当成成功。
fixture 还要覆盖空列表、长标题、分页末页、加载和失败状态。返回一批整齐的短标题,只能证明最理想的画面没问题。
隔离存储和平台能力
沿用原平台接口,在演示构建中换成不会触碰真实资源的实现:
| 能力 | 演示处理 |
|---|---|
| 用户、收藏、设置 | 从演示初始数据恢复,修改仅影响演示状态 |
| 文件扫描 | 返回虚构列表,不读取本机目录 |
| 文件选择、删除、真实下载 | 返回明确的取消或不支持结果,不假装文件已处理 |
| 外部链接、登录 | 阻止真实跳转,不读取账号 Cookie 或令牌 |
| 同步、日志、通知通道 | 不启动轮询、WebSocket 或上传任务 |
| 媒体 | 可演示队列和控制状态;真实播放另行验证,或使用随包提供的本地素材 |
存储可以使用类似 demo: 的前缀,但要检查是否与同源下其他应用冲突。IndexedDB、缓存名称也要隔离;重置只清理演示自己的键和数据库,不调用全局清空。
同源 Cookie 并不会因为换了 localStorage 前缀就被隔离。优先使用独立的演示 origin,并检查认证初始化和请求拦截器是否仍在读取真实凭据。
图片、字体、CSS 背景图和媒体同样会发请求。要真正离线,它们必须随产物提供,或使用本地占位资源;只替换 API 不够。
保留真实状态变化
演示环境复用真实 action:搜索条件改变、列表排序、选中条目、弹窗确认,都应产生原来定义的状态变化。
只关闭状态变化引发的外部副作用。例如媒体应用可以保留播放队列、上一项和下一项操作,但不解析在线媒体地址、不上传播放记录。
这时能验证的是控制状态和布局,不是解码、音频播放或锁屏续播。对于未实现的能力,给出明确提示,避免把按钮变色当作功能完成。
响应式问题也修在共享组件中,不给演示版增加一套专用 CSS。
构建和验收
根据项目现有工具执行正常构建、演示构建及相关测试。命令名称由仓库脚本决定,不假设一定存在 build:demo。
通常使用本地静态服务器打开产物;不要默认双击 HTML 就能运行,模块加载和路由可能受 file:// 限制。服务端口先确认空闲,资源 base 与访问路径一致;使用 history 路由时还需配置页面回退。
| 检查项 | 通过条件 |
|---|---|
| 共享代码 | 演示使用真实路由、组件和状态逻辑,没有复制第二套页面 |
| 数据与交互 | 搜索、筛选、变更后重新读取符合预期,重置后恢复初始状态 |
| 异常分支 | 空数据、错误、取消、分页边界有对应表现 |
| 响应式 | 手机、平板、桌面和横屏下无意外横向溢出,菜单、抽屉和弹窗可操作 |
| 请求隔离 | 全流程没有业务 API、统计、同步、日志或外部媒体请求 |
| 存储隔离 | 演示操作及重置不影响正常版本数据 |
| 正常构建 | 演示开关关闭,原请求与平台实现仍正常接入 |
尺寸可以从 320×568、375×667、768×900、1440×900 开始,再补项目断点附近的宽度。检查按钮点击范围、长文本和弹窗滚动,不只看首页截图。
Network 检查从页面加载前开始,覆盖导航、操作和等待后台任务触发的过程。自动化测试可以阻止非演示 origin 的请求,发现尝试外联就判失败;这种拦截是兜底,不能代替正确的适配实现。
最后禁用缓存,在阻断外网但保留本地静态服务访问的条件下重测。真实登录、权限、文件操作、媒体和后端同步,仍需要在各自环境单独验收。
后续增加页面时,先补对应接口和平台数据,再检查共享组件、交互及网络。原型与产品维护的是同一套页面,而不是两份逐渐分开的实现。

暂无评论