渐进增强实战:构建稳健可靠的前端体验

UX文鑫 优化 阅读 1,627
赞 12 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周重构一个老项目,用户反馈说“在低端机上点按钮没反应”。我一开始以为是 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 做响应式增强,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论