2026年3月

如题

前几年(准确地是 18,19 年左右

看这个板块,都是各公司的人在发招人的帖子

这两年再看,多半是失业的人在发各种迷茫贴

在职的人都在发 AI 危机,感觉也很焦虑的样子

我年前也拿了大礼包,4x 岁了,感觉再求职会非常难了

也焦虑的要死,唉

image
我感觉 情趣内衣 才是最应该评测的, 因为买了觉得不好的话, 一般人也不会退货.
看多了主要就那几个系列, 吊带, 体操服, 碎短裙, 女仆装, 三点式, 和服, 角色 cos.
LA 那款, 和 霏慕 那款 甜美娇妻 款式差不多, 但是它面料更滑更透. 就是价格太贵了, 适合富哥.
倾城鸟 的体操服还可以吧, 这个价格不错了.
霏慕 的碎花连衣裙, 作为情趣大牌, 包装和质量都可以的.
JK 女仆装也不错.
死库水款式是我喜欢的, 但是这款面料很垃圾, 清洗掉色还很大异味.
京造 出的冰丝蕾丝花边碎裙一般般吧.
霏慕 的没得说, 还是整个行业中可以的品牌.
蜜链发货晚了, 没收到货就取消了, 京造的话还是一般.
浪莎 也是牌子货, 但是包装都很随意, 我到手还是打开的, 哎非常垃圾.
日系那个价格在那里, 还算可以吧.

总评下来还是 霏慕 比较稳, 缺点是款式比较少, 风格比较保守.
像我这样比较喜欢死库水风格的话, 相关情趣内衣少的, 买正常款式, 自己动手改情趣也是可以的.
还有那些几十块, 几十套一次性丝袜的那种, 就因人而异了, 总体还是吃模特颜值和身材的.

最近给朋友准备劳动仲裁,突然发现劳动法规真的很完善了。我罗列一下学习到的知识点。

  1. 法定计薪天数是 21.75 天;
  2. 工作日加班是 1.5 工资;
  3. 双休日加班是 2 倍工资,双休日加班可以用其他时间的调休置换,调休后可免发双倍工资;
  4. 法定节假日加班是 3 倍工资,法定节假日的加班不能用调休来免发加班费;
  5. 工龄满一年就享有 5 天年假,满 10 年就是 10 天,满 20 年就是 15 天。没休年假的天数也是 3 倍工资。这里的工龄不限公司,A 公司工作 3 年之后跳槽到 B 公司又工作 2 年,依然累计 5 年工龄;
  6. 违法开除是 2N,合法解除是 N+1(这里的“+1”是指代通知金。如果公司提前 30 天书面通知你了,法律上是可以只付 N 而不付那“+1”的) 。

朋友工作 6 年被违法开除了,6 年来一直单休,光是 6 年来周六单休的双倍工资加班费就算出来 16 万。我们都吓了一跳,反复核算依照法律法规就是这个钱。

以前总是嘲笑《劳动法》没用,看来是错怪它了。它虽然没有强制双休,但是也给到了保障的底线。

Pve cx3 的网卡 10G 光口 - 2.5G 电口*5 带双 10G 光口的交换机 - Windows x520sr2

连接都是用的 dac 铜缆线 72 101

网卡加起来 105 线材 30 块 交换机 100

现阶段够我使用了因为我的 pve 里面和 nas 没有 nvme ,sata 10G 完全足够了

后续升级的话就是加两块 40G 网卡 pve 和 Windows 换掉 pve 插两个 10G 一个 40G 网卡
Windows 网络板载 2.5G 提供网络,插一个 40G 网卡,但是鉴于现在 cx4 的网卡还比较贵,正在研究,连接就直接 Windows 直连 pve 就行了然后直通给需要的飞牛或者其他 nas 系统

本文深入探讨前端图片懒加载的各种实现方案,从传统的滚动监听到现代的 IntersectionObserver API,再到浏览器原生支持的 loading="lazy",帮助开发者选择最适合的技术方案。

一、为什么需要图片懒加载?

1.1 性能问题

在现代 Web 应用中,图片往往占据了页面总资源的 60-70%。如果一次性加载所有图片,会导致:

  • 首屏加载时间过长:用户需要等待所有图片下载完成
  • 带宽浪费:用户可能永远不会滚动到页面底部
  • 内存占用过高:大量图片同时存在于内存中
  • 用户体验差:页面卡顿、白屏时间长

1.2 典型应用场景

  • 长列表页面(商品列表、新闻列表)
  • 图片墙/瀑布流
  • 社交媒体信息流
  • 文章详情页
  • 相册/画廊

二、图片懒加载的演进历程

2.1 演进时间线

2010 年前     → 滚动监听 + getBoundingClientRect
2016 年       → IntersectionObserver API 发布
2019 年       → 浏览器原生 loading="lazy" 支持
2020 年至今   → 混合方案 + 渐进增强

三、方案一:传统滚动监听(已过时)

3.1 实现原理

监听 scroll 事件,计算图片是否进入视口,如果进入则加载图片。

3.2 基本实现

class ScrollLazyLoad {
  constructor(options = {}) {
    this.images = [];
    this.threshold = options.threshold || 0; // 提前加载的距离
    this.handleScroll = this.debounce(this._checkImages.bind(this), 200);
  }
  
  init() {
    // 收集所有需要懒加载的图片
    this.images = Array.from(document.querySelectorAll('img[data-src]'));
    
    // 监听滚动事件
    window.addEventListener('scroll', this.handleScroll);
    window.addEventListener('resize', this.handleScroll);
    
    // 初始检查
    this._checkImages();
  }
  
  _checkImages() {
    this.images = this.images.filter((img) => {
      if (this._isInViewport(img)) {
        this._loadImage(img);
        return false; // 已加载,从列表中移除
      }
      return true; // 未加载,保留在列表中
    });
    
    // 所有图片都加载完成,移除监听器
    if (this.images.length === 0) {
      this.destroy();
    }
  }
  
  _isInViewport(element) {
    const rect = element.getBoundingClientRect();
    const windowHeight = window.innerHeight || document.documentElement.clientHeight;
    
    return (
      rect.top <= windowHeight + this.threshold &&
      rect.bottom >= -this.threshold
    );
  }
  
  _loadImage(img) {
    const src = img.dataset.src;
    if (!src) return;
    
    // 创建临时 Image 对象预加载
    const tempImg = new Image();
    tempImg.onload = () => {
      img.src = src;
      img.classList.add('loaded');
      img.removeAttribute('data-src');
    };
    tempImg.onerror = () => {
      img.classList.add('error');
    };
    tempImg.src = src;
  }
  
  debounce(func, wait) {
    let timeout;
    return function(...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(this, args), wait);
    };
  }
  
  destroy() {
    window.removeEventListener('scroll', this.handleScroll);
    window.removeEventListener('resize', this.handleScroll);
  }
}

// 使用示例
const lazyLoad = new ScrollLazyLoad({ threshold: 200 });
lazyLoad.init();

3.3 HTML 结构

<!-- 使用 data-src 存储真实图片地址 -->
<img 
  src="placeholder.jpg" 
  data-src="real-image.jpg" 
  alt="描述"
  class="lazy-image"
/>

<!-- 或者使用透明占位图 -->
<img 
  src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" 
  data-src="real-image.jpg" 
  alt="描述"
/>

3.4 优势

兼容性好:支持所有浏览器(包括 IE6+)
实现简单:逻辑清晰,易于理解
可控性强:可以精确控制加载时机

3.5 劣势

性能差:频繁触发 scroll 事件,即使使用防抖也会影响性能
计算开销大:每次都要调用 getBoundingClientRect()
阻塞主线程:scroll 事件在主线程执行,可能导致卡顿
代码复杂:需要手动管理监听器、防抖、清理等

3.6 性能问题分析

// 问题 1:频繁触发
window.addEventListener('scroll', () => {
  console.log('scroll 事件触发'); // 滚动时每秒触发 60+ 次
});

// 问题 2:getBoundingClientRect 触发重排
const rect = element.getBoundingClientRect(); // 强制浏览器重新计算布局

// 问题 3:主线程阻塞
// scroll 事件在主线程执行,如果处理逻辑复杂,会导致页面卡顿

3.7 适用场景

  • 需要兼容老旧浏览器(IE9-)
  • 需要精确控制加载时机
  • 项目已有成熟的滚动监听方案

3.8 结论

⚠️ 不推荐使用:除非有特殊的兼容性要求,否则应该使用更现代的方案。


四、方案二:IntersectionObserver API(推荐)⭐️

4.1 实现原理

使用浏览器原生的 IntersectionObserver API,监听元素与视口的交叉状态,当元素进入视口时自动触发回调。

4.2 核心优势

异步执行:不阻塞主线程,性能优秀
自动优化:浏览器内部优化,无需手动防抖
精确控制:支持 rootMargin、threshold 等配置
代码简洁:无需手动计算位置

4.3 基本实现

class IntersectionLazyLoad {
  constructor(options = {}) {
    this.rootMargin = options.rootMargin || '50px'; // 提前加载距离
    this.threshold = options.threshold || 0; // 交叉比例
    this.onLoad = options.onLoad; // 加载完成回调
    this.observer = null;
  }
  
  init() {
    // 创建 IntersectionObserver
    this.observer = new IntersectionObserver(
      (entries) => this._handleIntersection(entries),
      {
        rootMargin: this.rootMargin,
        threshold: this.threshold,
      }
    );
    
    // 观察所有需要懒加载的图片
    const images = document.querySelectorAll('img[data-src]');
    images.forEach((img) => this.observer.observe(img));
  }
  
  _handleIntersection(entries) {
    entries.forEach((entry) => {
      // 元素进入视口
      if (entry.isIntersecting) {
        const img = entry.target;
        this._loadImage(img);
        
        // 加载后停止观察
        this.observer.unobserve(img);
      }
    });
  }
  
  _loadImage(img) {
    const src = img.dataset.src;
    if (!src) return;
    
    // 创建临时 Image 对象预加载
    const tempImg = new Image();
    
    tempImg.onload = () => {
      img.src = src;
      img.classList.add('loaded');
      img.removeAttribute('data-src');
      this.onLoad?.(img, true);
    };
    
    tempImg.onerror = () => {
      img.classList.add('error');
      this.onLoad?.(img, false);
    };
    
    tempImg.src = src;
  }
  
  // 动态添加图片时调用
  observe(img) {
    if (this.observer && img.dataset.src) {
      this.observer.observe(img);
    }
  }
  
  // 停止观察某个图片
  unobserve(img) {
    if (this.observer) {
      this.observer.unobserve(img);
    }
  }
  
  destroy() {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }
}

// 使用示例
const lazyLoad = new IntersectionLazyLoad({
  rootMargin: '100px', // 提前 100px 开始加载
  threshold: 0.01, // 元素 1% 可见时触发
  onLoad: (img, success) => {
    console.log(`图片加载${success ? '成功' : '失败'}:`, img.src);
  },
});

lazyLoad.init();

// 动态添加图片
const newImg = document.createElement('img');
newImg.dataset.src = 'new-image.jpg';
document.body.appendChild(newImg);
lazyLoad.observe(newImg); // 观察新图片

4.4 高级用法

4.4.1 渐进式加载(先加载缩略图)

class ProgressiveLazyLoad {
  constructor(options = {}) {
    this.observer = new IntersectionObserver(
      (entries) => this._handleIntersection(entries),
      { rootMargin: '50px' }
    );
  }
  
  init() {
    const images = document.querySelectorAll('img[data-src]');
    images.forEach((img) => {
      // 先加载缩略图
      if (img.dataset.thumbnail) {
        img.src = img.dataset.thumbnail;
      }
      this.observer.observe(img);
    });
  }
  
  _handleIntersection(entries) {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target;
        this._loadHighResImage(img);
        this.observer.unobserve(img);
      }
    });
  }
  
  _loadHighResImage(img) {
    const src = img.dataset.src;
    if (!src) return;
    
    const tempImg = new Image();
    tempImg.onload = () => {
      // 淡入效果
      img.style.opacity = 0;
      img.src = src;
      img.style.transition = 'opacity 0.3s';
      setTimeout(() => {
        img.style.opacity = 1;
      }, 10);
      img.classList.add('loaded');
    };
    tempImg.src = src;
  }
}

4.4.2 响应式图片懒加载

class ResponsiveLazyLoad {
  constructor() {
    this.observer = new IntersectionObserver(
      (entries) => this._handleIntersection(entries),
      { rootMargin: '50px' }
    );
  }
  
  init() {
    const images = document.querySelectorAll('img[data-srcset]');
    images.forEach((img) => this.observer.observe(img));
  }
  
  _handleIntersection(entries) {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target;
        this._loadResponsiveImage(img);
        this.observer.unobserve(img);
      }
    });
  }
  
  _loadResponsiveImage(img) {
    // 根据屏幕宽度选择合适的图片
    const srcset = img.dataset.srcset;
    const sizes = img.dataset.sizes;
    
    if (srcset) {
      img.srcset = srcset;
    }
    if (sizes) {
      img.sizes = sizes;
    }
    
    // 设置默认 src(兜底)
    if (img.dataset.src) {
      img.src = img.dataset.src;
    }
  }
}

HTML 结构:

<img 
  data-srcset="
    small.jpg 480w,
    medium.jpg 800w,
    large.jpg 1200w
  "
  data-sizes="
    (max-width: 600px) 480px,
    (max-width: 1000px) 800px,
    1200px
  "
  data-src="medium.jpg"
  alt="响应式图片"
/>

4.4.3 背景图片懒加载

class BackgroundLazyLoad {
  constructor() {
    this.observer = new IntersectionObserver(
      (entries) => this._handleIntersection(entries),
      { rootMargin: '50px' }
    );
  }
  
  init() {
    const elements = document.querySelectorAll('[data-bg]');
    elements.forEach((el) => this.observer.observe(el));
  }
  
  _handleIntersection(entries) {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const el = entry.target;
        const bg = el.dataset.bg;
        
        if (bg) {
          // 预加载背景图
          const img = new Image();
          img.onload = () => {
            el.style.backgroundImage = `url(${bg})`;
            el.classList.add('loaded');
          };
          img.src = bg;
        }
        
        this.observer.unobserve(el);
      }
    });
  }
}

HTML 结构:

<div 
  class="hero-section" 
  data-bg="hero-background.jpg"
  style="background-color: #f0f0f0;"
>
  <!-- 内容 -->
</div>

4.5 配置参数详解

const observer = new IntersectionObserver(callback, {
  // root: 指定根元素(默认为视口)
  root: document.querySelector('#scrollArea'),
  
  // rootMargin: 根元素的外边距(提前/延迟加载)
  rootMargin: '50px 0px', // 上下提前 50px,左右不变
  
  // threshold: 交叉比例阈值
  threshold: [0, 0.25, 0.5, 0.75, 1], // 多个阈值
});

rootMargin 示例:

// 提前 100px 开始加载
rootMargin: '100px'

// 上下提前 100px,左右提前 50px
rootMargin: '100px 50px'

// 上提前 100px,右提前 50px,下提前 80px,左提前 30px
rootMargin: '100px 50px 80px 30px'

// 延迟加载(元素完全进入视口后才加载)
rootMargin: '-50px'

threshold 示例:

// 元素刚进入视口就触发
threshold: 0

// 元素 50% 可见时触发
threshold: 0.5

// 元素完全可见时触发
threshold: 1

// 多个阈值(0%, 25%, 50%, 75%, 100% 时都会触发)
threshold: [0, 0.25, 0.5, 0.75, 1]

4.6 性能对比

指标滚动监听IntersectionObserver
CPU 占用高(主线程)低(异步)
内存占用
触发频率高(60+ 次/秒)低(按需触发)
代码复杂度
浏览器优化
性能评分60 分95 分

4.7 浏览器兼容性

浏览器版本
Chrome51+
Firefox55+
Safari12.1+
Edge15+
IE❌ 不支持

Polyfill 方案:

// 检测浏览器是否支持
if (!('IntersectionObserver' in window)) {
  // 动态加载 polyfill
  import('intersection-observer').then(() => {
    // 初始化懒加载
    const lazyLoad = new IntersectionLazyLoad();
    lazyLoad.init();
  });
} else {
  // 直接使用
  const lazyLoad = new IntersectionLazyLoad();
  lazyLoad.init();
}

4.8 适用场景

  • ✅ 现代浏览器项目(Chrome 51+, Safari 12.1+)
  • ✅ 需要高性能的懒加载
  • ✅ 长列表、瀑布流、信息流
  • ✅ 需要精确控制加载时机

4.9 结论

⭐️ 强烈推荐:IntersectionObserver 是目前最佳的图片懒加载方案,性能优秀、代码简洁、易于维护。


五、方案三:浏览器原生 loading="lazy"(最简单)

5.1 实现原理

HTML5 新增的 loading 属性,浏览器原生支持图片懒加载,无需任何 JavaScript 代码。

5.2 基本用法

<!-- 懒加载 -->
<img src="image.jpg" loading="lazy" alt="描述" />

<!-- 立即加载(默认) -->
<img src="image.jpg" loading="eager" alt="描述" />

<!-- 自动(浏览器决定) -->
<img src="image.jpg" loading="auto" alt="描述" />

5.3 iframe 也支持

<iframe src="video.html" loading="lazy"></iframe>

5.4 优势

零代码:无需任何 JavaScript
性能最优:浏览器底层优化
自动优化:浏览器根据网络状况自动调整
维护成本低:无需管理监听器、Observer 等

5.5 劣势

兼容性差:Safari 15.4+ 才支持
无法自定义:无法控制提前加载距离
无回调:无法监听加载完成事件

5.6 浏览器兼容性

浏览器版本
Chrome77+
Firefox75+
Safari15.4+
Edge79+
IE❌ 不支持

5.7 渐进增强方案

<!-- 方案 1:loading + data-src(兼容老浏览器) -->
<img 
  src="placeholder.jpg" 
  data-src="real-image.jpg" 
  loading="lazy" 
  alt="描述"
  class="lazy-image"
/>

<script>
// 检测浏览器是否支持 loading="lazy"
if ('loading' in HTMLImageElement.prototype) {
  // 支持:直接设置 src
  document.querySelectorAll('img[data-src]').forEach((img) => {
    img.src = img.dataset.src;
  });
} else {
  // 不支持:使用 IntersectionObserver 降级
  const lazyLoad = new IntersectionLazyLoad();
  lazyLoad.init();
}
</script>
<!-- 方案 2:使用 <picture> 标签 -->
<picture>
  <source 
    srcset="image-large.jpg" 
    media="(min-width: 1200px)" 
    loading="lazy"
  />
  <source 
    srcset="image-medium.jpg" 
    media="(min-width: 768px)" 
    loading="lazy"
  />
  <img 
    src="image-small.jpg" 
    loading="lazy" 
    alt="响应式图片"
  />
</picture>

5.7 适用场景

  • ✅ 只需要兼容现代浏览器(Chrome 77+, Safari 15.4+)
  • ✅ 追求零代码、零维护
  • ✅ 不需要自定义加载逻辑
  • ✅ 简单的图片懒加载需求

5.8 结论

⭐️ 推荐使用:如果不需要兼容老浏览器,loading="lazy" 是最简单、最优雅的方案。


六、方案对比总结

维度滚动监听IntersectionObserverloading="lazy"
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
兼容性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
代码复杂度零代码
可定制性⭐⭐⭐⭐⭐⭐⭐⭐⭐
维护成本零维护
推荐指数⭐⭐⭐⭐⭐⭐⭐⭐⭐

七、业界最佳实践

7.1 混合方案(推荐)⭐️

结合 loading="lazy"IntersectionObserver,实现渐进增强:

class HybridLazyLoad {
  constructor() {
    this.supportsNativeLazy = 'loading' in HTMLImageElement.prototype;
    this.observer = null;
  }
  
  init() {
    const images = document.querySelectorAll('img[data-src]');
    
    if (this.supportsNativeLazy) {
      // 支持原生懒加载:直接设置 src 和 loading 属性
      images.forEach((img) => {
        img.src = img.dataset.src;
        img.loading = 'lazy';
        img.removeAttribute('data-src');
      });
    } else {
      // 不支持:使用 IntersectionObserver 降级
      this.observer = new IntersectionObserver(
        (entries) => this._handleIntersection(entries),
        { rootMargin: '50px' }
      );
      
      images.forEach((img) => this.observer.observe(img));
    }
  }
  
  _handleIntersection(entries) {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute('data-src');
        this.observer.unobserve(img);
      }
    });
  }
}

// 使用
const lazyLoad = new HybridLazyLoad();
lazyLoad.init();

7.2 响应式图片懒加载

<!-- 使用 srcset 和 sizes -->
<img 
  srcset="
    small.jpg 480w,
    medium.jpg 800w,
    large.jpg 1200w,
    xlarge.jpg 1600w
  "
  sizes="
    (max-width: 600px) 480px,
    (max-width: 1000px) 800px,
    (max-width: 1400px) 1200px,
    1600px
  "
  src="medium.jpg"
  loading="lazy"
  alt="响应式图片"
/>

<!-- 使用 <picture> 标签 -->
<picture>
  <source 
    media="(min-width: 1200px)" 
    srcset="desktop.jpg"
    loading="lazy"
  />
  <source 
    media="(min-width: 768px)" 
    srcset="tablet.jpg"
    loading="lazy"
  />
  <img 
    src="mobile.jpg" 
    loading="lazy" 
    alt="响应式图片"
  />
</picture>

7.3 WebP 格式支持

<picture>
  <!-- WebP 格式(现代浏览器) -->
  <source 
    srcset="image.webp" 
    type="image/webp"
    loading="lazy"
  />
  <!-- JPEG 格式(降级) -->
  <img 
    src="image.jpg" 
    loading="lazy" 
    alt="描述"
  />
</picture>

7.4 避免布局抖动(CLS)

问题: 图片加载前后高度变化,导致页面跳动

解决方案 1:设置固定尺寸

<img 
  src="image.jpg" 
  loading="lazy"
  width="800"
  height="600"
  alt="描述"
/>

解决方案 2:使用 aspect-ratio

<img 
  src="image.jpg" 
  loading="lazy"
  style="aspect-ratio: 16/9; width: 100%;"
  alt="描述"
/>

解决方案 3:使用 padding-top 技巧

<div class="image-wrapper">
  <img 
    src="image.jpg" 
    loading="lazy"
    alt="描述"
  />
</div>

<style>
.image-wrapper {
  position: relative;
  width: 100%;
  padding-top: 56.25%; /* 16:9 比例 = 9/16 * 100% */
}

.image-wrapper img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
</style>

7.5 预加载关键图片

<!-- 首屏关键图片:使用 preload -->
<link rel="preload" as="image" href="hero.jpg" />

<!-- 首屏图片:不使用懒加载 -->
<img src="hero.jpg" loading="eager" alt="首屏大图" />

<!-- 非首屏图片:使用懒加载 -->
<img src="image.jpg" loading="lazy" alt="描述" />

7.6 图片加载优先级

<!-- 高优先级(首屏关键图片) -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="首屏" />

<!-- 低优先级(非关键图片) -->
<img src="icon.jpg" loading="lazy" fetchpriority="low" alt="图标" />

<!-- 自动优先级(默认) -->
<img src="image.jpg" loading="lazy" fetchpriority="auto" alt="描述" />

八、参考资料


本文深入对比了前端实现 SSE(Server-Sent Events)流式请求的三种主流方案,包括原理剖析、代码示例、优劣对比和实战踩坑经验,帮助开发者选择最适合的技术方案。

一、什么是 SSE?

SSE(Server-Sent Events)是 HTML5 标准的一部分,允许服务器主动向客户端推送数据。与 WebSocket 双向通信不同,SSE 是单向的(服务器 → 客户端),但实现更简单,且基于 HTTP 协议。

SSE 的典型应用场景

  • AI 对话流式输出:ChatGPT、Claude 等 AI 助手的打字机效果
  • 实时通知推送:消息提醒、系统通知
  • 实时数据更新:股票行情、体育比分
  • 进度监控:文件上传进度、任务执行状态

SSE 协议格式

data: 消息内容\n\n

完整格式包含多个字段:

event: message\n
id: 1\n
retry: 3000\n
data: 消息内容\n\n
  • data: - 消息内容(必需)
  • event: - 事件类型(可选,默认为 message)
  • id: - 消息 ID(可选,用于断线重连)
  • retry: - 重连间隔(可选,单位毫秒)
  • \n\n - 事件分隔符(两个换行符)

二、三种实现方案对比

方案一:原生 EventSource API

基本用法

const eventSource = new EventSource('/api/sse');

eventSource.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

eventSource.onerror = (error) => {
  console.error('连接错误:', error);
  eventSource.close();
};

// 手动关闭连接
eventSource.close();

完整封装示例

class SSEClient {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.eventSource = null;
    this.isCompleted = false;
  }
  
  connect() {
    this.eventSource = new EventSource(this.url);
    
    this.eventSource.onmessage = (event) => {
      // 检查结束信号
      if (event.data === '[DONE]') {
        this.close('done');
        return;
      }
      
      // 触发消息回调
      this.options.onMessage?.(event.data);
    };
    
    this.eventSource.onerror = () => {
      if (!this.isCompleted) {
        this.options.onError?.(new Error('SSE 连接异常'));
      }
      this.close('error');
    };
  }
  
  close(reason = 'manual') {
    if (this.isCompleted) return;
    
    this.isCompleted = true;
    this.eventSource?.close();
    this.options.onComplete?.(reason);
  }
}

// 使用示例
const client = new SSEClient('/api/chat', {
  onMessage: (data) => {
    console.log('收到:', data);
  },
  onComplete: (reason) => {
    console.log('结束:', reason);
  },
  onError: (err) => {
    console.error('错误:', err);
  },
});

client.connect();

// 手动停止
client.close();

优势

零依赖:浏览器原生 API,无需引入任何库
自动解析:浏览器自动处理 SSE 协议解析
自动重连:连接断开后自动重连(可配置 retry 时间)
稳定可靠:浏览器原生实现,经过充分测试
代码简洁:核心逻辑仅需 20-30 行代码

劣势

仅支持 GET 请求:无法发送 POST 请求体
无法自定义 Header:除了 Cookie,无法添加其他 Header(如 Authorization)
认证限制:只能通过 URL 参数或 Cookie 传递认证信息

适用场景

  • 后端接口支持 GET 请求
  • 认证信息通过 Cookie 自动携带
  • 追求零依赖和最简实现
  • 不需要自定义 Header

常见踩坑

1. 跨域问题

// 错误:跨域请求会失败
const es = new EventSource('https://other-domain.com/sse');

// 解决方案 1:服务器设置 CORS
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Credentials: true

// 解决方案 2:使用代理
const es = new EventSource('/api/proxy/sse');

2. 自动重连导致重复请求

// 问题:连接断开后会自动重连,可能导致重复请求

// 解决方案:服务器端使用 id 字段去重
// event: message
// id: 123
// data: 消息内容

// 客户端重连时会自动发送 Last-Event-ID header

3. 无法传递 POST 数据

// 错误:EventSource 不支持 POST
const es = new EventSource('/api/chat', {
  method: 'POST',  // 无效!
  body: JSON.stringify({ message: 'Hello' })
});

// 解决方案:先通过 POST 保存数据,再用 GET 建立 SSE 连接
async function sendMessage(message) {
  // 1. POST 保存消息
  const res = await fetch('/api/messages', {
    method: 'POST',
    body: JSON.stringify({ message }),
  });
  const { messageId } = await res.json();
  
  // 2. GET 建立 SSE 连接
  const es = new EventSource(`/api/sse?messageId=${messageId}`);
}

方案二:Fetch API + ReadableStream

基本用法

async function fetchSSE(url, options = {}) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer token',
    },
    body: JSON.stringify({ message: 'Hello' }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    
    if (done) {
      options.onComplete?.();
      break;
    }

    // 解码二进制数据
    buffer += decoder.decode(value, { stream: true });
    
    // 按行分割(SSE 以 \n\n 分隔事件)
    const lines = buffer.split('\n\n');
    buffer = lines.pop() || ''; // 保留不完整的行

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6);
        
        if (data === '[DONE]') {
          options.onComplete?.();
          return;
        }
        
        options.onMessage?.(data);
      }
    }
  }
}

// 使用示例
fetchSSE('/api/chat', {
  onMessage: (data) => {
    console.log('收到:', data);
  },
  onComplete: () => {
    console.log('完成');
  },
});

核心原理详解

1. ReadableStream 流式读取

const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  
  if (done) break;
  
  // value 是 Uint8Array 类型的二进制数据
  console.log(value);
}

2. TextDecoder 解码

const decoder = new TextDecoder();

// { stream: true } 很重要!
// 处理多字节字符被拆分到不同 chunk 的情况
const text = decoder.decode(value, { stream: true });

为什么需要 { stream: true }

// 假设 "你好" 的 UTF-8 编码被拆分到两个 chunk
// chunk1: [0xE4, 0xBD]  // "你" 的前两个字节
// chunk2: [0xA0, 0xE5, 0xA5, 0xBD]  // "你" 的最后一个字节 + "好"

const decoder = new TextDecoder();

// 不使用 stream: true(错误)
decoder.decode(chunk1);  // 乱码或报错
decoder.decode(chunk2);  // 乱码或报错

// 使用 stream: true(正确)
decoder.decode(chunk1, { stream: true });  // "" (等待完整字符)
decoder.decode(chunk2, { stream: true });  // "你好"

3. Buffer 缓冲区管理

let buffer = '';

// 收到 chunk1: "data: Hello\n"
buffer += "data: Hello\n";
const lines = buffer.split('\n\n');  // ["data: Hello\n"]
buffer = lines.pop();  // buffer = "data: Hello\n"

// 收到 chunk2: "\ndata: World\n\n"
buffer += "\ndata: World\n\n";  // "data: Hello\n\ndata: World\n\n"
const lines = buffer.split('\n\n');  // ["data: Hello", "data: World", ""]
buffer = lines.pop();  // buffer = ""

// 处理完整的事件
for (const line of lines) {
  if (line.startsWith('data: ')) {
    const data = line.slice(6);
    onMessage(data);  // "Hello", "World"
  }
}

4. 支持中断

const controller = new AbortController();

fetch(url, {
  signal: controller.signal,
});

// 中断请求
controller.abort();

完整封装示例

class FetchSSEClient {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.controller = new AbortController();
  }
  
  async connect() {
    try {
      const response = await fetch(this.url, {
        method: this.options.method || 'POST',
        headers: this.options.headers || {},
        body: this.options.body,
        signal: this.controller.signal,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';

      while (true) {
        const { done, value } = await reader.read();
        
        if (done) {
          this.options.onComplete?.('done');
          break;
        }

        buffer += decoder.decode(value, { stream: true });
        
        const lines = buffer.split('\n\n');
        buffer = lines.pop() || '';

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = line.slice(6);
            
            if (data === '[DONE]') {
              this.options.onComplete?.('done');
              return;
            }
            
            this.options.onMessage?.(data);
          }
        }
      }
    } catch (error) {
      if (error.name === 'AbortError') {
        this.options.onComplete?.('abort');
      } else {
        this.options.onError?.(error);
        this.options.onComplete?.('error');
      }
    }
  }
  
  abort() {
    this.controller.abort();
  }
}

// 使用示例
const client = new FetchSSEClient('/api/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token',
  },
  body: JSON.stringify({ message: 'Hello' }),
  onMessage: (data) => {
    console.log('收到:', data);
  },
  onComplete: (reason) => {
    console.log('结束:', reason);
  },
  onError: (err) => {
    console.error('错误:', err);
  },
});

client.connect();

// 手动停止
client.abort();

优势

支持 POST 请求:可以发送请求体
自定义 Header:可以添加任意 Header(如 Authorization)
完全控制:可以自定义超时、重试等逻辑
零依赖:浏览器原生 API
支持中断:使用 AbortController 中断请求

劣势

需要手动解析 SSE 协议:需要处理 data:event:id: 等字段
需要处理 chunk 边界:一个事件可能被拆分到多个 chunk
需要管理 buffer:手动拼接不完整的数据
代码复杂度高:核心逻辑需要 50+ 行代码
容易出错:边界情况处理不当会导致数据丢失或重复

适用场景

  • 需要 POST 请求和自定义 Header
  • 需要完全控制请求行为
  • 后端返回的不是标准 SSE 格式
  • 需要读取响应 Header 或状态码

常见踩坑

1. 忘记使用 { stream: true }

// 错误:多字节字符会乱码
const text = decoder.decode(value);

// 正确
const text = decoder.decode(value, { stream: true });

2. buffer 管理错误

// 错误:直接 split 会丢失不完整的数据
const lines = buffer.split('\n\n');
for (const line of lines) {
  // 处理...
}
buffer = '';  // 错误!最后一个可能不完整

// 正确:保留最后一个元素
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';  // 保留不完整的部分
for (const line of lines) {
  // 处理...
}

3. 没有处理中断

// 错误:没有处理 AbortError
try {
  await fetch(url, { signal });
} catch (error) {
  console.error(error);  // AbortError 也会被当作错误
}

// 正确:区分 AbortError
try {
  await fetch(url, { signal });
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('用户取消');
  } else {
    console.error('请求失败:', error);
  }
}

4. 代理层缓冲问题

// 问题:Nginx 等代理可能会缓冲响应,导致消息延迟

// 解决方案:服务器端配置
// Nginx:
// proxy_buffering off;
// proxy_cache off;
// proxy_set_header Connection '';
// proxy_http_version 1.1;
// chunked_transfer_encoding off;

// Node.js:
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();  // 立即发送 headers

方案三:@microsoft/fetch-event-source

基本用法

npm install @microsoft/fetch-event-source
import { fetchEventSource } from '@microsoft/fetch-event-source';

fetchEventSource('/api/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token',
  },
  body: JSON.stringify({ message: 'Hello' }),
  
  onmessage(event) {
    if (event.data === '[DONE]') {
      return;
    }
    console.log('收到:', event.data);
  },
  
  onclose() {
    console.log('连接关闭');
  },
  
  onerror(err) {
    console.error('错误:', err);
    throw err; // 抛出错误会停止重连
  },
  
  openWhenHidden: true, // 页面隐藏时保持连接
});

高级用法

class MSFetchSSEClient {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.ctrl = new AbortController();
  }
  
  async connect() {
    await fetchEventSource(this.url, {
      method: this.options.method || 'POST',
      headers: this.options.headers || {},
      body: this.options.body,
      signal: this.ctrl.signal,
      
      onmessage: (event) => {
        if (event.data === '[DONE]') {
          this.abort();
          return;
        }
        this.options.onMessage?.(event.data);
      },
      
      onclose: () => {
        this.options.onComplete?.('done');
      },
      
      onerror: (err) => {
        this.options.onError?.(err);
        // 抛出错误会停止重连
        throw err;
      },
      
      openWhenHidden: true,
    });
  }
  
  abort() {
    this.ctrl.abort();
  }
}

// 使用示例
const client = new MSFetchSSEClient('/api/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token',
  },
  body: JSON.stringify({ message: 'Hello' }),
  onMessage: (data) => {
    console.log('收到:', data);
  },
  onComplete: (reason) => {
    console.log('结束:', reason);
  },
  onError: (err) => {
    console.error('错误:', err);
  },
});

client.connect();

// 手动停止
client.abort();

优势

支持 POST 请求:可以发送请求体
自定义 Header:可以添加任意 Header
自动解析 SSE 协议:无需手动处理
内置自动重连:连接断开后自动重连
页面隐藏时保持连接openWhenHidden 选项
体积小:~2KB gzip
TypeScript 支持:完整的类型定义

劣势

需要引入外部依赖:增加打包体积
自动重连可能不符合需求:需要手动控制
错误处理复杂:需要抛出错误才能停止重连
中断机制不直观:需要使用 AbortController

适用场景

  • 需要 POST 请求和自定义 Header
  • 需要自动重连功能
  • 不介意引入外部依赖
  • 追求开发效率

常见踩坑

1. 自动重连导致重复请求

// 问题:连接断开后会自动重连,可能导致重复请求

// 解决方案 1:抛出错误停止重连
onerror(err) {
  console.error(err);
  throw err;  // 停止重连
}

// 解决方案 2:使用 AbortController
const ctrl = new AbortController();
fetchEventSource(url, {
  signal: ctrl.signal,
  onerror(err) {
    ctrl.abort();  // 停止重连
  },
});

2. 无法获取完成状态

// 问题:onclose 回调无法区分正常结束还是错误结束

// 解决方案:自己维护状态
let isCompleted = false;

fetchEventSource(url, {
  onmessage(event) {
    if (event.data === '[DONE]') {
      isCompleted = true;
      ctrl.abort();
    }
  },
  onclose() {
    if (isCompleted) {
      console.log('正常结束');
    } else {
      console.log('异常结束');
    }
  },
});

3. 页面隐藏时连接断开

// 问题:默认情况下,页面隐藏时连接会断开

// 解决方案:设置 openWhenHidden
fetchEventSource(url, {
  openWhenHidden: true,  // 页面隐藏时保持连接
});

三、方案对比总结

维度EventSourceFetch + ReadableStream@microsoft/fetch-event-source
支持 POST❌ 仅 GET
自定义 Header❌ 仅 Cookie
SSE 协议解析✅ 浏览器原生❌ 需手动实现✅ 自动处理
自动重连✅ 浏览器原生❌ 需手动实现✅ 内置
手动中断✅ close()✅ AbortController⚠️ AbortController + throw
代码复杂度低(~30行)高(~50行)中(~10行)
外部依赖零依赖零依赖~2KB
浏览器兼容性IE 10+现代浏览器现代浏览器
学习成本
适用场景GET + Cookie 认证完全自定义POST + 自动重连

四、选型建议

选择 EventSource 的情况

  • ✅ 后端接口支持 GET 请求
  • ✅ 认证信息通过 Cookie 自动携带
  • ✅ 追求零依赖和最简实现
  • ✅ 不需要自定义 Header

选择 Fetch + ReadableStream 的情况

  • ✅ 需要完全控制请求行为
  • ✅ 后端返回的不是标准 SSE 格式
  • ✅ 需要读取响应 Header 或状态码
  • ✅ 需要自定义超时、重试逻辑

选择 @microsoft/fetch-event-source 的情况

  • ✅ 需要 POST 请求和自定义 Header
  • ✅ 需要自动重连功能
  • ✅ 不介意引入外部依赖
  • ✅ 追求开发效率

五、实战经验总结

1. 如何处理认证

方案 A:Cookie(推荐 EventSource)

// 服务器设置 Cookie
res.cookie('token', 'xxx', { httpOnly: true });

// 客户端自动携带
const es = new EventSource('/api/sse');

方案 B:URL 参数(适用 EventSource)

const token = 'xxx';
const es = new EventSource(`/api/sse?token=${token}`);

方案 C:Authorization Header(需要 Fetch)

fetch('/api/sse', {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
});

2. 如何处理超时

EventSource(无法设置超时)

// 解决方案:手动实现超时
const es = new EventSource('/api/sse');
const timeout = setTimeout(() => {
  es.close();
  console.log('超时');
}, 30000);

es.onmessage = () => {
  clearTimeout(timeout);
};

Fetch(使用 AbortController)

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);

fetch('/api/sse', {
  signal: controller.signal,
});

3. 如何处理重连

EventSource(自动重连)

// 服务器端设置重连间隔
res.write('retry: 3000\n\n');  // 3 秒后重连

// 客户端无法禁用自动重连
// 只能通过 close() 关闭连接

Fetch(手动重连)

async function connectWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await fetchSSE(url, { /* ... */ });
      break;  // 成功,退出循环
    } catch (error) {
      if (i === maxRetries - 1) {
        throw error;  // 最后一次重试失败
      }
      await new Promise(resolve => setTimeout(resolve, 3000));
    }
  }
}

4. 如何处理代理层缓冲

问题: Nginx 等代理可能会缓冲响应,导致消息延迟

解决方案:

# Nginx 配置
location /api/sse {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
}
// Node.js 服务器
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');  // 禁用 Nginx 缓冲
res.flushHeaders();  // 立即发送 headers

5. 如何处理跨域

CORS 配置:

// 服务器端
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

代理方案:

// 开发环境使用代理
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://backend.com',
        changeOrigin: true,
      },
    },
  },
};

六、总结

SSE 是实现服务器推送的简单而强大的技术,三种实现方案各有优劣:

  • EventSource:零依赖、最简单,适合 GET 请求场景
  • Fetch + ReadableStream:完全控制、最灵活,适合复杂场景
  • @microsoft/fetch-event-source:功能完善、开箱即用,适合快速开发

选择哪种方案取决于具体需求,建议优先考虑 EventSource,只有在需要 POST 或自定义 Header 时才使用其他方案。


参考资料:

24 年被业务员推销办了个华夏的信用卡,当时给我说每年刷任意金额 6 笔即可免年费。后来 25 年下半年不怎么用了,今天突然发现有一个 680 的年费账单。
打电话给客服,强调当时说了只要 6 笔就免年费,客服咬定要消费 20000 积分兑换(1 元=1 积分)。我说你看去年账号,我是不是没有消费 2w 也给我免年费了。客服说去年也扣了。
妈的,给我气笑了,一看账单还真是,没注意就扣了。
有啥追回的办法吗?目前仅向 【金融消保服务平台】投诉

作为一个内向,但是又不想让别人觉得自己过于奇怪的人,我学习社交的方法其实就是统计模型。

我的终极目标是,以最小的社交输出让别人觉得我不是一个很古怪的人。因此我的方法是无时无刻不刻意观察群体里大家社交的方式,然后根据统计规律找出来一些大概率要说的话,大概率要做的动作,以及针对大概率被问的问题,有哪些大概率的回答。最后我会设定一个大概率还算可以的对话深度,一旦达到这个深度就判断对方不会再认为自己是个怪人,最后以一个大概率还算合体的离开方式结束社交。

那我在这一块,和 LLM 有啥本质区别呢?

请问各位大佬,我在 macbook 设置了网页代理,访问 localhost 和 127.0.01 时,代理服务器没有抓到请求,是因为代理对本地地址不生效吗。代理是在 系统设置-wifi-详细信息-代理-网页代理 设置的

但是并没有消失,只是换了一种名称,比如说:Viber ,用来指代那些过度依赖于 LLM 自动代码生成的开发人员。

Programmer 仍会继续存在,因为要让 AI 生成好的代码,前提是自己头脑清楚,包括对设计层面的把控,软件各个模块的设计和分工,对各种实现方式利弊的考量,以及能否在 review 中看出 AI 埋下的坑。相当一部分工作,无法由 AI 替代。在软件开发领域,没有银弹,数量也无法弥补质量。

代码的行数,以及编码的速度(或者说生成速度),远没有代码的质量更加重要。同样的 spec/功能/行为,可以用上千行代码实现,也可以几百行代码优雅的实现,不同的实现方式的后续可维护性、可读性、可扩展性天差地别。

你可以选择成为一名 Viber ,或者继续在 Programmer/Developer 的道路上精进。

一、引言

在 Vue 项目中实现路由懒加载时,我们经常会看到三种不同的写法:

// 方式一:ES6 动态 import()
component: () => import('@/views/Home.vue')

// 方式二:Webpack require.ensure
component: resolve => require(['@/views/Home.vue'], resolve)

// 方式三:import() + babel-plugin-dynamic-import-node
// babel.config.js 配置了 'dynamic-import-node' 插件
component: () => import('@/views/Home.vue')

这三种方式看起来相似,但在 Webpack 编译过程中的行为完全不同,最终产物也有很大差异。本文将深入分析:

  • ES6 import 语句和 import() 表达式的本质区别
  • Webpack 如何特殊处理 import() 实现代码分割
  • 每种方式的工作原理和转换过程
  • 它们各自的优势和适用场景
  • 如何根据项目需求选择最佳方案
  • Webpack target 配置的关键作用

二、理解 import:ES6 标准 vs Webpack 实现

在深入路由懒加载之前,我们需要先理解 JavaScript 中 import 的两种完全不同的形式。

2.1 ES6 静态 import 语句

语法:

import React from 'react';
import { useState } from 'react';

核心特点:

根据阮一峰《ES6 入门教程》

"ES6 模块是编译时输出接口,在代码静态解析阶段就会生成。"
  • 静态语法:必须在文件顶层使用,不能在条件语句、函数内部使用
  • 编译时处理:在代码静态解析阶段就确定了模块依赖关系
  • 路径固定:路径必须是字符串字面量,不能是变量
// ❌ 错误:不能在函数内部使用
function loadModule() {
  import module from './module';  // SyntaxError
}

// ❌ 错误:不能使用变量
const path = './module';
import module from path;  // SyntaxError

2.2 ES6 动态 import() 表达式

语法:

import('./module.js').then(module => {
  // 使用模块
});

// 或使用 async/await
const module = await import('./module.js');

核心特点:

根据阮一峰《ES6 入门教程》

"CommonJS 模块加载 ES6 模块,不能使用 require 命令,而要使用 import() 函数。"
  • 动态语法:可以在任何地方使用(条件语句、函数内部等)
  • 运行时处理:在代码执行时才加载模块
  • 异步加载:返回一个 Promise
  • 路径可变:路径可以是变量或表达式
// ✅ 正确:可以在函数内部使用
function loadModule() {
  return import('./module.js');
}

// ✅ 正确:可以使用变量
const language = 'zh';
import(`./i18n/${language}.js`).then(module => {
  // 使用模块
});

2.3 Webpack 对 import() 的特殊处理

虽然 import() 是 ES6 标准语法,但 Webpack 对它进行了特殊处理,赋予了额外的功能。

根据 Webpack 官方文档 - Code Splitting

"Two similar techniques are supported by webpack when it comes to dynamic code splitting. The first and recommended approach is to use the import() syntax that conforms to the ECMAScript proposal for dynamic imports."

"Calls to import() are treated as split points, meaning the requested module and its children are split out into a separate chunk."

Webpack 的特殊处理:

  1. 代码分割点:Webpack 将 import() 识别为代码分割点
  2. 创建独立 chunk:被导入的模块会被打包成独立的 chunk 文件
  3. 生成加载代码:Webpack 生成异步加载该 chunk 的运行时代码
  4. 支持魔法注释:可以通过注释控制 chunk 名称、预加载等行为

示例:

// 普通动态导入
import('./module.js')

// Webpack 魔法注释
import(
  /* webpackChunkName: "my-chunk" */
  /* webpackPrefetch: true */
  './module.js'
)

Webpack 编译后的效果:

// 源代码
const module = await import('./Home.vue');

// Webpack 编译后(简化版)
const module = await __webpack_require__.e(/* chunkId */ 123)
  .then(__webpack_require__.bind(null, /* moduleId */ 456));

// 生成的文件
// dist/main.js          - 主 bundle
// dist/123.js           - Home.vue 的独立 chunk

2.4 关键区别总结

特性静态 import动态 import()Webpack 处理的 import()
语法类型声明语句表达式表达式
使用位置仅文件顶层任何位置任何位置
加载时机编译时运行时运行时
加载方式同步异步异步
返回值直接导入PromisePromise
路径类型字符串字面量任意表达式任意表达式
代码分割否(原生)✅ 是(Webpack)
独立 chunk否(原生)✅ 是(Webpack)

重要理解:

  • ES6 原生的 import() 只是异步加载模块,不会自动进行代码分割
  • Webpack 处理的 import() 在异步加载的基础上,额外实现了代码分割和 chunk 生成
  • 在浏览器中,原生 import() 会发起网络请求加载模块文件
  • 在 Webpack 打包后,import() 会被转换为 Webpack 的运行时代码,加载打包后的 chunk

三、方式一:ES6 动态 import()

2.1 用法

// src/router.js
{
  path: '/home',
  component: () => import('@/views/Home.vue')
}

2.2 工作原理

Babel 处理阶段

在没有额外 Babel 插件的情况下,import() 语法会被保留或由 @vue/babel-preset-app 处理,但不会被转换为其他形式。

Webpack 编译阶段

  1. 识别 import() 语法:Webpack 识别到 import() 是一个代码分割点
  2. 创建独立 chunk:为该模块创建一个单独的 chunk 文件
  3. 生成运行时代码:生成异步加载该 chunk 的运行时代码

Webpack 生成的运行时代码(target: 'web' 默认):

// Webpack 运行时代码 - ES2015 风格
__webpack_require__.e = (chunkId) => {  // 箭头函数
  const promises = [];  // const
  const installedChunkData = installedChunks[chunkId];
  
  if (installedChunkData !== 0) {
    const promise = new Promise((resolve, reject) => {  // 箭头函数
      installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    promises.push(installedChunkData[2] = promise);
    
    const script = document.createElement('script');
    script.src = `${__webpack_require__.p}${chunkId}.js`;  // 模板字符串
    document.head.appendChild(script);
  }
  
  return Promise.all(promises);
};

// 调用方式
__webpack_require__.e(/* chunkId */ 123).then(__webpack_require__.bind(null, /* moduleId */ 456))

2.3 优势

  1. 符合 ES 标准import() 是 ECMAScript 标准语法,未来兼容性好
  2. 代码分割:自动实现代码分割,生成独立的 chunk 文件
  3. 按需加载:只在需要时才加载对应的代码,减少首屏加载时间
  4. 代码现代化:代码更简洁、易读

2.4 适用场景

  • 现代浏览器环境(Chrome 63+, Safari 11.1+, Firefox 67+)
  • 需要代码分割和按需加载的项目
  • 追求代码现代化的新项目

2.5 潜在问题

老旧浏览器兼容性问题:

Webpack 生成的运行时代码默认使用 ES2015 语法(箭头函数、const、模板字符串),在不支持 ES6 的老旧浏览器中会报错:

Uncaught SyntaxError: Unexpected token '=>'

解决方案: 配置 Webpack target(详见第五章)


三、方式二:Webpack require.ensure

3.1 用法

// src/router.js
{
  path: '/home',
  component: resolve => require(['@/views/Home.vue'], resolve)
}

3.2 工作原理

Babel 处理阶段

require.ensure 是 Webpack 特有的语法,Babel 不会对其进行转换,直接传递给 Webpack。

Webpack 编译阶段

根据 Webpack 官方文档

"require.ensure: Split out the given dependencies to a separate bundle that will be loaded asynchronously."
  1. 识别 require.ensure 语法:Webpack 识别到这是一个代码分割点
  2. 创建独立 chunk:为该模块创建一个单独的 chunk 文件
  3. 生成运行时代码:生成异步加载该 chunk 的运行时代码

生成的运行时代码与 import() 相同:

// 与 import() 生成的运行时代码完全一致
__webpack_require__.e(/* chunkId */ 123).then(__webpack_require__.bind(null, /* moduleId */ 456))

3.3 优势

  1. 代码分割:实现代码分割,生成独立的 chunk 文件
  2. 按需加载:只在需要时才加载对应的代码
  3. 历史兼容:在 Webpack 1/2 时代广泛使用

3.4 劣势

  1. 非标准语法:这是 Webpack 特有的语法,不是 JavaScript 标准
  2. 已被废弃:Webpack 官方推荐使用 import() 替代
  3. 代码不够现代化:语法相对复杂,可读性较差

3.5 适用场景

  • 维护老旧项目(Webpack 1/2 时代的项目)
  • 需要兼容老旧浏览器,但不想配置 target(不推荐)

3.6 重要说明

require.ensure 和 import() 生成的运行时代码相同,兼容性取决于 Webpack 的 target 配置,而不是语法本身。


四、方式三:babel-plugin-dynamic-import-node

4.1 用法

源代码:

// src/router.js
{
  path: '/home',
  component: () => import('@/views/Home.vue')
}

Babel 配置:

// babel.config.js
module.exports = {
  presets: ['@vue/app'],
  plugins: ['dynamic-import-node']
}

4.2 工作原理

Babel 处理阶段

根据 babel-plugin-dynamic-import-node 文档:

"Babel plugin to transpile import() to a deferred require(), for node."

转换过程:

// 转换前
component: () => import('@/views/Home.vue')

// 转换后
component: () => require('@/views/Home.vue')

Webpack 编译阶段

  1. 识别 require() 语法:Webpack 看到的是 require() 而不是 import()
  2. 同步打包require() 是同步导入,Webpack 不会创建新的 chunk
  3. 打包到当前文件:模块被直接打包到当前 chunk 中

Webpack 生成的代码:

// 直接同步加载,没有异步逻辑
component: () => __webpack_require__(/* moduleId */ 456)

构建产物对比:

# 不使用 dynamic-import-node(正常拆包)
dist/js/chunk-vendors.js
dist/js/app.js
dist/js/home.js          # 单独的路由 chunk ✅
dist/js/profile.js       # 单独的路由 chunk ✅

# 使用 dynamic-import-node(拆包失效)
dist/js/chunk-vendors.js
dist/js/app.js           # 所有路由都在这里 ❌

4.3 优势

  1. 提升编译速度:同步导入比异步导入编译更快
  2. 避免运行时代码:没有异步加载的运行时代码,避免了 ES6 语法问题
  3. 适合 Node.js 环境:这个插件最初是为 Node.js 环境设计的

4.4 劣势

  1. 代码拆包失效:所有路由组件被打包到一个文件中
  2. 首屏加载慢:失去了按需加载的性能优势
  3. bundle 体积大:所有代码都在一个文件中,体积过大

4.5 适用场景

推荐场景:仅在开发环境使用

// babel.config.js
module.exports = {
  presets: ['@vue/app'],
  plugins: [
    // 只在开发环境使用,提升构建速度
    process.env.NODE_ENV === 'development' && 'dynamic-import-node'
  ].filter(Boolean)
};

原因:

  • 开发环境不需要代码分割,使用同步导入可以加快编译速度
  • 生产环境需要代码分割以优化性能

不推荐场景:生产环境使用

// ❌ 不要在生产环境使用
plugins: ['dynamic-import-node']

原因:

  • 代码拆包失效,失去按需加载的性能优势
  • 首屏加载时间过长
  • bundle 体积过大

4.6 常见误区

误区:dynamic-import-node 能解决兼容性问题

真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 Webpack 的 target。


五、Webpack target 配置的关键作用

5.1 target 配置的作用

根据 Webpack 官方文档 - target 配置

"Note that webpack runtime code is not the same as the user code you write, you should transpile that code with transpilers like Babel if you want to target specific environments."

"When no information about the target or the environment features is provided, then ES2015 will be used."

关键理解:

  1. Webpack 的运行时代码不会被 Babel 转译
  2. target 配置决定运行时代码的 ES 版本
  3. 默认 target: 'web' 生成 ES2015 代码

5.2 target: 'web' (默认)

// webpack.config.js 或 vue.config.js
module.exports = {
  configureWebpack: {
    target: 'web'  // 默认值
  }
};

生成的运行时代码:

__webpack_require__.e = (chunkId) => {  // ES6 箭头函数
  const promises = [];  // ES6 const
  const script = document.createElement('script');
  script.src = `${__webpack_require__.p}${chunkId}.js`;  // ES6 模板字符串
  // ...
};

结果:

  • ✅ 现代浏览器正常运行
  • ❌ 老旧浏览器报语法错误

5.3 target: ['web', 'es5']

// webpack.config.js 或 vue.config.js
module.exports = {
  configureWebpack: {
    target: ['web', 'es5']  // 生成 ES5 代码
  }
};

生成的运行时代码:

__webpack_require__.e = function requireEnsure(chunkId) {  // ES5 function
  var promises = [];  // ES5 var
  var script = document.createElement('script');
  script.src = __webpack_require__.p + chunkId + '.js';  // 字符串拼接
  // ...
};

结果:

  • ✅ 现代浏览器正常运行
  • ✅ 老旧浏览器正常运行
  • ✅ 代码分割功能正常工作

5.4 target 配置的重要性

无论使用 import() 还是 require.ensure,兼容性都取决于 target 配置,而不是语法本身。


六、三种方式对比总结

6.1 完整对比表

特性import()require.ensureimport() + dynamic-import-node
标准化✅ ES 标准❌ Webpack 特有✅ ES 标准(源码)
代码分割✅ 是✅ 是❌ 否
按需加载✅ 是✅ 是❌ 否
编译速度中等中等✅ 快
代码现代化✅ 高❌ 低✅ 高(源码)
老旧浏览器兼容取决于 target取决于 target✅ 是(但失去拆包)
推荐使用✅ 是❌ 否⚠️ 仅开发环境

6.2 转换流程对比

场景一:import() + target: 'web' (默认)

源代码: () => import('@/views/Home.vue')
    ↓
Babel: 不转换
    ↓
Webpack: 创建 chunk,生成 ES2015 运行时代码
    ↓
结果: 
  - 代码拆包 ✅
  - 现代浏览器 ✅
  - 老旧浏览器 ❌

场景二:import() + target: ['web', 'es5'] (推荐)

源代码: () => import('@/views/Home.vue')
    ↓
Babel: 不转换
    ↓
Webpack: 创建 chunk,生成 ES5 运行时代码
    ↓
结果: 
  - 代码拆包 ✅
  - 现代浏览器 ✅
  - 老旧浏览器 ✅

场景三:import() + dynamic-import-node

源代码: () => import('@/views/Home.vue')
    ↓
Babel: 转换为 () => require('@/views/Home.vue')
    ↓
Webpack: 同步打包,不创建 chunk
    ↓
结果: 
  - 代码拆包 ❌
  - 编译速度 ✅
  - 适合开发环境

场景四:require.ensure + target: ['web', 'es5']

源代码: resolve => require(['@/views/Home.vue'], resolve)
    ↓
Babel: 不转换
    ↓
Webpack: 创建 chunk,生成 ES5 运行时代码
    ↓
结果: 
  - 代码拆包 ✅
  - 现代浏览器 ✅
  - 老旧浏览器 ✅
  - 但使用了历史遗留 API ⚠️

七、最佳实践配置

7.1 配置一:标准配置(推荐)

适用场景: 需要支持老旧浏览器,需要代码分割

// vue.config.js
module.exports = {
  configureWebpack: {
    target: ['web', 'es5']  // 关键配置
  }
};

// babel.config.js
module.exports = {
  presets: ['@vue/cli-plugin-babel/preset']
};

// router.js
{
  path: '/home',
  component: () => import('@/views/Home.vue')
}

优势:

  • ✅ 配置简单
  • ✅ 兼容老旧浏览器
  • ✅ 代码分割正常工作
  • ✅ 开发和生产环境行为一致

7.2 配置二:性能优化配置(推荐)

适用场景: 需要支持老旧浏览器,需要代码分割,希望提升开发环境编译速度

// vue.config.js
module.exports = {
  configureWebpack: {
    target: ['web', 'es5']  // 关键配置
  }
};

// babel.config.js
module.exports = {
  presets: ['@vue/cli-plugin-babel/preset'],
  plugins: [
    // 只在开发环境使用
    process.env.NODE_ENV === 'development' && 'dynamic-import-node'
  ].filter(Boolean)
};

// router.js
{
  path: '/home',
  component: () => import('@/views/Home.vue')
}

优势:

  • ✅ 开发环境编译快
  • ✅ 生产环境代码分割正常
  • ✅ 兼容老旧浏览器
  • ✅ 兼顾性能和开发体验

7.3 配置三:现代浏览器配置

适用场景: 只需要支持现代浏览器(Chrome 63+, Safari 11.1+)

// vue.config.js
module.exports = {
  configureWebpack: {
    target: 'web'  // 默认值
  }
};

// babel.config.js
module.exports = {
  presets: ['@vue/cli-plugin-babel/preset']
};

// router.js
{
  path: '/home',
  component: () => import('@/views/Home.vue')
}

优势:

  • ✅ 配置最简单
  • ✅ 生成的代码更小
  • ✅ 运行时性能更好

八、常见误区澄清

误区 1:import() 不兼容老旧浏览器

真相: import() 语法本身不是问题,Webpack 生成的运行时代码才是问题。配置 target: ['web', 'es5'] 后完全兼容。

误区 2:require.ensure 更兼容

真相: require.ensureimport() 生成的运行时代码相同,兼容性取决于 target 配置,而不是语法本身。

误区 3:dynamic-import-node 能解决兼容性问题

真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 target。


九、参考资料


如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题欢迎在评论区讨论。

UvOQbDgG0FYoihtVbFh5aors9Ar0K1yr

折腾到凌晨,终于搞定了! 用 AI 写了一个脚本,可以用一行命令让你正在使用的免费 SLL 可以自动化续签! 对小白和初级用户非常友好,无学习成本。 完全免费,只需关注我公众号,哈哈不容易多支持

https://ssl.cab

欢迎交流,大佬可以 pass 了!只适合初级用户!!

适用场景:就是你在宝塔里面已经用了一堆的免费 SSL,想一键自动化。这种情况下,用我这个比较方便。

今天看的知乎热榜第二的话题
回答也很多看着看着刷新一下就没了

提示:
你似乎来到了没有知识存在的荒原

那么谁把知乎热榜话题连全部回答都删了?

搜索引擎里面还可以搜到
《如果百度当年梭哈 AI ,现在的市值能达到腾讯、阿里的水平吗?》
原始链接现在已无法访问:
https://www.zhihu.com/question/1950611259528549544

应该不是知乎删的吧?
毕竟整个话题包含成千上万个用户的评论和回复,
这么一删,
这么多网友花时间精力的那么多文字回复都没了!
知乎应该不会做这种自掘坟墓的事情吧?
毕竟这样以后谁还愿意花时间去知乎参与问答呢!

主要是有几个前百度大牛的回复写的很好
还没看完就没了
就好比你追的精彩小说彻底断更了
很膈应人啊!

在线求助,帮忙用我的 service id: B44T-FSWJ-XCJS-JZMA 生成一个试用的 lisence

访问 http://my.atlassian.com
登录或注册 Atlassian 账号(免费)
点击 "New License" 或 "Generate License"
选择产品:Jira Software / Jira Service Management (按需选)
选择 "Evaluation"(试用) 类型
输入 Server ID:B44T-FSWJ-XCJS-JZMA
生成后复制 License Key ,粘贴给我, 在线等,急用,我一直报下面的 reCAPTCHA 验证错误信息

这篇文章从基本原理出发完整拆解变分自编码器(VAE)的构建过程。重点不在数学推导而在于把概念落到足够具体的层面:完成实现、训练、调试和部署。每个组件做了什么、为什么需要它、代码里怎么写文章都会逐一交代,后半部分会逐行走读一个最小化的 PyTorch 实现,并介绍训练完成后的几种推理模式。

VAE 为什么存在

当需要理解数据中潜藏的模式时,变分自编码器(VAE)是一个值得考虑的选项。它不只是做数据压缩而是以连续、可插值的方式去捕捉数据背后的生成结构:覆盖可能性的完整范围。训练结束后模型既能重建已有样本,也能生成新的逼真样本,还能对新数据做异常检测。

标准自编码器的做法是把数据压缩到低维表示再还原。问题出在它学到的潜在空间往往缺乏结构:相邻的点未必对应相似的数据,随机采样产出的结果也大多没有意义。

VAE 对潜在空间施加了正则化约束。输入不再被映射到单一的潜在向量,而是映射到一个分布:通常是高斯分布。训练期间,模型让这些分布贴近一个已知的先验(一般取标准正态分布),最终得到的潜在空间平滑且有组织,可以安全地从中采样和推断。

这正是 VAE 在异常检测和表示学习中发挥作用的原因。重建偏差可以在原始特征空间中得到直观解读;潜在偏差如果实现了解耦也具有可解释性,但缺少专门的训练目标(如 β-VAE、FactorVAE 等)时,解耦并无保证。

VAE 在学习重建数据的同时,也在学习一个形态接近简单概率分布的潜在空间。

三个核心组件

VAE 由三个概念层面的部件组成——编码器、潜在空间(通过采样/重参数化技巧实现)、解码器。

编码器接收输入,输出两个向量:均值 μ 和方差 σ²,二者共同定义潜在空间上的概率分布。采样步骤利用一种保持可微性的技巧从该分布中抽取潜在向量,解码器再拿到这个向量去重建原始输入。

训练过程同时平衡两个目标:重建要准确,潜在分布要贴近先验。VAE 的结构特性正来源于这种平衡。

损失函数

VAE 的损失函数同时追求两件事:把输入数据重建得尽可能准,并约束潜在空间服从标准正态分布。重建损失(比如均方误差)度量输出与输入的偏差;KL 散度度量学习到的潜在分布与标准正态分布之间的距离。

换个角度看——重建损失在奖励保真度,KL 损失则在防止模型死记硬背,迫使潜在空间维持良好的分布形态。

VAE 的损失函数同时追求两件事:把输入数据重建得尽可能准,并约束潜在空间服从标准正态分布。

从理论到代码

下面逐步走读一个最小化的 PyTorch 实现。示例假定输入为表格或展平后的数据,但同样的思路适用于图像和序列。

定义编码器

编码器将输入向量映射为两个输出:潜在分布的均值和对数方差。

 import torch                     # PyTorch核心库
import torch.nn as nn            # 神经网络构建模块
import torch.nn.functional as F  # 常用激活函数和工具函数
  class Encoder(nn.Module):        # 将编码器定义为神经网络模块
        def __init__(self, input_dim, latent_dim):  
            super().__init__()        # 初始化父类nn.Module
       # 第一个全连接层:
        # 接收输入数据并将其映射到隐藏表示
        self.fc1 = nn.Linear(input_dim, 128)  
          
        # 输出潜在分布均值(mu)的线性层
        self.fc_mu = nn.Linear(128, latent_dim)  
          
        # 输出潜在分布对数方差(log σ²)的线性层
        # 使用对数方差是为了数值稳定性
        self.fc_logvar = nn.Linear(128, latent_dim)  

    def forward(self, x):  
        # 将输入通过第一层并应用ReLU激活函数
        # 从原始输入中提取有用特征
        h = F.relu(self.fc1(x))  
          
        # 计算潜在分布的均值
        mu = self.fc_mu(h)  
          
        # 计算潜在分布的对数方差
        logvar = self.fc_logvar(h)  
          
        # 返回两个参数,以便后续从分布中采样
         return mu, logvar

到这一步为止,还没有涉及任何概率运算。代码只是在预测分布的参数。

编码器将输入数据压缩为紧凑的潜在表示(μ 和 σ),捕捉其关键特征。

重参数化技巧

直接从分布中采样会切断梯度的传播路径。重参数化技巧的做法是把采样拆解为一个确定性函数加上随机噪声。

 def reparameterize(mu, logvar):  
    # 将对数方差转换为标准差
    # std = sqrt(variance)
    # 使用logvar保持训练的数值稳定性
    std = torch.exp(0.5 * logvar)  
      
    # 从标准正态分布中采样随机噪声
    # 这个噪声是使VAE具有随机性的关键
    eps = torch.randn_like(std)  
      
    # 创建潜在向量z
    # z = 均值 + (随机噪声 * 标准差)
    # 这使得梯度在训练过程中能够通过mu和std流动
     return mu + eps * std

梯度照常经由 μ 和 σ 回传,随机性又被保留下来。

重参数化技巧让 VAE 能在潜在空间中采一个随机点,同时不破坏反向传播。本质上,它把随机性改写成了网络可以求导的形式。

定义解码器

解码器负责把潜在向量映射回原始输入空间。

 class Decoder(nn.Module):  
    def __init__(self, latent_dim, output_dim):  
        # 初始化PyTorch父模块
        super().__init__()  
          
        # 第一个全连接层
        # 接收潜在向量z并将其扩展为隐藏表示
        self.fc1 = nn.Linear(latent_dim, 128)  
          
        # 输出层
        # 将隐藏表示映射回原始输入大小
        # 产生重建结果
        self.fc_out = nn.Linear(128, output_dim)  
  def forward(self, z):  
      # 将潜在向量z通过第一个线性层
      # 并应用ReLU引入非线性
      h = F.relu(self.fc1(z))  
      
      # 将隐藏表示通过输出层
      # 产生重建的输入
       return self.fc_out(h)

解码器不需要知道任何关于概率分布的事情,它只做重建。

解码器从采样得到的潜在向量出发,尝试恢复出与原始输入匹配的数据。

组合在一起:VAE

 class VAE(nn.Module):  
    def __init__(self, input_dim, latent_dim):  
        super().__init__()  
          
        # 编码器将输入数据映射到潜在分布(mu, logvar)
        self.encoder = Encoder(input_dim, latent_dim)  
          
        # 解码器将潜在向量z映射到重建的输入
        self.decoder = Decoder(latent_dim, input_dim)  

    def forward(self, x):  
        # 将输入通过编码器获取潜在分布参数
        mu, logvar = self.encoder(x)  
          
        # 使用重参数化技巧采样潜在向量z
        z = reparameterize(mu, logvar)  
          
        # 将潜在向量解码回输入空间
        recon_x = self.decoder(z)  
          
        # 返回重建结果和潜在统计量(用于计算损失)
         return recon_x, mu, logvar

前向传播的流程与概念上的流程完全一致。

损失函数的代码实现

  def vae_loss(recon_x, x, mu, logvar, beta=1.0):  
    # 重建损失:
    # 衡量输出与原始输入的接近程度
    recon_loss = F.mse_loss(recon_x, x, reduction='mean')  
      
    # KL散度损失:
    # 惩罚潜在分布偏离标准正态分布过远的情况
    kl_loss = -0.5 * torch.mean(  
        1 + logvar - mu.pow(2) - logvar.exp()  
    )  
      
    # 总损失平衡重建质量
    # 和潜在空间正则化
     return recon_loss + beta * kl_loss

beta 参数控制重建质量与潜在正则化之间的权衡。当 beta > 1 时即为 β-VAE——以牺牲重建精度为代价换取更解耦的潜在因子。

训练循环

训练阶段模型只接触数据样本,优化上述组合损失。有一点需要特别留意:用于异常检测时,VAE 通常仅在正常数据上训练,模型由此学会"正常"的分布形态,异常样本则在推理时暴露为高重建误差或偏离常规的潜在分布。

 # 创建优化器来更新VAE的参数
# Adam是训练神经网络的常用且稳定的选择
optimizer = torch.optim.Adam(vae.parameters(), lr=1e-3)  

# 多次遍历数据集
for epoch in range(num_epochs):  
      
    # 遍历数据的批次
    for batch in dataloader:  
          
        # 获取当前批次的输入数据
        x = batch  
          
        # 前向传播:
        # 编码 -> 采样潜在z -> 解码
        recon_x, mu, logvar = vae(x)  
          
        # 计算VAE损失(重建损失 + KL散度)
        loss = vae_loss(recon_x, x, mu, logvar)  
          
        # 清除上一步的旧梯度
        optimizer.zero_grad()  
          
        # 反向传播:
        # 计算损失相对于模型参数的梯度
        loss.backward()  
          
        # 使用优化器更新模型参数
         optimizer.step()
用于异常检测时,VAE 通常仅在正常数据上训练。模型学到"正常"的分布形态后,异常样本会以高重建误差或偏离常规的潜在分布暴露出来。

训练完成后得到了什么

训练结束后,手里拿到的远不止一个重建模型。潜在空间中的距离和偏差都携带语义:可以检视哪些潜在维度在异常出现时发生了漂移,可以对比重建结果,也可以跟踪 KL 散度随时间的变化趋势。

可解释性在异常检测中就是这样进行。如果潜在空间已解耦,某个因子的异常即可定位到具体原因;即使未能解耦,仍然可以在原始特征空间中分析重建偏差,回溯根因。

实际应用——训练好的 VAE 的推理模式

VAE 的用途不止一种。训练完成后根据目标不同可以被复用在多个场景中,编码器-解码器架构加上结构化的潜在空间给了它足够的适应余地。

异常检测

训练结束后 VAE 已经建立了对"正常"数据的内部表征。新输入经过编码器→潜在空间→解码器后,将输出与原始输入做比较即可判断异常——重建误差越大,样本越可能偏离正常模式。以信用卡交易为例,消费模式异常的交易在重建时会产生明显偏差,对应较高的异常得分。类似场景还包括设备监控和医疗异常检测。关键推理信号是重建误差,或样本在已学分布下的似然度。

合成数据生成

无需任何特定输入,直接在潜在空间中采样再经解码器输出,即可生成新的逼真样本。潜在空间在训练期间已被约束为近似标准正态分布,从中采样的点解码后会产生与训练数据风格相近的新数据。典型场景包括数据增强、系统仿真和压力测试。在医学领域,可以产出罕见病的逼真影像,或合成客户交易历史用于测试。

关键推理信号是从潜在先验分布采样(z ~ N(0,1)),经解码输出新样本。

条件生成

在标准 VAE 基础上引入额外的条件信息,就得到了条件 VAE(CVAE)。例如基于标签生成图像,或基于客户群体生成合成交易、生成某类肿瘤的影像,或某商户类别的交易记录。应用方向包括定向数据增强、场景模拟、受控合成实验。

潜在空间操作与可解释性

对潜在空间做分析和修改,可以观察输出如何随之变化。潜在遍历——固定其余维度、单独改变一个维度——能揭示各因子的语义含义;潜在空间本身也可用于聚类。一个具体的例子:在机械传感器数据中,某个潜在因子可能对应振动频率,调整它就能模拟机器提速后的状态。这类操作在可解释性分析、根因定位和场景规划中都有用处。

数据填补与重建

训练好的 VAE 可以处理不完整输入——编码后在潜在空间中采样,再解码出完整的重建,从而填补缺失数据。典型场景有数据清洗、预处理和错误修正,比如补全图像中的缺失像素、物联网数据中丢失的传感器读数,或者残缺不全的交易记录。

总结

VAE模型要做的核心决策是:哪些信息值得保留,哪些可以丢弃。理清这一点之后数学只是执行层面的工具,不再是障碍。

一个经过良好训练的 VAE 产出的不只是重建结果,它提供了一个观察数据行为的视角——数据在哪里偏离,复杂系统如何被压缩进一个紧凑且可解读的表示里。本系列的下一篇将聚焦 VAE 在异常检测中的实际应用。

https://avoid.overfit.cn/post/17cbb214d50f4e469a458c061b3c5138

by Ayo Akinkugbe