大环境真的变得很差,从这个板块看就知道了
前几年(准确地是 18,19 年左右
看这个板块,都是各公司的人在发招人的帖子
这两年再看,多半是失业的人在发各种迷茫贴
在职的人都在发 AI 危机,感觉也很焦虑的样子
我年前也拿了大礼包,4x 岁了,感觉再求职会非常难了
也焦虑的要死,唉
xiaohack博客专注前沿科技动态与实用技术干货分享,涵盖 AI 代理、大模型应用、编程工具、文档解析、SEO 实战、自动化部署等内容,提供开源项目教程、科技资讯日报、工具使用指南,助力开发者、AI 爱好者获取前沿技术与实战经验。

我感觉 情趣内衣 才是最应该评测的, 因为买了觉得不好的话, 一般人也不会退货.
看多了主要就那几个系列, 吊带, 体操服, 碎短裙, 女仆装, 三点式, 和服, 角色 cos.
LA 那款, 和 霏慕 那款 甜美娇妻 款式差不多, 但是它面料更滑更透. 就是价格太贵了, 适合富哥.
倾城鸟 的体操服还可以吧, 这个价格不错了.
霏慕 的碎花连衣裙, 作为情趣大牌, 包装和质量都可以的.
JK 女仆装也不错.
死库水款式是我喜欢的, 但是这款面料很垃圾, 清洗掉色还很大异味.
京造 出的冰丝蕾丝花边碎裙一般般吧.
霏慕 的没得说, 还是整个行业中可以的品牌.
蜜链发货晚了, 没收到货就取消了, 京造的话还是一般.
浪莎 也是牌子货, 但是包装都很随意, 我到手还是打开的, 哎非常垃圾.
日系那个价格在那里, 还算可以吧.
总评下来还是 霏慕 比较稳, 缺点是款式比较少, 风格比较保守.
像我这样比较喜欢死库水风格的话, 相关情趣内衣少的, 买正常款式, 自己动手改情趣也是可以的.
还有那些几十块, 几十套一次性丝袜的那种, 就因人而异了, 总体还是吃模特颜值和身材的.
最近给朋友准备劳动仲裁,突然发现劳动法规真的很完善了。我罗列一下学习到的知识点。
朋友工作 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 系统
在现代 Web 应用中,图片往往占据了页面总资源的 60-70%。如果一次性加载所有图片,会导致: 监听 ✅ 兼容性好:支持所有浏览器(包括 IE6+) ❌ 性能差:频繁触发 scroll 事件,即使使用防抖也会影响性能 ⚠️ 不推荐使用:除非有特殊的兼容性要求,否则应该使用更现代的方案。 使用浏览器原生的 ✅ 异步执行:不阻塞主线程,性能优秀 HTML 结构: HTML 结构: rootMargin 示例: threshold 示例: Polyfill 方案: ⭐️ 强烈推荐:IntersectionObserver 是目前最佳的图片懒加载方案,性能优秀、代码简洁、易于维护。 HTML5 新增的 ✅ 零代码:无需任何 JavaScript ❌ 兼容性差:Safari 15.4+ 才支持 ⭐️ 推荐使用:如果不需要兼容老浏览器, 结合 问题: 图片加载前后高度变化,导致页面跳动 解决方案 1:设置固定尺寸 解决方案 2:使用 aspect-ratio 解决方案 3:使用 padding-top 技巧本文深入探讨前端图片懒加载的各种实现方案,从传统的滚动监听到现代的 IntersectionObserver API,再到浏览器原生支持的 loading="lazy",帮助开发者选择最适合的技术方案。
一、为什么需要图片懒加载?
1.1 性能问题
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 优势
✅ 实现简单:逻辑清晰,易于理解
✅ 可控性强:可以精确控制加载时机3.5 劣势
❌ 计算开销大:每次都要调用 getBoundingClientRect()
❌ 阻塞主线程:scroll 事件在主线程执行,可能导致卡顿
❌ 代码复杂:需要手动管理监听器、防抖、清理等3.6 性能问题分析
// 问题 1:频繁触发
window.addEventListener('scroll', () => {
console.log('scroll 事件触发'); // 滚动时每秒触发 60+ 次
});
// 问题 2:getBoundingClientRect 触发重排
const rect = element.getBoundingClientRect(); // 强制浏览器重新计算布局
// 问题 3:主线程阻塞
// scroll 事件在主线程执行,如果处理逻辑复杂,会导致页面卡顿3.7 适用场景
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;
}
}
}<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);
}
});
}
}<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], // 多个阈值
});// 提前 100px 开始加载
rootMargin: '100px'
// 上下提前 100px,左右提前 50px
rootMargin: '100px 50px'
// 上提前 100px,右提前 50px,下提前 80px,左提前 30px
rootMargin: '100px 50px 80px 30px'
// 延迟加载(元素完全进入视口后才加载)
rootMargin: '-50px'// 元素刚进入视口就触发
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 浏览器兼容性
浏览器 版本 Chrome 51+ Firefox 55+ Safari 12.1+ Edge 15+ IE ❌ 不支持 // 检测浏览器是否支持
if (!('IntersectionObserver' in window)) {
// 动态加载 polyfill
import('intersection-observer').then(() => {
// 初始化懒加载
const lazyLoad = new IntersectionLazyLoad();
lazyLoad.init();
});
} else {
// 直接使用
const lazyLoad = new IntersectionLazyLoad();
lazyLoad.init();
}4.8 适用场景
4.9 结论
五、方案三:浏览器原生 loading="lazy"(最简单)
5.1 实现原理
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 优势
✅ 性能最优:浏览器底层优化
✅ 自动优化:浏览器根据网络状况自动调整
✅ 维护成本低:无需管理监听器、Observer 等5.5 劣势
❌ 无法自定义:无法控制提前加载距离
❌ 无回调:无法监听加载完成事件5.6 浏览器兼容性
浏览器 版本 Chrome 77+ Firefox 75+ Safari 15.4+ Edge 79+ 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 适用场景
5.8 结论
loading="lazy" 是最简单、最优雅的方案。六、方案对比总结
维度 滚动监听 IntersectionObserver loading="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)
<img
src="image.jpg"
loading="lazy"
width="800"
height="600"
alt="描述"
/><img
src="image.jpg"
loading="lazy"
style="aspect-ratio: 16/9; width: 100%;"
alt="描述"
/><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="描述" />八、参考资料
比如一个根据提示词搜索资源并下载的 Agent
这些都是受近期地缘政治影响较大的内容。
是的,不构成投资建议。
SSE(Server-Sent Events)是 HTML5 标准的一部分,允许服务器主动向客户端推送数据。与 WebSocket 双向通信不同,SSE 是单向的(服务器 → 客户端),但实现更简单,且基于 HTTP 协议。 完整格式包含多个字段: ✅ 零依赖:浏览器原生 API,无需引入任何库 ❌ 仅支持 GET 请求:无法发送 POST 请求体 1. 跨域问题 2. 自动重连导致重复请求 3. 无法传递 POST 数据 1. ReadableStream 流式读取 2. TextDecoder 解码 为什么需要 3. Buffer 缓冲区管理 4. 支持中断 ✅ 支持 POST 请求:可以发送请求体 ❌ 需要手动解析 SSE 协议:需要处理 1. 忘记使用 2. buffer 管理错误 3. 没有处理中断 4. 代理层缓冲问题 ✅ 支持 POST 请求:可以发送请求体 ❌ 需要引入外部依赖:增加打包体积 1. 自动重连导致重复请求 2. 无法获取完成状态 3. 页面隐藏时连接断开 方案 A:Cookie(推荐 EventSource) 方案 B:URL 参数(适用 EventSource) 方案 C:Authorization Header(需要 Fetch) EventSource(无法设置超时) Fetch(使用 AbortController) EventSource(自动重连) Fetch(手动重连) 问题: Nginx 等代理可能会缓冲响应,导致消息延迟 解决方案: CORS 配置: 代理方案: SSE 是实现服务器推送的简单而强大的技术,三种实现方案各有优劣: 选择哪种方案取决于具体需求,建议优先考虑 EventSource,只有在需要 POST 或自定义 Header 时才使用其他方案。 参考资料:本文深入对比了前端实现 SSE(Server-Sent Events)流式请求的三种主流方案,包括原理剖析、代码示例、优劣对比和实战踩坑经验,帮助开发者选择最适合的技术方案。
一、什么是 SSE?
SSE 的典型应用场景
SSE 协议格式
data: 消息内容\n\nevent: message\n
id: 1\n
retry: 3000\n
data: 消息内容\n\ndata: - 消息内容(必需)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();优势
✅ 自动解析:浏览器自动处理 SSE 协议解析
✅ 自动重连:连接断开后自动重连(可配置 retry 时间)
✅ 稳定可靠:浏览器原生实现,经过充分测试
✅ 代码简洁:核心逻辑仅需 20-30 行代码劣势
❌ 无法自定义 Header:除了 Cookie,无法添加其他 Header(如 Authorization)
❌ 认证限制:只能通过 URL 参数或 Cookie 传递认证信息适用场景
常见踩坑
// 错误:跨域请求会失败
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');// 问题:连接断开后会自动重连,可能导致重复请求
// 解决方案:服务器端使用 id 字段去重
// event: message
// id: 123
// data: 消息内容
// 客户端重连时会自动发送 Last-Event-ID header// 错误: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('完成');
},
});核心原理详解
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// value 是 Uint8Array 类型的二进制数据
console.log(value);
}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 }); // "你好"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"
}
}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();优势
✅ 自定义 Header:可以添加任意 Header(如 Authorization)
✅ 完全控制:可以自定义超时、重试等逻辑
✅ 零依赖:浏览器原生 API
✅ 支持中断:使用 AbortController 中断请求劣势
data:、event:、id: 等字段
❌ 需要处理 chunk 边界:一个事件可能被拆分到多个 chunk
❌ 需要管理 buffer:手动拼接不完整的数据
❌ 代码复杂度高:核心逻辑需要 50+ 行代码
❌ 容易出错:边界情况处理不当会导致数据丢失或重复适用场景
常见踩坑
{ stream: true }// 错误:多字节字符会乱码
const text = decoder.decode(value);
// 正确
const text = decoder.decode(value, { stream: true });// 错误:直接 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) {
// 处理...
}// 错误:没有处理 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);
}
}// 问题: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-sourceimport { 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();优势
✅ 自定义 Header:可以添加任意 Header
✅ 自动解析 SSE 协议:无需手动处理
✅ 内置自动重连:连接断开后自动重连
✅ 页面隐藏时保持连接:openWhenHidden 选项
✅ 体积小:~2KB gzip
✅ TypeScript 支持:完整的类型定义劣势
❌ 自动重连可能不符合需求:需要手动控制
❌ 错误处理复杂:需要抛出错误才能停止重连
❌ 中断机制不直观:需要使用 AbortController适用场景
常见踩坑
// 问题:连接断开后会自动重连,可能导致重复请求
// 解决方案 1:抛出错误停止重连
onerror(err) {
console.error(err);
throw err; // 停止重连
}
// 解决方案 2:使用 AbortController
const ctrl = new AbortController();
fetchEventSource(url, {
signal: ctrl.signal,
onerror(err) {
ctrl.abort(); // 停止重连
},
});// 问题:onclose 回调无法区分正常结束还是错误结束
// 解决方案:自己维护状态
let isCompleted = false;
fetchEventSource(url, {
onmessage(event) {
if (event.data === '[DONE]') {
isCompleted = true;
ctrl.abort();
}
},
onclose() {
if (isCompleted) {
console.log('正常结束');
} else {
console.log('异常结束');
}
},
});// 问题:默认情况下,页面隐藏时连接会断开
// 解决方案:设置 openWhenHidden
fetchEventSource(url, {
openWhenHidden: true, // 页面隐藏时保持连接
});三、方案对比总结
维度 EventSource Fetch + ReadableStream @microsoft/fetch-event-source 支持 POST ❌ 仅 GET ✅ ✅ 自定义 Header ❌ 仅 Cookie ✅ ✅ SSE 协议解析 ✅ 浏览器原生 ❌ 需手动实现 ✅ 自动处理 自动重连 ✅ 浏览器原生 ❌ 需手动实现 ✅ 内置 手动中断 ✅ close() ✅ AbortController ⚠️ AbortController + throw 代码复杂度 低(~30行) 高(~50行) 中(~10行) 外部依赖 零依赖 零依赖 ~2KB 浏览器兼容性 IE 10+ 现代浏览器 现代浏览器 学习成本 低 高 中 适用场景 GET + Cookie 认证 完全自定义 POST + 自动重连 四、选型建议
选择 EventSource 的情况
选择 Fetch + ReadableStream 的情况
选择 @microsoft/fetch-event-source 的情况
五、实战经验总结
1. 如何处理认证
// 服务器设置 Cookie
res.cookie('token', 'xxx', { httpOnly: true });
// 客户端自动携带
const es = new EventSource('/api/sse');const token = 'xxx';
const es = new EventSource(`/api/sse?token=${token}`);fetch('/api/sse', {
headers: {
'Authorization': `Bearer ${token}`,
},
});2. 如何处理超时
// 解决方案:手动实现超时
const es = new EventSource('/api/sse');
const timeout = setTimeout(() => {
es.close();
console.log('超时');
}, 30000);
es.onmessage = () => {
clearTimeout(timeout);
};const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
fetch('/api/sse', {
signal: controller.signal,
});3. 如何处理重连
// 服务器端设置重连间隔
res.write('retry: 3000\n\n'); // 3 秒后重连
// 客户端无法禁用自动重连
// 只能通过 close() 关闭连接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 配置
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(); // 立即发送 headers5. 如何处理跨域
// 服务器端
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,
},
},
},
};六、总结
24 年被业务员推销办了个华夏的信用卡,当时给我说每年刷任意金额 6 笔即可免年费。后来 25 年下半年不怎么用了,今天突然发现有一个 680 的年费账单。
打电话给客服,强调当时说了只要 6 笔就免年费,客服咬定要消费 20000 积分兑换(1 元=1 积分)。我说你看去年账号,我是不是没有消费 2w 也给我免年费了。客服说去年也扣了。
妈的,给我气笑了,一看账单还真是,没注意就扣了。
有啥追回的办法吗?目前仅向 【金融消保服务平台】投诉
作为一个内向,但是又不想让别人觉得自己过于奇怪的人,我学习社交的方法其实就是统计模型。
我的终极目标是,以最小的社交输出让别人觉得我不是一个很古怪的人。因此我的方法是无时无刻不刻意观察群体里大家社交的方式,然后根据统计规律找出来一些大概率要说的话,大概率要做的动作,以及针对大概率被问的问题,有哪些大概率的回答。最后我会设定一个大概率还算可以的对话深度,一旦达到这个深度就判断对方不会再认为自己是个怪人,最后以一个大概率还算合体的离开方式结束社交。
那我在这一块,和 LLM 有啥本质区别呢?
但是并没有消失,只是换了一种名称,比如说:Viber ,用来指代那些过度依赖于 LLM 自动代码生成的开发人员。
Programmer 仍会继续存在,因为要让 AI 生成好的代码,前提是自己头脑清楚,包括对设计层面的把控,软件各个模块的设计和分工,对各种实现方式利弊的考量,以及能否在 review 中看出 AI 埋下的坑。相当一部分工作,无法由 AI 替代。在软件开发领域,没有银弹,数量也无法弥补质量。
代码的行数,以及编码的速度(或者说生成速度),远没有代码的质量更加重要。同样的 spec/功能/行为,可以用上千行代码实现,也可以几百行代码优雅的实现,不同的实现方式的后续可维护性、可读性、可扩展性天差地别。
你可以选择成为一名 Viber ,或者继续在 Programmer/Developer 的道路上精进。
在 Vue 项目中实现路由懒加载时,我们经常会看到三种不同的写法: 这三种方式看起来相似,但在 Webpack 编译过程中的行为完全不同,最终产物也有很大差异。本文将深入分析: 在深入路由懒加载之前,我们需要先理解 JavaScript 中 语法: 核心特点: 语法: 核心特点: 虽然 根据 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 的特殊处理: 示例: Webpack 编译后的效果: 重要理解: 在没有额外 Babel 插件的情况下, Webpack 生成的运行时代码(target: 'web' 默认): 老旧浏览器兼容性问题: Webpack 生成的运行时代码默认使用 ES2015 语法(箭头函数、const、模板字符串),在不支持 ES6 的老旧浏览器中会报错: 解决方案: 配置 Webpack target(详见第五章) 根据 Webpack 官方文档: 生成的运行时代码与 import() 相同: require.ensure 和 import() 生成的运行时代码相同,兼容性取决于 Webpack 的 target 配置,而不是语法本身。 源代码: Babel 配置: 根据 babel-plugin-dynamic-import-node 文档: 转换过程: Webpack 生成的代码: 构建产物对比: 原因: 原因: 误区:dynamic-import-node 能解决兼容性问题 真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 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." 关键理解: 生成的运行时代码: 结果: 生成的运行时代码: 结果: 无论使用 import() 还是 require.ensure,兼容性都取决于 target 配置,而不是语法本身。 适用场景: 需要支持老旧浏览器,需要代码分割 优势: 适用场景: 需要支持老旧浏览器,需要代码分割,希望提升开发环境编译速度 优势: 适用场景: 只需要支持现代浏览器(Chrome 63+, Safari 11.1+) 优势: 真相: 真相: 真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 target。 如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题欢迎在评论区讨论。一、引言
// 方式一: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')二、理解 import:ES6 标准 vs Webpack 实现
import 的两种完全不同的形式。2.1 ES6 静态 import 语句
import React from 'react';
import { useState } from 'react';"ES6 模块是编译时输出接口,在代码静态解析阶段就会生成。"
// ❌ 错误:不能在函数内部使用
function loadModule() {
import module from './module'; // SyntaxError
}
// ❌ 错误:不能使用变量
const path = './module';
import module from path; // SyntaxError2.2 ES6 动态 import() 表达式
import('./module.js').then(module => {
// 使用模块
});
// 或使用 async/await
const module = await import('./module.js');"CommonJS 模块加载 ES6 模块,不能使用 require 命令,而要使用 import() 函数。"
// ✅ 正确:可以在函数内部使用
function loadModule() {
return import('./module.js');
}
// ✅ 正确:可以使用变量
const language = 'zh';
import(`./i18n/${language}.js`).then(module => {
// 使用模块
});2.3 Webpack 对 import() 的特殊处理
import() 是 ES6 标准语法,但 Webpack 对它进行了特殊处理,赋予了额外的功能。import() 识别为代码分割点// 普通动态导入
import('./module.js')
// Webpack 魔法注释
import(
/* webpackChunkName: "my-chunk" */
/* webpackPrefetch: true */
'./module.js'
)// 源代码
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 的独立 chunk2.4 关键区别总结
特性 静态 import 动态 import() Webpack 处理的 import() 语法类型 声明语句 表达式 表达式 使用位置 仅文件顶层 任何位置 任何位置 加载时机 编译时 运行时 运行时 加载方式 同步 异步 异步 返回值 直接导入 Promise Promise 路径类型 字符串字面量 任意表达式 任意表达式 代码分割 否 否(原生) ✅ 是(Webpack) 独立 chunk 否 否(原生) ✅ 是(Webpack) import() 只是异步加载模块,不会自动进行代码分割import() 在异步加载的基础上,额外实现了代码分割和 chunk 生成import() 会发起网络请求加载模块文件import() 会被转换为 Webpack 的运行时代码,加载打包后的 chunk三、方式一:ES6 动态 import()
2.1 用法
// src/router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}2.2 工作原理
Babel 处理阶段
import() 语法会被保留或由 @vue/babel-preset-app 处理,但不会被转换为其他形式。Webpack 编译阶段
import() 是一个代码分割点// 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 优势
import() 是 ECMAScript 标准语法,未来兼容性好2.4 适用场景
2.5 潜在问题
Uncaught SyntaxError: Unexpected token '=>'三、方式二: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 编译阶段
"require.ensure: Split out the given dependencies to a separate bundle that will be loaded asynchronously."
// 与 import() 生成的运行时代码完全一致
__webpack_require__.e(/* chunkId */ 123).then(__webpack_require__.bind(null, /* moduleId */ 456))3.3 优势
3.4 劣势
import() 替代3.5 适用场景
3.6 重要说明
四、方式三:babel-plugin-dynamic-import-node
4.1 用法
// src/router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}// babel.config.js
module.exports = {
presets: ['@vue/app'],
plugins: ['dynamic-import-node']
}4.2 工作原理
Babel 处理阶段
"Babel plugin to transpile import() to a deferred require(), for node."
// 转换前
component: () => import('@/views/Home.vue')
// 转换后
component: () => require('@/views/Home.vue')Webpack 编译阶段
require() 而不是 import()require() 是同步导入,Webpack 不会创建新的 chunk// 直接同步加载,没有异步逻辑
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 优势
4.4 劣势
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']4.6 常见误区
五、Webpack target 配置的关键作用
5.1 target 配置的作用
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 配置的重要性
六、三种方式对比总结
6.1 完整对比表
特性 import() require.ensure import() + 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 配置三:现代浏览器配置
// 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.ensure 和 import() 生成的运行时代码相同,兼容性取决于 target 配置,而不是语法本身。误区 3:dynamic-import-node 能解决兼容性问题
九、参考资料


很好奇谁是第 66 位用户
节点好像有点少哦,社区节点下只有一个反馈
一个人可以吃苦,但两个人不行,不然会以为苦是对方带来的

折腾到凌晨,终于搞定了! 用 AI 写了一个脚本,可以用一行命令让你正在使用的免费 SLL 可以自动化续签! 对小白和初级用户非常友好,无学习成本。 完全免费,只需关注我公众号,哈哈不容易多支持
欢迎交流,大佬可以 pass 了!只适合初级用户!!
适用场景:就是你在宝塔里面已经用了一堆的免费 SSL,想一键自动化。这种情况下,用我这个比较方便。

在线求助,帮忙用我的 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、FactorVAE 等)时,解耦并无保证。 VAE 由三个概念层面的部件组成——编码器、潜在空间(通过采样/重参数化技巧实现)、解码器。 编码器接收输入,输出两个向量:均值 μ 和方差 σ²,二者共同定义潜在空间上的概率分布。采样步骤利用一种保持可微性的技巧从该分布中抽取潜在向量,解码器再拿到这个向量去重建原始输入。 训练过程同时平衡两个目标:重建要准确,潜在分布要贴近先验。VAE 的结构特性正来源于这种平衡。 VAE 的损失函数同时追求两件事:把输入数据重建得尽可能准,并约束潜在空间服从标准正态分布。重建损失(比如均方误差)度量输出与输入的偏差;KL 散度度量学习到的潜在分布与标准正态分布之间的距离。 换个角度看——重建损失在奖励保真度,KL 损失则在防止模型死记硬背,迫使潜在空间维持良好的分布形态。 下面逐步走读一个最小化的 PyTorch 实现。示例假定输入为表格或展平后的数据,但同样的思路适用于图像和序列。 编码器将输入向量映射为两个输出:潜在分布的均值和对数方差。 到这一步为止,还没有涉及任何概率运算。代码只是在预测分布的参数。 直接从分布中采样会切断梯度的传播路径。重参数化技巧的做法是把采样拆解为一个确定性函数加上随机噪声。 梯度照常经由 μ 和 σ 回传,随机性又被保留下来。 解码器负责把潜在向量映射回原始输入空间。 解码器不需要知道任何关于概率分布的事情,它只做重建。 前向传播的流程与概念上的流程完全一致。 beta 参数控制重建质量与潜在正则化之间的权衡。当 beta > 1 时即为 β-VAE——以牺牲重建精度为代价换取更解耦的潜在因子。 训练阶段模型只接触数据样本,优化上述组合损失。有一点需要特别留意:用于异常检测时,VAE 通常仅在正常数据上训练,模型由此学会"正常"的分布形态,异常样本则在推理时暴露为高重建误差或偏离常规的潜在分布。 训练结束后,手里拿到的远不止一个重建模型。潜在空间中的距离和偏差都携带语义:可以检视哪些潜在维度在异常出现时发生了漂移,可以对比重建结果,也可以跟踪 KL 散度随时间的变化趋势。 可解释性在异常检测中就是这样进行。如果潜在空间已解耦,某个因子的异常即可定位到具体原因;即使未能解耦,仍然可以在原始特征空间中分析重建偏差,回溯根因。 VAE 的用途不止一种。训练完成后根据目标不同可以被复用在多个场景中,编码器-解码器架构加上结构化的潜在空间给了它足够的适应余地。 训练结束后 VAE 已经建立了对"正常"数据的内部表征。新输入经过编码器→潜在空间→解码器后,将输出与原始输入做比较即可判断异常——重建误差越大,样本越可能偏离正常模式。以信用卡交易为例,消费模式异常的交易在重建时会产生明显偏差,对应较高的异常得分。类似场景还包括设备监控和医疗异常检测。关键推理信号是重建误差,或样本在已学分布下的似然度。 无需任何特定输入,直接在潜在空间中采样再经解码器输出,即可生成新的逼真样本。潜在空间在训练期间已被约束为近似标准正态分布,从中采样的点解码后会产生与训练数据风格相近的新数据。典型场景包括数据增强、系统仿真和压力测试。在医学领域,可以产出罕见病的逼真影像,或合成客户交易历史用于测试。 关键推理信号是从潜在先验分布采样(z ~ N(0,1)),经解码输出新样本。 在标准 VAE 基础上引入额外的条件信息,就得到了条件 VAE(CVAE)。例如基于标签生成图像,或基于客户群体生成合成交易、生成某类肿瘤的影像,或某商户类别的交易记录。应用方向包括定向数据增强、场景模拟、受控合成实验。 对潜在空间做分析和修改,可以观察输出如何随之变化。潜在遍历——固定其余维度、单独改变一个维度——能揭示各因子的语义含义;潜在空间本身也可用于聚类。一个具体的例子:在机械传感器数据中,某个潜在因子可能对应振动频率,调整它就能模拟机器提速后的状态。这类操作在可解释性分析、根因定位和场景规划中都有用处。 训练好的 VAE 可以处理不完整输入——编码后在潜在空间中采样,再解码出完整的重建,从而填补缺失数据。典型场景有数据清洗、预处理和错误修正,比如补全图像中的缺失像素、物联网数据中丢失的传感器读数,或者残缺不全的交易记录。 VAE模型要做的核心决策是:哪些信息值得保留,哪些可以丢弃。理清这一点之后数学只是执行层面的工具,不再是障碍。 一个经过良好训练的 VAE 产出的不只是重建结果,它提供了一个观察数据行为的视角——数据在哪里偏离,复杂系统如何被压缩进一个紧凑且可解读的表示里。本系列的下一篇将聚焦 VAE 在异常检测中的实际应用。 https://avoid.overfit.cn/post/17cbb214d50f4e469a458c061b3c5138 by Ayo AkinkugbeVAE 为什么存在
VAE 在学习重建数据的同时,也在学习一个形态接近简单概率分布的潜在空间。
三个核心组件

损失函数
VAE 的损失函数同时追求两件事:把输入数据重建得尽可能准,并约束潜在空间服从标准正态分布。
从理论到代码
定义编码器
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训练循环
# 创建优化器来更新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 通常仅在正常数据上训练。模型学到"正常"的分布形态后,异常样本会以高重建误差或偏离常规的潜在分布暴露出来。
训练完成后得到了什么
实际应用——训练好的 VAE 的推理模式
异常检测

合成数据生成

条件生成

潜在空间操作与可解释性

数据填补与重建

总结