JavaScript入门次阅读

页面局部更新:不刷新页面改内容的几种做法

页面只想更新一块区域,整页刷新体验太差。这份 demo 用原生 JS 从 fetch + DOM 替换讲起,再到提交表单不跳转、轮询的取舍、SSE 服务端推送、以及什么时候该上 WebSocket。不依赖任何框架,复制就能跑。

局部更新AJAXfetchSSEWebSocket实战Demo

"点个赞数字要变""提交评论后列表追加一条""仪表盘数据要自动刷新"——这些都是局部更新。整页刷新的问题是:滚动位置丢失、闪白屏、无谓地重新加载全部资源。

下面按场景从简到繁给几种做法,全部原生 JS,不引框架。

场景一:点按钮拉数据,替换某块区域

最基础的形态。

<div id="status">
  <span id="cpu"></span>
</div>
<button id="refresh">刷新</button>

<script>
async function loadCpu() {
  const el = document.getElementById('cpu');
  el.textContent = '加载中...';
  try {
    const res = await fetch('/api/cpu');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    el.textContent = data.value + '%';
  } catch (e) {
    el.textContent = '加载失败';
    console.error(e);
  }
}

document.getElementById('refresh').addEventListener('click', loadCpu);
loadCpu();   // 首次自动加载
</script>

配套的后端(Express):

app.get('/api/cpu', (req, res) => {
  const load = require('os').loadavg()[0];
  res.json({ value: (load * 10).toFixed(1) });
});

要点fetch 默认不带 cookie,跨域时要 credentials: 'include'res.ok 要判断,404/500 不会自动抛异常。

场景二:表单提交不跳转,成功后局部追加

比如评论框。关键是 e.preventDefault() 阻止默认提交。

<form id="comment-form">
  <input name="content" placeholder="说点什么" required />
  <button type="submit">发布</button>
</form>
<ul id="comment-list"></ul>

<script>
const form = document.getElementById('comment-form');
const list = document.getElementById('comment-list');

form.addEventListener('submit', async (e) => {
  e.preventDefault();                       // 阻止整页跳转

  const btn = form.querySelector('button');
  btn.disabled = true;                      // 防重复提交
  btn.textContent = '发布中...';

  try {
    const res = await fetch('/api/comments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: form.content.value }),
    });
    if (!res.ok) throw new Error('提交失败');
    const saved = await res.json();

    // 局部追加,不动其他 DOM
    const li = document.createElement('li');
    li.textContent = saved.content;         // 用 textContent 而不是 innerHTML
    list.prepend(li);

    form.reset();
  } catch (err) {
    alert(err.message);
  } finally {
    btn.disabled = false;
    btn.textContent = '发布';
  }
});
</script>

两个容易踩的点

  1. list.innerHTML += ... 这种写法会把整块重新解析,已绑定的事件监听全丢,还有 XSS 风险。用 createElement + textContent 更稳。
  2. 提交期间要把按钮 disabled,网速慢的时候用户会连点好几下。

场景三:定时轮询

简单,但要知道代价。

let timer = null;

function startPolling(intervalMs = 5000) {
  stopPolling();
  timer = setInterval(loadCpu, intervalMs);
}
function stopPolling() {
  if (timer) clearInterval(timer);
}

startPolling(5000);

// 页面切到后台就停,回来再启动——省流量也省服务端资源
document.addEventListener('visibilitychange', () => {
  document.hidden ? stopPolling() : startPolling(5000);
});

轮询的取舍:实现成本最低,N 个用户就是 N 倍无效请求,数据没变化也在问。间隔设多少很纠结——1 秒太浪费,30 秒又不实时。数据变化不频繁、实时性要求不高的场景(比如每 5 分钟更新一次的报表)用它可以,别用在聊天或实时监控上。

场景四:服务端推送 SSE

服务端有变化才推,比轮询干净得多。适合监控面板、日志尾随、通知。

后端:

app.get('/api/stream', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();                       // 先把响应头发过去

  const timer = setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 2000);

  // 客户端断开时清理,否则定时器泄漏
  req.on('close', () => {
    clearInterval(timer);
    res.end();
  });
});

前端:

const es = new EventSource('/api/stream');

es.onmessage = (e) => {
  const data = JSON.parse(e.data);
  document.getElementById('cpu').textContent = new Date(data.time).toLocaleTimeString();
};

es.onerror = () => {
  // EventSource 会自动重连,这里只做提示
  console.warn('连接断开,等待自动重连');
};

// 不用了记得关
// es.close();

SSE 的几个特点:单向(服务端→客户端)、基于 HTTP、自动重连、只能传文本(二进制要 base64)。如果只需要服务端推,SSE 比 WebSocket 简单太多,还天然兼容现有 HTTP 基础设施(nginx 记得关掉对该路径的缓冲)。

nginx 反代 SSE 时要加:

location /api/stream {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_buffering off;        # 关键,否则数据被缓冲住不往下发
    proxy_cache off;
    proxy_read_timeout 3600s;
}

什么时候才需要 WebSocket

需要双向实时通信才上它——聊天室、协同编辑、多人游戏、需要客户端频繁主动发消息的场景。代价是要自己处理心跳、重连、消息顺序、水平扩展时的会话共享。能用 SSE 解决的别上 WebSocket。

调试小贴士

  • Chrome DevTools → Network → 勾 Preserve log,看每次异步请求的完整记录
  • 筛选 Fetch/XHR 只看异步请求
  • 请求慢时看 Timing 标签,区分是排队、DNS、还是服务端处理慢
  • console.log 打点不如直接在 Network 面板看,后者不会漏

选哪个

场景 方案
用户点一下才更新 fetch + DOM 替换
表单提交 preventDefault + fetch
数据几分钟变一次 轮询(配合 visibilitychange)
服务端主动推(监控/通知/日志) SSE
双向实时(聊天/协同) WebSocket