渐进增强实战:构建稳健可靠的前端体验
先看效果,再看代码
上周重构一个老项目,用户反馈说“在低端机上点按钮没反应”。我一开始以为是 JS 报错,结果打开 DevTools 一看,根本没加载 JS 文件——因为网络太差,JS 资源超时了。这时候页面就完全瘫痪,连个表单都提交不了。
这让我想起渐进增强(Progressive Enhancement)这个老概念。很多人觉得它过时了,但其实它在弱网、老旧设备、JS 失败的场景下依然救命。我立马用渐进增强重写了核心交互,效果立竿见影:即使 JS 没加载,用户也能正常提交表单、跳转页面。
核心思路很简单:先保证 HTML + CSS 能跑通基本功能,再用 JS 增强体验。下面直接上代码。
最经典的例子:表单提交
别一上来就写 fetch,先写一个能用的原生表单:
<form action="/submit" method="POST">
<input type="text" name="username" required>
<button type="submit">提交</button>
</form>
这个表单在没 JS 的情况下照样能提交,服务器也能处理。然后我们再加一层 JS 增强,让它变成无刷新提交:
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const res = await fetch('/submit', {
method: 'POST',
body: formData
});
if (res.ok) {
// 成功后更新 UI
document.body.innerHTML = '<p>提交成功!</p>';
}
} catch (err) {
// 如果 fetch 失败,回退到原生提交
e.target.submit();
}
});
注意那个 catch 里的 e.target.submit() —— 这是关键。如果 JS 请求失败(比如网络断了),就回退到浏览器原生行为,保证功能不丢。亲测有效,比单纯弹个“网络错误”友好得多。
这个场景最好用:导航菜单
很多开发者一做下拉菜单就直接上 JS 控制显隐,结果 JS 一挂,菜单就永远打不开。正确的做法是:默认用 CSS 实现可访问的交互,JS 只负责优化体验。
<nav>
<ul>
<li>
<a href="/products">产品</a>
<ul class="submenu">
<li><a href="/products/a">产品 A</a></li>
<li><a href="/products/b">产品 B</a></li>
</ul>
</li>
</ul>
</nav>
.submenu {
display: none;
}
/* 用 :focus-within 实现键盘可访问的展开 */
li:focus-within .submenu,
li:hover .submenu {
display: block;
}
这样即使没 JS,用户用 Tab 键聚焦到“产品”链接时,子菜单也会自动展开。加上 JS 后,我们可以做更流畅的动画或点击展开:
document.querySelectorAll('nav > ul > li').forEach(item => {
const toggle = item.querySelector('a');
const submenu = item.querySelector('.submenu');
// 先检查是否支持 JS
item.classList.add('js-enabled');
toggle.addEventListener('click', (e) => {
e.preventDefault();
submenu.classList.toggle('show');
});
});
/* JS 启用后才隐藏默认行为 */
.js-enabled .submenu {
display: none;
}
.js-enabled .submenu.show {
display: block;
}
这里注意:**不要一开始就隐藏 .submenu**,否则没 JS 时用户完全看不到子菜单。先让 HTML/CSS 能用,再用 JS 动态加类名覆盖默认行为。
踩坑提醒:这三点一定注意
- 别用 JS 生成关键内容:比如把整个商品列表用 JS 渲染。万一 JS 挂了,页面就空了。正确做法是服务端渲染基础列表,JS 只负责分页、筛选等增强功能。
- 事件委托要小心:如果你用
document.addEventListener('click', ...)来处理动态元素,记得在回调里先判断目标元素是否存在。否则在低版本浏览器或 JS 部分加载失败时,可能报错阻塞后续逻辑。 - 别过度依赖现代 API:比如
fetch在 IE 上不支持。如果你的用户可能用老旧设备,要么用 polyfill,要么像前面表单例子那样,做好回退到原生提交的准备。我之前在一个政府项目里就栽在这点上,折腾了半天发现得兼容 IE11。
高级技巧:用 feature detection 决定增强策略
有时候不是所有用户都需要同样的增强。比如,支持 Intersection Observer 的浏览器可以用懒加载,不支持的就直接加载图片。代码可以这样写:
if ('IntersectionObserver' in window) {
// 高级浏览器:懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
} else {
// 老旧浏览器:直接加载
document.querySelectorAll('img[data-src]').forEach(img => {
img.src = img.dataset.src;
});
}
这样既享受了新特性,又保证了兼容性。不过要注意,data-src 的图片在 HTML 里得有默认的 src 占位图,否则没 JS 时图片区域会空白。我一般放一个极小的 base64 图:
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="https://jztheme.com/image.jpg" alt="示例">
别追求 100% 完美,够用就行
渐进增强不是要你为每个功能写三套实现。我的经验是:只对核心路径做增强回退。比如电商网站,商品浏览、加入购物车、下单这三个流程必须保证无 JS 也能走通,其他像“猜你喜欢”这种推荐模块,JS 挂了就挂了,不影响主流程。
另外,有些场景其实没必要硬套渐进增强。比如后台管理系统,用户环境可控,JS 基本不会挂,那直接上 React/Vue 也没问题。渐进增强主要用在面向公众的、用户环境不可控的场景。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多,比如结合 Service Worker 做离线增强,或者用 CSS container queries 做响应式增强,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论