普通视图

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

ANSI-Escapes 命令行光标操作完全指南

作者 三年三月
2025年12月10日 14:20

1. 命令行中光标操作的底层原理

在命令行界面中,光标操作的底层原理基于ANSI转义序列(ANSI Escape Sequences),这是一套标准化的控制字符序列,用于控制终端的各种行为,包括光标移动、文本样式、屏幕清除等。

1.1 ANSI转义序列基础

  • 转义字符:所有ANSI转义序列都以转义字符ESC(ASCII码27,十六进制0x1B,通常表示为\033\u001B)开头
  • 序列格式:基本格式为 ESC[参数;参数;...命令 或简写为 \033[参数;参数;...命令
  • 命令类型
    • 光标移动命令(如A上移、B下移、C右移、D左移)
    • 屏幕操作命令(如J清除屏幕、K清除行)
    • 样式设置命令(如文本颜色、背景色、粗体等)

1.2 终端解析机制

当终端接收到包含ANSI转义序列的输出时:

  1. 识别ESC字符作为转义序列的开始
  2. 解析[之后的参数(以分号分隔)
  3. 执行参数对应的命令操作
  4. 转义序列不会显示在终端上,只会触发相应的控制行为

1.3 光标定位原理

  • 终端屏幕被划分为行列网格,通常从左上角(0,0)或(1,1)开始计数
  • 光标移动命令通过指定行列位置或相对偏移来控制光标位置
  • 不同终端可能有细微差异,但基本遵循ANSI标准

2. ANSI-Escapes 工具介绍

ansi-escapes是一个Node.js库,它提供了一组简洁的API,用于生成各种ANSI转义序列,简化了在命令行应用中控制光标、清除屏幕、操作终端等复杂功能的实现。

2.1 主要功能分类

  1. 光标控制:移动、定位、保存/恢复位置、隐藏/显示
  2. 屏幕操作:清除行、清除屏幕、滚动
  3. 终端控制:切换屏幕、设置工作目录
  4. 实用功能:创建链接、显示图片、发出蜂鸣

2.2 优势

  • 跨平台兼容性:自动处理不同终端和操作系统的差异
  • 简化API:将复杂的转义序列封装为易记的函数调用
  • 类型安全:提供TypeScript支持,减少使用错误
  • 模块化设计:按需导入所需功能

3. ANSI-Escapes 核心功能详解

3.1 光标移动与定位

cursorTo(x, y) - 移动光标到指定位置

功能:将光标移动到屏幕上的指定坐标位置

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

// 移动到第2行第6列
process.stdout.write('第一行\n');
process.stdout.write('第二行\n');
process.stdout.write('第三行\n');
process.stdout.write(ansiEscapes.cursorTo(5, 1)); // x:5, y:1 (行列从0开始)
process.stdout.write('✓ 光标移动到这里了\n');

底层实现

export const cursorTo = (x, y) => {
    if (typeof x !== 'number') {
        throw new TypeError('The `x` argument is required');
    }

    if (typeof y !== 'number') {
        return ESC + (x + 1) + 'G'; // 只指定x坐标
    }

    return ESC + (y + 1) + SEP + (x + 1) + 'H'; // 指定x,y坐标
};

解析

  • 当只提供x坐标时,生成序列 \033[6G(移动到第6列)
  • 当提供x和y坐标时,生成序列 \033[2;6H(移动到第2行第6列)
  • 注意:终端通常使用1-based索引,而API使用0-based索引,所以需要+1转换

cursorMove(x, y) - 相对移动光标

功能:从当前位置相对移动光标

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

process.stdout.write('Hello World');
process.stdout.write(ansiEscapes.cursorMove(-5, -1)); // 左移5字符,上移1行
process.stdout.write('✓ 相对移动到这里');

底层实现

export const cursorMove = (x, y) => {
    if (typeof x !== 'number') {
        throw new TypeError('The `x` argument is required');
    }

    let returnValue = '';

    if (x < 0) {
        returnValue += ESC + (-x) + 'D'; // 左移
    } else if (x > 0) {
        returnValue += ESC + x + 'C'; // 右移
    }

    if (y < 0) {
        returnValue += ESC + (-y) + 'A'; // 上移
    } else if (y > 0) {
        returnValue += ESC + y + 'B'; // 下移
    }

    return returnValue;
};

解析

  • 根据x和y的正负值决定移动方向
  • 左移:\033[5D
  • 右移:\033[5C
  • 上移:\033[1A
  • 下移:\033[1B

cursorUp(count) / cursorDown(count) - 上下移动光标

功能:向上或向下移动指定行数

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

process.stdout.write('第一行\n');
process.stdout.write('第二行\n');
process.stdout.write('第三行\n');
process.stdout.write(ansiEscapes.cursorUp(2)); // 上移2行
process.stdout.write('✓ 上移到第二行');

底层实现

export const cursorUp = (count = 1) => ESC + count + 'A';
export const cursorDown = (count = 1) => ESC + count + 'B';

解析

  • cursorUp:生成 \033[2A 表示上移2行
  • cursorDown:生成 \033[2B 表示下移2行
  • 默认移动1行

cursorForward(count) / cursorBackward(count) - 左右移动光标

功能:向左或向右移动指定字符数

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

process.stdout.write('Hello World');
process.stdout.write(ansiEscapes.cursorBackward(6)); // 左移6字符
process.stdout.write('✓ 左移到Hello后');

底层实现

export const cursorForward = (count = 1) => ESC + count + 'C';
export const cursorBackward = (count = 1) => ESC + count + 'D';

解析

  • cursorForward:生成 \033[2C 表示右移2字符
  • cursorBackward:生成 \033[6D 表示左移6字符
  • 默认移动1字符

3.2 光标状态控制

cursorHide / cursorShow - 隐藏和显示光标

功能:控制光标的可见性

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';
import { delay } from './utils'; // 假设存在延迟函数

process.stdout.write('光标可见...');
await delay(1500);
process.stdout.write(ansiEscapes.cursorHide);
process.stdout.write('\n光标隐藏了...');
await delay(2000);
process.stdout.write(ansiEscapes.cursorShow);
process.stdout.write('\n光标显示了✓');

底层实现

export const cursorHide = ESC + '?25l';
export const cursorShow = ESC + '?25h';

解析

  • cursorHide:生成 \033[?25l 隐藏光标
  • cursorShow:生成 \033[?25h 显示光标
  • 常用于需要暂时隐藏光标以提升用户体验的场景

cursorSavePosition / cursorRestorePosition - 保存和恢复光标位置

功能:保存当前光标位置,稍后可以恢复到该位置

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';
import { delay } from './utils';

process.stdout.write('第一行\n');
process.stdout.write('第二行');
process.stdout.write(ansiEscapes.cursorSavePosition); // 保存位置
process.stdout.write('保存点');
await delay(1000);
process.stdout.write('\n第三行\n第四行\n');
await delay(1000);
process.stdout.write(ansiEscapes.cursorRestorePosition); // 恢复位置
process.stdout.write('✓ 恢复到保存点');

底层实现

export const cursorSavePosition = isTerminalApp ? '\u001B7' : ESC + 's';
export const cursorRestorePosition = isTerminalApp ? '\u001B8' : ESC + 'u';

解析

  • 针对Apple Terminal使用不同的转义序列 \u001B7\u001B8
  • 其他终端使用标准序列 \033[s(保存)和 \033[u(恢复)

3.3 屏幕操作

eraseLine / eraseScreen - 清除行和屏幕

功能:清除当前行或整个屏幕

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

// 清除当前行
process.stdout.write('这是一行要清除的文本');
process.stdout.write(ansiEscapes.eraseLine);
process.stdout.write('✓ 行已被清除,在同一位置输出新文本\n');

// 清除屏幕
await delay(1000);
process.stdout.write(ansiEscapes.eraseScreen);
process.stdout.write('✓ 屏幕已被清除\n');

底层实现

export const eraseEndLine = ESC + 'K';      // 清除从光标到行尾
export const eraseStartLine = ESC + '1K';   // 清除从光标到行首
export const eraseLine = ESC + '2K';        // 清除整行
export const eraseDown = ESC + 'J';         // 清除从光标到屏幕底部
export const eraseUp = ESC + '1J';          // 清除从光标到屏幕顶部
export const eraseScreen = ESC + '2J';      // 清除整个屏幕

解析

  • eraseLine 生成 \033[2K 清除整行
  • eraseScreen 生成 \033[2J 清除整个屏幕
  • 这些命令不会移动光标位置

clearScreen / clearViewport - 清屏操作

功能:更彻底的清屏操作

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

// 清除屏幕并将光标移到左上角
process.stdout.write('大量输出内容...\n'.repeat(20));
process.stdout.write(ansiEscapes.clearScreen);
process.stdout.write('✓ 屏幕已清除,光标在左上角\n');

底层实现

export const clearScreen = '\u001Bc';

export const clearViewport = `${eraseScreen}${ESC}H`;

解析

  • clearScreen 生成 \033c 重置终端(更彻底的清屏)
  • clearViewport 结合了 eraseScreen 和光标移动到左上角 \033[H

3.4 终端屏幕控制

enterAlternativeScreen / exitAlternativeScreen - 切换屏幕

功能:创建一个临时的替代屏幕,退出后恢复原屏幕内容

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

// 进入替代屏幕
process.stdout.write(ansiEscapes.enterAlternativeScreen);
process.stdout.write('这是在替代屏幕中\n');
process.stdout.write('原屏幕内容被保存\n');

// 等待用户输入后退出
process.stdin.once('data', () => {
    process.stdout.write(ansiEscapes.exitAlternativeScreen);
    process.stdout.write('已返回原屏幕\n');
    process.exit(0);
});

底层实现

export const enterAlternativeScreen = ESC + '?1049h';
export const exitAlternativeScreen = ESC + '?1049l';

解析

  • enterAlternativeScreen 生成 \033[?1049h 进入替代屏幕
  • exitAlternativeScreen 生成 \033[?1049l 退出替代屏幕
  • 常用于全屏应用或需要临时切换显示内容的场景

3.5 其他实用功能

beep - 发出蜂鸣音

功能:让终端发出蜂鸣提示音

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

process.stdout.write('即将发出蜂鸣...\n');
process.stdout.write(ansiEscapes.beep);
process.stdout.write('蜂鸣已发出✓');

底层实现

export const beep = BEL; // BEL = '\u0007'

解析

  • 生成 \u0007 (BEL字符),大多数终端会将其解释为蜂鸣音

link(text, url) - 创建可点击链接

功能:在支持的终端中创建可点击的URL链接

Demo代码

import ansiEscapes from 'ansi-escapes';
import process from 'process';

process.stdout.write(ansiEscapes.link('访问GitHub', 'https://github.com') + '\n');
process.stdout.write('在支持的终端中,上面的文本是可点击的链接');

底层实现

export const link = (text, url) => {
    const openLink = wrapOsc(`${OSC}8${SEP}${SEP}${url}${BEL}`);
    const closeLink = wrapOsc(`${OSC}8${SEP}${SEP}${BEL}`);
    return openLink + text + closeLink;
};

解析

  • 使用OSC (Operating System Command)序列创建链接
  • 格式为 \033]8;;url\a文本\033]8;;\a
  • 在支持的终端(如iTerm2, GNOME Terminal)中,文本会显示为可点击的链接

4. 跨平台兼容性处理

ansi-escapes库内置了跨平台兼容性处理,确保在不同终端和操作系统上都能正常工作:

  1. 终端类型检测

    const isTerminalApp = !isBrowser && process.env.TERM_PROGRAM === 'Apple_Terminal';
    const isWindows = !isBrowser && process.platform === 'win32';
    const isTmux = !isBrowser && (process.env.TERM?.startsWith('screen') || process.env.TERM?.startsWith('tmux') || process.env.TMUX !== undefined);
    
  2. Windows特殊处理

    const isOldWindows = () => {
        // 检测旧版Windows系统并返回不同的清屏命令
    };
    
  3. Tmux支持

    const wrapOsc = sequence => {
        if (isTmux) {
            // Tmux需要特殊的OSC序列包装
            return '\u001BPtmux;' + sequence.replaceAll('\u001B', '\u001B\u001B') + '\u001B\\';
        }
        return sequence;
    };
    

5. 最佳实践

  1. 资源清理:使用 cursorShow 确保程序退出时光标可见
  2. 异常处理:在异步操作中处理可能的错误
  3. 用户体验:在长时间操作中隐藏光标,完成后显示
  4. 兼容性检查:对于高级功能,检查终端是否支持
  5. 性能考量:批量执行操作,减少I/O调用

6. 总结

ansi-escapes是一个强大的库,它将复杂的ANSI转义序列封装为简洁易用的API,极大地简化了命令行应用中光标控制、屏幕操作等功能的实现。通过本文的介绍,你应该已经掌握了其核心功能和使用方法。

无论是创建简单的命令行工具还是复杂的交互式终端应用,ansi-escapes都能为你提供强大的支持,帮助你打造出更加专业和友好的用户体验。

数据标注平台重磅上线 | 标注赚现金 低门槛真收益

作者 掘金酱
2025年12月10日 14:19

68f71ddf171f8850e881f947f1cfb861.png

  • 你是否想亲身体验最前沿的 AI 数据工作,却找不到专业的参与路径?
  • 你是否希望让精通的编程技能,在业余时间创造实实在在的价值?

一个多领域的数据采集与标注服务平台,为你而来!

低门槛、真收益! 利用碎片时间,将你的代码洞察力直接变现

这不仅仅是一个任务平台,更是一个专为资深开发者打造的、用技术换取回报的高价值社区。

📒 招募介绍

我们是谁

AIDP(AI Data Platform) 提供完整的 AI 数据解决方案,涵盖:

  • 数据采集
  • 数据标注
  • 数据合成
  • 多种数据生产能力

平台支持 Seed、Stone、Flow、头抖、剪映、TT 等核心业务的快速迭代和发展。

需要你做什么

参与后训练高质量代码数据生产,包括但不限于:

任务类型 说明
强化学习数据生产 为 RLHF 训练提供高质量反馈数据
监督微调(SFT) 生产指令-响应对数据
奖励模型(RM) 对模型输出进行偏好排序和评分
数据质检 审核和优化已有数据质量

你将获得的回报

  • 💰现金收益:深度参与数据标注的代码专家,月均获取 ¥10000+ 以上回报
  • 时薪范围:50 - 1000 元,具体以项目最终定价为准,多劳多得,任务明码标价;
  • 计税方式:劳务独立计税,收益清晰透明。
  • 参与智能 Agent 调优,掌握前沿技术动态

需要具备的条件

  • 对 AI 有热情,日常高频使用 AI Coding 工具
  • 具备扎实的编程基础和代码理解能力

加分项

  • LeetCode Hard 题目高通过率
  • GitHub 高星仓库贡献者
  • 开源社区活跃成员

同时欢迎:PM、QA 同学参与,平台同时提供非技术类题目

👀 我们的目标用户

我们在寻找这样的你:

  • 技术栈:熟悉大前端、Python、Java、C++、Go 等主流编程语言
  • 质量意识:熟悉测试与质量保障流程
  • 探索精神:对前沿技术有自己的理解,乐于探索新知识
  • 社区参与:有开源贡献、LeetCode 活跃经历者优先

🚀你的专家任务

标注实验

对代码片段、日志、技术文档等进行精准标注

具体职责

  • 按照项目制定的标注规范,对指定数据(文本、代码片段、日志、技术文档等)进行准确标注
  • 识别并修正数据中存在的歧义、错误或不一致之处
  • 对复杂样本给出专业判断,确保标注结果的准确性与一致性

质量把控

互审他人成果,保障数据纯度

具体职责

  • 参与标注标准的制定与完善,提出改进意见
  • 执行专家互审机制,对其他标注结果进行审核和反馈
  • 结合实际经验,对标注样本的合理性和业务适配度进行评估

知识贡献

沉淀最佳实践,打造可复用的知识宝库

具体职责

  • 将个人专业知识转化为标注规范、最佳实践或案例,供团队参考
  • 在遇到疑难问题时,作为专家提供判定依据,形成可复用的知识库
  • 协助项目组优化任务分配、质量监控和流程设计

☎️ 社群交流

参加活动的掘友一定要入群:

  • 重要消息不错过
  • 大家互相鼓励
  • 有问题可以在群内咨询

134067c882fcc733a803fb9c71f0d2a6.png

⁉️ 常见问题 FAQ

Q1:没有标注经验可以参与吗?

我们需要你具备扎实的编程基础即可快速上手,所以需要你有一定的编程经验

Q2: 每天需要投入多少时间?

完全灵活,可以根据自己的时间安排参与,充分利用碎片时间。

Q3: 如何保证收益?

任务明码标价,多劳多得,平台提供清晰的计价规则和结算流程。

Q4: 非程序员可以参与吗?

可以。平台同时提供非技术类题目,欢迎 PM、QA 等岗位同学参与。

React Native 样式系统详解:与 Web CSS 的“似是而非”

2025年12月10日 14:14

很多从 Web 转战 React Native 的开发者最先问的问题通常是:“我能直接把 CSS 文件复制进去吗?”

答案是不能。虽然 React Native 的样式系统在命名和行为上极力模仿 CSS,但它本质上是JavaScript 对象,运行机制也完全不同。以下是关于这两者差异的完整技术总结。

1. 核心语法:从 Kebab-case 到 CamelCase

在 Web 中,CSS 是文本;在 RN 中,样式是代码(对象)。由于 JavaScript 对象的属性名不能包含连字符(-),所有 CSS 属性都必须转换为 小驼峰命名法 (camelCase)

特性 Web CSS React Native Style
背景色 background-color: red; backgroundColor: 'red'
字体大小 font-size: 16px; fontSize: 16 (注意是数字)
外边距 margin-top: 20px; marginTop: 20
复合属性 border: 1px solid red; 不支持。必须拆分为 borderWidth, borderColor, borderStyle

为什么这样做?

因为样式是 JS 对象,这意味着你可以利用编程语言的所有能力:变量、条件判断、函数计算等。

// React Native 允许动态计算样式
<View style={{
  backgroundColor: isActive ? 'blue' : 'gray', // 条件样式
  width: windowWidth * 0.5 // 动态计算
}} />

2. 继承与层叠:数组覆盖法

Web CSS 的全称是“层叠样式表”(Cascading Style Sheets),依赖选择器权重(Specificity)来决定谁生效。

React Native 没有选择器(没有 .class 或 #id),也没有隐式的样式继承(子元素不会自动继承父元素的字体颜色)。

RN 的“层叠”通过数组实现:

RN 允许你给 style 属性传递一个数组。数组中越靠后的样式优先级越高。

const styles = {
  base: { fontSize: 14, color: 'black' },
  active: { color: 'blue' } // 激活状态覆盖颜色
};

// 数组最后一个生效,最终颜色为 blue
<Text style={[styles.base, styles.active]}>Hello</Text>

这种方式让样式覆盖变得显式且可预测,彻底消除了 Web 开发中“不知道这个样式是从哪里继承来的”痛苦。

3. 布局系统:Flexbox 是唯一真理

React Native 移除了 Web 中复杂的 float, display: block/inline, grid 等布局方式,只保留并强制使用 Flexbox

但有一个巨大的陷阱需要注意:默认主轴方向不同

  • Web Flexbox: 默认 flex-direction: row (横向排列)。

  • RN Flexbox: 默认 flexDirection: 'column' (纵向排列)。

    • 原因: 手机屏幕是窄长的,垂直滚动是移动端的默认交互模式。

4. 尺寸与单位:没有 px,只有逻辑点

在 Web 上,我们纠结于 px, em, rem, vw, vh。

在 RN 上,几乎所有尺寸属性(width, height, margin, padding, fontSize)都只接受不带单位的数字。

  • 含义: 这些数字代表 逻辑像素 (Logical Pixels / Points)

  • 自动适配: RN 会根据设备的屏幕密度(DPI/PixelRatio)自动将其转换为屏幕上的物理像素。

    • width: 100,在普通屏是 100px,在 Retina 屏可能是 200px 或 300px。
    • 例外: 也可以使用百分比字符串,如 width: '50%'

5. 常见痛点与已知限制 (Known Issues)

根据你提供的文档片段,RN 并不是完美复刻了 CSS 引擎,这里有几个著名的“坑”:

A. 触摸区域与父级边界 (Parent Bounds)

  • Web: 子元素设为 absolute 并移出父元素框外,通常依然可见且可点击。

  • RN (Android): 子元素的触摸事件无法超出父组件的边界。如果你把按钮用 position: absolute 移到了父 View 的外面,你看着它在那里,但点它没反应。

    • 注: 视觉上,Android 默认 overflow: hidden 行为较强,虽新版本有改善,但点击判定依然严格遵循父级区域。

B. 负边距 (Negative Margins)

  • Web: margin-top: -50px 是常用的重叠布局技巧。
  • RN: 文档明确提到 "on Android negative margin is not supported" (或支持受限)。虽然现代 RN 版本对负 margin 的支持已经好转,但在某些复杂嵌套或旧版本 Android 上,它依然会导致布局塌陷或裁剪。

C. 圆角与图片 (Border Radius)

如前文所述,iOS 的 <Image> 组件对 borderTopLeftRadius单独圆角属性支持不佳。必须通过包裹一个 <View> 并设置 overflow: 'hidden' 来实现异形图片。

D. 阴影 (Shadows)

这是最分裂的地方:

  • iOS: 使用 shadowColor, shadowOffset, shadowOpacity (类似 CSS)。
  • Android: 必须使用 elevation (一个数字,对应 Material Design 的层级高度)。为了跨平台,通常需要根据平台写两套代码。

6. 最佳实践:StyleSheet.create

虽然你可以直接写内联样式对象 style={{color: 'red'}},但官方推荐使用 StyleSheet.create

import { StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
});

为什么?

  1. 性能: 系统可以将这些样式对象 ID 化并缓存,避免每次渲染组件时都重新创建新的对象。
  2. 验证: 它会在开发阶段检查你的属性名是否合法(比如如果你写了 background-color,它会直接报错提醒你改成 backgroundColor)。

总结

当你开始在 React Native 中写样式时,请记住:

  1. ❌ 不要用 Kebab-case (font-size),要用 CamelCase (fontSize)。
  2. ❌ 不要加 px 单位,直接写数字
  3. ❌ 不要指望样式自动继承(Text 组件内的嵌套除外)。
  4. ⚠️ 默认布局是 纵向 (Column) 的。
  5. ⚠️ 所有的边框、阴影、圆角,在 iOS 和 Android 上可能表现不一致,多真机测试。

隐形追踪者:当你删除了 Cookies,谁还在看着你?——揭秘浏览器指纹

2025年12月10日 14:05

韭菜们是否经历过这样的诡异时刻:你在某个购物网站搜索了一双球鞋,仅仅过了一分钟,当你打开新闻网站或社交媒体时,那双球鞋的广告就出现在了显眼的位置。

通常,我们会把这归咎于 Cookies。于是,聪明的韭菜打开了“无痕模式”,或者彻底清除了浏览器的缓存和 Cookies,认为这样就能隐身于互联网。

然而,广告依然如影随形。

这是因为,由于 “浏览器指纹”(Browser Fingerprinting) 技术的存在,你实际上一直在“裸奔”。

什么是浏览器指纹?

在现实生活中,指纹是我们独一无二的生理特征。而在互联网世界中,浏览器指纹是指当你访问一个网站时,你的浏览器不仅会请求网页内容,还会无意中暴露一系列关于你设备的软硬件配置信息。

这些信息单独看起来都很普通,比如:

  • 你的操作系统(Windows, macOS, Android...)
  • 屏幕分辨率(1920x1080...)
  • 浏览器版本(Chrome 120...)
  • 安装的字体列表
  • 时区和语言设置
  • 显卡型号和电池状态

神奇之处在于组合: 当把这几十甚至上百个特征组合在一起时,它们就形成了一个极高精度的“特征值”。据研究,对于绝大多数互联网用户来说,这个组合是全球唯一

它是如何工作的?

为了生成这个指纹,追踪者使用了一些非常巧妙的技术:

1. Canvas 指纹(画布指纹)

这是最著名的指纹技术。网站会命令你的浏览器在后台偷偷绘制一张复杂的隐藏图片(包含文字和图形)。

由于不同的操作系统、显卡驱动、字体渲染引擎处理图像的方式有微小的像素级差异,每台电脑画出来的图在哈希值上是完全不同的。

2. AudioContext 指纹(音频指纹)

原理类似 Canvas。网站会让浏览器生成一段人耳听不到的音频信号。不同的声卡和音频驱动处理信号的方式不同,生成的数字指纹也就不同

3. 字体枚举

你安装了 Photoshop?或者安装了一套冷门的编程字体?网站可以通过脚本检测你系统里安装了哪些字体。安装的字体越独特,你的指纹辨识度就越高

为什么它比 Cookies 更可怕?

特性 Cookies (传统的追踪) 浏览器指纹 (新型追踪)
存储位置 你的电脑硬盘里 不需要存储,实时计算
用户控制 你可以随时一键删除 你无法删除,它是你设备的属性
隐身模式 无效(隐身模式不读旧Cookies) 依然有效(隐身模式下设备配置不变)
持久性 易丢失 极难改变,甚至跨浏览器追踪

这就好比:

  • Cookies 就像是进门时发给你的一张胸牌,你把它扔了,保安就不认识你了
  • 浏览器指纹 就像是保安记住了你的身高、长相、穿衣风格和走路姿势。这和你戴不戴胸牌没有任何关系

主要用途

浏览器指纹技术在现代网络中有多种用途,主要可以分为追踪识别安全防护两大类:

追踪与用户画像

  • 跨网站追踪用户:广告网络会在不同站点嵌入脚本,通过指纹标记“同一访客”,进而在B站推送你在A站浏览过的商品或内容,实现“精准广告”。
  • 绘制用户画像:即使未登录,只要指纹相同,网站就能合并浏览记录、点击路径、停留时长等数据,推测兴趣偏好、消费水平,再反向优化推荐算法。
  • “无Cookie” 追踪:指纹在无痕/隐私模式下依旧存在,且无法像Cookie那样一键清空,因此被视为更顽固的追踪手段。

反欺诈与风控

  • 账号安全:银行、支付、社交平台把指纹作为“设备信任度”指标。若登录指纹突然大变(新系统、虚拟机、海外设备),可触发二次验证或冻结交易。
  • 薅羊毛/作弊识别:投票、抽奖、优惠券领取页面用指纹判断“是否同一设备反复参与”,防止批量注册、刷单。
  • 广告反欺诈:验证广告点击是否来自真实浏览器,而非自动化脚本或虚假流量农场。

多账号管理

  • 跨境电商/社媒运营:卖家或营销人员需要在一台电脑同时登录几十个Amazon、eBay、Facebook、TikTok账号。若用普通浏览器,平台会因指纹相同判定“关联店铺”并封号。指纹浏览器可为每个账号伪造独立的设备环境(分辨率、字体、Canvas、WebGL、MAC地址、IP等),实现“物理级隔离”。
  • 数据抓取与测试:爬虫或自动化测试脚本通过切换指纹模拟不同真实用户,降低被目标站点封锁的概率。

合规与隐私保护

  • 反指纹追踪:隐私插件或“高级指纹保护”功能会故意把Canvas、音频、WebGL结果做随机噪声,或统一返回常见值,削弱指纹的唯一性,减少被跨站跟踪。

React Native 图片机制深度解析:设计哲学、性能优化与避坑指南

2025年12月10日 14:02

1. 核心哲学:体验优先于便利 (UX > DX)

React Native 在图片处理上与 Web 浏览器有着本质的不同。RN 宁愿让开发者多写一点代码,也要保证用户体验的极致顺滑。

  • 拒绝“布局抖动” (No Layout Shift):

    • Web 痛点: 浏览器加载图片时默认是 0x0,下载完瞬间撑开,导致页面跳动。
    • RN 策略: 强制要求开发者预先指定远程图片的宽高。这意味着图片加载前位置就已留好,加载后只是填空,界面纹丝不动。
  • 例外: 本地静态图片(require('./icon.png'))因在编译时已知尺寸,可自动推断宽高。

2. 底层性能:为了不卡顿 (Performance)

RN 在幕后做了大量工作,确保即使加载高清大图,App 的 UI 线程(Main Thread)也不会阻塞。

  • 后台解码 (Off-thread Decoding):

    • 图片解码(JPEG/PNG转像素)非常耗时。RN 将其移至后台线程执行,解码完成后再送回主线程显示。这避免了 Web 常见的“滚动时因加载图片导致的掉帧”。
  • iOS 智能选图 (The 50% Rule):

    • 从相册加载图片时,RN 不会无脑加载原图(太费内存),也不会选太小的图(太糊)。
    • 它会自动寻找第一张比显示区域大 50% 以上的缩略图版本。既保证了清晰度(避免拉伸模糊),又最大程度节省了内存。

3. 架构设计:面向未来的扩展性 (Extensibility)

  • Source 是对象而非字符串:

    • <Image source={{uri: '...'}} /> 的设计看似繁琐,实则为了扩展。
    • 它允许携带元数据(Metadata),并为未来特性(如雪碧图裁剪 crop 属性)预留了接口,保证了代码的向后兼容性。

4. iOS 实战:避坑与进阶调优 (iOS Specifics)

针对 iOS 平台,有一些特殊的限制和高级配置需要注意:

  • 圆角样式的坑:

    • 问题: borderTopLeftRadius 等单独圆角属性在 iOS <Image> 上往往不生效。
    • 解法: 使用“外部裁剪法”。将 Image 包裹在 <View> 中,在 View 上设置圆角并加上 overflow: 'hidden'
  • 手动控制缓存 (Cache Limits):

    • 能力: 可以在 AppDelegate.m 中调用 RCTSetImageCacheLimits
    • 参数: 可以设定“单张图片最大体积”(超过不缓存)和“总缓存池上限”(超过踢掉旧图),从而在内存紧张或图片密集的 App 中找到性能平衡点。

总结

React Native 的 Image 组件不仅仅是一个简单的 UI 元素,它是一个高度封装的、自带性能优化策略的子系统。理解这些机制,能帮你写出即便在数千张图片的瀑布流中依然如丝般顺滑的 App。

vxe-tree 树组件拖拽排序功能的使用教程

2025年12月10日 13:39

vxe-tree 树组件拖拽排序功能的使用教程,通过 drag 启用行拖拽排序功能,支持同层级、跨层级、拖拽到子级非常强大的拖拽功能等

官网:vxeui.com github:github.com/x-extends/v… gitee:gitee.com/x-extends/v…

同层级拖拽

通过 drag-config.isPeerDrag 启用同层级拖拽

image

<template>
  <div>
    <vxe-tree v-bind="treeOptions"></vxe-tree>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const treeOptions = reactive({
  transform: true,
  drag: true,
  dragConfig: {
    isPeerDrag: true
  },
  data: [
    { title: '节点2', id: '2', parentId: null },
    { title: '节点3', id: '3', parentId: null },
    { title: '节点3-1', id: '31', parentId: '3' },
    { title: '节点3-2', id: '32', parentId: '3' },
    { title: '节点3-2-1', id: '321', parentId: '32' },
    { title: '节点3-2-2', id: '322', parentId: '32' },
    { title: '节点3-3', id: '33', parentId: '3' },
    { title: '节点3-3-1', id: '331', parentId: '33' },
    { title: '节点3-3-2', id: '332', parentId: '33' },
    { title: '节点3-3-3', id: '333', parentId: '33' },
    { title: '节点3-4', id: '34', parentId: '3' },
    { title: '节点4', id: '4', parentId: null },
    { title: '节点4-1', id: '41', parentId: '4' },
    { title: '节点4-1-1', id: '411', parentId: '42' },
    { title: '节点4-1-2', id: '412', parentId: '42' },
    { title: '节点4-2', id: '42', parentId: '4' },
    { title: '节点4-3', id: '43', parentId: '4' },
    { title: '节点4-3-1', id: '431', parentId: '43' },
    { title: '节点4-3-2', id: '432', parentId: '43' },
    { title: '节点5', id: '5', parentId: null }
  ]
})
</script>

跨层级拖拽

通过 drag-config.isCrossDrag 启用跨层级拖拽

tree_drag_coss_drag

<template>
  <div>
    <vxe-tree v-bind="treeOptions"></vxe-tree>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const treeOptions = reactive({
  transform: true,
  drag: true,
  dragConfig: {
    isCrossDrag: true
  },
  data: [
    { title: '节点2', id: '2', parentId: null },
    { title: '节点3', id: '3', parentId: null },
    { title: '节点3-1', id: '31', parentId: '3' },
    { title: '节点3-2', id: '32', parentId: '3' },
    { title: '节点3-2-1', id: '321', parentId: '32' },
    { title: '节点3-2-2', id: '322', parentId: '32' },
    { title: '节点3-3', id: '33', parentId: '3' },
    { title: '节点3-3-1', id: '331', parentId: '33' },
    { title: '节点3-3-2', id: '332', parentId: '33' },
    { title: '节点3-3-3', id: '333', parentId: '33' },
    { title: '节点3-4', id: '34', parentId: '3' },
    { title: '节点4', id: '4', parentId: null },
    { title: '节点4-1', id: '41', parentId: '4' },
    { title: '节点4-1-1', id: '411', parentId: '42' },
    { title: '节点4-1-2', id: '412', parentId: '42' },
    { title: '节点4-2', id: '42', parentId: '4' },
    { title: '节点4-3', id: '43', parentId: '4' },
    { title: '节点4-3-1', id: '431', parentId: '43' },
    { title: '节点4-3-2', id: '432', parentId: '43' },
    { title: '节点5', id: '5', parentId: null }
  ]
})
</script>

gitee.com/x-extends/v…

UniApp Workspaces 编译报错(`Invalid pattern ...`)解决办法

作者 heyCHEEMS
2025年12月10日 13:36

问题

在 Workspaces 组织 UniApp 项目时,编译为 mp-weixin 时出现报错:Invalid pattern "../../....../xxx.js" for "output.chunkFileNames", patterns can be neither absolute nor relative paths. If you want your files to be stored in a subdirectory, write its name without a leading slash like this: subdirectory/pattern."

解决办法

UniApp 编译工具没有正确处理 Workspaces 引入依赖时带有的相对路径 ../。可以直接修改utils内的对应函数,将相对路径替换成空。

文件位于:

node_modules/@dcloudio/uni-cli-shared/dist/utils.js

找到 normalizeNodeModules 函数,修改:

【修改前】

function normalizeNodeModules(str) {
  // ... 原有代码
  return str;
}

【修改后】

function normalizeNodeModules(str) {
  // ... 原有代码

  // 匹配删除路径中的所有相对路径标识符 '../'
  str = str.replace(/\.\.\//g, '')

  return str;
}

重启编译即可解决。


注意: 这是一个临时补丁。如果运行 npm install 等命令,该文件会被覆盖,需要重新修改。

原型-2:prototype 和 __proto__ 的区别详解

作者 之恒君
2025年12月10日 13:19

这两个属性都与 JavaScript 的原型系统相关,但扮演完全不同的角色,经常容易混淆。

1. 核心区别总结

特性 prototype __proto__
所有者 函数 对象
作用 构造函数创建对象的原型 对象的原型链引用
访问 构造函数.prototype 对象.proto
标准 正式标准 非标准(但被广泛实现)
推荐访问 Object.getPrototypeOf(obj)
推荐设置 Object.setPrototypeOf(obj, proto)

2. 基本概念

2.1 prototype 属性

// 构造函数才有 prototype
function Person(name) {
  this.name = name;
}

// prototype 用于定义通过 new 创建的对象共享的属性和方法
Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

Person.prototype.species = 'Human';

// 查看 prototype
console.log(typeof Person.prototype);  // "object"
console.log(Person.prototype.constructor === Person);  // true

2.2 __proto__属性

// 对象才有 __proto__
const person = new Person('Alice');

// __proto__ 指向创建该对象的构造函数的 prototype
console.log(person.__proto__ === Person.prototype);  // true

// 原型链查找
console.log(person.name);        // "Alice" - 自有属性
console.log(person.species);     // "Human" - 从原型找到
console.log(person.toString());  // [object Object] - 从 Object.prototype 找到

3. 详细对比

3.1 属性所有者

function Animal() {}

// prototype 属于函数
console.log(Animal.hasOwnProperty('prototype'));  // true
console.log(typeof Animal.prototype);              // "object"

// __proto__ 属于对象
const dog = new Animal();
console.log(dog.hasOwnProperty('__proto__'));      // false
console.log(dog.__proto__ === Animal.prototype);   // true
console.log(typeof dog.__proto__);                 // "object"

// 函数也是对象,所以函数也有 __proto__
console.log(Animal.__proto__ === Function.prototype);  // true

3.2 创建时的行为

function Car(brand) {
  this.brand = brand;
}

// 设置 prototype
Car.prototype.drive = function() {
  console.log(`${this.brand} is driving`);
};

// 创建实例
const myCar = new Car('Toyota');

// 此时发生:
// 1. 创建新对象:{}
// 2. 设置对象的 __proto__ 指向 Car.prototype
// 3. 执行构造函数,this 指向新对象
// 4. 返回对象

console.log(myCar.__proto__ === Car.prototype);  // true
console.log(Object.getPrototypeOf(myCar) === Car.prototype);  // true

4. 原型链示例

function Vehicle(type) {
  this.type = type;
}
Vehicle.prototype.move = function() {
  console.log(`${this.type} is moving`);
};

function Car(brand) {
  Vehicle.call(this, 'car');
  this.brand = brand;
}

// 继承 Vehicle
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

Car.prototype.honk = function() {
  console.log(`${this.brand} honks!`);
};

// 创建实例
const toyota = new Car('Toyota');

// 原型链:
console.log(toyota.__proto__ === Car.prototype);          // true
console.log(toyota.__proto__.__proto__ === Vehicle.prototype);  // true
console.log(toyota.__proto__.__proto__.__proto__ === Object.prototype);  // true
console.log(toyota.__proto__.__proto__.__proto__.__proto__);  // null

// 方法查找:
toyota.honk();   // 在 Car.prototype 找到
toyota.move();   // 在 Vehicle.prototype 找到
toyota.toString();  // 在 Object.prototype 找到

5. 标准 vs 非标准

5.1 标准访问方法

function Person() {}
const p = new Person();

// 标准获取原型的方法
console.log(Object.getPrototypeOf(p) === Person.prototype);  // true

// 标准设置原型的方法
const obj = {};
const newProto = { x: 1 };
Object.setPrototypeOf(obj, newProto);
console.log(Object.getPrototypeOf(obj) === newProto);  // true

// 检查是否是原型
console.log(newProto.isPrototypeOf(obj));  // true

5.2 __proto__的问题

// __proto__ 不是标准属性,但被广泛支持
const obj = {};

// 在一些环境中可能不可用
console.log('__proto__' in {});  // 大多数是 true

// 但可以修改
const originalProto = Object.getPrototypeOf(obj);
console.log(obj.__proto__ === originalProto);  // true

// 修改 __proto__
obj.__proto__ = { custom: true };
console.log(obj.custom);  // true

// 但性能差,不推荐
// Object.setPrototypeOf 是更好的选择

6. 特殊情况

6.1 箭头函数

// 箭头函数没有 prototype
const arrow = () => {};
console.log(arrow.prototype);  // undefined

// 普通函数有 prototype
function normal() {}
console.log(normal.prototype);  // { constructor: normal }

6.2 内置对象

// 数组的原型链
const arr = [];
console.log(arr.__proto__ === Array.prototype);  // true
console.log(arr.__proto__.__proto__ === Object.prototype);  // true
console.log(arr.__proto__.__proto__.__proto__);  // null

// 字符串的原型链
const str = 'hello';
console.log(str.__proto__ === String.prototype);  // true
console.log(str.__proto__.__proto__ === Object.prototype);  // true

// 函数的原型链
function fn() {}
console.log(fn.__proto__ === Function.prototype);  // true
console.log(fn.__proto__.__proto__ === Object.prototype);  // true

6.3 Object.create

// 使用 Object.create 创建对象
const proto = { x: 10 };
const obj = Object.create(proto);

console.log(Object.getPrototypeOf(obj) === proto);  // true
console.log(obj.__proto__ === proto);  // true
console.log(obj.x);  // 10,从原型继承

// 创建没有原型的对象
const nullObj = Object.create(null);
console.log(Object.getPrototypeOf(nullObj));  // null
console.log(nullObj.__proto__);  // undefined
console.log(nullObj.toString);  // undefined

7. 继承机制详解

7.1 构造函数继承

function Parent(name) {
  this.name = name;
  this.parentProperty = 'parent';
}

Parent.prototype.parentMethod = function() {
  console.log('Parent method:', this.name);
};

function Child(name, age) {
  // 1. 构造函数继承(实例属性)
  Parent.call(this, name);
  this.age = age;
  this.childProperty = 'child';
}

// 2. 原型继承(方法)
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.childMethod = function() {
  console.log('Child method:', this.age);
};

// 创建实例
const child = new Child('Alice', 10);

console.log(child.__proto__ === Child.prototype);  // true
console.log(child.__proto__.__proto__ === Parent.prototype);  // true
console.log(child.hasOwnProperty('name'));  // true
console.log(child.hasOwnProperty('parentMethod'));  // false

7.2 ES6 类继承

class Parent {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    console.log(`Hello from ${this.name}`);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
  
  sayAge() {
    console.log(`I'm ${this.age} years old`);
  }
}

const child = new Child('Bob', 12);

// 本质上还是原型继承
console.log(child.__proto__ === Child.prototype);  // true
console.log(child.__proto__.__proto__ === Parent.prototype);  // true
console.log(Child.__proto__ === Parent);  // true
console.log(Child.prototype.__proto__ === Parent.prototype);  // true

8. 性能考虑

8.1 原型链查找优化

// JavaScript 引擎会优化原型链查找
function getProperty(obj, prop) {
  // 1. 检查对象自身属性
  if (obj.hasOwnProperty(prop)) {
    return obj[prop];
  }
  
  // 2. 查找原型链
  let proto = Object.getPrototypeOf(obj);
  while (proto) {
    if (proto.hasOwnProperty(prop)) {
      return proto[prop];
    }
    proto = Object.getPrototypeOf(proto);
  }
  
  return undefined;
}

8.2 避免修改 __proto__

// ❌ 不推荐:直接修改 __proto__
const obj = { x: 1 };
console.time('修改 __proto__');
for (let i = 0; i < 10000; i++) {
  obj.__proto__ = { y: 2 };
}
console.timeEnd('修改 __proto__');

// ✅ 推荐:使用 Object.setPrototypeOf
console.time('Object.setPrototypeOf');
for (let i = 0; i < 10000; i++) {
  Object.setPrototypeOf(obj, { y: 2 });
}
console.timeEnd('Object.setPrototypeOf');

// 最佳:创建时就确定原型
console.time('Object.create');
for (let i = 0; i < 10000; i++) {
  const newObj = Object.create({ y: 2 });
  newObj.x = 1;
}
console.timeEnd('Object.create');


// 输出:
// 修改 __proto__: 16.87109375 ms
// Object.setPrototypeOf: 37.158203125 ms
// Object.create: 19.46484375 ms

9. 实际应用

9.1 对象扩展

// 使用原型扩展内置对象
if (!Array.prototype.find) {
  Array.prototype.find = function(predicate) {
    for (let i = 0; i < this.length; i++) {
      if (predicate(this[i], i, this)) {
        return this[i];
      }
    }
    return undefined;
  };
}

// 检查是否是自定义扩展
console.log([].hasOwnProperty('find'));  // false
console.log([].__proto__.hasOwnProperty('find'));  // true

9.2 混入模式

// 使用原型实现混入
const canEat = {
  eat(food) {
    console.log(`${this.name} eats ${food}`);
  }
};

const canSleep = {
  sleep(hours) {
    console.log(`${this.name} sleeps for ${hours} hours`);
  }
};

function Animal(name) {
  this.name = name;
}

// 混入到原型
Object.assign(Animal.prototype, canEat, canSleep);

const dog = new Animal('Dog');
dog.eat('meat');
dog.sleep(8);

// 输出:
// Dog eats meat
// Dog sleeps for 8 hours

9.3 原型检测

// 检测对象关系
function getPrototypeChain(obj) {
  const chain = [];
  let current = obj;
  
  while (current) {
    chain.push(current.constructor.name || '[Anonymous]');
    current = Object.getPrototypeOf(current);
  }
  
  return chain;
}

const arr = [];
console.log(getPrototypeChain(arr));  // ["Array", "Object", "[Anonymous]"]
console.log(getPrototypeChain(Function));  // ["Function", "Object", "[Anonymous]"]

10. 常见误区

10.1 混淆 prototype 和 __proto__

function MyClass() {}

// ❌ 错误理解
console.log(MyClass.__proto__ === MyClass.prototype);  // false

// ✅ 正确理解
console.log(MyClass.prototype.__proto__ === Object.prototype);  // true
console.log(MyClass.__proto__ === Function.prototype);  // true

const instance = new MyClass();
console.log(instance.__proto__ === MyClass.prototype);  // true

10.2 修改构造函数的 prototype

function Original() {}
Original.prototype.value = 1;

const obj1 = new Original();
console.log(obj1.value);  // 1

// 修改构造函数的 prototype
Original.prototype = { value: 2 };

const obj2 = new Original();
console.log(obj2.value);  // 2
console.log(obj1.value);  // 1,已存在的对象不受影响
console.log(obj1.__proto__ === obj2.__proto__);  // false

10.3 原型污染

// 原型污染攻击
// ❌ 危险:用户输入控制原型
function unsafeMerge(target, source) {
  for (const key in source) {
    target[key] = source[key];
  }
  return target;
}

const userInput = JSON.parse('{"__proto__": {"admin": true}}');
const config = {};
unsafeMerge(config, userInput);

console.log({}.admin);  // true!污染了所有对象

// ✅ 安全的方法
function safeMerge(target, source) {
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      target[key] = source[key];
    }
  }
  return target;
}

11. 现代最佳实践

11.1 使用 ES6 类

class Base {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    console.log(`Hello, ${this.name}`);
  }
}

class Derived extends Base {
  constructor(name, value) {
    super(name);
    this.value = value;
  }
  
  showValue() {
    console.log(`Value: ${this.value}`);
  }
}

// 清晰的继承关系
console.log(Object.getPrototypeOf(Derived) === Base);  // true
console.log(Object.getPrototypeOf(Derived.prototype) === Base.prototype);  // true

11.2 使用 Object.create

// 组合式继承
const canFly = {
  fly() {
    console.log(`${this.name} is flying`);
  }
};

const canSwim = {
  swim() {
    console.log(`${this.name} is swimming`);
  }
};

function createAnimal(name, abilities) {
  const animal = Object.create(
    abilities.reduce((proto, ability) => {
      return Object.assign(Object.create(proto), ability);
    }, { name })
  );
  return animal;
}

const duck = createAnimal('Duck', [canFly, canSwim]);
duck.fly();
duck.swim();

11.3 使用代理

// 使用 Proxy 控制原型访问
const handler = {
  get(target, prop) {
    if (prop === '__proto__') {
      throw new Error('Direct __proto__ access is forbidden');
    }
    return Reflect.get(target, prop);
  },
  set(target, prop, value) {
    if (prop === '__proto__') {
      throw new Error('Direct __proto__ modification is forbidden');
    }
    return Reflect.set(target, prop, value);
  }
};

const safeObject = new Proxy({}, handler);
console.log(safeObject.x = 1);  // 正常
// safeObject.__proto__ = {};  // 抛出错误

总结

关键点

  1. prototype是函数的属性,用于定义通过该函数作为构造函数创建的对象所共享的属性和方法
  2. __proto__是对象的属性,指向创建该对象的构造函数的 prototype
  3. 原型链是通过 __proto__链接的,而不是 prototype
  4. 使用标准方法Object.getPrototypeOf()Object.setPrototypeOf()

记忆口诀

函数有prototype,对象有__proto__

new的时候,对象.__proto__ = 函数.prototype

原型链是对象链,函数只在链的开端

标准方法更可靠,__proto__要慎用

现代开发建议

  • 使用 ES6 类语法
  • 避免直接操作 __proto__
  • 使用 Object.create()创建对象
  • 了解原型机制,但让框架/引擎处理细节
  • 注意原型污染的安全问题

原型-1: 理解 JavaScript 中的 原型

作者 之恒君
2025年12月10日 13:14

要理解 JavaScript 中的「原型」,首先要跳出其他面向对象语言(如 Java、C#)的「类」思维——JS 是通过 原型(Prototype) 实现继承和属性共享的,核心是「对象基于原型关联,而非类的实例化」。

一、先搞懂:为什么需要原型?

原型的本质是 “资源复用工具” 。它解决了一个核心问题:避免重复创建相同属性/方法,节省内存

举个例子:如果创建 100 个「人」对象,每个对象都需要 sayHello 方法。如果直接在每个对象里定义 sayHello,会生成 100 个完全相同的函数,造成内存浪费。

而原型的思路是: sayHello 放在一个“公共模板”(原型对象)里,所有「人」对象都“关联”这个模板,需要时直接从模板里拿方法——这样只需要 1 个 sayHello 函数,实现资源复用。

二、核心概念:3 个关键角色

理解原型,必须先分清 3 个紧密关联的概念:原型对象(Prototype)prototype 属性__proto__ 属性(或 Object.getPrototypeOf() 方法)。

概念 作用 归属
原型对象(Prototype) 存储公共属性/方法的“模板对象”,被其他对象共享 每个对象都有一个原型对象(除了 Object.prototype
prototype 属性 函数特有,指向该函数创建的「实例对象」的原型对象 函数(如构造函数、普通函数)
__proto__ 属性 对象特有,指向该对象的原型对象(非标准,但浏览器普遍支持) 所有对象(除了 null

三、直观理解:原型链的“关联关系”

所有对象的原型会形成一条「原型链」:对象 → 它的原型对象 → 原型的原型对象 → ... → 最终指向 Object.prototype(原型链的顶端),Object.prototype 的原型是 null(没有更上层的原型)。

我们通过一个具体例子,拆解原型链的关联逻辑:

1. 用构造函数创建对象(经典场景)

// 1. 定义一个构造函数(首字母大写,约定用于创建对象)
function Person(name) {
  this.name = name; // 实例属性:每个 Person 实例都有独立的 name
}

// 2. 在构造函数的 prototype 上定义“公共方法”
Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

// 3. 用 new 关键字创建 Person 实例
const zhangsan = new Person('张三');

2. 拆解原型关联(关键!)

上面代码中,zhangsanPersonPerson.prototype 三者的原型关系如下:

  • zhangsan (实例对象)的原型zhangsan.__proto__ === Person.prototype(或 Object.getPrototypeOf(zhangsan) === Person.prototype
    → 实例的原型,指向构造函数的 prototype 属性。
  • Person.prototype (原型对象)的原型Person.prototype.__proto__ === Object.prototype
    → 所有自定义原型对象,最终都关联到 Object.prototype(JS 内置的顶层原型)。
  • Object.prototype 的原型Object.prototype.__proto__ === null
    → 原型链的终点,没有更上层的原型。
  • Person (构造函数)的原型Person.__proto__ === Function.prototype
    → 函数也是对象,所有函数的原型都指向 Function.prototypeFunction.prototype.__proto__ 最终也指向 Object.prototype)。

3. 原型链的作用:属性查找规则

当你访问一个对象的属性(如 zhangsan.sayHello)时,JS 会按以下规则查找:

  1. 先在对象自身查找:如果 zhangsan 自己有 sayHello 方法,直接使用;
  2. 自身没有,就去「对象的原型对象」查找:即 Person.prototype,找到 sayHello,使用;
  3. 原型对象没有,就去「原型的原型」查找:即 Object.prototype
  4. 一直查到 null 还没找到,就返回 undefined(不会报错)。

比如:zhangsan.toString()toStringObject.prototype 的方法,zhangsan 自身没有,但能通过原型链找到)。

四、原型的核心特性

1. 共享性:原型上的属性/方法被所有实例共享

const lisi = new Person('李四');
zhangsan.sayHello(); // Hello, I'm 张三
lisi.sayHello();     // Hello, I'm 李四
// 两个实例调用的是同一个 sayHello 方法(来自 Person.prototype)
console.log(zhangsan.sayHello === lisi.sayHello); // true

2. 动态性:原型的修改会影响所有已创建的实例

即使实例已经创建,修改原型对象的属性/方法,实例也能立即访问到新的内容:

// 给 Person.prototype 新增一个方法
Person.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 已创建的 zhangsan 能直接调用新方法
zhangsan.eat(); // 张三 is eating

3. 实例可“覆盖”原型属性(不修改原型本身)

如果实例自身定义了和原型同名的属性/方法,会优先使用实例自身的(原型链查找时会先找到自身属性,停止向上查找):

// 给 zhangsan 自身定义 sayHello 方法
zhangsan.sayHello = function() {
  console.log(`Hi, I'm ${this.name}`);
};

zhangsan.sayHello(); // Hi, I'm 张三(用自身的方法)
lisi.sayHello();     // Hello, I'm 李四(用原型的方法,不受影响)

五、常见误区澄清

  1. “原型是类”?错!
    JS 直到 ES6 才引入 class 语法,但 class 只是「原型的语法糖」——本质还是基于原型实现继承,并非像 Java 那样的“类”。
  2. prototype __proto__ 分不清?
    • 记住:prototype函数的属性,指向实例的原型;__proto__对象的属性,指向自己的原型。
    • 简单公式:实例.__proto__ === 构造函数.prototype
  1. “所有对象都有 __proto__ ”?错!
    null 没有 __proto__Object.getPrototypeOf(null) 会返回 null),它是原型链的终点。

六、总结:原型的本质

原型的核心是「对象之间的关联关系」——每个对象都通过 __proto__ 关联到一个原型对象,从而共享原型上的资源。这种关联形成的「原型链」,决定了 JS 中属性查找、继承的逻辑。

理解原型,就能理解 JS 面向对象的底层逻辑(比如 new 关键字的作用、继承的实现方式),也是后续学习 classextends 等语法的基础。

unplugin-vue-router文件路由实操

作者 LCC
2025年12月10日 13:04

unplugin-vue-router 会根据你的文件结构自动生成路由配置。虽然路由是自动生成的,但你仍然可以通过 路由元信息(meta) 来对组件做一些特殊处理。

<route lang="yaml">
    name: login
    meta:
      whiteList: true
      title: 登录跳转
      constant: true
      layout: index
</route>

安装
pnpm i unplugin-vue-router -D

配置// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueRouter from "unplugin-vue-router/vite";

export default defineConfig({
  plugins: [
    VueRouter({ routesFolder: "src/views" }),
    vue(),
  ]
});

使用
import { routes } from "vue-router/auto/routes" // 引入文件路由表
import { createRouter, createWebHistory } from "vue-router"

const router = createRouter({
  routes: routes, // 注册文件路由表
  history: createWebHistory(),
})

运行项目打印routes就会看到生成好的路由

image.png

一般搭配vite-plugin-vue-meta-layouts使用

安装
pnpm i vite-plugin-vue-meta-layouts -D

配置// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueRouter from "unplugin-vue-router/vite";
import MetaLayouts from "vite-plugin-vue-meta-layouts"

export default defineConfig({
  plugins: [
    VueRouter({ routesFolder: "src/views" }),
    Layouts({
      target: "src/layouts", // 布局目录,默认 src/layouts
      defaultLayout: "index", // 默认布局,默认为 default
      skipTopLevelRouteLayout: true, 
    }),
    vue(),
  ]
});

使用
import { routes } from "vue-router/auto/routes" // 引入文件路由表
import { setupLayouts } from "virtual:meta-layouts"
import { createRouter, createWebHistory } from "vue-router"

const router = createRouter({
  routes: setupLayouts(routes), // 注册文件路由表
  history: createWebHistory(),
})

如果你是 `ts` 项目,还可以在 `tsconfig.json` 中配置以下声明
{
  "compilerOptions": {
    "types": ["vite-plugin-vue-meta-layouts/client"]
  }
}

在layouts文件夹创建不同的的布局文件

image.png

在页面组件中通过路由元信息配置layout的值来让页面使用哪种布局,一般在vite.config.ts配置默认使用的布局

<route lang="yaml">
    meta:
      layout: empty 
</route>

然后可以通过createGetRoutes查看生成的路由

import { createGetRoutes } from "virtual:meta-layouts"

const getRoutes = createGetRoutes(router)

// 获取路由表但是不包含布局路由
console.log(getRoutes())

image.png

文件路由使用路由query 传参跳转页面需要为 router-view 添加唯一 key 不然就会出现多次跳转同一页面,如果只是参数不同,页面不会重新执行生命周期方法

<router-view v-slot="{ Component, route }">
   <keep-alive :include="cachedComponents">
      <component :is="Component" :key="route.fullPath" />
   </keep-alive>
</router-view>

当然,如果是使用动态路由就不会有这个问题

  • src/views/users/[id].vue → /users/:id
  • src/views/posts/[...slug].vue → /posts/:slug(.*)*

JavaScript 内存机制与闭包解析

作者 冻梨政哥
2025年12月10日 12:22

JavaScript 内存机制与闭包解析

JavaScript 作为一门动态弱类型语言,其内存管理机制和闭包特性是理解 JS 运行原理的核心。本文将结合具体代码示例,深入解析 JS 的内存机制以及闭包的工作原理。

一、JavaScript 内存空间划分

JavaScript 的内存空间主要分为三大类:

  1. 代码空间:用于存储执行的代码
  2. 栈内存:主要存储简单数据类型和引用类型的地址
  3. 堆内存:主要存储复杂数据类型(对象等)

2bad8a6ec6a17f7a384612ea3e0cbdcd.png 栈内存的特点是操作速度快、大小固定、存储连续,适合存储体积小的数据;而堆内存空间大,可存储大型复杂对象,但分配和回收内存相对耗时。

二、数据类型的内存存储

1. 简单数据类型的存储

简单数据类型(如数字、字符串、布尔值等)直接存储在栈内存中,赋值时会进行值拷贝:

// 调用栈在栈内存中
// 体积小
function foo() {
    var a = 1; // 赋值,直接存储在栈内存
    var b = a; // 拷贝,在栈内存中创建新值
    a = 2;
    console.log(a); // 2
    console.log(b); // 1
}
foo();

上述代码中,ab是相互独立的变量,修改a的值不会影响b,因为它们在栈内存中占据不同的空间。

2. 复杂数据类型的存储

复杂数据类型(如对象)在栈内存中存储的是指向堆内存的地址,赋值时进行的是引用拷贝:

function foo() {
    var a = {name:"极客时间"}; // 栈内存存储地址,堆内存存储对象
    var b = a; // 引用式拷贝,ba指向堆中同一个对象
    a.name = "极客邦";
    console.log(a); // {name: "极客邦"}
    console.log(b); // {name: "极客邦"}
}
foo();

这里ab在栈内存中存储的是同一个地址,指向堆内存中的同一个对象,因此修改a的属性会影响b

三、JavaScript 的动态弱类型特性

JS 作为动态弱类型语言,变量的类型可以在运行过程中动态改变,不需要预先声明类型:

// JS 是动态弱类型语言
var bar; 
console.log(typeof bar); // undefined
bar = 12; // 变为number类型
console.log(typeof bar); // number
bar = '极客时间'; // 变为string类型
console.log(typeof bar); // string
bar = true; // 变为boolean类型
console.log(typeof bar); // boolean
bar = null;
console.log(typeof bar);  // Object (JS设计的bug)
bar = {name:'极客时间'}  // 变为Object类型
console.log(typeof bar); // Object

四、执行上下文与调用栈

JavaScript 引擎通过调用栈来管理执行上下文,每个函数执行时都会创建一个执行上下文,包含:

  • 变量环境
  • 词法环境
  • outer(词法作用域链)
  • this

lQLPJw1WF0yif-fNAhTNBHawwsioxubOAzAJEmTtM6BTAA_1142_532.png 执行上下文的切换通过调整调用栈的栈顶指针来实现,这也是栈内存需要高效操作的原因。

五、闭包的内存机制

闭包是指内部函数可以访问外部函数作用域中变量的特性,其实现依赖于特殊的内存管理机制。

闭包示例解析

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName() // 输出1
console.log(bar.getName()) // 输出1和"极客邦"

闭包的内存工作原理

  1. 编译阶段:JS 引擎编译foo函数时,会扫描内部函数(setNamegetName
  2. 识别闭包:发现内部函数引用了外部函数的变量(myNametest1),判断形成闭包
  3. 创建闭包对象:在堆内存中创建一个closure(foo)对象,用于保存被内部函数引用的外部变量
  4. 维持引用:当foo函数执行完毕后,虽然其执行上下文出栈,但由于bar仍引用着内部函数,而内部函数又引用着closure(foo),所以这些变量不会被回收
  5. 访问变量:内部函数通过引用closure(foo)来访问外部函数的变量

lQLPKGXzE8qWf4fNAjTNBHawul-TTHmpmmMJEm9r57TIAA_1142_564.png

六、内存管理的特点

  • JavaScript 不需要手动管理内存(不同于 C/C++ 需要使用mallocfree
  • 栈内存的回收通过调整栈指针实现,效率高
  • 堆内存的回收通过垃圾回收机制实现,当对象没有任何引用时会被回收
  • 闭包会延长变量的生命周期,可能导致内存占用增加,需要合理使用

JavaScript 内存机制深度解析:从执行上下文到闭包的内存视角

作者 wwwwW
2025年12月10日 12:21

JavaScript 内存机制深度解析:从执行上下文到闭包的内存视角

JavaScript 作为一门广泛应用于 Web 开发的动态弱类型语言,其运行机制与内存管理方式对开发者理解程序行为、优化性能以及避免内存泄漏至关重要。本文将从 JavaScript 的执行机制出发,深入剖析其内存模型——包括栈内存与堆内存的分工协作,并结合闭包这一核心特性,揭示 JavaScript 引擎(如 V8)如何高效管理内存。


一、JavaScript 是什么语言?

JavaScript 是一门动态弱类型语言。所谓“动态”,意味着变量的数据类型在运行时才确定,无需在声明时指定;而“弱类型”则表示不同类型之间可以自动转换(例如 "5" + 3 得到 "53")。这与 C/C++ 等静态强类型语言形成鲜明对比。

更重要的是,JavaScript 不需要开发者直接操作内存。在 C/C++ 中,程序员需手动调用 malloc 分配内存、free 释放内存;而在 JavaScript 中,内存的分配与回收完全由引擎自动管理,开发者只需关注逻辑本身。


二、JavaScript 的八种数据类型与内存分类

ECMAScript 标准定义了八种数据类型:

  • 七种原始类型(简单数据类型)undefinednullbooleannumberbigintstringsymbol
  • 一种引用类型(复杂数据类型)Object(包括数组、函数、日期等)

这些类型在内存中的存储方式截然不同:

  • 简单数据类型:存储在栈内存中,值直接保存,访问速度快。
  • 复杂数据类型:实际对象存储在堆内存中,栈中仅保存指向堆中对象的引用地址(指针)。

为何要这样设计?关键在于效率与灵活性的平衡


三、内存空间的划分:代码空间、栈内存与堆内存

当 JavaScript 代码从硬盘加载到内存中执行时,会涉及三种主要内存区域:

1. 代码空间

存放编译后的字节码或机器码,供引擎执行。

2. 栈内存(Stack)

  • 用于维护调用栈(Call Stack) ,是 JavaScript 执行机制的核心。
  • 特点:连续、固定大小、分配/释放极快
  • 存放内容:执行上下文(Execution Context) ,包括变量环境(Variable Environment)、词法环境(Lexical Environment)、this 绑定等。
  • 由于函数调用频繁,上下文切换必须高效。若将大对象也放入栈中,会导致栈帧过大、不连续,严重影响性能。

3. 堆内存(Heap)

  • 用于存储动态分配的对象,如对象、数组、函数等。
  • 特点:空间大、不连续、分配和回收较慢
  • 引擎通过垃圾回收机制(如标记-清除算法)自动回收不再被引用的对象。

关键设计思想:栈负责“轻量级、快速切换”的上下文状态,堆负责“重量级、长期存在”的数据存储。二者协同,既保证执行效率,又支持复杂数据结构。


四、执行上下文与作用域链

每当 JavaScript 执行一段代码(全局代码或函数),引擎会创建一个执行上下文,包含:

  • 变量环境:存放 var 声明的变量(函数提升阶段初始化)。
  • 词法环境:存放 letconst 及函数参数,具有块级作用域。
  • outer 引用:指向外层词法环境,构成词法作用域链
  • this 绑定:根据调用方式确定。

作用域链使得内部函数可以访问外部函数的变量,这是闭包的基础。


五、闭包的内存机制:为什么能“记住”外部变量?

闭包(Closure)是指内部函数引用了外部函数的变量,且该内部函数在外部函数执行完毕后仍可被调用的现象。从内存角度看,闭包的实现依赖于堆内存的特殊处理。

闭包的执行流程(以 V8 引擎为例):

  1. 编译阶段:当解析到函数 foo 时,引擎进行词法扫描,发现其内部定义了 getNamesetName 函数。
  2. 变量捕获检测:引擎检查这些内部函数是否引用了 foo 中的变量(如 myName)。若有,则判定存在闭包。
  3. 创建 Closure 对象:在堆内存中为 foo 创建一个特殊的 closure(foo) 对象,用于保存被内部函数引用的自由变量(如 myName)。
  4. 绑定引用getNamesetName 的词法环境中,outer 指向这个堆中的 closure(foo),而非原本的栈帧。
  5. 函数返回后:即使 foo 的执行上下文从调用栈弹出(栈内存回收),closure(foo) 仍因被内部函数引用而保留在堆中,直到所有引用消失才被垃圾回收。

关键点:闭包的本质是将本应随栈帧销毁的变量,提升到堆中持久化存储。这打破了“函数执行完即释放局部变量”的常规,但也带来了内存占用增加的风险。


六、为什么简单类型放栈、复杂类型放堆?

这个问题的答案源于计算机体系结构与程序执行效率的权衡:

  • 栈的优势:连续内存、指针偏移即可切换上下文,速度极快。但容量有限,不适合存储大对象。
  • 堆的必要性:对象大小不确定、生命周期长,需动态分配。虽然管理开销大,但空间几乎无限。

若将对象也放入栈中:

  • 函数调用时栈帧会变得巨大;
  • 上下文切换成本剧增;
  • 递归或深层调用极易导致栈溢出。

因此,JavaScript 引擎采用“栈存引用,堆存实体”的策略,既保持执行效率,又支持灵活的数据结构。


七、内存回收机制

  • 栈内存:随函数执行结束自动释放(通过栈顶指针回退)。

  • 堆内存:依赖垃圾回收器(GC) 。V8 使用分代式垃圾回收:

    • 新生代(小对象,频繁回收)
    • 老生代(长期存活对象,较少回收)

当一个对象没有任何变量或闭包引用它时,GC 会将其标记为可回收,并在合适时机释放内存。

注意:闭包若持有大量数据且未及时解除引用,会导致内存泄漏。例如,DOM 事件处理器中使用闭包引用外部大对象,而未在组件卸载时移除监听器。


结语

JavaScript 的内存机制是其高效运行的基石。通过栈与堆的合理分工,引擎在保证执行速度的同时,支持复杂的编程范式如闭包。理解这一机制,不仅能帮助我们写出更高效的代码,还能有效规避内存泄漏等常见问题。作为开发者,虽无需手动管理内存,但“知其所以然”方能游刃有余于现代前端工程的复杂场景之中。

正如一句老话:“你不需要成为汽车工程师才能开车,但了解引擎原理能让你开得更好。” —— 对 JavaScript 内存机制的理解,正是如此。

vue里的ref和reactive的工作原理是什么?以及它们的区别是什么?为什么有人说reactive“废弃”了?

2025年12月10日 11:51

这是一个非常硬核且高质量的问题。要理解 Vue 3 的响应式系统,必须深入到源码层面。

首先,我需要纠正一个重要的误区:Vue 3 并没有“弃用” reactive 。它仍然是 Vue 3 核心 API 的一部分。但是,确实存在一种趋势(包括 Vue 官方团队的建议),推荐优先使用 ref 。

下面我将从源码级别(基于 Vue 3 packages/reactivity 模块)为你详细剖析两者的原理、区别,以及为什么现在大家更倾向于用 ref


一、 reactive 的源码工作原理

reactive 的核心是利用了 ES6 的 Proxy 对象。它拦截对象的操作(增删改查),从而实现依赖收集和触发更新。

1. 核心流程 (reactive.ts)

当你调用 reactive(obj) 时,Vue 内部实际上执行了 createReactiveObject 函数。

简化版源码逻辑:

// 存放代理对象的缓存,防止同一个对象被代理多次
const reactiveMap = new WeakMap();

function createReactiveObject(target) {
  // 1. 如果不是对象(是基础类型),直接返回,无法代理
  if (!isObject(target)) {
    return target;
  }

  // 2. 检查缓存,如果已经代理过,直接返回缓存的 Proxy
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target);
  }

  // 3. 创建 Proxy
  const proxy = new Proxy(target, mutableHandlers);

  // 4. 存入缓存
  reactiveMap.set(target, proxy);
  
  return proxy;
}

2. 拦截器 (baseHandlers.ts)

Proxy 的威力在于第二个参数 mutableHandlers。它定义了 get(读取)和 set(修改)的拦截行为。

  • get (依赖收集) :当副作用函数(Effect,如 computed 或 render)读取属性时,触发 track 函数,将当前 Effect 记录下来。
  • set (派发更新) :当修改属性时,触发 trigger 函数,找到之前收集的 Effect 并执行它们。

简化版 Handler 逻辑:

const mutableHandlers = {
  get(target, key, receiver) {
    // 1. 收集依赖
    track(target, key);
    
    // 2. 获取原本的值
    const res = Reflect.get(target, key, receiver);

    // 3. 【深度响应关键点】如果获取到的 res 是对象,递归将其转为 reactive
    // 这与 Vue 2 不同,Vue 3 是懒代理(访问时才代理),性能更好
    if (isObject(res)) {
      return reactive(res); 
    }
    
    return res;
  },
  
  set(target, key, value, receiver) {
    // 1. 获取旧值
    const oldValue = target[key];
    // 2. 设置新值
    const result = Reflect.set(target, key, value, receiver);
    
    // 3. 如果值发生变化,触发更新
    if (hasChanged(value, oldValue)) {
      trigger(target, key);
    }
    
    return result;
  }
};


二、 ref 的源码工作原理

ref 的设计初衷是为了解决 基本数据类型(Primitives) 无法使用 Proxy 代理的问题(Proxy 只能代理对象)。

1. 核心流程 (ref.ts)

ref 本质上是一个 对象的包装器。它通过定义一个类 RefImpl,利用 ES6 的类属性访问器(getter/setter)来拦截 .value 的访问。

简化版源码逻辑:

function ref(value) {
  return createRef(value);
}

function createRef(rawValue) {
  if (isRef(rawValue)) return rawValue;
  return new RefImpl(rawValue);
}

class RefImpl {
  public _value; // 存储当前值
  public _rawValue; // 存储原始值(用于比较)
  public dep; // 依赖容器
  public __v_isRef = true; // 标记这是一个 Ref

  constructor(value) {
    this._rawValue = value;
    // 核心差异点:如果传入的是对象,内部会自动调用 reactive()!
    this._value = isObject(value) ? reactive(value) : value;
  }

  get value() {
    // 1. 收集依赖
    trackRefValue(this);
    return this._value;
  }

  set value(newVal) {
    // 2. 检查值是否变化
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      // 如果新赋的值是对象,再次转为 reactive
      this._value = isObject(newVal) ? reactive(newVal) : newVal;
      // 3. 触发更新
      triggerRefValue(this);
    }
  }
}

重点总结

  • 如果 ref(10):利用 RefImpl 的 get value 和 set value 进行拦截。
  • 如果 ref({ count: 1 })RefImpl 会将内部的 value 变成 reactive({ count: 1 }) 的 Proxy 对象。

三、 Ref 与 Reactive 的关键区别

特性 Ref Reactive
数据类型 支持所有类型(基本类型 + 对象)。 仅支持对象(Array, Object, Map, Set)。
底层原理 RefImpl 类(getter/setter)。如果是对象,内部转调 reactive 直接使用 Proxy
访问方式 必须通过 .value 访问(模板中自动解包除外)。 直接访问属性。
重新赋值 myRef.value = {} 依然保持响应式。 let state = reactive({}); state = {} 会丢失响应性
结构解构 解构会丢失响应性(需用 toRefs)。 解构会丢失响应性(需用 toRefs)。

四、 为什么说 Vue 3 “想弃用” reactive(实际上是推荐 ref)?

这是一个由 “开发者体验(DX)” 驱动的趋势。虽然 reactive 并没有被官方删除,但社区和尤雨溪(Evan You)都倾向于 “Ref 一把梭” ,主要原因如下:

1. reactive 的局限性会导致 Bug

Vue 新手最常遇到的坑就是 reactive 丢失响应性:

  • 赋值替换问题

    let list = reactive([]);
    // 错误!这样赋值会切断 Proxy 的连接,页面不会更新
    list = [1, 2, 3]; 
    
    // 正确写法(很麻烦)
    list.push(...[1, 2, 3]);
    // 或者再包一层
    const state = reactive({ list: [] });
    state.list = [1, 2, 3];
    

    而 ref 没有这个问题:list.value = [1, 2, 3] 总是安全的。

  • 解构丢失问题

    const state = reactive({ count: 0 });
    let { count } = state; // count 变成了普通数字,不再是响应式的
    

2. 类型不一致带来的心智负担

在一个组件中,如果混用两者:

  • 有些变量直接读(reactive)。
  • 有些变量要加 .value(ref)。
  • 有些对象是通过 props 传进来的,你不知道它是普通对象还是 Proxy。

使用 ref 可以统一心智模型

  • 在 JS 代码中,凡是响应式数据,都要带 .value 。这种显式调用虽然写起来多几个字符,但让代码更清晰,更容易区分哪些是普通变量,哪些是响应式变量。

3. 官方的态度

尤雨溪曾在社区讨论中表示,如果 Vue 3 重新设计,也许会只保留 ref。虽然 reactive 为了 API 的完整性被保留了,但在实际业务开发中,全量使用 ref 是目前最推荐的最佳实践。

总结

  • Reactive: 基于 Proxy,只对对象有效,存在解构和重新赋值丢失响应性的风险。
  • Ref: 基于 class 的 getter/setter,统一了基本类型和对象的处理(对象内部调用 reactive)。
  • 结论: 建议在项目中优先使用 ref 。它更加稳健,不易出错,且能够清晰地标识出“这是一个响应式数据”。

详细说说vue2何vue3的工作原理以及区别?为什么vue3会做此升级?

2025年12月10日 11:46

这是一个非常有分量的架构级问题。要讲清楚 Vue 2 到 Vue 3 的演进,我们需要从底层的响应式原理、虚拟 DOM (Virtual DOM) 的编译策略以及运行时性能三个维度深入剖析。

以下是从源码和架构设计层面的详细解读:


一、 什么是虚拟 DOM (Virtual DOM)?

在理解 Vue 原理之前,必须先理解虚拟 DOM。

1. 定义:
虚拟 DOM 本质上是一个 JavaScript 对象(VNode) ,它是真实 DOM 的“蓝图”或“替身”。

2. 为什么需要它?

  • 操作真实 DOM 代价高昂:真实 DOM 节点非常重(包含大量属性和事件)。频繁操作 DOM 会导致浏览器频繁重排(Reflow)和重绘(Repaint),性能极差。
  • JS 计算很快:在 JS 层面通过对比两个对象(新旧 VNode)的差异(Diff),计算出最小的更变操作,然后再去更新真实 DOM,效率最高。

3. 结构示例:

// 真实 DOM: <div class="box">Hello</div>
// 虚拟 DOM (VNode):
const vnode = {
  tag: 'div',
  props: { class: 'box' },
  children: 'Hello',
  // Vue3 新增了 patchFlag 等编译优化标记
}


二、 Vue 2 的工作原理

Vue 2 的核心是 Options API 和基于 Object.defineProperty 的响应式系统。

1. 响应式原理 (Reactivity)

Vue 2 在初始化(initState)时,会递归遍历 data 中的所有属性。

  • 核心 APIObject.defineProperty

  • 源码逻辑

    • Observer(观察者) :递归把对象属性转为 getter/setter。
    • Dep(依赖容器) :每个属性闭包里都有一个 Dep 实例,用来存放到到底谁用了我。
    • Watcher(订阅者) :组件渲染函数、computed、watch 都是 Watcher。
// Vue 2 响应式简化版
Object.defineProperty(obj, key, {
  get() {
    // 1. 依赖收集:如果当前有正在计算的 Watcher,就把它收集进 Dep
    if (Dep.target) dep.depend();
    return value;
  },
  set(newVal) {
    if (newVal === value) return;
    value = newVal;
    // 2. 派发更新:通知 Dep 里所有的 Watcher 去 update
    dep.notify();
  }
});

2. Vue 2 的痛点

  1. 初始化慢:因为是递归遍历,如果 data 对象很大,启动(Init)阶段会非常耗时,且内存占用高。
  2. 动态性不足:无法监听对象属性的新增(add)和删除(delete),必须用 $set / $delete
  3. 数组限制:无法拦截数组索引修改(arr[0] = 1),Vue 2 重写了数组的 7 个变异方法(push, pop...)来实现响应式。

3. 虚拟 DOM 与 Diff (Vue 2)

Vue 2 的 Diff 算法是 全量对比
当数据变化时,Vue 2 会重新生成整个组件的 VNode 树,然后和旧的 VNode 树进行对比(双端比较算法)。即使有些节点及其子节点永远不会变(静态节点),Vue 2 依然会去比对它们。


三、 Vue 3 的工作原理

Vue 3 在响应式系统和编译优化上做了彻底的重构。

1. 响应式原理 (Reactivity)

Vue 3 使用 Proxy 替代了 defineProperty。代码位于 packages/reactivity

  • 核心 APIProxy + Reflect

  • 源码逻辑

    • 不再需要 Observer 类,直接返回一个 Proxy 代理。
    • Track(依赖收集) :当读取属性时触发 track(target, key),将副作用函数(Effect)存入全局的 WeakMap
    • Trigger(派发更新) :当修改属性时触发 trigger(target, key),从 WeakMap 取出 Effect 执行。
// Vue 3 响应式简化版
new Proxy(target, {
  get(target, key, receiver) {
    track(target, key); // 收集依赖
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver);
    trigger(target, key); // 触发更新
    return res;
  }
})

  • 优势

    • 懒代理(Lazy) :只有访问到深层对象时,才会将其转为 Proxy,初始化飞快。
    • 全能拦截:支持新增、删除属性,支持数组索引修改,支持 Map/Set。

2. 编译优化 (Compiler Optimization) —— 核心升级

Vue 3 的 Diff 算法不仅仅是快,而是**“更聪明”** 。它在编译阶段(Template -> Render Function) 做了大量标记,让运行时(Runtime) 跑得更快。

  • PatchFlags (动态标记) :
    在编译时,Vue 3 会分析模板,给动态节点打上“二进制标记”。
    比如:<div :class="cls">123</div>
    Vue 3 知道只有 class 是动态的,Diff 时只对比 class ,完全忽略内容。

  • Block Tree (区块树) :
    Vue 3 将模板切分成 Block,配合 PatchFlags,Diff 时直接跳过静态节点,只遍历动态节点数组。

    • Vue 2 Diff 复杂度 = 模板总体积
    • Vue 3 Diff 复杂度 = 动态节点的数量
  • Hoist Static (静态提升) :
    静态的节点(如 <p>永远不变</p>)在内存中只创建一次,后续更新直接复用,不再重复创建 VNode。


四、 Vue 2 和 Vue 3 的比较与区别

特性 Vue 2 Vue 3
响应式底层 Object.defineProperty Proxy
检测能力 无法检测属性增删、数组索引修改 完全支持
初始化性能 递归遍历所有属性(慢、内存高) 懒代理,访问时才转换(快)
代码组织 Options API (data, methods 分离) Composition API (逻辑关注点聚合)
Diff 算法 全量双端比较,静态节点也要比 静态标记 + Block Tree,只比动态节点
TypeScript 支持较弱,类型推断困难 核心由 TS 编写,TS 支持极其友好
体积 较大,难以 Tree-shaking 模块化拆分,支持 Tree-shaking,体积更小
Fragment 组件只能有一个根节点 支持多根节点 (Fragment)

五、 为什么 Vue 3 要做这些升级?

尤雨溪和团队进行 Vue 3 重构主要为了解决 Vue 2 的三个核心瓶颈:

1. 性能瓶颈 (Performance)

Vue 2 的响应式初始化是递归的,对于大数据量的表格或列表,启动非常慢。且 Diff 算法在大型复杂组件中,无谓的静态节点对比消耗了大量 CPU。
Vue 3 通过 Proxy 和编译优化(静态标记),实现了“按需响应”和“靶向更新”,性能大幅提升。

2. 代码组织与复用瓶颈 (Scalability)

在 Vue 2 的 Options API 中,一个功能的逻辑被拆分到 datamethodswatch 里。当组件变得巨大(几千行代码)时,维护代码需要在文件里上下反复横跳(Jumping)。且 Mixin 代码复用存在命名冲突和来源不清晰的问题。
Vue 3 引入 Composition API (组合式 API) ,允许开发者按“逻辑功能”组织代码,完美解决了大型项目的维护难题,Hooks 更是取代了 Mixin。

3. TypeScript 支持 (Developer Experience)

Vue 2 的源码是 JS 写的,通过 Flow 做类型检查,对 TS 的支持是后期补丁(this 指向在 TS 中很难推断)。随着前端工程化对 TS 需求的爆发,Vue 2 显得力不从心。
Vue 3 使用 TypeScript 重写,提供了原生的、极佳的类型推断体验。

总结

  • 原理层面:Vue 2 是劫持 setter/getter,Vue 3 是代理整个对象。
  • 更新机制:Vue 2 是全量树对比,Vue 3 是基于静态标记的动态节点追踪。
  • 目的:Vue 3 的升级是为了更快(性能)、更小(体积)、更易维护(组合式 API)以及更好的 TS 支持

从 nvm 到 fnm:一个前端老兵的版本管理工具迁移实录

2025年12月10日 11:54

从 nvm 到 fnm:一个前端老兵的版本管理工具迁移实录

引子:那个让我崩溃的早晨

某个周一早晨,我像往常一样打开终端准备开始工作。

# 打开 iTerm2... 等待... 等待...
# 终于出现命令提示符,耗时 2.3 秒

接着切换到一个需要 Node 16 的老项目:

$ nvm use 16
Now using node v16.20.2 (npm v8.19.4)
# 又是 0.5 秒过去了

然后跳到另一个 Node 20 的新项目:

$ cd ../new-project
$ nvm use 20
Now using node v20.19.6 (npm v10.8.2)
# 再等 0.5 秒

一天下来,在 5-6 个项目间来回切换,光是等 nvm 响应就浪费了不知道多少时间。更别提有时候忘记 nvm use,直接 npm install 导致 node_modules 版本混乱的惨剧了。

是时候做出改变了。

为什么要离开 nvm?

痛点一:Shell 启动慢得令人发指

nvm 是纯 Bash 脚本实现的。每次打开新终端,它都要:

  1. 加载 nvm.sh 脚本(几千行)
  2. 解析已安装的 Node 版本
  3. 设置环境变量
  4. 初始化自动补全

我用 time 测了一下我的 .zshrc 加载时间:

# 有 nvm
$ time zsh -i -c exit
zsh -i -c exit  0.42s user 0.23s system 95% cpu 0.678 total

# 注释掉 nvm 后
$ time zsh -i -c exit
zsh -i -c exit  0.08s user 0.05s system 92% cpu 0.142 total

nvm 让我的终端启动慢了近 5 倍!

痛点二:版本切换不够智能

每次进入不同项目都要手动 nvm use,太原始了。虽然有 avnnvm-auto 等插件可以实现自动切换,但:

  • 又增加了一层依赖
  • 又拖慢了 Shell 速度
  • 配置起来也挺麻烦

痛点三:Windows 支持是个笑话

nvm 官方根本不支持 Windows。nvm-windows 是另一个独立项目,命令和行为都有差异。团队里用 Windows 的同事经常遇到各种奇怪问题。

痛点四:偶发的诡异 Bug

用了几年 nvm,遇到过各种奇怪问题:

  • 全局安装的包莫名消失
  • npm prefix 路径错乱
  • 多个终端窗口版本不同步
  • .nvmrc 有时候不生效

遇见 fnm:Rust 带来的性能革命

fnm(Fast Node Manager)是用 Rust 写的 Node.js 版本管理器。第一次用的时候,我的反应是:

"就这?结束了?怎么这么快?"

性能实测对比

我在自己的 M1 MacBook Pro 上做了详细测试:

终端启动时间
# nvm
$ time zsh -i -c exit
0.678 total

# fnm
$ time zsh -i -c exit
0.089 total

# 提升:7.6 倍
版本切换时间
# nvm
$ time nvm use 20
Now using node v20.19.6
real    0m0.347s

# fnm
$ time fnm use 20
Using Node v20.19.6
real    0m0.012s

# 提升:29 倍
安装新版本
# nvm install 22
# 总耗时约 45 秒(包含下载)

# fnm install 22
# 总耗时约 38 秒(包含下载)

# 下载速度差不多,但 fnm 的解压和配置更快

为什么 fnm 这么快?

  1. 原生二进制:Rust 编译成机器码,不需要解释器
  2. 并行处理:充分利用多核 CPU
  3. 惰性加载:只在需要时才读取版本信息
  4. 高效的文件操作:Rust 的 I/O 性能本就出色

真实场景:fnm 如何改变我的工作流

场景一:多项目并行开发

我同时维护着这些项目:

项目 Node 版本 原因
老后台系统 14 历史包袱,依赖不支持高版本
主站前端 18 稳定的 LTS
新管理后台 20 需要新特性
实验性项目 22 尝鲜最新 API
Electron 应用 18 Electron 版本限制

以前用 nvm:

$ cd legacy-admin
$ nvm use  # 等待...
Found '/Users/me/legacy-admin/.nvmrc' with version <14>
Now using node v14.21.3

$ cd ../main-site
$ nvm use  # 又等待...
Found '/Users/me/main-site/.nvmrc' with version <18>
Now using node v18.20.8

# 经常忘记 nvm use,然后...
$ npm install
# 装了一堆错误版本的依赖 💥

现在用 fnm:

$ cd legacy-admin
Using Node v14.21.3  # 自动切换,瞬间完成

$ cd ../main-site
Using Node v20.19.6  # 无感切换

# 永远不会忘记切换版本,因为是自动的

场景二:CI/CD 环境统一

我们团队的 CI 配置以前是这样的:

# .gitlab-ci.yml (使用 nvm)
before_script:
  - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
  - export NVM_DIR="$HOME/.nvm"
  - . "$NVM_DIR/nvm.sh"
  - nvm install
  - nvm use

换成 fnm 后:

# .gitlab-ci.yml (使用 fnm)
before_script:
  - curl -fsSL https://fnm.vercel.app/install | bash
  - eval "$(fnm env)"
  - fnm install
  - fnm use

CI 构建时间减少了约 15 秒(主要是 nvm 初始化太慢)。

场景三:团队协作与跨平台

我们团队成员使用的系统:

  • 60% macOS
  • 30% Windows
  • 10% Linux

nvm 时代的痛苦:

# macOS/Linux 同事
$ nvm use 20

# Windows 同事(nvm-windows)
$ nvm use 20.19.6  # 必须写完整版本号!
# 而且 .nvmrc 经常不生效

fnm 时代的统一:

# 所有平台,相同命令,相同行为
$ fnm use 20

Windows 同事终于不用单独维护一套文档了。

场景四:Docker 开发环境

在 Dockerfile 中安装 Node:

# 以前用 nvm(不推荐,但有人这么干)
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \
    && . ~/.nvm/nvm.sh \
    && nvm install 20 \
    && nvm alias default 20
# 镜像体积大,层数多

# 现在用 fnm
RUN curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir /usr/local/bin \
    && fnm install 20 \
    && fnm default 20
# 更简洁,体积更小

场景五:Monorepo 中的多版本需求

我们有一个 Monorepo,不同 package 需要不同 Node 版本:

monorepo/
├── packages/
│   ├── legacy-sdk/        # 需要 Node 14(兼容老用户)
│   │   └── .node-version  # 14
│   ├── web-app/           # 需要 Node 20
│   │   └── .node-version  # 20
│   └── cli-tool/          # 需要 Node 18
│       └── .node-version  # 18
└── .node-version          # 20(默认)

fnm 的 --use-on-cd 让我在不同 package 间跳转时完全无感:

$ cd packages/legacy-sdk
Using Node v14.21.3

$ cd ../web-app
Using Node v20.19.6

$ cd ../cli-tool
Using Node v18.20.8

完整迁移指南

第一步:安装 fnm

macOS (Homebrew):

brew install fnm

Windows (Scoop):

scoop install fnm

Windows (Chocolatey):

choco install fnm

Linux/macOS (curl):

curl -fsSL https://fnm.vercel.app/install | bash

Cargo (Rust 用户):

cargo install fnm

第二步:配置 Shell

这是最关键的一步,配置正确才能享受自动切换的便利。

Zsh (~/.zshrc):

# fnm - Fast Node Manager
eval "$(fnm env --use-on-cd --shell zsh)"

Bash (~/.bashrc):

# fnm - Fast Node Manager
eval "$(fnm env --use-on-cd --shell bash)"

Fish (~/.config/fish/config.fish):

# fnm - Fast Node Manager
fnm env --use-on-cd --shell fish | source

PowerShell ($PROFILE):

# fnm - Fast Node Manager
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression

参数说明:

参数 作用
--use-on-cd 进入目录时自动读取 .node-version.nvmrc 并切换
--shell <shell> 指定 Shell 类型,生成对应的环境变量设置命令
--version-file-strategy recursive 向上递归查找版本文件(可选)
--corepack-enabled 自动启用 Corepack(可选)

第三步:迁移已安装的 Node 版本

查看 nvm 安装了哪些版本:

ls ~/.nvm/versions/node/
# v10.24.1 v14.21.3 v16.20.2 v18.20.8 v20.19.6 v22.21.0

在 fnm 中安装对应版本:

# 方式一:通过 LTS 代号安装(推荐,自动获取最新补丁版本)
fnm install lts/fermium   # v14.x
fnm install lts/gallium   # v16.x
fnm install lts/hydrogen  # v18.x
fnm install lts/iron      # v20.x
fnm install lts/jod       # v22.x

# 方式二:通过大版本号安装
fnm install 14
fnm install 16
fnm install 18
fnm install 20
fnm install 22

# 方式三:安装精确版本
fnm install 18.20.8
fnm install 20.19.6

设置默认版本:

fnm default 22

第四步:处理全局 npm 包

这是很多人忽略的一步!nvm 下安装的全局包不会自动迁移。

查看 nvm 中的全局包:

# 切换到 nvm 的某个版本
export PATH="$HOME/.nvm/versions/node/v20.19.6/bin:$PATH"
npm list -g --depth=0

在 fnm 的对应版本中重新安装:

fnm use 20
npm install -g typescript ts-node nodemon pm2 # 你需要的包

Pro Tip: 可以写个脚本批量处理:

#!/bin/bash
# migrate-global-packages.sh

# 你常用的全局包
PACKAGES="typescript ts-node nodemon pm2 pnpm yarn"

for version in 18 20 22; do
  echo "Installing global packages for Node $version..."
  fnm use $version
  npm install -g $PACKAGES
done

第五步:注释掉 nvm 配置

编辑你的 Shell 配置文件:

# ~/.zshrc 或 ~/.bashrc

# nvm - 已迁移到 fnm,注释掉避免冲突
# export NVM_DIR="$HOME/.nvm"
# [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

第六步:验证迁移结果

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

# 验证 fnm 工作正常
fnm list
fnm current
node -v
npm -v
which node

# 测试自动切换
cd /path/to/project-with-nvmrc
# 应该看到 "Using Node vX.X.X"
node -v

第七步:删除 nvm(可选但推荐)

确认一切正常后,可以删除 nvm 释放磁盘空间:

rm -rf ~/.nvm

# 如果遇到权限问题
sudo rm -rf ~/.nvm

命令速查表

功能 nvm 命令 fnm 命令
安装指定版本 nvm install 20 fnm install 20
安装最新 LTS nvm install --lts fnm install --lts
安装指定 LTS nvm install --lts=iron fnm install lts/iron
切换版本 nvm use 20 fnm use 20
设置默认版本 nvm alias default 20 fnm default 20
查看已安装版本 nvm ls fnm list
查看远程可用版本 nvm ls-remote fnm list-remote
查看当前版本 nvm current fnm current
卸载版本 nvm uninstall 18 fnm uninstall 18
在指定版本执行命令 nvm exec 18 node -v fnm exec --using=18 node -v

LTS 版本代号参考

代号 版本 发布日期 LTS 开始 维护结束 状态
Jod v22.x 2024-04 2024-10 2027-04 ✅ Active LTS
Iron v20.x 2023-04 2023-10 2026-04 ✅ Active LTS
Hydrogen v18.x 2022-04 2022-10 2025-04 ⚠️ Maintenance
Gallium v16.x 2021-04 2021-10 2024-09 ❌ End-of-Life
Fermium v14.x 2020-04 2020-10 2023-04 ❌ End-of-Life

建议:新项目使用 v20 或 v22,老项目尽快升级到至少 v18。

进阶技巧

1. 配置版本文件查找策略

默认情况下,fnm 只在当前目录查找 .node-version.nvmrc。如果你的项目结构比较深,可以启用递归查找:

eval "$(fnm env --use-on-cd --version-file-strategy recursive)"

2. 启用 Corepack

Corepack 是 Node.js 内置的包管理器版本管理工具,可以锁定 pnpm/yarn 版本:

eval "$(fnm env --use-on-cd --corepack-enabled)"

3. 自定义安装目录

默认安装在 ~/.local/share/fnm,可以自定义:

export FNM_DIR="/path/to/custom/fnm"
eval "$(fnm env --use-on-cd)"

4. 使用国内镜像加速

# 临时使用
fnm install 20 --node-dist-mirror=https://npmmirror.com/mirrors/node

# 永久配置
export FNM_NODE_DIST_MIRROR="https://npmmirror.com/mirrors/node"

5. 在脚本中使用 fnm

#!/bin/bash
# 确保 fnm 环境已加载
eval "$(fnm env)"

# 使用指定版本执行
fnm use 20
node your-script.js

# 或者用 exec
fnm exec --using=20 node your-script.js

常见问题 FAQ

Q: fnm 安装的 Node 在哪里?

~/.local/share/fnm/node-versions/

每个版本是一个独立目录,结构清晰。

Q: 为什么 which node 显示的路径很奇怪?

fnm 使用"多 Shell"机制,每个 Shell 会话有独立的 PATH:

$ which node
/Users/xxx/.local/state/fnm_multishells/12345_1234567890/bin/node

这是正常的,确保了不同终端窗口可以使用不同版本。

Q: 如何在 VS Code 中使用 fnm 管理的 Node?

VS Code 会自动检测 fnm。如果遇到问题,可以在 settings.json 中配置:

{
  "terminal.integrated.env.osx": {
    "PATH": "${env:PATH}"
  }
}

或者在项目根目录创建 .vscode/settings.json

{
  "eslint.runtime": "node"
}

Q: fnm 支持 .nvmrc 吗?

完全支持!fnm 会按以下顺序查找版本文件:

  1. .node-version
  2. .nvmrc
  3. package.jsonengines.node 字段

Q: 如何回退到 nvm?

如果你想回退(虽然我不建议),只需:

  1. 注释掉 fnm 配置
  2. 取消注释 nvm 配置
  3. 重新加载 Shell

你的 nvm 数据(如果没删)还在 ~/.nvm

总结:值得迁移吗?

绝对值得。

迁移成本:

  • 时间:约 15-30 分钟
  • 学习曲线:几乎为零(命令高度相似)
  • 风险:极低(可随时回退)

获得收益:

  • 终端启动快 5-10 倍
  • 版本切换快 20-30 倍
  • 自动切换版本,告别手动 nvm use
  • 跨平台一致性,团队协作更顺畅
  • 更少的 Bug 和怪异行为

如果你每天要打开几十次终端、在多个项目间切换,fnm 节省的时间累积起来是非常可观的。更重要的是,那种丝滑无感的体验,会让你的开发心情都变好。


参考资料:


如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流!

JavaScript call、apply、bind 方法解析

2025年12月10日 11:42

JavaScript call、apply、bind 方法解析

在 JavaScript 中,callapplybind 都是用来**this** 改变函数执行时 指向 的核心方法,它们的核心目标一致,但使用方式、执行时机和传参形式有明显区别。

const dog = {
  name: "旺财",
  sayName() {
    console.log(this.name);
  },
  eat(food) {
    console.log(`${this.name} 在吃${food}`);
  },
  eats(food1, food2) {
    console.log(`${this.name} 在吃${food1}${food2}`);
  },
};

const cat = {
  name: "咪咪",
};
// call 会立即执行函数,并且改变 this 指向
dog.sayName.call(cat); // 输出 '咪咪'
dog.eat.call(cat, "🐟"); // 输出 '咪咪 在吃🐟'

dog.sayName.apply(cat); // 输出 '咪咪'

dog.eats.call(cat, "🐟", "🐔"); // 输出 '咪咪 在吃🐟和🐔'

dog.eats.apply(cat, ["🐟", "🐔"]); // 输出 '咪咪 在吃🐟和🐔'

const boundEats = dog.eats.bind(cat);
boundEats("🐟", "🐔"); // 输出 '咪咪 在吃🐟和🐔'

一、核心共性

三者的核心作用:this 手动指定函数执行时的 指向,突破函数默认的 this 绑定规则(比如对象方法的 this 原本指向对象本身,通过这三个方法可以强制指向其他对象)。

以示例中的 dog.sayName() 为例,默认执行时 this 指向 dog,但通过 call/apply/bind 可以让 this 指向 cat,从而输出 咪咪 而非 旺财

二、逐个解析

1. call

  • 执行时机立即执行 函数

  • 传参方式:第一个参数是 this 要指向的目标对象,后续参数逐个单独传递(逗号分隔)

  • 语法函数.call(thisArg, arg1, arg2, ...)

示例解析:
// this 指向 cat,无额外参数,立即执行 sayName
dog.sayName.call(cat); // 输出 '咪咪'

// this 指向 cat,额外参数 '🐟' 逐个传递,立即执行 eat
dog.eat.call(cat, "🐟"); // 输出 '咪咪 在吃🐟'

// 多参数场景:参数逐个传递,立即执行 eats
dog.eats.call(cat, "🐟", "🐔"); // 输出 '咪咪 在吃🐟和🐔'

2. apply

  • 执行时机立即执行 函数(和 call 一致)

  • 传参方式:第一个参数是 this 要指向的目标对象,后续参数必须放在一个数组(或类数组)中传递

  • 语法函数.apply(thisArg, [arg1, arg2, ...])

示例解析:
// 无额外参数,数组可以为空(或不传),立即执行 sayName
dog.sayName.apply(cat); // 输出 '咪咪'

// 多参数场景:参数放在数组中传递,立即执行 eats
dog.eats.apply(cat, ["🐟", "🐔"]); // 输出 '咪咪 在吃🐟和🐔'

注意:apply 适合参数数量不固定、或参数已存在于数组中的场景(比如 Math.max.apply(null, [1,2,3]) 求数组最大值)。

3. bind

  • 执行时机不立即执行 函数,而是返回一个绑定了新 this 指向的新函数,后续需要手动调用这个新函数才会执行

  • 传参方式:第一个参数是 this 要指向的目标对象,后续参数可以提前绑定(柯里化),也可以在调用新函数时补充

  • 语法const 新函数 = 函数.bind(thisArg, arg1, arg2, ...); 新函数(剩余参数);

示例解析:
// 第一步:bind 不执行,仅绑定 this 为 cat,返回新函数 boundEats(原变量名 boundSayName 已修改)
const boundEats = dog.eats.bind(cat);

// 第二步:手动调用新函数,传递参数 '🐟' 和 '🐔',此时才执行 eats
boundEats("🐟", "🐔"); // 输出 '咪咪 在吃🐟和🐔'
进阶用法:
// 提前绑定部分参数(柯里化),this 仍指向 cat
const boundEatWithFish = dog.eats.bind(cat, "🐟");
// 调用时补充剩余参数,同样输出目标结果
boundEatWithFish("🐔"); // 输出 '咪咪 在吃🐟和🐔'

三、核心区别总结

特性 call apply bind
执行时机 立即执行 立即执行 不立即执行,返回新函数
传参形式 逐个传递(逗号分隔) 数组/类数组传递 可提前绑定,也可调用时传
返回值 函数执行结果 函数执行结果 绑定 this 后的新函数

四、常见使用场景

  1. call:适用于参数数量明确、需要立即执行的场景(比如继承:Parent.call(this, arg1));

  2. apply:适用于参数是数组/类数组的场景(比如求数组最大值:Math.max.apply(null, arr));

  3. bind:适用于需要延迟执行、或需要重复使用绑定 this 后的函数的场景(比如事件回调、定时器:btn.onclick = fn.bind(obj))。

五、补充注意点

  • 如果第一个参数传 null/undefined,在非严格模式下,this 会指向全局对象(浏览器中是 window,Node 中是 global);严格模式下 thisnull/undefined

  • bind 返回的新函数不能通过 call/apply 再次修改 this 指向(bind 的绑定是永久的)。

用户说卡但我测不出来?RUM 监控:直接去 “用户手机里” 抓薛定谔的 Bug

2025年12月10日 11:36

⚡️ 告别“薛定谔的 Bug”:RUM 如何精准捕获线上卡顿,让性能优化不再靠玄学?

前端性能优化专栏 - 第二篇

在性能优化的路上,我们总会遇到一个让人抓狂的“灵异事件”:用户反馈页面卡得像幻灯片,但你在本地、在公司、在高速网络下测试,它却流畅得像德芙巧克力。

我们把这种现象戏称为“薛定谔的 Bug”——你打开看的时候,它就消失了。那么,如何才能打破这个魔咒,让性能优化从“玄学”变成“科学”呢?答案就是:RUM(真实用户监控)

⚠️ “薛定谔的 Bug”:线上卡顿但无法复现

想象一下这个场景:

  1. 用户反馈: “你们的页面太卡了,点个按钮要等半天!”
  2. 你测试: 刷新、点击、滚动,一切丝滑流畅,耗时不到 100ms。
  3. 你的内心: “是不是用户手机太烂了?”

这种线上卡顿,本地流畅的差异,往往让开发者陷入深深的自我怀疑。问题根源可能隐藏在资源加载、渲染或复杂的交互延迟中,而这些问题,在你的“完美”开发环境中根本无从察觉。

✨ 环境差异的根源:性能的“黑洞”

为什么你的环境是“天堂”,用户的环境却是“地狱”?因为你们的环境差异太大了!

差异维度 你的环境(开发/测试) 用户的环境(真实线上) 性能影响
网络条件 稳定 Wi-Fi / 专线 3G/4G 切换、地铁弱信号 资源加载耗时、TTFB
设备性能 高配笔电、旗舰手机 低端手机、老旧平板 JS 执行速度、渲染速度
浏览器版本 最新 Chrome/Safari 各种版本、不同内核 API 支持、渲染机制差异
地理位置 靠近服务器的 CDN 节点 偏远地区的 CDN 节点 首字节时间(TTFB)

与其在本地一遍遍地尝试“复现问题”,不如换个思路:直接去用户的“案发现场”收集证据! 这正是 RUM(Real User Monitoring) 的核心思想。

环境差异对比图

🔧 什么是 RUM(真实用户监控)?

RUM,全称 Real User Monitoring,顾名思义,就是一套采集真实用户访问数据的监控体系。

它就像一个潜伏在用户浏览器中的“性能侦探”,默默地捕获并回传用户的性能指标、环境信息和异常日志

专业名词解释:RUM 是一种被动式的性能监控方法,它通过在用户浏览器中植入一段 JavaScript 代码,来实时收集用户在页面上的各种性能数据和行为数据,并将数据上报到服务器进行分析。

RUM 的目标非常明确: 帮助开发者还原现场、定位瓶颈、验证优化效果。它将“用户说卡”这个模糊的定性描述,转化为可量化的数据指标。

🚀 RUM 的核心组成:三大法宝

一个完整的 RUM 体系,通常由以下三个核心部分组成:

1. 性能数据采集:量化“卡顿”的体感

“卡顿”是一种主观感受,但 RUM 能用客观指标来量化它。我们主要关注 Google 推荐的 Core Web Vitals(核心 Web 指标)以及其他关键指标:

指标名称 英文缩写 衡量目标 对应“卡顿”体感
最大内容绘制 LCP 页面加载速度(最大元素出现时间) “页面白屏很久”
首次输入延迟 FID 页面交互响应速度(首次点击到响应) “点按钮没反应”
累积布局偏移 CLS 页面视觉稳定性 “页面元素乱跳”
首次绘制 FP/FCP 页面开始渲染的时间 “页面开始有东西了”
首字节时间 TTFB 服务器响应速度 “网络慢不慢”

技术实现:PerformanceObserver API

现代浏览器提供了强大的 PerformanceObserver API,让我们能够实时监听这些关键性能指标的变化,并将其上报。

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    // 收集 LCP, FID, CLS 等数据
    console.log(entry.name, entry.startTime, entry.duration)
    // report(entry) // 上报到 RUM 服务器
  })
})

// 监听关键指标
observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] })

2. 异常日志收集:捕获“意外”现场

性能问题不只是慢,还包括“崩”。RUM 体系必须能捕获各种意料之外的错误,防止用户体验彻底中断。

  • JS 执行错误: 通过 window.onerror 捕获未被 try...catch 的同步错误。
  • 资源加载失败: 监听全局的 error 事件,捕获图片、CSS、JS 等资源加载失败的情况。
  • Promise 未处理异常: 通过 unhandledrejection 捕获 Promise 链中没有 catch 的错误。
// 捕获 JS 错误
window.addEventListener('error', e => {
  report({ type: 'js-error', message: e.message })
})

// 捕获 Promise 异常
window.addEventListener('unhandledrejection', e => {
  report({ type: 'promise-rejection', reason: e.reason })
})

3. 环境信息:还原用户的“案发现场”

光有性能数据还不够,我们还需要知道是谁、在哪里、用什么遇到了问题。这些环境信息是数据聚合分析的关键,能帮助我们快速定位共性问题(比如“所有华为手机用户都卡顿”)。

  • 🖥️ 设备信息: 设备型号、内存、屏幕分辨率。
  • ⏱️ 操作系统: 操作系统类型与版本(如 iOS 17.0, Android 14)。
  • 🌐 浏览器信息: 浏览器类型与版本(如 Chrome 120, Safari 17)。
  • 📊 网络类型: 用户的网络连接类型(如 4G, Wi-Fi)。
  • 🗺️ 地理位置: 用户的国家/城市,用于分析地域性 CDN 差异。

RUM 数据流示意图

💡 总结:让数据成为你最可靠的依据

“薛定谔的 Bug”并不可怕,可怕的是我们没有工具去揭开它的面纱。

无法复现 ≠ 无法解决。

RUM 体系将性能优化从“凭感觉”和“靠运气”的阶段,带入了数据驱动的科学时代。建立一个系统性的 RUM 体系,让每一次“卡顿反馈”都能被精准捕获、分析和优化。

性能优化是一场持久战,而 RUM 就是我们最可靠的雷达。


下一篇预告: 既然我们已经了解了 RUM 的重要性,那么下一篇我们将深入讲解 RUM 的核心采集利器——PerformanceObserver API,手把手教你如何用它来精准监控页面性能指标。敬请期待!

JavaScript 内存机制与闭包:从栈到堆的深入浅出

作者 ohyeah
2025年12月10日 11:35

在前端开发中,JavaScript 是我们最常用的编程语言之一。然而,很多人在学习 JS 的过程中,常常忽略了它背后运行的底层机制——内存管理。今天我们就结合几段代码和图示,来聊聊 JavaScript 中最重要的两个概念:内存机制闭包


一、JS 的执行环境:三大内存空间

在 JavaScript 执行过程中,程序会使用三种主要的内存空间:

  1. 代码空间
  2. 栈内存(Stack)
  3. 堆内存(Heap)

lQLPJxDjlfllrWfNBJ_NBHawALUXmftxe5cJEmLLH90MAA_1142_1183.png

图1:JavaScript 的三大内存空间

1. 代码空间

这是存放源代码的地方。当浏览器加载 HTML 文件时,会把 <script> 标签中的代码从硬盘读取到内存中,形成“代码空间”。这部分内容不会直接参与运行,而是供引擎解析和编译用。

2. 栈内存

栈内存是 JS 执行的主角,用于维护函数调用过程中的执行上下文。它的特点是:

  • 空间小、速度快
  • 连续存储,便于快速切换
  • 每次函数调用都会创建一个执行上下文并压入栈顶
function foo() {
  var a = 1;
  var b = a;
  a = 2;
  console.log(a); // 2
  console.log(b); // 1
}
foo();

这段代码执行时,foo() 被调用,会生成一个新的执行上下文,并压入调用栈。这个上下文包含变量 ab,它们都是简单数据类型,直接存储在栈中。

3. 堆内存

堆内存用来存储复杂数据类型,比如对象、数组等。这些数据体积大、结构复杂,并且是动态的,不能放在栈里,所以被分配到堆中。 它的特点是:

  • '辅助栈内存'
  • 空间大 不连续
  • 存储复杂数据类型 对象
function foo() {
  var a = { name: '极客时间' };
  var b = a; // 引用拷贝
  a.name = '极客邦';
  console.log(a); // {name: "极客邦"}
  console.log(b); // {name: "极客邦"}
}
foo();

这里 a 是一个对象,它实际存储在堆内存中,而 a 变量只是保存了该对象的地址(引用)。b = a 并不是复制对象,而是让 b 也指向同一个地址。因此修改 a.name 后,b 也能看到变化。

为什么堆内存是不连续的?

  • 因为对象是动态的 可以去给它添加属性或方法 如果是连续的情况下 显然就不好进行操作了

二、简单 vs 复杂 数据类型:存储方式不同

JavaScript 有八种原始数据类型:

undefined, null, boolean, string, number, symbol, bigint, object

其中前七种是简单数据类型,最后一个是复杂数据类型

类型 存储位置
简单类型(如 number, string) 栈内存
复杂类型(如 object, array) 堆内存

示例说明

var a = '极客时间'; // 字符串 → 栈内存
var b = a;         // 拷贝值 → b 也是 '极客时间'
a = '极客邦';      // 修改 a 不影响 b
console.log(a); // 极客邦
console.log(b); // 极客时间
var c = { name: '极客时间' }; // 对象 → 堆内存
var d = c;                  // 引用拷贝 → d 指向同一地址
c.name = '极客邦';          // 修改共享对象
console.log(c.name); // 极客邦
console.log(d.name); // 极客邦

✅ 结论:

  • 简单类型是“值传递”,每个变量独立存在
  • 复杂类型是“引用传递”,多个变量可能指向同一块堆内存

lQLPJxNqPKGZwofNAifNBHawc4pej93FtxkJEmmk8M9qAA_1142_551.png

图2:变量 c 存储的是堆内存中对象的地址(1003)


三、执行上下文与调用栈

JavaScript 的执行流程依赖于调用栈(Call Stack),它是函数调用的“记录本”。

每当一个函数被执行,就会创建一个执行上下文(Execution Context),包括:

  • 变量环境(Variable Environment)
  • 词法环境(Lexical Environment)
  • outer(外层作用域链)

执行上下文会被压入调用栈顶部。当函数执行完毕后,该上下文就会被弹出并回收。

示例:函数调用前后

function foo() {
  var a = 1;
  var b = 2;
}

foo(); // 调用 foo

执行过程如下:

  1. 创建全局执行上下文(已存在)
  2. 调用 foo(),创建新的执行上下文,压入调用栈
  3. 执行完 foo(),将其执行上下文弹出,指针回到全局上下文

lQLPJw1WF0yif-fNAhTNBHawwsioxubOAzAJEmTtM6BTAA_1142_532.png

图3:foo() 执行前后调用栈的变化

注意:栈顶指针移动非常快,因为栈是连续内存,只需要改变指针位置即可完成上下文切换(栈顶指针的切换通过一个机制,做内存的减法)。如果将复杂对象也放在栈中,会导致栈空间过大、不连续,严重影响性能。


四、动态弱类型语言:JS 的灵活性

JavaScript 是一种动态弱类型语言,这意味着:

  • 动态:变量可以在运行时改变类型
  • 弱类型:不同类型之间可以自动转换
var bar;
console.log(typeof bar); // undefined

bar = 12;
console.log(typeof bar); // number

bar = '极客时间';
console.log(typeof bar); // string

bar = true;
console.log(typeof bar); // boolean

bar = null;
console.log(typeof bar); // object(JS 设计缺陷)

bar = { name: '极客时间' };
console.log(typeof bar); // object

这正是 JS 的魅力所在:无需提前声明类型,灵活自由。但这也带来了潜在问题,比如 typeof null === 'object' 就是一个经典 bug。


五、闭包的本质:延长变量生命周期

现在我们来看一个关键概念——闭包

什么是闭包?

闭包是指:内部函数能够访问外部函数的变量,即使外部函数已经执行完毕。

<script>
function foo() {
  var myName = "极客时间";
  let test1 = 1;
  const test2 = 2;

  var innerBar = {
    setName: function (newName) {
      myName = newName;
    },
    getName: function () {
      console.log(test1);
      return myName;
    }
  };

  return innerBar;
}

var bar = foo();
bar.setName("极客邦");
bar.getName(); // 输出:1, 极客邦
</script>

在这个例子中,innerBar 返回了一个对象,其方法 setNamegetName 都能访问 myNametest1。但 foo() 已经执行完了,按理说这些变量应该被销毁了,为什么还能访问?

这就是闭包的作用!


六、闭包背后的内存机制

闭包是如何工作的?答案就在堆内存中。

步骤解析:

  1. foo() 被调用时,V8 引擎会扫描内部函数 setNamegetName
  2. 发现这两个函数引用了外部变量 myNametest1
  3. 引擎判断:这是一个闭包!需要保留这些变量
  4. 堆内存中创建一个特殊的对象:closure(foo)
  5. myNametest1 的值保存到这个对象中
  6. closure(foo) 的地址存入栈中(作为 innerBar 的一部分)

lQLPJxNqPKGZwofNAifNBHawc4pej93FtxkJEmmk8M9qAA_1142_551.png

图4:闭包 closure(foo) 存储在堆内存中,栈中只保存地址

关键点总结:

  • 闭包本质:通过堆内存延长了外部变量的生命周期
  • 栈中保存的是地址,堆中保存的是真实数据
  • 执行上下文出栈 ≠ 变量消失,只要还有引用,就继续存活

七、为什么 JS 不需要手动管理内存?

像 C/C++ 这样的语言,开发者必须使用 mallocfree 来手动分配和释放内存:

int main(){
  int a = 1;
  char* b = '极客时间';
  bool c = true;

  c = a;
  return 0;
}

而在 JavaScript 中,你完全不需要关心这些。V8 引擎自动完成内存分配和垃圾回收

  • 栈内存:执行上下文结束 → 自动弹出 → 快速回收
  • 堆内存:没有变量引用的对象 → 垃圾回收器(GC)定期清理

✅ 所以 JS 开发者可以专注于业务逻辑,而不必担心内存泄漏(虽然也要注意,比如事件监听器未解绑可能导致内存泄漏)


八、总结:JS 内存机制核心要点

内容 说明
栈内存 调用栈在其内部,存放执行上下文、简单数据类型,速度快,连续
堆内存 存放复杂对象,空间大,不连续
变量提升 编译阶段为变量预留空间,初始值为 undefined
引用传递 对象赋值是地址拷贝,多个变量共享同一对象
闭包 内部函数引用外部变量 → 堆内存保存自由变量 → 延长生命周期
栈顶指针 函数调用时移动,上下文切换高效

九、写在最后

理解 JavaScript 的内存机制,不仅能帮助我们写出更高效的代码,还能更好地掌握闭包、作用域、垃圾回收等高级概念。虽然我们不需要像 C++ 那样手动操作内存,但了解背后的原理,会让你的代码更加稳健。

💡 提醒:不要滥用闭包,因为它会让变量长期驻留内存,增加 GC 压力。合理使用,才是高手之道。


📌 如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!关注我,带你深入浅出地学透前端技术!

📸 图片来源:本文所有图片均为原创示意图,用于辅助理解 JS 内存模型。

Node项目部署到阿里云服务器

作者 南游
2025年12月10日 11:19

一、服务器基础准备

1. 服务器配置

  • 购买云服务器(阿里云)
  • 选择适合的操作系统(CentOS 7.9)
  • 开放安全组端口:22(SSH)、80(HTTP)、443(HTTPS)、你的应用端口(如3000)

2. 本地项目准备

  • 确保项目在本地运行正常
  • 清理 node_modules 和测试文件

二、环境安装与配置

1. 安装Node.js

在Linux上部署Node.js,本文选择使用NVM(Node Version Manager)。与包管理器安装相比,NVM不受系统仓库版本限制,确保获取最新Node.js版本;与下载预编译二进制包相比,NVM省去了繁琐的环境变量配置;与从源代码编译安装相比,NVM大大缩短了安装时间,且对用户编译技能无要求。更重要的是,NVM支持多版本管理,方便切换,且安装的Node.js位于用户家目录,无需sudo权限,有效降低了安全风险。

  1. 安装分布式版本管理系统Git。

    Alibaba Cloud Linux 3/2、CentOS 7.x

    sudo yum install git -y
    
  2. 使用Git将NVM的源码克隆到本地的~/.nvm目录下,并检查最新版本。

    git clone https://gitee.com/mirrors/nvm.git ~/.nvm && cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`
    
  3. 依次运行以下命令,配置NVM的环境变量。

    sudo sh -c 'echo ". ~/.nvm/nvm.sh" >> /etc/profile'
    source /etc/profile
    
  4. 运行以下命令,修改npm镜像源为阿里云镜像,以加快Node.js下载速度。

    export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
    
  5. 运行以下命令,查看Node.js版本。

    nvm list-remote
    
  6. 安装多个Node.js版本。

    Alibaba Cloud Linux 2 和 CentOS 7.x 仅支持 Node.js 17.x 及以下版本,例如需要安装 v17.9.1,则执行nvm install v17.9.1。

    nvm install v17.9.1
    

    image.png

    image.png

  7. 查看已安装的Node.js版本。

    nvm ls
    

    返回结果如下所示,表示当前已安装v22.11.0、v23.3.0两个版本,正在使用的是v22.11.0版本。

    image.png

  8. 切换版本

    您可以使用nvm use <版本号>命令切换Node.js的版本。 例如,切换至Node.js v23.3.0版本的命令为nvm use v23.3.0。

2. 安装PM2(进程管理)

sudo npm install -g pm2

3. 安装Nginx(反向代理)

sudo yum install -y nginx

# 启动Nginx
sudo systemctl start nginx
sudo systemctl enable nginx #开机自动启动

三、部署Node.js项目

假设有两个项目:

  • 项目1:端口3000,子域名api1.example.com
  • 项目2:端口4000,子域名api2.example.com

1. 上传项目代码

# 创建项目目录
mkdir -p /var/www/api1 /var/www/api2

# 上传代码(使用git)

cd /var/www/api1
git clone your-repo-url
npm install

cd /var/www/api2
git clone your-second-repo-url
npm install

2. 使用PM2启动项目

# 启动项目1
cd /var/www/api1
pm2 start app.js --name "api1" -i max --watch

# 启动项目2
cd /var/www/api2
pm2 start app.js --name "api2" -i max --watch

# 保存PM2配置
pm2 save

# 设置PM2开机启动
pm2 startup
# 执行输出的命令(会显示类似下面的命令)
# sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup centos -u root --hp /root

四、配置子域名和Nginx反向代理

1. 域名解析准备

在阿里云DNS解析控制台添加子域名解析:

  • api1.example.com → 服务器IP
  • api2.example.com → 服务器IP

2. 配置Nginx反向代理

为项目1创建配置 (api1.example.com)

server {
    listen 80;
    server_name api1.example.com;

    location / {
        proxy_pass http://localhost:3000;  # 假设项目1运行在3000端口
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

为项目2创建配置 (api2.example.com)

server {
    listen 80;
    server_name api2.example.com;

    location / {
        proxy_pass http://localhost:4000;  # 假设项目2运行在4000端口
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

3. 测试并重启Nginx

nginx -t
systemctl restart nginx

五、维护与监控

1. PM2常用命令

pm2 list          # 查看所有应用
pm2 restart api1  # 重启特定应用
pm2 stop api2     # 停止应用
pm2 delete api1   # 删除应用

2. Nginx常用命令

systemctl restart nginx  # 重启Nginx
nginx -t                # 测试配置
journalctl -u nginx     # 查看Nginx系统日志

3. 查看服务器资源

top
htop
df -h
free -m

通过以上步骤,你可以在CentOS 7.9服务器上部署多个Node.js项目,并通过不同的子域名访问它们。每个项目都运行在独立的端口上,通过Nginx反向代理和PM2进程管理实现稳定运行。

❌
❌