核心理念
从基础功能出发,逐层增强体验 - 构建一个在任何环境下都能工作的表单,然后为现代浏览器用户提供更流畅的交互体验。
第一层:核心服务(纯 HTML)
基础表单结构
<!-- 完全自洽的基础表单 - 无JS依赖 -->
<form id="searchForm" action="/search" method="GET" class="basic-form">
<div class="form-group">
<label for="search">搜索产品</label>
<input
type="text"
id="search"
name="q"
required
minlength="2"
placeholder="请输入至少2个字符..."
>
<div class="error-message" id="lengthError" style="display: none;">
搜索词至少需要2个字符
</div>
</div>
<div class="form-group">
<label for="category">产品分类</label>
<select id="category" name="category">
<option value="">全部</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="books">图书</option>
</select>
</div>
<div class="form-actions">
<button type="submit">搜索</button>
<button type="reset">重置</button>
</div>
</form>核心服务特性
- ✅ 原生表单验证:使用 HTML 5
required、minlength属性 - ✅ 语义化结构:正确的标签关联和表单分组
- ✅ 无 JS 提交:依赖浏览器默认行为,向
/search?q=...&category=...发起 GET 请求 - ✅ 普适兼容:在所有能解析 HTML 的客户端中工作
- ✅ 优雅重置:原生
type="reset"按钮
第二层:增强服务(JavaScript)
渐进式 JavaScript 架构
// 增强脚本 - 安全地添加交互功能
class SearchFormEnhancer {
constructor(formId) {
this.form = document.getElementById(formId);
this.isEnhanced = false;
this.init();
}
init() {
// 能力检测:确保浏览器支持必要的API
if (!this.form || !window.fetch || !window.Promise) {
return; // 静默降级到基础表单
}
this.enhanceForm();
this.isEnhanced = true;
}
enhanceForm() {
// 1. 接管表单提交
this.form.addEventListener('submit', this.handleSubmit.bind(this));
// 2. 实时输入验证
this.setupLiveValidation();
// 3. 自动完成增强
this.setupAutocomplete();
// 4. 搜索历史
this.setupSearchHistory();
// 添加增强状态标识
this.form.classList.add('enhanced');
}
async handleSubmit(event) {
event.preventDefault();
const formData = new FormData(this.form);
const searchData = this.serializeForm(formData);
// 客户端验证
if (!this.validateForm(searchData)) {
return;
}
try {
// 显示加载状态
this.setLoadingState(true);
// 异步搜索
const results = await this.performSearch(searchData);
// 动态更新结果
this.renderResults(results);
// 更新搜索历史
this.updateSearchHistory(searchData.q);
} catch (error) {
console.error('搜索失败:', error);
this.handleSearchError(error);
} finally {
this.setLoadingState(false);
}
}
setupLiveValidation() {
const searchInput = this.form.querySelector('#search');
searchInput.addEventListener('input', (event) => {
const value = event.target.value;
const errorElement = this.form.querySelector('#lengthError');
if (value.length > 0 && value.length < 2) {
this.showError(errorElement, '搜索词至少需要2个字符');
event.target.setCustomValidity('搜索词太短');
} else {
this.hideError(errorElement);
event.target.setCustomValidity('');
}
});
// 失去焦点时进行最终验证
searchInput.addEventListener('blur', (event) => {
if (event.target.value.length === 1) {
this.showError(
this.form.querySelector('#lengthError'),
'搜索词至少需要2个字符'
);
}
});
}
setupAutocomplete() {
const searchInput = this.form.querySelector('#search');
let autocompleteContainer = this.form.querySelector('.autocomplete-suggestions');
if (!autocompleteContainer) {
autocompleteContainer = document.createElement('div');
autocompleteContainer.className = 'autocomplete-suggestions';
searchInput.parentNode.appendChild(autocompleteContainer);
}
let debounceTimer;
searchInput.addEventListener('input', (event) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
this.fetchSuggestions(event.target.value);
}, 300);
});
// 点击外部关闭建议
document.addEventListener('click', (event) => {
if (!this.form.contains(event.target)) {
this.hideAutocomplete();
}
});
}
async fetchSuggestions(query) {
if (query.length < 2) {
this.hideAutocomplete();
return;
}
try {
const response = await fetch(`/api/suggestions?q=${encodeURIComponent(query)}`);
const suggestions = await response.json();
this.showAutocomplete(suggestions);
} catch (error) {
// 静默失败,不影响核心功能
console.warn('获取搜索建议失败:', error);
}
}
setupSearchHistory() {
this.searchHistory = this.loadSearchHistory();
this.renderSearchHistory();
}
loadSearchHistory() {
try {
return JSON.parse(localStorage.getItem('searchHistory') || '[]');
} catch {
return []; // 本地存储不可用时静默降级
}
}
updateSearchHistory(query) {
if (!query.trim()) return;
// 去重并限制数量
this.searchHistory = [
query,
...this.searchHistory.filter(item => item !== query)
].slice(0, 10);
try {
localStorage.setItem('searchHistory', JSON.stringify(this.searchHistory));
} catch {
// 静默处理存储错误
}
this.renderSearchHistory();
}
validateForm(data) {
if (data.q.length < 2) {
this.showError(
this.form.querySelector('#lengthError'),
'搜索词至少需要2个字符'
);
this.form.querySelector('#search').focus();
return false;
}
return true;
}
async performSearch(data) {
const queryString = new URLSearchParams(data).toString();
const response = await fetch(`/api/search?${queryString}`);
if (!response.ok) {
throw new Error(`搜索失败: ${response.status}`);
}
return response.json();
}
renderResults(results) {
let resultsContainer = document.getElementById('searchResults');
if (!resultsContainer) {
resultsContainer = document.createElement('div');
resultsContainer.id = 'searchResults';
this.form.after(resultsContainer);
}
resultsContainer.innerHTML = `
<div class="search-results-header">
<h3>找到 ${results.total} 个结果</h3>
<button onclick="this.closest('#searchResults').remove()">关闭</button>
</div>
<div class="results-list">
${results.items.map(item => `
<div class="result-item">
<h4><a href="${item.url}">${item.title}</a></h4>
<p>${item.description}</p>
<span class="price">${item.price}</span>
</div>
`).join('')}
</div>
`;
}
handleSearchError(error) {
// 降级策略:回退到原生表单提交
const fallback = confirm('搜索遇到问题,是否使用基础搜索?');
if (fallback) {
this.form.submit(); // 触发原生表单提交
}
}
setLoadingState(loading) {
const submitButton = this.form.querySelector('button[type="submit"]');
if (loading) {
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner"></span> 搜索中...';
this.form.classList.add('loading');
} else {
submitButton.disabled = false;
submitButton.textContent = '搜索';
this.form.classList.remove('loading');
}
}
// 工具方法
showError(element, message) {
element.textContent = message;
element.style.display = 'block';
element.setAttribute('role', 'alert');
}
hideError(element) {
element.style.display = 'none';
element.removeAttribute('role');
}
serializeForm(formData) {
const data = {};
for (let [key, value] of formData) {
data[key] = value;
}
return data;
}
}
// 安全初始化
document.addEventListener('DOMContentLoaded', () => {
new SearchFormEnhancer('searchForm');
});渐进式 CSS 增强
/* 基础样式 - 所有浏览器 */
.basic-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error-message {
color: #d32f2f;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* 增强样式 - 现代浏览器 */
.enhanced .form-group {
position: relative;
}
.enhanced input:focus,
.enhanced select:focus {
outline: none;
border-color: #2196f3;
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}
/* 自动完成建议 */
.autocomplete-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 1000;
max-height: 200px;
overflow-y: auto;
}
.autocomplete-suggestion {
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.autocomplete-suggestion:hover,
.autocomplete-suggestion:focus {
background: #f5f5f5;
}
/* 加载状态 */
.loading .form-actions button[type="submit"] {
position: relative;
color: transparent;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 搜索历史 */
.search-history {
margin-top: 1rem;
padding: 1rem;
background: #f9f9f9;
border-radius: 4px;
}
.search-history-item {
display: inline-block;
margin: 0.25rem;
padding: 0.25rem 0.5rem;
background: #e3f2fd;
border-radius: 16px;
cursor: pointer;
}
/* 渐进式特性检测 */
@supports (display: grid) {
.enhanced .form-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
}用户体验对比
无 JavaScript 环境
| 功能 | 体验 |
|---|---|
| 表单提交 | 整页跳转,传统 Web 体验 |
| 输入验证 | 提交时浏览器原生提示 |
| 自动完成 | 无 |
| 搜索历史 | 无 |
| 错误处理 | 浏览器默认错误页面 |
有 JavaScript 环境
| 功能 | 体验 |
|---|---|
| 表单提交 | 无刷新异步提交,SPA 体验 |
| 输入验证 | 实时验证,即时反馈 |
| 自动完成 | 输入时智能提示 |
| 搜索历史 | 本地存储,快速访问 |
| 错误处理 | 优雅的错误提示和降级 |
关键设计原则
-
功能分层
- 核心层:纯 HTML 表单,确保基本功能
- 增强层:JavaScript 添加交互和优化
-
能力检测
- 检测 Fetch API、Promise 等现代特性
- 不支持时静默降级
-
优雅降级
- 网络错误时回退到原生提交
- 本地存储不可用时静默处理
-
渐进加载
- 核心功能立即可用
- 增强功能按需加载
-
可访问性
- 语义化 HTML 结构
- 适当的 ARIA 属性
- 键盘导航支持
这种渐进增强的表单设计确保了:
- ✅ 鲁棒性:在任何环境下都能工作
- ✅ 可访问性:所有用户都能完成核心任务
- ✅ 现代体验:支持现代浏览器的用户获得最佳体验
- ✅ 可维护性:清晰的关注点分离