普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月20日技术

回首 jQuery 20 年:从辉煌到没落

作者 冴羽
2026年1月20日 18:53

2006 年 1 月 14 日,John Resig 发布了名为 jQuery 的 JavaScript 库。

至今已经过去了 20 年!

20 周年之际,jQuery 4.0 正式发布了!

是的,就是那个被无数人宣布“已死”的 jQuery,经过 10 年的等待后迎来了重大更新。

更让人意想不到的是,根据 W3Techs 的数据,jQuery 仍然被全球 78% 的网站使用

这个数字意味着什么?

在 React、Vue、Angular 等现代框架横行的今天,那个曾经被我们嫌弃“老掉牙”的 jQuery,依然在互联网的角落里默默发光发热。

从 2006 年 John Resig 在 BarCampNYC 大会上首次发布,到今天 4.0 版本的现代化重生,jQuery 走过了整整 20 年。

它不仅是一个 JavaScript 库,更是一个时代的缩影,见证了前端技术从混沌到繁荣的完整历程。

本篇让我们一起回顾 jQuery 的 20 年,见证它的辉煌与没落。

1. 混沌时代

回望 2006 年,彼时正值第一次浏览器战争的尾声,微软 IE 与网景 Navigator 刚刚打完仗,但遗留下来的兼容性问题却让无数前端开发者头疼不已。

当时开发者需要面对各种浏览器的“奇技淫巧”,光是一个事件绑定就要写一大串兼容代码。

来看看这段早期的 jQuery 源码:

// 如果使用Mozilla
if (jQuery.browser == "mozilla" || jQuery.browser == "opera") {
    jQuery.event.add(document, "DOMContentLoaded", jQuery.ready);
}
// 如果使用IE
else if (jQuery.browser == "msie") {
    document.write("<scr" +="" "ipt="" id="__ie_init" defer="true" "="" "src="javascript:void(0)"><\/script>");
    var script = document.getElementById("__ie_init");
    script.onreadystatechange = function() {
        if (this.readyState == "complete") jQuery.ready();
    };
}
// 如果使用Safari
else if (jQuery.browser == "safari") {
    jQuery.safariTimer = setInterval(function(){
        if (document.readyState == "loaded" || document.readyState == "complete") {
            clearInterval(jQuery.safariTimer);
            jQuery.ready();
        }
    }, 10);
}
</scr">

看到没?仅仅是处理页面加载事件就要写这么多兼容代码!这在今天是难以想象的。

2. 横空出世

就在这时,jQuery 横空出世,彻底改变了游戏规则。

John Resig 提出了一个简单而优雅的理念:

Write Less,Do More

jQuery 通过精简常见的重复性任务,去除所有不必要的标记,使代码简洁、高效且易于理解,从而实现这一目标。

jQuery 带来了两大革命性改变:

  1. 强大的选择器引擎:不再局限于简单的 ID 和类选择,可以进行复杂的关系选择
  2. 优雅的 API 设计:链式操作让代码既简洁又易读

看看这个对比:

// 传统DOM操作
var elements = document.getElementById("contacts").getElementsByTagName("ul")[0].getElementsByClassName("people");
for (var i = 0; i < elements.length; i++) {
  var items = elements[i].getElementsByTagName("li");
  for (var j = 0; j < items.length; j++) {
    // 操作每个item
  }
}

// jQuery方式
$("#contacts ul.people li").each(function () {
  // 操作每个item
});

差距一目了然!

jQuery 的出现让前端开发变得如此优雅,以至于迅速在开发者群体中传播开来。

3. 辉煌岁月

随着 jQuery 的普及,一个庞大的插件生态迅速建立起来。

从日期选择器到轮播图,从表单验证到动画效果,几乎你能想到的功能都有对应的 jQuery 插件。

那时候前端开发的标准流程是:

  1. 下载 jQuery 核心库
  2. 搜索并下载所需的 jQuery 插件
  3. 组合这些插件完成项目

同时,jQuery 的管理也变得正式。

2011 年,jQuery 团队正式成立了 jQuery 理事会。2012 年,jQuery 理事会成立了 jQuery 基金会。

4. 影响深远

jQuery 的影响力远远超出了技术本身,它推动了整个前端行业的发展:

  • **大幅降低了前端开发的门槛:**让更多的开发者能够参与到前端开发中来
  • 提升了前端工程师的社会地位:让前端开发变得更加专业和重要
  • 促进了浏览器厂商的标准化:jQuery 的成功证明了统一 API 的重要性
  • 催生了现代前端工具链:为后来的模块化、构建工具奠定了基础

甚至连 jQuery 的选择器引擎 Sizzle 后来都被提取出来,影响了整个选择器标准的发展。

5. 价值动摇

jQuery 之所以能够快速普及,很大程度上是因为浏览器的“不争气”。

而当浏览器厂商开始认真对待标准化问题时,jQuery 的核心价值就开始动摇了。

2009 年后,浏览器标准化进程大幅加速:

  • querySelectorquerySelectorAll的出现
  • classList API 的普及
  • fetch API 替代 Ajax 需求
  • CSS3 动画替代 JavaScript 动画

现代浏览器 API 的完善,让很多 jQuery 功能都有了原生替代品:

// jQuery方式
$("#btn").on("click", () => $("#box").addClass("active"));

// 原生方式
document.querySelector("#btn").addEventListener("click", () => {
  document.querySelector("#box").classList.add("active");
});

你可以发现,差距已经不再那么明显!

6. 框架打击

2010 年,React、Angular、Vue 等现代框架相继登场,带来了革命性的变化:

  1. 组件化思维:从 DOM 操作转向组件构建
  2. 声明式编程:描述“什么”而不是“如何”
  3. 状态管理:解决了复杂应用的维护问题
  4. 工具链完善:从构建到部署的完整解决方案

这些框架从架构层面解决了 jQuery 时代的问题,就像从手工制作转向了工业化生产。

7. 惨遭背叛

2018 年,GitHub 公开宣布从其前端移除 jQuery,这个标志性事件被广泛解读为“jQuery 时代的终结”。

GitHub 在博客中详细说明了迁移的理由:现代浏览器 API 已经足够完善,React 的组件化模式更适合大型应用的维护。

这个“背叛”对 jQuery 的声誉造成了重大打击,也加速了它在新技术栈中的衰落。

8. 瘦死骆驼

尽管在技术前沿领域失势,但 jQuery 在存量市场中的地位依然稳固:

  • 78% 的顶级网站仍在使用 jQuery
  • WordPress 等 CMS 系统大量依赖 jQuery
  • 企业级应用中 jQuery 代码基数庞大

为什么企业不直接抛弃 jQuery?

因为现实远比理想复杂:

  1. 业务逻辑与 DOM 深度耦合:重构成本巨大
  2. 第三方插件依赖:很多插件没有现代替代方案
  3. 迁移风险:新功能开发受阻,影响营收
  4. 技能断层:团队对旧技术熟悉,对新技术陌生

比如一个电商网站如果要重构支付流程的 jQuery 代码,任何 bug 都可能导致直接的经济损失。这种风险评估让很多公司望而却步。

此外,WordPress 支撑着全球 43% 的网站,它的核心仍然依赖 jQuery。这个庞大的生态系统意味着:

  • 数十万主题和插件依赖 jQuery
  • 内容管理系统对稳定性的要求远超先进性
  • 托管服务商倾向于保持现有技术栈

所以即使所有前端开发者都不再使用 jQuery,仅 WordPress 生态系统就能让它继续存在很多年。

9. 拥抱现代

2026 年 1 月 17 日,jQuery 4.0 正式发布,在这次发布中:

  • 移除对 IE11 以下版本的支持:摆脱历史包袱
  • 迁移到 ES 模块:与现代构建工具兼容
  • 增加 Trusted Types 支持:提升安全性
  • 移除已弃用 API:清理技术债务

这次更新像是 jQuery 面向现代 Web 的断舍离。

10. 结语:一个时代的完结

jQuery 20 年的发展史,就是一部前端技术的缩影。

它从解决现实问题出发,推动了整个行业的发展,最终也随着时代的变化而淡出主流。

这并不意味着 jQuery 是失败的。恰恰相反,它超额完成了自己的历史使命

  • 它让无数人学会了前端开发
  • 它推动了浏览器厂商的标准化
  • 它催生了现代前端生态
  • 它证明了开源协作的力量

正如那句经典的台词:“并不是英雄迟暮,而是时代需要新的英雄。”

jQuery 4.0 的发布不是回光返照,它告诉我们:技术没有绝对的对错,只有是否适合那个时代的需求

今天,当我们在 React、Vue 的组件化世界中忙碌时,偶尔回望一下 jQuery 的简单优雅,也许能获得一些关于技术本质的思考:

好的工具应该让人更专注于创造价值,而不是被技术本身所困扰。

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

MCP、Agent、大模型应用架构解读

作者 sorryhc
2026年1月20日 17:58

前言

随着大语言模型(LLM)的快速发展,如何让 AI 能够有效地与外部世界交互,已成为 AI 应用开发的核心课题。Anthropic 推出的 MCP(Model Context Protocol)、智能代理(Agent)和大模型应用三者的结合,形成了一套完整的 AI 系统架构。

接下来,我们深入解读这三个核心概念及其相互关系。


一、3个核心概念的定义

1.1 大模型应用(AI Application)

大模型应用是整个系统的最外层容器。它包括:

  • 应用程序框架和生命周期管理
  • 用户交互界面(CLI、Web、API等)
  • 系统配置和资源管理
  • 外部集成(数据库、监控等)
大模型应用
  ├─ 启动应用
  ├─ 管理配置
  ├─ 处理用户输入
  ├─ 返回处理结果
  └─ 关闭应用

1.2 Agent(智能代理)

Agent 是大模型应用的大脑和执行引擎。它的职责是:

  • 理解用户意图(通过大模型)
  • 规划执行步骤
  • 决定调用什么工具
  • 处理工具执行结果
  • 持续优化和迭代

Agent 的核心价值在于将大模型的推理能力与外部工具执行能力结合。

1.3 MCP(Model Context Protocol)

MCP 是一个开放的通信协议规范。它定义了:

  • 工具的统一调用接口
  • 消息的标准格式(JSON-RPC 2.0)
  • 服务的发现和注册机制
  • 错误处理规范

MCP 的核心价值在于解耦工具调用的复杂性,实现工具即插即用。


二、三者的包含关系

┌──────────────────────────────────────────────────┐
│                 大模型应用                        │
│                                                  │
│  ┌────────────────────────────────────────────┐ │
│  │              Agent                         │ │
│  │                                            │ │
│  │  ├─ 初始化 MCP (建立连接、获取工具)       │ │
│  │  ├─ 与大模型交互 (发送提示词、接收响应)  │ │
│  │  ├─ 解析大模型输出 (识别工具调用)        │ │
│  │  ├─ 通过 MCP 调用工具 (执行具体任务)     │ │
│  │  ├─ 处理工具结果 (反馈给大模型)          │ │
│  │  └─ 循环迭代 (直到任务完成)              │ │
│  │                                            │ │
│  │         ◄──────────────────────►          │ │
│  │            MCP (工具协议)                  │ │
│  │         ◄──────────────────────►          │ │
│  │                                            │ │
│  └────────────────────────────────────────────┘ │
│                                                  │
│  用户输入  ──►  应用处理  ──►  用户输出        │
│                                                  │
└──────────────────────────────────────────────────┘

三、工作流程详解

3.1 初始化阶段

第一步:读取配置文件(mcp.json)
  ├─ 检查有哪些 MCP Server
  ├─ 验证配置的合法性
  └─ 记录工具来源信息

第二步:连接所有 MCP Server
  ├─ 为每个 Server 创建 MCP Client
  ├─ 建立传输连接(stdio/HTTP/WebSocket)
  ├─ 发送 initialize 信息握手
  └─ 获取 Server 能力信息

第三步:获取所有工具列表
  ├─ 从每个 Server 调用 listTools()
  ├─ 收集返回的工具定义
  ├─ 合并工具列表并检查冲突
  └─ 标记每个工具来自哪个 Server

第四步:准备就绪
  └─ Agent 获得完整的工具清单,可以开始工作

代码示例:

class AIApplication {
  private agent: Agent
  
  async initialize() {
    // Agent 初始化
    this.agent = new Agent("mcp.json")
    await this.agent.initialize()
    
    console.log("✓ 应用初始化完成")
    console.log(`✓ 可用工具数: ${this.agent.toolCount}`)
  }
}

3.2 处理请求阶段

当用户输入一个请求时,完整的处理流程如下:

用户输入: "帮我计算 (10 + 5) * 2 的结果"
  │
  ▼
┌─────────────────────────────────────────┐
│  Agent 第一步:准备提示词                 │
│  ├─ 获取当前的工具列表                   │
│  ├─ 组织成 Claude 能理解的格式          │
│  └─ 加入用户的原始请求                   │
└──────────────┬──────────────────────────┘
               │
  ┌────────────▼─────────────┐
  │  Claude API              │
  │  (处理用户请求)          │
  │  ├─ 理解用户意图         │
  │  ├─ 规划执行步骤         │
  │  └─ 决定调用哪些工具     │
  │                          │
  │  Claude 响应:            │
  │  "我需要先调用 add(10,5)"│
  └────────────┬─────────────┘
               │
  ┌────────────▼──────────────────────┐
  │  Agent 第二步:处理工具调用请求     │
  │  ├─ 解析 Claude 的响应             │
  │  ├─ 识别出要调用 "add" 工具        │
  │  ├─ 找到 "add" 来自哪个 Server    │
  │  └─ 获取该 Server 的 MCP Client    │
  └────────────┬──────────────────────┘
               │
  ┌────────────▼──────────────────────┐
  │  Agent 第三步:通过 MCP 调用工具    │
  │  ├─ 构建标准化的 RPC 请求          │
  │  ├─ 调用: client.callTool("add",   │
  │  │         {a: 10, b: 5})         │
  │  └─ 等待工具执行完毕               │
  └────────────┬──────────────────────┘
               │
  ┌────────────▼──────────────────────┐
  │  MCP Server (实际执行工具)         │
  │  ├─ 接收 RPC 请求                  │
  │  ├─ 执行: 10 + 5 = 15             │
  │  └─ 返回结果: {result: 15}        │
  └────────────┬──────────────────────┘
               │
  ┌────────────▼──────────────────────┐
  │  Agent 第四步:反馈给 Claude        │
  │  ├─ 把结果添加到对话历史           │
  │  ├─ "add(10, 5) 的结果是 15"      │
  │  └─ 重新调用 Claude               │
  └────────────┬──────────────────────┘
               │
  ┌────────────▼──────────────────────┐
  │  Claude 继续推理                  │
  │  ├─ 看到了第一步的结果             │
  │  ├─ 继续规划下一步                 │
  │  └─ "现在我需要调用 multiply(15,2)"│
  └────────────┬──────────────────────┘
               │
  (重复步骤 2-4 直到 Claude 说完成)
               │
  ┌────────────▼──────────────────────┐
  │  Claude 最终响应                   │
  │  ├─ stop_reason = "end_turn"      │
  │  ├─ content = "答案是 30"         │
  │  └─ Agent 停止循环                 │
  └────────────┬──────────────────────┘
               │
               ▼
        返回用户: "答案是 30"

3.3 循环机制的关键

Agent 的循环处理是理解整个架构的关键:

async process(userInput: string): Promise<string> {
  let messages = [{ role: "user", content: userInput }]
  
  for (let iteration = 0; iteration < maxIterations; iteration++) {
    // 1. 调用 Claude
    const response = await claude.messages.create({
      messages,
      tools: this.tools  // 传递所有可用工具
    })
    
    // 2. 添加 Claude 的响应到历史
    messages.push({ role: "assistant", content: response.content })
    
    // 3. 检查 Claude 是否完成
    if (response.stop_reason === "end_turn") {
      // Claude 完成了,返回最终答案
      const textBlock = response.content.find(b => b.type === "text")
      return textBlock.text
    }
    
    // 4. Claude 要求调用工具
    if (response.stop_reason === "tool_use") {

      const toolResults = [ ]

      
      for (const block of response.content) {
        if (block.type === "tool_use") {
          // 通过 MCP 调用工具
          const result = await this.callToolViaMCP(
            block.name,
            block.input
          )
          
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: JSON.stringify(result)
          })
        }
      }
      
      // 5. 把工具结果添加到历史(关键!Claude 需要看到结果)
      messages.push({
        role: "user",
        content: toolResults
      })
      
      // 循环回第 1 步,Claude 基于工具结果继续推理
    }
  }
}

关键点:

  • messages 数组是"记忆",不断积累
  • 每次调用 Claude 时,都传递完整的历史
  • Claude 基于之前的工具执行结果进行下一步决策

四、MCP 的泛化调用设计

4.1 为什么需要泛化?

不泛化的方式(混乱):

// 需要为每个工具写特定代码
if (toolName === "add") {
  result = calculator.add(args.a, args.b)
} else if (toolName === "query") {
  result = database.query(args.sql)
} else if (toolName === "analyzeCode") {
  result = codeAnalyzer.analyze(args.code)
}
// ... 100+ 个 else if ...

// 问题:新增工具时要改应用代码

泛化的方式(MCP):

// 一个函数搞定所有工具
const result = await this.callToolViaMCP(toolName, args)

// 问题解决:新增工具时只需改配置

4.2 泛化的实现原理

┌──────────────────────────────────────────┐
│   统一的工具调用接口                      │
│   callTool(name: string, args: any)      │
└──────────────┬───────────────────────────┘
               │
      ┌────────┴────────┐
      │                 │
      ▼                 ▼
  ┌────────┐      ┌──────────┐
  │ Server │      │ Server   │
  │ A      │      │ B        │
  │        │      │          │
  │ add    │      │ query    │
  │ sub    │      │ insert   │
  └────────┘      └──────────┘

所有 Server 遵守相同的 MCP 规范:
  ├─ 都支持 listTools() 方法
  ├─ 都支持 callTool(name, args) 调用
  ├─ 都返回标准格式的结果
  └─ 应用无需关心 Server 差异

4.3 MCP 规范的约束

MCP 定义了统一的消息格式:

// 工具列表请求
{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "tools/list"
}

// 工具列表响应
{
  "jsonrpc": "2.0",
  "id": "1",
  "result": {
    "tools": [
      {
        "name": "add",
        "description": "Add two numbers",
        "inputSchema": {
          "type": "object",
          "properties": {
            "a": {"type": "number"},
            "b": {"type": "number"}
          },
          "required": ["a", "b"]
        }
      }
    ]
  }
}

// 工具调用请求
{
  "jsonrpc": "2.0",
  "id": "2",
  "method": "tools/call",
  "params": {
    "name": "add",
    "arguments": {"a": 5, "b": 3}
  }
}

// 工具调用响应
{
  "jsonrpc": "2.0",
  "id": "2",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "5 + 3 = 8"
      }
    ]
  }
}

四、结尾

因此有了对这三者的核心概念的了解,其实对大模型应用开发也有了比较深入的认识了。

评论区欢迎讨论。

用 Intersection Observer 打造丝滑的级联滚动动画

作者 阿明Drift
2026年1月20日 17:49

无需任何动画库,仅用原生 Web API 实现滚动时丝滑的淡入滑入效果,兼顾性能与体验。

你是否见过这样的交互动效:

  • 用户滚动页面时,一组卡片像被“唤醒”一样,依次从下方滑入并淡入;

滚动触发动画示例

  • 如果这些元素在页面加载时已在视口内,它们也会自动按顺序浮现。

初始加载动画示例

这种效果不仅视觉流畅,还能有效引导用户注意力,提升内容层次感。更重要的是——它不依赖 GSAP、AOS 等第三方库,仅靠 Intersection Observer + CSS 动画 + 少量 JavaScript,就能实现高性能、可访问、且高度可控的滚动触发型级联动画。

今天,我们就来一步步拆解这个经典动效,并给出一套可直接复用的轻量级方案


🔧 核心原理概览

整个动画系统依赖三个关键技术点:

技术 作用
IntersectionObserver 监听元素是否进入视口,避免频繁 scroll 事件
CSS @keyframes 定义滑入 + 淡入动画
--animation-order 自定义属性 通过 calc() 动态设置 animation-delay,实现“逐个延迟”的级联感

最关键的设计哲学是:动画只在用户能看到它的时候才执行,既节省性能,又避免“闪现”。


🧱 HTML 结构(简化版)

为便于理解,我们剥离业务逻辑,只保留动效核心:

<div class="container">
    <ul class="card-list">
        <li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 1;"
            >Card 1</li
        >
        <li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 2;"
            >Card 2</li
        >
        <li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 3;"
            >Card 3</li
        >
        <!-- 更多卡片... -->
    </ul>
</div>

💡 类名与属性说明

  • .scroll-trigger:表示该元素需要被滚动监听;
  • .animate--slide-in:启用滑入动画;
  • data-cascade:JS 识别“需设置动画顺序”的标志;
  • --animation-order:CSS 自定义属性,用于计算延迟时间(如第 2 个元素延迟 150ms)。

🎨 CSS 动画定义

:root {
    --duration-extra-long: 600ms;
    --ease-out-slow: cubic-bezier(0, 0, 0.3, 1);
}

/* 仅在用户未开启“减少运动”时启用动画(晕动症用户友好) */
@media (prefers-reduced-motion: no-preference) {
    .scroll-trigger:not(.scroll-trigger--offscreen).animate--slide-in {
        animation: slideIn var(--duration-extra-long) var(--ease-out-slow) forwards;
        animation-delay: calc(var(--animation-order) * 75ms);
    }

    @keyframes slideIn {
        from {
            transform: translateY(2rem);
            opacity: 0.01;
        }
        to {
            transform: translateY(0);
            opacity: 1;
        }
    }
}

✨ 参数说明

属性 作用
transform translateY(2rem) → 0 由下往上滑入
opacity 0.01 → 1 淡入(避免完全透明导致布局跳动)
animation-delay n × 75ms 第1个延迟75ms,第2个150ms……形成级联
animation-fill-mode forwards 动画结束后保持最终状态

无障碍提示:通过 @media (prefers-reduced-motion) 尊重用户偏好,对晕动症用户更友好。


🕵️ JavaScript:Intersection Observer 监听逻辑

为什么不用 scroll 事件?

传统方式:

// ❌ 性能差,频繁触发
window.addEventListener('scroll', checkVisibility);

现代方案:

// ✅ 高性能,浏览器底层优化
const observer = new IntersectionObserver(callback, options);

完整监听逻辑

const SCROLL_ANIMATION_TRIGGER_CLASSNAME = 'scroll-trigger';
const SCROLL_ANIMATION_OFFSCREEN_CLASSNAME = 'scroll-trigger--offscreen';

function onIntersection(entries, observer) {
    entries.forEach((entry, index) => {
        const el = entry.target;

        if (entry.isIntersecting) {
            // 进入视口:移除 offscreen 类,允许动画播放
            el.classList.remove(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);

            // 若为级联元素,动态设置顺序(兜底)
            if (el.hasAttribute('data-cascade')) {
                el.style.setProperty('--animation-order', index + 1);
            }

            // 只触发一次,停止监听
            observer.unobserve(el);
        } else {
            // 离开视口:加上 offscreen 类,禁用动画
            el.classList.add(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
        }
    });
}

function initScrollAnimations(root = document) {
    const triggers = root.querySelectorAll(`.${SCROLL_ANIMATION_TRIGGER_CLASSNAME}`);
    if (!triggers.length) return;

    const observer = new IntersectionObserver(onIntersection, {
        rootMargin: '0px 0px -50px 0px', // 元素进入视口 50px 后才触发
        threshold: [0, 0.25, 0.5, 0.75, 1.0],
    });

    triggers.forEach((el) => observer.observe(el));
}

// 页面加载完成后启动
document.addEventListener('DOMContentLoaded', () => {
    initScrollAnimations();
});

🎯 关键设计细节

  • rootMargin: '0px 0px -50px 0px':确保元素完全进入用户视野后再触发动画,避免“刚看到就结束”;
  • 初始所有 .scroll-trigger 元素默认带有 .scroll-trigger--offscreen 类,阻止 CSS 动画生效;
  • unobserve:动画只播放一次,避免重复触发,节省资源。

📊 两种场景下的行为对比

场景 初始状态 触发时机 动画表现
卡片已在视口内 --offscreen 页面加载后立即 依次淡入(基于 --animation-order
卡片在视口外 --offscreen 滚动到视口(超过 50px) 滚动时依次淡入

这正是你感受到的“丝滑感”来源:无论用户如何进入页面,动画总是在最合适的时机出现


💡 总结:这套方案的优势

能力 说明
高性能 使用 IntersectionObserver 替代 scroll 事件,避免频繁计算
精准控制 通过 rootMarginthreshold 灵活调整触发时机
无障碍友好 尊重 prefers-reduced-motion 用户偏好
轻量可复用 无依赖,仅 50 行 JS + 简洁 CSS,适合嵌入任何项目
懒加载兼容 可扩展用于图片懒加载、广告曝光统计等场景

完整 Demo 已上传 CodePen:
👉 codepen.io/AMingDrift/…

如果你正在开发电商、博客、SaaS 产品页等内容密集型网站,不妨将这套方案集成进去,给用户带来更优雅的浏览体验!


学习优秀作品,是提升技术的最佳路径。本文既是我的学习笔记,也希望对你有所启发。

CSS 动效进阶:从“能动就行”到“性能优化”,一个呼吸球背后的 3 个思考

作者 Flinton
2026年1月20日 17:31

大家好,我叫【小奇腾】,今天我们聊一个场景题:“同心圆呼吸动画”,很多同学 5 分钟就能写出来。

但是代码能跑,就OK了吗?

当出题人问你:“为什么你的动画看起来卡卡的,或者你是怎么确定缩放比例的?”,这时候就不是考察你会不会CSS语法了,而是考察你对浏览器渲染原理工程化思维的理解。 今天让我们分3步来进行思考,从能动就行极致性能

本期详细的视频教程bilibili:# CSS 动效进阶:从“能动就行”到“性能优化”,一个呼吸球背后的 3 个思考

01. 场景复现

题目: 场景复现
利用所学的盒子模型和动画,考虑如何实现如下图的同心圆。该同心圆会放大缩小的运动轨迹:
1. 定义:目前两圈的大小为常规大小
2. 正常运动轨迹:
外圈向外扩大 10px (2000ms)
外圈向内回归正常大小 (2000ms)
内圈向内缩小 12px (2500ms)
内圈放大至常规大小 (2500ms)
循环

思考一:布局的健壮性 —— 为什么不推荐 margin?

拿到题目,第一步是布局。很多初学者习惯用 margin 来控制位置,比如:

/* ❌ 脆弱的布局写法 */
.circle {
    width: 100px;
    margin: 100px auto; /* 依赖外部容器高度,不够灵活 */
}

或者用 calc 配合 margin-left 负值来居中。这些写法在静态页面没问题,但在动画场景下,一旦圆的尺寸发生变化(比如宽高改变),中心点很容易偏移。

这里有三个理由: 第一,脱离文档流,防止动画放大时撑开页面; 第二,自适应垂直居中,不用人肉计算 margin-top; 第三,锚定圆心,确保多层圆圈在缩放时,圆心永远重合,不会跑偏。 这就是为什么在做动效组件时,绝对定位永远是首选。”

✅ 推荐方案:绝对定位 + 变换

.center-abs {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

解析:

  • top/left: 50%:将元素的左上角推到容器中心。
  • translate(-50%, -50%):将元素自身往回拉一半。
  • 优势: 这种居中方式完全不依赖元素的具体宽高。无论你怎么缩放,它的几何中心永远锚定在父容器的正中心。

思考二:渲染性能 —— 为什么宁愿算 Scale 也不改 Width?

这里是区分“初级”和“高级”的分水岭

需求是:外圈从 100px 变大到 110px。 直觉告诉我们,直接改 width 最方便:

❌ 性能较差的写法 */
@keyframes expand {
    0% { width: 100px; height: 100px; }
    50% { width: 110px; height: 110px; }
    100% { width: 100px; height: 100px; }
}

为什么说它性能差? 这涉及到了浏览器的渲染流水线(Rendering Pipeline)

  • Layout (重排/回流): 当你改变 widthheightmargin 时,浏览器会惊恐地发现:“天呐,元素的大小变了!它会不会挤到旁边的字?我是不是要重新计算整个文档的布局?”这个过程非常消耗 CPU。

  • Paint (重绘): 布局变了,颜色可能也要重画。

  • Composite (合成): 最后合成图层。

✅ 优化方案:使用 Transform: Scale

如果我们使用 transform: scale,浏览器会意识到:“哦,你只是想视觉上放大它,不需要改变实际占据的空间。”

此时,浏览器会跳过 Layout 和 Paint,直接进行 Composite。这个过程通常由 GPU(硬件加速) 处理,动画会丝般顺滑。

数学计算: 既然不能直接写 110px,我们需要算出缩放比例:

  • 外圈: 目标 110px / 原始 100px = 1.1 倍
  • 内圈: 目标 48px / 原始 60px = 0.8 倍
/* ✅ 性能优化的写法 */
@keyframes outer-move {
    0%, 100% { transform: translate(-50%, -50%) scale(1); }
    50%      { transform: translate(-50%, -50%) scale(1.1); }
}

注意: 这里的 transform 必须包含 translate(-50%, -50%),否则动画播放时,元素会因为覆盖了原有的 transform 而瞬间跳回非居中状态。

思考三:交互体验 —— 让动画“活”过来

解决了性能,最后一步是体验。 很多工程师写出来的动画像机器人,机械且生硬。通常是因为忽略了两个参数:Timing Function(时间曲线)Stagger(交错感)

  1. 拒绝 Linear: 呼吸是自然的生理过程,有吸气的急促和呼气的平缓。使用 linear(匀速)会显得非常怪异。推荐使用 ease-in-out(慢进慢出)。
  2. 制造时间差: 如果外圈和内圈完全同步(比如都是 2s 一圈),画面会显得单调。 我们可以让外圈慢一点(4s),内圈快一点(5s)。
  • 外圈周期:4s (2s 变大,2s 变小)
  • 内圈周期:5s (2.5s 变小,2.5s 变大)

由于 4 和 5 的最小公倍数是 20,这意味着观众要看 20秒 才能看到一次完全重复的画面,这种错落感会让动画显得更有生命力。

最终代码清单

结合以上三个思考,我们得到了这份既优雅又高效的答卷:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>css动画同心圆</title>
    <style>
        .box {
            width: 200px; height: 300px;
            background-color: #000;
            position: relative; /* 相对定位基准 */
            overflow: hidden; /* 防止未来动画增大倍数“溢出”事故 */
        }

        /* 思考一:健壮的居中 */
        .circle-outer, .circle-inner {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            border-radius: 50%;
        }

        .circle-outer {
            width: 100px; height: 100px;
            border: 4px solid #ccc;

            /* 思考三:错开时间节奏 */
            animation: outer-move 4s ease-in-out infinite;
        }

        .circle-inner {
            width: 60px; height: 60px;
            border: 4px solid #fff;

            /* 思考三:错开时间节奏 */
            animation: inner-move 5s ease-in-out infinite;
        }

        .text {
            position: absolute;
            bottom: 40px;
            color: #fff;
            font-size: 20px;
            text-align: center;
            width: 100%;
        }

        /* 思考二:Scale 优化性能 */
        @keyframes outer-move {
            0%,100%  { transform: translate(-50%, -50%) scale(1) }
            50% { transform: translate(-50%, -50%) scale(1.1) } /* 100px -> 110px */
        }

        @keyframes inner-move {
            0%,100% { transform: translate(-50%, -50%) scale(1) } 
            50% { transform: translate(-50%, -50%) scale(0.8) } /* 60px -> 48px */
        }
    </style>
</head>

<body>
    <div class="box">
        <div class="circle-outer "></div>
        <div class="circle-inner "></div>
        <div class="text">Hi</div>
    </div>
</body>

</html>

总结

当我们谈论“CSS 进阶”时,往往不是指记住了多少偏门的属性,而是指在简单的场景下,能否做出最优的技术选择

通过这道题,我们知道了:

  1. translate 居中的原理。
  2. Reflow(重排)Composite(合成) 对性能的影响。
  3. 动画曲线与节奏对用户体验的微调。

希望你在职业生涯中遇到 CSS 动画题,你也能自信地对面试官说:“为了性能,我选择用 Scale。” 感谢大家的支持和鼓励,一起加油!

Bipes项目二次开发/扩展积木功能(八)

2026年1月20日 17:22

Bipes项目二次开发/扩展积木功能(八)

新年第一篇文章,这一篇开发扩展积木功能。先看一段VCR。 广告:需要二开Bipes,Scratch,blockly可以找我。 项目地址:maxuecan.github.io/Bipes/index…

VCR

[video(video-CjWu9kdf-1768899636737)(type-csdn)(url-live.csdn.net/v/embed/510…)]

第一:模式选择

在这里插入图片描述 在三种模式中,暂时对海龟编程加了扩展积木功能,点击选择海龟编程,就可以看到积木列表多了个添加按钮。其它模式下不会显示

第二:积木扩展

在这里插入图片描述

点击扩展按钮,会弹窗一个扩展积木弹窗,接着点击卡片,会显示确认添加按钮,最后点击确认添加,就能动态添加扩展积木。

第三:代码解析

ui/components/extensions-btn.js(扩展积木按钮)

import EventEmitterController from '../utils/event-emitter-controller'
import { resetPostion } from '../utils/utils'

export default class extensionsBtn {
    constructor(props) {
        this.settings = props.settings
        this.resetPostion = resetPostion
        if (document.getElementById('content_blocks')) {
            $('#content_blocks').append(this.render())
            this.initEvent()
        }

        // 根据模式,控制扩展按钮的显示
        setTimeout(() => {
            let { mode } = this.settings
            resetPostion()
            $('#extensions-btn').css('display', mode === 'turtle' ? 'block' : 'none')
        }, 1000);
    }
    // 初始化事件
    initEvent() {
        window.addEventListener('resize', (e) => {
            this.resetPostion()
        })

        $('#extensions-btn').on('click', () => {
            EventEmitterController.emit('open-extensions-dialog')
        })
    }

    render() {
        return `
            <div id="extensions-btn">
                <div class="extensions-add"></div>
            </div>
        `
    }
}
ui/components/extensions-dialog.js(扩展积木弹窗)

import ExtensionsList from '../config/extensions-blocks.js'
import { resetPostion } from '../utils/utils'

export default class extensionsDialog {
    constructor() {
        this._xml = undefined
        this._show = false
        this.list = ExtensionsList
        this.use = []
        this.after_extensions = [] // 记录已经添加过的扩展积木
    }
    // 初始化事件
    initEvent() {
        $('.extensions-modal-close').on('click', this.close.bind(this))
        $('.extensions-modal-confirm').on('click', this.confirm.bind(this))
        $('.extensions-modal-list').on('click', this.select.bind(this))
    }
    // 销毁事件
    removeEvent() {
        $('.extensions-modal-close').off('click', this.close.bind(this))
        $('.extensions-modal-confirm').off('click', this.confirm.bind(this))
        $('.extensions-modal-list').off('click', this.select.bind(this))
    }
    // 显示隐藏弹窗
    show() {
        if (this._show) {
            $('.extensions-dialog').remove()
            this.removeEvent()
        } else {
            $('body').append(this.render())
            this.initEvent()
            this.createList()
        }

        this._show = !this._show
    }
    // 创建扩展列表
    createList() {
        $('.extensions-list').empty()
        for (let i in this.list) {
            let li = $('<li>')
                    .attr('key', this.list[i]['type'])
                    .css({
                        background: `url(${this.list[i]['image']}) center/cover no-repeat`,
                    })
            let box = $('<div>')
                    .addClass('extensions-list-image')
                    .attr('key', this.list[i]['type'])
            let detail = $('<div>')
                .addClass('extensions-list-detail')
                .attr('key', this.list[i]['type'])

            let name = $('<h4>').text(this.list[i]['name']).attr('key', this.list[i]['type'])
            let remark = $('<span>').text(this.list[i]['remark']).attr('key', this.list[i]['type'])
            detail.append(name).append(remark)
            $('.extensions-modal-list').append(li.append(box).append(detail))
        }
    }
    // 选择列表
    select(e) {
        let key = e.target.getAttribute('key')
        if (key !== null) {
            let index = this.use.indexOf(key)
            let type = undefined
            if (index !== -1) {
                this.use.splice(index, 1)
                type = 'delete'
            } else {
                this.use.push(key)
                type = 'add'
            }
            this.highlightList(type, key)
            this.showConfirm()
        }
    }
    // 高亮列表项
    highlightList(action, key) {
        $('.extensions-modal-list li').each(function(index) {
            let c_key = $(this).attr('key')
            if (key === c_key) {
                if (action === 'add') {
                    $(this).addClass('extensions-modal-list-act')
                } else if (action === 'delete') {
                    $(this).removeClass('extensions-modal-list-act')
                }
            }
        })
    }
    // 显示确认按钮
    showConfirm() {
        if (this.use.length > 0) {
            $('.extensions-modal-footer').css('display', 'block')
        } else {
            $('.extensions-modal-footer').css('display', 'none')
        }
    }
    // 关闭
    close() {
        this.show()
    }
    // 确认操作
    confirm() {
        let str = ''
        this.use.forEach(item => {
            let index = this.after_extensions.indexOf(item)
            if (index === -1) {
                this.after_extensions.push(item)
                str += this.getExtendsionsXML(item)
            }
        })

        if (str) {
            if (!this._xml) this._xml = window._xml.cloneNode(true)
            let toolbox = this._xml
            toolbox.children[0].innerHTML += str
            Code.reloadToolbox(toolbox)
        }

        this.show()
        resetPostion()
    }
    /* 获取扩展积木的XML */
    getExtendsionsXML(type) {
        let item = ExtensionsList.filter(itm => itm.type === type)
        return item[0].xml
    }
    // 重置toolbox
    resetToolbox() {
        return new Promise((resolve) => {
            this._xml = window._xml.cloneNode(true)
            Code.reloadToolbox(this._xml)
            this.use = []
            this.after_extensions = []
            setTimeout(resolve(true), 200)
        })
    }

    render() {
        return `
            <div class="extensions-dialog">
                <div class="extensions-modal">
                    <div class="extensions-modal-header">
                        <h4></h4>
                        <ul class="extensions-modal-nav">
                            <li class="extensions-modal-nav-act" key="basic">
                                <span key="basic">扩展积木</span>
                            </li>
                        </ul>
                        <div class="extensions-modal-close"></div>
                    </div>

                    <div class="extensions-modal-content">
                        <ul class="extensions-modal-list"></ul>
                    </div>

                    <div class="extensions-modal-footer">
                        <button class="extensions-modal-confirm">确认添加</button>
                    </div>
                </div>
            </div>
        `
    }
}
ui/config/extensions-blocks.js(扩展积木配置)

let turtle = require('./turtle.png')

module.exports = [
  {
    type: 'turtle',
    name: '海龟函数',
    image: turtle,
    remark: '可以调用海龟编辑器中对应Python函数。',
    xml: `
            <category name="海龟" colour="%{BKY_TURTLE_HUE}">
                <block type="variables_set" id="fg004w+XJ=maCm$V7?3T" x="238" y="138">
                    <field name="VAR" id="dfa$SFe(HK(10)Y+T-bS">海龟</field>
                    <value name="VALUE">
                        <block type="turtle_create" id="Hv^2jr?;yxhA=%oCs1=d"></block>
                    </value>
                </block>
                <block type="turtle_create"></block>
                <block type="turtle_move">
                    <value name="VALUE">
                        <block type="variables_get">
                            <field name="VAR">{turtleVariable}</field>
                        </block>
                    </value>
                    <value name="distance">
                        <shadow type="math_number">
                            <field name="NUM">50</field>
                        </shadow>
                    </value>
                </block>
                <block type="turtle_rotate">
                    <value name="VALUE">
                        <block type="variables_get">
                            <field name="VAR">{turtleVariable}</field>
                        </block>
                    </value>
                    <value name="angle">
                        <shadow type="math_number">
                            <field name="NUM">90</field>
                        </shadow>
                    </value>
                </block>
            <block type="turtle_move_xy">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="x">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
                <value name="y">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_position">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="position">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_draw_circle">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="radius">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
                <value name="extent">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
                <value name="steps">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_draw_polygon">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="num_sides">
                    <shadow type="math_number">
                        <field name="NUM">5</field>
                    </shadow>
                </value>
                <value name="radius">
                    <shadow type="math_number">
                        <field name="NUM">30</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_draw_point">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="diameter">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_write">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="text">
                    <shadow type="text">
                        <field name="TEXT">Hello</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_heading">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="angle">
                    <shadow type="math_number">
                        <field name="NUM">90</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_pendown">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_set_pensize">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="size">
                    <shadow type="math_number">
                        <field name="NUM">5</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_speed">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="speed">
                    <shadow type="math_number">
                        <field name="NUM">5</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_get_position">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_show_hide">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_clear">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_stop">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_set_bgcolor">
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>
            <block type="turtle_set_pencolor">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>
            <block type="turtle_set_fillcolor">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>

            <block type="turtle_set_colormode">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <shadow type="math_number">
                        <field name="NUM">255</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_fill">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_set_color">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>
        </category>
        `,
  },
]

总结

扩展积木功能改动挺多的,功能也时不断的完善,讲解可能比较粗糙,也在尽量写注解,有需要可以看下提交日志,信息会比较全。

详解TypedArray的内存机制——从backing store到 Native Heap 与 JS Heap

2026年1月20日 17:12

前言:TypedArray与普通数组的比较

在js中创建一个200万元素的二维数组,

  const array = [];

  const step = 0.01;
  let x = 0;
  while (x < 20000) {
    const y = Math.sin(x);
    array.push([x, y]);
    x = Number((x + step).toFixed(2));
  }

该数组对象占用内存如下 image.png

但如果改为Float64Array:

 const array = new Float64Array(4000000);
  const step = 0.01;
  let x = 0;
  let i = 0;
  while (x < 20000) {
    const y = Math.sin(x);
    array[i] = x;
    array[i + 1] = y;
    x = Number((x + step).toFixed(2));
    i += 2;
  }

占用内存如下: image.png

Float64Array内存只有数组的30%。其中最明显的差距是浅层大小,也就是该对象本身占用的内存。该数组对象浅层大小11738kB是因为数组容器本身具有开销,而Float64Array对象的0.1kB则只存储了该对象本身的元信息,其真正内容是存储于backing_store——这就是我们今天要介绍的主角。


一、什么是 Backing Store?

Backing Store(底层存储区) 是指:

在 V8 中,用于支撑(back)某些 JavaScript 对象(如 ArrayBufferTypedArraySharedArrayBufferWebAssembly.Memory)实际数据的 底层原生内存区域

可以理解为:

JS 层的对象只是“壳”或“视图”,而 backing store 是真正存放数据的地方


二、为什么需要 Backing Store?

JavaScript 是一种 托管语言 —— 内存由 GC 自动管理;
ArrayBufferTypedArray 等对象需要存放大规模、结构化、可直接访问的二进制数据

如果这些数据都放在 JS Heap(GC 管理区),会导致:

  • GC 扫描时间急剧上升;
  • 无法保证内存连续性;
  • 与 C/C++/WebAssembly 交互效率低。

因此,V8 设计了 backing store 机制 ——
通过在 Native Heap(C++ 层) 中分配一块连续的内存块来存放这些数据。


三、结构示意(简化)

┌────────────────────────────────────────────┐
│               JS Heap (GC 管理)           │
│ ┌──────────────────────────────┐          │
│ │  ArrayBuffer Object          │          │
│ │  ├─ byteLength: 1048576      │          │
│ │  ├─ pointer → backing store ─┼──────────┼─►
│ │  └─ internal fields          │          │
│ └──────────────────────────────┘          │
└────────────────────────────────────────────┘
                       │
                       ▼
┌────────────────────────────────────────────┐
│           Native Heap (C++ 管理)           │
│ ┌──────────────────────────────────────┐   │
│ │  Backing Store (1MB binary buffer)   │   │
│ │  [00 FF 12 8A ...]                  │   │
│ └──────────────────────────────────────┘   │
└────────────────────────────────────────────┘

四、V8 中 Backing Store 的生命周期

1️. 创建阶段

当你在 JS 层执行:

const buf = new ArrayBuffer(1024 * 1024);

V8 会在内部:

  • 调用 C++ 层函数,分配一块 1MB 原生内存;
  • 把该地址封装为一个 BackingStore 对象;
  • JS 对象 ArrayBuffer 只持有指针(引用)到这块内存。

2. 使用阶段

访问 TypedArray 时,例如:

const arr = new Uint8Array(buf);
arr[0] = 255;
  • JS 层通过 arr 的引用,直接映射到 backing store;
  • 写入的数据会直接修改底层的原生内存;
  • 无需拷贝、无 GC 干预;
  • 性能接近原生内存访问。

3️. 销毁阶段

当 JS 层的对象(ArrayBufferTypedArray)不再被引用:

  • GC 会回收它们;
  • GC 通知 C++ 层释放对应的 backing store;
  • 对应的原生内存被 freemunmap 回收。

五、JS Heap 和 Native Heap

V8 内存总体结构

在 V8(Chrome 和 Node.js 的 JavaScript 引擎)中,内存主要可以分为两大块:

  1. JS Heap(JavaScript 堆)

    • 用于存放 JavaScript 层面可见的对象、闭包、字符串、数组等。
    • 由 V8 自己的垃圾回收器(GC)管理。
    • 典型 GC 算法:分代垃圾回收(Generational GC) ,包括新生代(New Space)和老生代(Old Space)。
  2. Native Heap(原生堆)

    • 用于存放 V8 引擎内部的 C++ 对象、编译后的代码、内建结构,以及 JS 对象引用的底层资源(例如 ArrayBuffer 的底层内存)。
    • 由操作系统或 C++ 层通过 malloc / new 分配。
    • 不由 V8 的 GC 直接回收,但 V8 会间接追踪引用关系。

Native Heap 与 JS Heap 的关系

  • JS 对象(如 ArrayBuffer)可能在 JS Heap 中只保存一个 指针或句柄
  • 实际的二进制数据存在 Native Heap
  • GC 负责追踪 JS 层对象的引用,当 JS 对象不可达时,会 触发 C++ 层的释放钩子(如 Finalizer 或 WeakRef)。

例子:

const buffer = new ArrayBuffer(1024 * 1024); // 1MB

此时:

  • JS Heap 中只有一个轻量对象 ArrayBuffer
  • 实际的 1MB 数据分配在 Native Heap
  • buffer 被 GC 回收时,底层的 native 内存也会被释放。

六、ArrayBuffer到底占不占用JS Heap

基于上述结论,ArrayBuffer存储的内容不占JS Heap。但如果拿这个问题去问ai,有些会回答占,有些回答不占。

如果在node中测试如下test.js

function formatMB(bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + " MB";
}

function logMemory(label) {
  const m = process.memoryUsage();
  console.log(`\n[${label}]`);
  console.log("  rss:", formatMB(m.rss));
  console.log("  heapTotal:", formatMB(m.heapTotal));
  console.log("  heapUsed:", formatMB(m.heapUsed));
  console.log("  external:", formatMB(m.external));
  console.log("  arrayBuffers:", formatMB(m.arrayBuffers));
}

async function run() {
  logMemory("Before allocation");

  // 分配 100MB ArrayBuffer
  const buf = new ArrayBuffer(1024 * 1024 * 100);
  logMemory("After allocation");

  // 等待一会儿,确保 GC 没回收
  await new Promise((r) => setTimeout(r, 5000));

  // 释放引用并手动触发 GC(需要 node 启动参数 --expose-gc)
  global.gc();
  // 等待一会儿,确保 GC 没回收
  await new Promise((r) => setTimeout(r, 5000));
  logMemory("After GC");

  console.log(
    "\n external 增加约 100MB,而 heapUsed 基本不变,说明 backing store 在 Native Heap。",
  );
}

run();


执行node --expose-gc .\test.js输出如下:

image.png 可以得到结论:ArrayBuffer使用Native Heap。

但如果在浏览器中执行,可能会得到相反的结论:

  const before = window.performance.memory.usedJSHeapSize;
  const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
  setTimeout(() => {
    const after = window.performance.memory.usedJSHeapSize;
    console.log(
      "backing_store 占用:",
      (after - before) / 1024 / 1024,
      "MB",
    ); // 约 100MB
  }, 5000);

这个现象gpt给的解释我觉得挺有道理,但没找到出处,如果有人找到了可以踢我下。

Chrome 团队在 2021 年左右做过一次调整:

在 DevTools 与 performance.memory 中,usedJSHeapSize 将反映 “对开发者而言可达的内存使用总量”。

具体参见 Chromium bug 1203451 讨论:

“Expose externalized ArrayBuffer memory in JSHeapSize metrics to match DevTools heap snapshot.”

也就是说:

  • 从内存管理角度:backing store 属于 Native Heap;
  • 从统计视角(performance.memory) :它被加进了 usedJSHeapSize,为了让前端开发者能直观看到分配代价。

runtime chunk 到底是什么?

作者 Soler
2026年1月20日 17:12

runtime chunk(运行时代码块)是 Webpack 生成的一小段核心代码,它不包含你的业务逻辑,而是负责:

  • 管理模块之间的依赖关系(比如哪个模块对应哪个文件);
  • 加载和执行打包后的模块(比如异步加载 chunk);
  • 维护模块的缓存和版本映射。

实操案例:从零看 runtime chunk 的生成

1. 准备极简项目结构

plaintext

├── src
│   ├── index.js       # 主入口
│   └── utils.js       # 工具模块
├── package.json
└── webpack.config.js

2. 编写业务代码

javascript

运行

// src/utils.js
export const add = (a, b) => a + b;

javascript

运行

// src/index.js
// 同步引入 + 异步引入,触发Webpack的模块管理逻辑
import { add } from './utils.js';
console.log('同步调用:', add(1, 2));

// 异步引入(关键:会让runtime逻辑更明显)
setTimeout(() => {
  import('./async-module.js').then(({ sayHello }) => {
    sayHello();
  });
}, 1000);

javascript

运行

// src/async-module.js
export const sayHello = () => console.log('异步模块加载成功!');

3. Webpack 配置(默认开启 runtime chunk)

javascript

运行

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js', // 主bundle
    chunkFilename: '[name].chunk.js', // 异步chunk
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    // 默认值为'single':将runtime提取为单独的chunk
    runtimeChunk: 'single', 
    splitChunks: {
      chunks: 'all', // 分割同步/异步chunk
    },
  },
};

4. 执行打包 & 查看输出

运行 npx webpack 后,dist 目录会生成 3 个文件:

plaintext

dist/
├── runtime.bundle.js   # runtime chunk(核心!)
├── main.bundle.js      # 主业务chunk(index + utils)
└── async-module.chunk.js # 异步chunk

5. 核心:runtime.bundle.js 里有什么?

打开 runtime.bundle.js,核心内容(简化后)如下:

javascript

运行

// runtime的核心逻辑:模块映射 + 加载器
(() => {
  // 1. 模块ID和文件路径的映射表(关键)
  const moduleMap = {
    "./src/async-module.js": () => import("./async-module.chunk.js"),
  };

  // 2. 模块加载器:处理异步import的核心逻辑
  window.__webpack_require__.e = (chunkId) => {
    // 加载对应的chunk文件(比如async-module.chunk.js)
    // 处理模块缓存、依赖解析
  };

  // 3. 模块缓存管理:避免重复加载
  const installedModules = {};
  window.__webpack_require__.c = installedModules;
})();

对比:禁用 runtime chunk 的效果

修改 Webpack 配置,将 runtimeChunk: false,重新打包后:

  • dist 目录只有 2 个文件:main.bundle.jsasync-module.chunk.js
  • main.bundle.js 里会包含原本 runtime.bundle.js 的所有逻辑(模块映射、加载器等);
  • 此时 main.bundle.js = 业务代码 + runtime 代码(即多个 chunk 合并到一个 bundle,对应上一轮你问的场景)。

为什么要单独提取 runtime chunk?

这是最关键的实战价值,用一个场景说明:假设你只修改了 utils.js 里的 add 函数(比如改成 a + b + 1),重新打包后:

  • main.bundle.js 的内容变了 → hash 值会变;
  • async-module.chunk.js 没改 → hash 值不变;
  • runtime.bundle.js 里的模块映射表没改 → hash 值不变;

用户浏览器缓存中:

  • 只会重新加载 main.bundle.jsruntime.bundle.jsasync-module.chunk.js 会复用缓存;

如果不提取 runtime chunk,main.bundle.js 包含 runtime 逻辑,哪怕只改一行业务代码,整个 main.bundle.js 的 hash 都变,用户需要重新加载全部内容 → 缓存失效,性能变差。


总结

  1. runtime chunk 本质:Webpack 的 “模块调度器”,包含模块映射、加载逻辑、缓存管理,无业务代码;
  2. 核心作用:管理打包后模块的加载和依赖,单独提取可提升缓存复用率;
  3. 表现形式:默认会生成单独的 runtime.bundle.js,禁用则合并到主 bundle 中(多 chunk→单 bundle)。

告别笨重的 Prometheus,这款 5 分钟部署的 Nginx 监控工具凭什么刷屏 GitHub?

2026年1月20日 16:49

前言

作为后端开发者,Nginx 几乎是我们每天都要打交道的“基础设施”。但说实话,Nginx 的运维体验一直很割裂:

  • 原生监控太简陋:stub_status 只能看个连接数,想看接口响应耗时?想看 502 错误分布?对不起,请去翻几 GB 的 Access Log。
  • 传统方案太重:为了监控几台机器,要搭一套 Prometheus + Grafana + Exporter “全家桶”。对于中小团队或个人项目来说,运维这套监控系统的时间,甚至比写业务代码还长。

最近,我在 GitHub 发现了一个名为 nginxpulse 的开源项目。它上线仅一周 star 就突破了 1k,彻底解决了“轻量级 Nginx 监控”这个老大难问题。今天和大家聊聊,这款“黑马”工具到底香在哪?


一、 痛点直击:为什么我们讨厌传统的 Nginx 监控?

在深入 nginxpulse 之前,先看看我们平时的痛点:

  1. 部署成本极高:传统方案涉及多个组件的联动配置,学习成本和资源成本双高。
  2. 监控与业务脱节:改完 Nginx 配置,不知道对性能有没有影响;报了 404/502,非得等用户反馈了才去查日志。
  3. 非侵入性差:很多工具需要编译特定的 Nginx 模块,这在生产环境简直是灾难。

nginxpulse 的核心逻辑很简单:用最轻量的方式,给 Nginx 装上“上帝视角”。


二、 架构设计:极简,但不简单

nginxpulse 并没有走“大而全”的路子,它采用了 Agent + Web UI + 数据存储 的极简架构:

  • nginxpulse-agent:基于 Go 编写的轻量级采集端,CPU 占用极低(<5%)。最惊艳的是,它无需重启 Nginx,通过 include 一行配置即可实现无侵入采集。
  • 可视化控制台:基于 Vue3 + Element Plus 开发,界面清爽,支持 1 秒粒度的实时刷新。
  • 灵活存储:小规模用本地文件,大规模支持 Redis 接入。

三、 杀手锏功能:不止是“能看”,更是“好用”

1. 深度异常分析(不再盲目翻日志)

以往查错需要 tail -f 盯着屏幕看,NP 内置了强大的分析指令。你只需运行一行命令,它就能告诉你谁是“罪魁祸首”:

codeBash

# 自动分析 Nginx 日志并生成 TOP 20 错误报告
nginxpulse analyze error --nginx-log /var/log/nginx/access.log --top 20

它会直接输出一份直观的报告,包含 4xx/5xx 分布、高频异常 URL、甚至后端 Upstream 的故障节点。

2. YAML 驱动的自动化告警

NP 彻底解决了“发现晚”的问题。你可以通过 YAML 灵活配置告警规则,支持钉钉、企业微信、邮件等主流渠道:

codeYaml

# 告警规则示例:响应时间过长即刻推送
alert_rules:
  - name: "API_Response_Slow"
    type: "response"
    condition: "p99_response_time > 800ms"
    duration: 60s  # 持续1分钟触发
    severity: "warning"
    targets:
      - type: "dingtalk"
        url: "https://oapi.dingtalk.com/robot/send?access_token=your_token"

3. 运维辅助:配置校验与安全重载

改完配置手抖?NP 提供了配置语法一键校验,避免因为一个分号导致整台服务器崩溃。

codeBash

# 安全校验配置
nginxpulse config check --conf /etc/nginx/nginx.conf

四、 实战体验:5 分钟完成部署

NP 的上手门槛极低,这也是它能快速传播的原因。我最推荐使用 Docker 部署,真正做到开箱即用:

codeBash

# 1. 拉取镜像
docker pull likaia/nginxpulse:latest

# 2. 启动全功能容器(Agent + UI)
docker run -d \
  --name nginxpulse \
  -p 9090:9090 \  # Agent 端口
  -p 8080:8080 \  # 控制台端口
  -v /var/log/nginx:/var/log/nginx \
  -v /etc/nginx:/etc/nginx \
  likaia/nginxpulse:latest

启动后,访问 http://localhost:8080,你会发现整个 Nginx 的运行状态、流量趋势、错误分布已经整整齐齐地摆在面前了。


五、 深度思考:好的开源项目长什么样?

nginxpulse 的走红再次印证了一个道理:开源项目的价值在于解决真实世界的“小痛点”。

它没有追求花哨的技术栈,而是聚焦在:

  • 低损耗: agent 占用内存不到 20MB。
  • 零门槛:运维新手也能看懂图表。
  • 场景化:针对 404 扫描、502 穿透等真实运维场景做了深度适配。

六、 结语

如果你正在被 Nginx 监控难、配置乱、排查慢的问题困扰,或者不想折腾笨重的 Prometheus 体系,nginxpulse 绝对是一个值得尝试的替代方案。

目前该项目还在快速迭代中,不仅完全开源(MIT 协议),社区响应也极快。

你平时是如何监控 Nginx 的?欢迎在评论区分享你的避坑指南!


本文纯技术分享,欢迎点赞、收藏。如果觉得有帮助,也欢迎去给开源作者点个 Star 鼓励一下。

Nuxt 3 vs Next.js:新手选型指南与项目实战对比

2026年1月20日 16:44

在现代Web开发中,两大全栈框架Nuxt 3和Next.js占据着服务端渲染(SSR)领域的主导地位。它们都提供了文件系统路由、自动代码分割、SEO优化等现代Web应用所需的核心功能,但技术选型背后的技术栈差异设计哲学却大不相同。

本文将通过对比分析,帮助前端新手理解这两大框架的区别,并提供实际的项目创建示例。


01 核心差异:Vue与React的技术栈选择

Nuxt 3与Next.js最根本的区别在于其底层技术栈

  • Nuxt 3:基于Vue 3生态系统,采用组合式API和响应式系统
  • Next.js:基于React生态系统,支持最新的React特性

这种核心差异决定了你的开发体验、学习曲线以及可用的第三方库生态。

学习曲线对比

对于完全没有前端经验的新手来说,Vue通常被认为比React学习曲线更平缓。Vue的模板语法更接近传统HTML,而React的JSX则需要适应将HTML与JavaScript混合编写的模式。

框架特性 Nuxt 3 Next.js
基础框架 Vue 3 React
路由系统 文件系统路由(pages/目录) 文件系统路由(app/目录)
数据获取 useAsyncData, useFetch 服务端组件、fetch API
状态管理 Pinia (推荐) Zustand, Redux等
样式方案 多种选择(CSS模块、Tailwind等) 多种选择(CSS模块、Tailwind等)
部署平台 Vercel、Netlify、Node服务器等 Vercel(官方)、Netlify等

生态圈对比

Next.js拥有更庞大的社区和更丰富的第三方库,这得益于React本身的普及度。Nuxt 3虽然社区规模较小,但其官方模块质量很高,且与Vue生态无缝集成。


02 快速入门:创建你的第一个应用

Nuxt 3入门示例

项目初始化

# 创建Nuxt 3项目
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev

创建页面和组件

  1. pages/index.vue中创建主页:
<template>
  <div class="container">
    <h1>欢迎使用Nuxt 3</h1>
    <p>当前时间:{{ currentTime }}</p>
    <button @click="refreshTime">刷新时间</button>
  </div>
</template>

<script setup>
// 使用组合式API
const currentTime = ref('')

// 获取服务器时间
onMounted(async () => {
  const { data } = await useFetch('/api/time')
  currentTime.value = data.value
})

// 客户端交互
const refreshTime = () => {
  currentTime.value = new Date().toLocaleString()
}
</script>
  1. 创建API端点server/api/time.get.ts
export default defineEventHandler(() => {
  return new Date().toISOString()
})

Next.js入门示例

项目初始化

# 创建Next.js项目(使用App Router)
npx create-next-app@latest my-next-app
cd my-next-app
npm install
npm run dev

创建页面和组件

  1. app/page.tsx中创建主页:
export default function HomePage() {
  return (
    <div className="container">
      <h1>欢迎使用Next.js</h1>
      <TimeDisplay />
    </div>
  )
}

// 服务端组件:自动在服务器上运行
async function TimeDisplay() {
  // 在服务端获取数据
  const response = await fetch('http://worldtimeapi.org/api/timezone/Asia/Shanghai')
  const data = await response.json()
  
  return (
    <div>
      <p>当前时间:{data.datetime}</p>
      <ClientComponent />
    </div>
  )
}

// 客户端组件:需要"use client"指令
'use client'
function ClientComponent() {
  const [count, setCount] = useState(0)
  
  return (
    <button onClick={() => setCount(count + 1)}>
      点击次数:{count}
    </button>
  )
}

03 特性深度对比:数据获取与渲染策略

数据获取方式对比

Nuxt 3的数据获取

<template>
  <div>
    <h2>文章列表</h2>
    <div v-if="pending">加载中...</div>
    <ul v-else>
      <li v-for="post in posts" :key="post.id">
        {{ post.title }}
      </li>
    </ul>
  </div>
</template>

<script setup>
// useAsyncData用于服务端获取数据
const { data: posts, pending } = await useAsyncData(
  'posts',
  () => $fetch('https://api.example.com/posts')
)

// useFetch是useAsyncData的简写
const { data: user } = await useFetch('/api/user')
</script>

Next.js的数据获取

// 在App Router中,页面组件默认为服务端组件
export default async function PostsPage() {
  // 直接使用fetch API,Next.js会自动优化
  const response = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // 每60秒重新验证
  })
  const posts = await response.json()
  
  return (
    <div>
      <h2>文章列表</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
      <LikeButton postId={posts[0].id} />
    </div>
  )
}

// 客户端交互组件需要"use client"指令
'use client'
function LikeButton({ postId }) {
  const [likes, setLikes] = useState(0)
  
  return (
    <button onClick={() => setLikes(likes + 1)}>
      点赞 ({likes})
    </button>
  )
}

渲染策略对比

两个框架都支持多种渲染策略,但实现方式不同:

渲染模式 Nuxt 3实现 Next.js实现
静态生成(SSG) nuxt generate output: 'static'
服务端渲染(SSR) 默认启用 默认启用(服务端组件)
客户端渲染(CSR) <ClientOnly>组件 "use client"指令
增量静态再生(ISR) 通过模块实现 原生支持(fetch选项)

04 实际应用场景分析

何时选择Nuxt 3?

  1. Vue技术栈项目:团队已熟悉Vue生态
  2. 快速原型开发:需要快速搭建MVP产品
  3. 内容型网站:博客、文档、营销页面
  4. 项目结构清晰:喜欢"约定优于配置"的理念

Nuxt 3优势场景示例

<!-- 快速创建SEO友好的内容页面 -->
<template>
  <div>
    <Head>
      <Title>产品介绍 - 我的网站</Title>
      <Meta name="description" :content="product.description" />
    </Head>
    
    <article>
      <h1>{{ product.title }}</h1>
      <!-- 内容自动渲染 -->
      <ContentRenderer :value="product" />
    </article>
  </div>
</template>

<script setup>
// 自动根据文件路径获取内容
const { data: product } = await useAsyncData('product', () => 
  queryContent('/products').findOne()
)
</script>

何时选择Next.js?

  1. React技术栈项目:团队已熟悉React生态
  2. 大型复杂应用:需要React丰富生态支持
  3. 需要最新特性:希望使用React最新功能
  4. Vercel平台部署:计划使用Vercel的完整能力

Next.js优势场景示例

// 复杂的动态仪表板应用
export default async function DashboardPage() {
  // 并行获取多个数据源
  const [sales, users, analytics] = await Promise.all([
    fetchSalesData(),
    fetchUserData(),
    fetchAnalyticsData(),
  ])
  
  return (
    <div className="dashboard">
      <SalesChart data={sales} />
      <UserTable users={users} />
      <AnalyticsOverview data={analytics} />
      {/* 实时更新的客户端组件 */}
      <LiveNotifications />
    </div>
  )
}

// 使用React Server Components实现部分渲染
'use client'
function LiveNotifications() {
  const [notifications, setNotifications] = useState([])
  
  useEffect(() => {
    // 建立WebSocket连接获取实时数据
    const ws = new WebSocket('wss://api.example.com/notifications')
    // ... 处理实时数据
  }, [])
  
  return <NotificationList items={notifications} />
}

05 开发体验与工具链对比

Nuxt 3的开发体验

  1. 零配置起步:大多数功能开箱即用
  2. 模块系统:官方和社区模块质量高
  3. TypeScript支持:一流的TypeScript体验
  4. 开发工具:Nuxt DevTools提供强大调试能力
# Nuxt 3的典型工作流
npx nuxi@latest init my-project  # 创建项目
npm install                       # 安装依赖
npm run dev                       # 开发模式
npm run build                     # 生产构建
npm run preview                   # 预览生产版本

Next.js的开发体验

  1. 灵活的配置:可根据需要深度定制
  2. TurboPack:极快的构建和刷新速度
  3. 完善的文档:官方文档质量极高
  4. Vercel集成:无缝部署和预览体验
# Next.js的典型工作流
npx create-next-app@latest my-app  # 创建项目
npm install                        # 安装依赖
npm run dev                        # 开发模式
npm run build                      # 生产构建
npm run start                      # 启动生产服务器

06 性能与优化对比

性能特征

  1. 首次加载性能:两者都优秀,Nuxt 3在小型项目上可能略快
  2. 开发服务器速度:Next.js的Turbopack在大型项目上优势明显
  3. 构建速度:取决于项目大小,两者都提供增量构建

优化技巧对比

Nuxt 3优化示例

<!-- 组件懒加载和图片优化 -->
<template>
  <div>
    <!-- 延迟加载重型组件 -->
    <LazyMyHeavyComponent v-if="showComponent" />
    
    <!-- 自动优化的图片 -->
    <NuxtImg
      src="/images/hero.jpg"
      width="1200"
      height="600"
      loading="lazy"
      format="webp"
    />
  </div>
</template>

Next.js优化示例

// 使用Next.js内置优化功能
import Image from 'next/image'
import dynamic from 'next/dynamic'

// 动态导入重型组件
const HeavyComponent = dynamic(() => import('./HeavyComponent'))

export default function OptimizedPage() {
  return (
    <>
      {/* 自动优化的图片组件 */}
      <Image
        src="/hero.jpg"
        alt="Hero image"
        width={1200}
        height={600}
        priority={false} // 非关键图片延迟加载
      />
      
      {/* 条件加载重型组件 */}
      <HeavyComponent />
    </>
  )
}

07 新手选择建议

根据背景选择

  1. 完全零基础

    • 如果喜欢更直观的模板语法 → 选择Nuxt 3
    • 如果看重就业市场需求 → 选择Next.js
  2. 有前端基础

    • 熟悉HTML/CSS/JS → 都可尝试,根据偏好选择
    • 有React经验 → 选择Next.js
    • 有Vue经验 → 选择Nuxt 3

根据项目类型选择

项目类型 推荐框架 理由
个人博客/作品集 Nuxt 3 快速搭建,SEO优秀
企业官网/营销页 Nuxt 3 开发效率高,维护简单
SaaS/管理后台 Next.js React生态丰富,组件库多
电商平台 Next.js 性能优化完善,生态成熟
实时应用 均可 根据团队技术栈选择

无论选择哪个框架,最重要的是开始构建。真正的经验来自于项目实践,而不是框架比较。

🗳️ 互动时间:你的选择是?

读完全文,相信你对 Nuxt 3 和 Next.js 有了更清晰的认识。技术选型没有标准答案,真实项目中的经验才是最宝贵的参考。

欢迎在评论区分享你的观点:

  1. 投票选择:你目前更倾向于或正在使用哪个框架?

    • A. Nuxt 3 (Vue阵营)
    • B. Next.js (React阵营)
    • C. 两个都在用/观望中
  2. 经验分享:在实际项目中,你使用 Nuxt 3 或 Next.js 时,遇到的最大挑战或最惊喜的体验是什么? 你的分享对其他开发者会非常有帮助!


关注我的公众号" 大前端历险记",掌握更多前端开发干货姿势!

学术界最大的室内运动捕捉设施为世界领先的无人机研究提供支持

作者 爱迪斯通
2026年1月20日 16:42

亚利桑那州立大学跟踪体积为 230,000 立方英尺的无人机工作室是世界上学术机构中最大的室内无人机研究动捕设施。该设施前身是一个篮球馆,经过五年多的建造,由亚利桑那州立大学机器人研究员和副教授Panagiotis Artemiadis博士设计,为跨学科研究的合作空间提供支持。

ScreenShot_2026-01-20_153410_194.png

该无人机研究设施的跟踪系统拥有 23 英尺高的天花板和 104 个广角 OptiTrack Prime 17W 摄像机,每秒能够同时捕获多达 250 个机器人的 360 次测量数据,每架无人机的运动跟踪精度在 0.5 毫米以内,使该空间非常适合研究无人机群的快速动态和适用于大型平台的机器人等应用。

Artemiadis博士在 2016 年凭借其“大脑活动驱动的无人机控制系统”的创新成果引起了轰动,亚利桑那州立大学人机控制 (HORC) 实验室的研究人员,与亚利桑那州立大学计算、信息学和决策系统工程学院的助理教授Stephanie Gil一起主导了无人机工作室的落成。

除了支持空中机器人系统的研究和开发外,无人机工作室还支持地面机器人,使研究人员能够检查和协调具有不同运动方式的机器人之间的相互作用。此类研究的现实意义可以帮助新兴技术的发展,例如无人驾驶搜索和救援车辆、自动驾驶车辆和无人机送货。

ScreenShot_2026-01-20_153437_707.png

Artemiadis博士在亚利桑那州立大学富尔顿工程学院杂志 Convergence 上介绍无人机工作室时表示:“这个先进的仪器空间使多学科的教师团队能够解决以前无法解决的研究问题,例如代理数百个机器人的协调和控制以及与人类群体的互动等。”

Gil指出,“我希望无人机工作室能够激励研究人员尝试新的实验类别,利用大空间工作与实时跟踪功能的结合,揭示对如何最好地协调、控制和建模大型机器人系统的新理解。”

Element Plus SCSS 变量覆盖用法

作者 码农张3
2026年1月20日 16:13

安装依赖

pnpm i sass -D

样式文件

element-plus-vars.scss

// 覆盖变量
@forward "element-plus/theme-chalk/src/common/var.scss" with (
  $colors: (
    "primary": (
      "base": #4080ff,
    ),
    "success": (
      "base": #23c343,
    ),
    "warning": (
      "base": #ff9a2e,
    ),
    "danger": (
      "base": #f76560,
    ),
    "info": (
      "base": #a9aeb8,
    ),
  ),

  $bg-color: (
    "page": #f5f8fd,
  )
);
// 引入 Element Plus 样式(必须在覆盖变量后)
@use "element-plus/theme-chalk/src/index.scss";

element-plus.scss

/**
 * element-plus 组件样式覆盖
 */
// 变量覆盖(必须在最前面)
@use "./element-plus-vars";
// 引入 Element Plus 样式(必须在覆盖变量后)
@use "element-plus/theme-chalk/src/index.scss";
...

导入样式

index.scss

// 重置样式
@use "./reset";

// element-plus
@use "./element-plus";
...

main.js

...
// ===== 样式导入 =====
import "@/assets/styles/index.scss";
...

vite.config.js

...
// 自动导入
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
...
plugins: [
    vue(),
    // 自动导入 Element Plus 组件和函数,无需手动 import
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    // 自动注册 Element Plus 组件,可在模板中直接使用,采用sass样式配色
    Components({
      resolvers: [ElementPlusResolver({ importStyle: "sass" })],
    }),
  ],  
...

大白话解释Vue响应式三要素

作者 cj8140
2026年1月20日 16:00

好的!咱们用最接地气的大白话聊聊 Vue 响应式系统的三个核心角色:Observer(观察者)Watcher(监听者)Dep(依赖收集器)。想象一下它们仨是怎么配合工作的:


🎯 场景比喻:班级通知系统

想象一个班级:

  • 数据 (data):班级里某个同学(比如小明)的作业本状态(是否写完)。
  • 模板 (template):班级公告栏(显示“小明作业:未完成”)。
  • 视图 (view):你眼睛看到的公告栏内容。

👤 1. Observer (观察者) - “数据管家”

  • 职责:把普通数据变成“聪明”数据,让数据自己知道谁在关心它。
  • 工作方式
    • 当你创建 Vue 实例时,Observer 会像管家一样,拿着小本本(Dep),去遍历你写在 data 里的所有属性(比如 homeworkStatus: '未完成')。
    • 它会给每个属性(homeworkStatus装上一个“监听器”Object.definePropertygetter/setter)。
    • getter (获取值时):属性被访问时(比如公告栏要显示状态),Observer 会悄悄在本子上记一笔:“谁(谁)在关心我(homeworkStatus)?”(这就是依赖收集)。
    • setter (设置值时):属性被修改时(比如你把 homeworkStatus 改成 '已完成'),Observer 会立刻大喊一声:“喂!我(homeworkStatus)变了!快通知所有关心我的人!”(这就是派发更新)。
  • 大白话总结Observer 就是个数据管家,给数据装上“耳朵”(getter)和“喇叭”(setter),让数据能“被听到”和“能广播”。

👂 2. Watcher (监听者) - “关心者”

  • 职责:谁关心数据?就是它!它负责在数据变化时执行动作(比如更新视图)。
  • 工作方式
    • 有两种主要类型的 Watcher
      • 渲染 Watcher (Render Watcher):最核心的“关心者”!它负责渲染整个组件的模板(也就是更新公告栏)。它一启动,就会去访问组件里用到的所有数据(比如 homeworkStatus),触发它们的 getter,从而在本子(Dep)上登记:“我是渲染 Watcher,我关心 homeworkStatus!”。
      • 用户 Watcher (User Watcher):你在代码里写的 watch 选项或者 this.$watch() 创建的监听器。比如你专门写代码监听 homeworkStatus 变化时弹个窗。它启动时也会去访问 homeworkStatus,触发 getter,登记:“我是用户 Watcher,我也关心 homeworkStatus!”。
    • 当数据变化时(setter 广播),Watcher 收到通知,就执行它该做的事:
      • 渲染 Watcher:重新执行渲染函数,更新公告栏内容。
      • 用户 Watcher:执行你写的回调函数(比如弹窗)。
  • 大白话总结Watcher 就是数据的粉丝!它主动去“追星”(访问数据),在数据“本子”(Dep)上留下联系方式。数据一“发新动态”(变化),粉丝(Watcher)立刻收到通知,按约定做动作(更新视图/执行回调)。

📒 3. Dep (Dependency - 依赖收集器) - “通讯录本子”

  • 职责:记录数据(属性)和关心它的 Watcher 之间的“依赖关系”。它是 ObserverWatcher 之间的桥梁
  • 工作方式
    • 每个被 Observer 装上“监听器”的数据属性(比如 homeworkStatus),都会拥有一个专属的 Dep 实例。这个 Dep 就像一个小本本。
    • Watcher(粉丝)去访问这个数据(触发 getter)时:
      • Observer 会把当前正在“关心”的 Watcher(比如渲染 Watcher)添加到这个数据属性专属的 Dep 本子里。登记:“粉丝 Watcher A 关心我!”。
    • 当数据变化(触发 setter)时:
      • Observer 会找到这个数据属性专属的 Dep 本子。
      • 它会遍历本子上记录的所有 Watcher(粉丝),挨个给他们打电话(调用 Watcherupdate 方法):“喂!数据变了!快更新!”。
  • 大白话总结Dep 就是数据的“粉丝通讯录”!它记录了“谁(Watcher)关心我(数据)”。数据变化时,它就翻通讯录,挨个通知粉丝(Watcher)。

🔄 三者协作流程(大白话版)

  1. 初始化 (装喇叭 & 翻通讯录)

    • Observer 管家:给 data 里的每个属性(如 homeworkStatus)装上“喇叭”(setter)和“耳朵”(getter)。每个属性都带一个空“通讯录”(Dep)。
    • 渲染 Watcher(公告栏):启动!为了显示内容,它必须访问 homeworkStatus(触发 getter)。
    • Observer 听到访问:立刻在 homeworkStatus 的“通讯录”(Dep)上记一笔:“公告栏 Watcher 关心你!”。
  2. 数据变化 (广播通知)

    • 你修改数据:this.homeworkStatus = '已完成'
    • Observer 管家:检测到 homeworkStatussetter 被调用!立刻大喊:“homeworkStatus 变了!”
    • Observer 管家:找到 homeworkStatus 的“通讯录”(Dep),翻开来看:“哦,只有公告栏 Watcher 关心你。”
    • Observer 管家:拿起电话,拨打“公告栏 Watcher”的号码:“喂!homeworkStatus 变成 '已完成' 了!快更新公告栏!”
  3. 更新视图 (粉丝行动)

    • 公告栏 Watcher(粉丝)接到电话:立刻行动!重新执行渲染函数。
    • 渲染函数访问 homeworkStatus(再次触发 getter,但这次主要是为了获取新值)。
    • 公告栏显示更新为:“小明作业:已完成”。

🎉 总结一句话

Observer 给数据装上“耳朵”和“喇叭”;Watcher 去听数据(触发耳朵),登记在数据的“通讯录”(Dep)上;数据变化时(喇叭响),Dep 翻通讯录通知所有 Watcher 去更新视图!

这样理解是不是清晰多了?它们仨就像一个高效的班级通知系统,确保数据一变,关心它的地方(视图、用户监听器)能立刻知道并做出反应!

执行了 git commit 然后 git pull,现在想要回退到 pull 之前的状态

作者 新晨437
2026年1月20日 15:45

如果你已经执行了 git commit 然后 git pull,现在想要回退到 pull 之前的状态,这里有几个解决方案:

解决方案

1. 使用 git reset(最常用)

# 查看提交历史,找到你想要回退到的commit
git log --oneline

# 回退到指定的commit(保留工作区更改)
git reset --soft <commit-hash>

# 或者回退并丢弃更改
git reset --hard <commit-hash>

2. 回退到 pull 之前的状态

# 使用 reflog 查看操作历史
git reflog

# 找到 pull 之前的 HEAD 位置,然后重置
git reset --hard HEAD@{n}  # n 是 reflog 中的编号

3. 如果你想要完全撤销 pull 和 commit

# 查看最近的操作,找到 pull 之前的提交
git log --oneline

# 重置到 pull 之前的提交
git reset --hard HEAD~2  # 回退2个提交(假设你commit了1次,pull了1次)

具体操作步骤建议:

  1. 先查看当前状态
git status
git log --oneline
  1. 安全方案(保留更改)
# 先保存当前工作状态(如果有未提交的更改)
git stash

# 回退到 pull 之前的commit
git reset --hard HEAD~1

# 如果需要,恢复stash的更改
git stash pop
  1. 如果你只是想修改 commit 信息
# 修改最后一次commit
git commit --amend

注意事项:

  • 如果已经 push 到远程仓库,需要谨慎操作,可能需要 git push --force
  • 如果是多人协作的项目,最好先和团队沟通
  • 使用 git reset --hard 会丢失所有未提交的更改,请确保已备份

你具体遇到了什么情况?我可以提供更针对性的建议。

Vue组件变量值更新过程记录

作者 cj8140
2026年1月20日 15:45

从 Vue 2.x 源码角度分析将组件变量 a 从空值修改为 1 的完整调用栈如下:


1. 组件初始化阶段

在组件创建时,Vue 会初始化响应式数据:

// 调用栈:
Vue.prototype._init (init.js)
  └── initState (state.js)
      └── initData (state.js)
          └── observe (observer/index.js)
              └── new Observer (observer/index.js)
                  └── walk (observer/index.js)
                      └── defineReactive (observer/index.js) // 为属性 `a` 创建响应式

关键步骤

  • defineReactivea 创建 getter/setter
    • 初始化 dep 实例(依赖收集器)。
    • 通过 Object.defineProperty 重写 a 的访问器:
      Object.defineProperty(obj, key, {
        get() { /* 依赖收集 */ },
        set(newVal) { /* 触发更新 */ }
      })
      

2. 修改 a 的值

执行 this.a = 1 时触发 setter

// 调用栈:
this.a = 1
  └── a 的 setter (defineReactive 内部)
      └── dep.notify() (observer/dep.js)
          └── subs[i].update() (observer/watcher.js)
              └── queueWatcher (scheduler.js)
                  └── nextTick (scheduler.js)
                      └── flushSchedulerQueue (scheduler.js)
                          └── watcher.run (observer/watcher.js)
                              └── watcher.get (observer/watcher.js)
                                  └── 组件重新渲染 (render 函数)

关键步骤详解

  1. setter 触发

    • 检查新值 1 是否与旧值不同(newVal !== oldVal)。
    • 若不同,调用 dep.notify() 通知所有依赖。
  2. dep.notify()

    • 遍历 dep.subs(存储所有订阅该属性的 Watcher)。
    • 调用每个 watcher.update()
  3. watcher.update()

    • 将 Watcher 加入异步队列(queueWatcher)。
    • 通过 nextTick 异步执行更新。
  4. flushSchedulerQueue

    • 遍历队列中的 Watcher,调用 watcher.run()
    • watcher.run()watcher.get() → 重新执行组件的 render 函数。
  5. 重新渲染

    • render 函数执行时访问 a,触发 getter 重新收集依赖。
    • 生成新的虚拟 DOM,对比差异后更新真实 DOM。

3. 依赖收集机制

在首次渲染和后续更新时,getter 负责收集依赖:

// getter 调用栈:
组件访问 a (render 函数)
  └── a 的 getter (defineReactive 内部)
      └── Dep.target (全局唯一 Watcher)
          └── dep.depend() (observer/dep.js)
              └── 将当前 Watcher 添加到 dep.subs

关键点

  • Dep.target:全局唯一变量,指向当前正在执行的 Watcher(如渲染 Watcher)。
  • dep.depend():将当前 Watcher 加入 dep.subs,建立 属性 → Watcher 的依赖关系。

4. 异步更新队列

Vue 使用异步队列合并更新:

// nextTick 流程:
queueWatcher (scheduler.js)
  └── nextTick (util/next-tick.js)
      └── 异步任务 (Promise/MutationObserver/setTimeout)
          └── flushSchedulerQueue (scheduler.js)

优化逻辑

  • 多次修改 a 会被合并为一次更新(避免重复渲染)。
  • 通过 nextTick 确保在 DOM 更新后执行回调。

Vue 3 Proxy 版本的差异

若使用 Vue 3(基于 Proxy):

  1. 初始化:通过 reactive 创建响应式代理。
  2. 修改值:直接触发 Proxy.set 拦截器,后续流程类似(依赖收集、异步更新)。
  3. 核心差异
    • 无需 Object.defineProperty,支持动态属性。
    • 依赖收集通过 Track 操作,更新通过 Trigger 操作。

总结

阶段 核心操作 关键函数/类
初始化 a 创建响应式 getter/setter defineReactiveDep
修改值 触发 setter → 通知依赖 dep.notify()
依赖更新 异步队列合并更新 queueWatchernextTick
重新渲染 执行 render 函数 Watcher.run()

整个流程体现了 Vue 响应式系统的核心:依赖收集getter)和 派发更新setter),通过 异步队列 优化性能。

rxjs基本语法

作者 米诺zuo
2026年1月20日 15:38

RxJS (Reactive Extensions for JavaScript) 是 Angular 中处理异步编程的核心库。 它通过使用 Observable(可观察对象) 序列来编写异步和基于回调的代码。


一、 核心概念

在 RxJS 中,一切基于数据流。

  • Observable (被观察者): 数据的源头,发出数据。
  • Observer (观察者): 数据的消费者,接收数据。
  • Subscription (订阅): 连接 Observable 和 Observer 的桥梁。注意:必须取消订阅,否则会内存泄漏。
  • Operators (操作符): 纯函数,用来处理、转换数据流(如 map, filter)。
  • Subject (主题): 既是 Observable 又是 Observer,可以多播数据(常用于组件通信)。

二、 基础写法

1. 创建 Observable 和 订阅

import { Observable } from 'rxjs';
// 1. 创建 Observable
const observable$ = new Observable(subscriber => {
  subscriber.next(1); // 发出数据
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete(); // 结束
  // subscriber.error('出错了'); // 抛出异常
});
// 2. 订阅
const subscription = observable$.subscribe({
  next: (x) => console.log('收到数据:', x),
  error: (err) => console.error('错误:', err),
  complete: () => console.log('流结束')
});
// 3. 取消订阅 (非常重要)
subscription.unsubscribe();

2. 简写订阅 (只关心 next)

observable$.subscribe(data => console.log(data));

三、 常用创建操作符

用于生成数据流。

import { of, from, interval, fromEvent, throwError } from 'rxjs';
// 1. of: 依次发出参数
of(1, 2, 3).subscribe(console.log); // 输出: 1, 2, 3
// 2. from: 将数组/Promise 转为 Observable
from([10, 20, 30]).subscribe(console.log); // 输出: 10, 20, 30
// 3. interval: 周期性发出数字 (每1秒发一个)
interval(1000).subscribe(n => console.log(n)); // 0, 1, 2...
// 4. fromEvent: 监听 DOM 事件
fromEvent(document.querySelector('button')!, 'click')
  .subscribe(() => console.log('按钮被点击'));
// 5. throwError: 创建一个只报错的流
// throwError(() => new Error('哎呀出错了')).subscribe();

四、 常用转换操作符

这是 RxJS 最强大的部分,管道 语法是 Angular 18+ 的标准写法。

import { map, filter, pluck } from 'rxjs/operators';
of(1, 2, 3, 4, 5).pipe(
  // 1. map: 转换数据 (类似数组的 map)
  map(x => x * 10), 
  
  // 2. filter: 过滤数据 (只有 true 才会通过)
  filter(x => x > 20)
).subscribe(console.log); 
// 输出: 30, 40, 50
// 3. pluck: 提取对象属性 (已废弃,推荐用 map)
// 旧写法: source$.pipe(pluck('user', 'name'))
// 新写法:
interface User { name: string; age: number; }
const user$: Observable<User> = of({ name: 'Tom', age: 18 });
user$.pipe(map(user => user.name)).subscribe(console.log);

五、 工具操作符 (面试高频)

用于处理流的逻辑,如限流、防抖、错误处理。

import { delay, tap, catchError, takeUntil, debounceTime } from 'rxjs/operators';
import { of, Subject, throwError } from 'rxjs';
// 1. tap: 副作用操作 (不修改数据,通常用于打印日志、存 LocalStorage)
of('Hello').pipe(
  tap(val => console.log('处理前:', val)), 
  delay(1000) // 延迟1秒发射
).subscribe(val => console.log('处理后:', val));
// 2. catchError: 错误捕获 (让流不中断)
throwError(() => new Error('网络错误')).pipe(
  catchError(err => {
    console.error(err);
    // 捕获错误后,返回一个新的 Observable 给下游,防止程序崩溃
    return of('默认数据'); 
  })
).subscribe(console.log); // 输出: 默认数据
// 3. debounceTime: 防抖 (用户停止输入 300ms 后才发送请求)
fromEvent(document.querySelector('input')!, 'input').pipe(
  debounceTime(300)
).subscribe((event: any) => console.log(event.target.value));
// 4. takeUntil: 立即取消订阅 (在 Angular 组件销毁时最常用)
const destroy$ = new Subject<void>();
interval(1000).pipe(
  takeUntil(destroy$) // 当 destroy$ 发出值时,上面的流自动停止
).subscribe(console.log);
// 模拟组件销毁
setTimeout(() => {
  destroy$.next(); // 停止上面的 interval
  destroy$.complete();
}, 5000);

六、 高阶操作符 (处理嵌套流)

当一个 Observable 发出的数据还是一个 Observable 时使用。

import { mergeMap, switchMap, concatMap, exhaustMap } from 'rxjs/operators';
// 场景:点击按钮 -> 发送 HTTP 请求
// 假设 click$ 是点击事件流, getData(id) 返回 Observable
// 1. mergeMap (并行): 点击一次发一次请求,不管上一个有没有完成。
// 适用:并发上传,互不干扰。
click$.pipe(
  mergeMap(() => this.http.get('/api/data'))
).subscribe();
// 2. switchMap (切换): **面试必考**。如果有新请求,取消旧请求。
// 适用:搜索框输入。
searchInput$.pipe(
  switchMap(keyword => this.http.search(keyword)) 
).subscribe();
// 3. concatMap (串行): 等前一个请求完成,再发下一个。
// 适用:必须按顺序执行的任务。
// 4. exhaustMap (排他): 如果有请求正在进行,忽略新的点击。
// 适用:防止重复提交表单。
submitBtn$.pipe(
  exhaustMap(() => this.http.submit())
).subscribe();

七、 Subject (多播)

普通的 Observable 是单播的;Subject 可以让多个订阅者共享同一个数据源。

import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';
// 1. Subject: 只有订阅后发出的数据才会收到。
const subject = new Subject<number>();
subject.subscribe(n => console.log('A:', n));
subject.next(1); // A 收到 1
subject.subscribe(n => console.log('B:', n));
subject.next(2); // A 收到 2, B 收到 2 (B 错过了 1)
// 2. BehaviorSubject: 必须有初始值,新订阅者会立即收到**最新**的值。
const bs = new BehaviorSubject<number>(0); // 初始值 0
bs.subscribe(n => console.log('C:', n)); // C 立即收到 0
bs.next(100);
// 3. ReplaySubject: 可以缓存最近的 N 个值,新订阅者会收到缓存的历史记录。
const rs = new ReplaySubject(2); // 缓存最近 2 个
rs.next(1);
rs.next(2);
rs.next(3);
rs.subscribe(n => console.log('D:', n)); // D 收到 2 和 3

八、 Angular 实战:AsyncPipe (语法糖)

在 Angular 中,你甚至不需要手动调用 .subscribe()

// 组件 TS
export class MyComponent {
  // 自动处理订阅、取消订阅、变化检测
  data$ = of([{ name: 'Tom' }, { name: 'Jerry' }]); 
}
// 组件 HTML
<div *ngFor="let item of data$ | async">
  {{ item.name }}
</div>

注意: 如果你需要拿到数据后在 TS 逻辑里做复杂处理,还是需要手动 subscribe 并配合 takeUntil 使用。

总结速查表

类别 操作符 作用
创建 of, from, interval 造数据
转换 map, filter 改数据
工具 tap, delay, debounceTime 辅助/拦截
组合 switchMap, mergeMap 处理嵌套流 (HTTP)
生命周期 takeUntil, first, take 管理订阅
错误 catchError, retry 异常处理
多播 Subject, BehaviorSubject 跨组件通信

开发者必看!TinyPro中后台系统最新Springboot上手指南~

2026年1月20日 15:32

本文由TinyPro中后台系统贡献者周泽龙原创。

在长达三个月的开发下,终于TinyPro的Springboot后端版本终于要问世了,在本期内容中我将带大家一步步去搭建整个后端的流程,也将带大家去探索对于最新版本的更改应该如何实现,以及如何使用本项目进行一个二次的开发和探索。 首先我们先要对于TinyPro项目进行一个整体的拉取,去到TinyPro的官方进行拉取,当我们获取到项目以后就可以进行开始今天的项目构建了。

接下来的流程就是对于前端i项目的搭建以及后端的springboot项目的搭建,最后再去介绍咱们新版本里面的一些特性和组件

1.前端部分的搭建

首先要确保咱们安装了Node.js、NPM、TinyCLI接下来就要正式初始化项目了首先我们进行初始化

(1)在命令行输入tiny init pro对项目进行一个初始化具体的流程可以看我的视频介绍

img_v3_02u4_d0333bf1-9980-431d-be2b-dbfe8a3e66cg.jpg
(2)接下来就让我们进入到我们的项目里面,tinyvue的前端代码里面我们首先进行一个项目的依赖的下载大家可以使用npm install进行项目依赖的下载。

(3)当我们项目依赖下载完成后就可以进入到一个启动流程了,使用npm start进行一个项目的启动启动后就会开启3031端口这样就可以看见项目的启动界面了!

img_v3_02u4_000ed57c-e850-4949-9f38-5309dd9e69ag.png

到目前为止我们的前端项目就算正式启动成功了,接下来让我们一起开始启动后端项目

2.后端项目的搭建

首先我们需要确保自己的本地环境里面有jdk17,maven,mysql,redis以及一个自己喜欢的开发软件可以idea或者vscode

好了准备工作做好以后接下来就让我们进入后端的开发和后端二次开发的一个介绍并且我也将带着大家去了解springboot里面的一些设计和里面的一些函数的内容接下来开始吧

项目结构的介绍: 当进入到项目里面的时候我们最直观的可以看见项目的一个整体结构

img_v3_02u4_fefceef2-2098-4f7d-8b32-9895a6f1faag.png
(1)先介绍一下项目的一个配置文件,对于所有的springboot项目上来第一件事就算看配置文件application.properties文件这个文件里面包含了所有项目需要的配置比如:mysql,redis,Springjpa,mybatis-plus(项目里面没有使用,但是基本的配置都配置好了,也就兼容了喜欢使用mybatis-plus的同学)大家可以更具自己的数据库信息和redis进行配置,需要自己填写好数据库的用户名,端口和驱动地址,还有redis的配置信息比如主机地址和端口号

到这里的同学,那就恭喜大家数据服务的配置我们就是做好了,接下来就是对项目的依赖的下载,这块主要涉及到maven的使用,如果还,没有下载maven的同学记得赶快去下载

(2)接下来开始项目依赖的初始化过程,在项目启动的时候,我们需要先对项目的依赖包去官方的仓库里面下载(这块给大家一个提醒,如果下载过慢的同学记得去配置一下maven的国内镜像源进行下载和配置),敲入命令 mvn install进行一个项目依赖的下载。

如果到这里都执行成功,大家就可以正式的启动项目,正式启动项目之前我希望大家可以去查看自己jdk的配置是否是17,因为接下来的必须要使用jdk17了

(3)进入到TinyProApplication文件里面进行启动项目,在这之前需要确保启动了redis和mysql的服务,并且配置好了密码,然后启动项目以后我们就会看到一个提示:

img_v3_02u4_fe85ca7b-734e-404e-87b6-88039cbcd7dg.png
这里就算证明项目的整体正式启动成功了,接下来就开始监听3000端口了。

项目启动成功以后就可以开始进行一个交互了,大家就可以进入到刚才启动的前端项目里面准备进行一个交互,账户和密码都是admin,这块是配置里面预先写好的,如果有人需要修改这个用户和角色名称,可以进到 DataInitializer文件里面找到user配置进行修改

3.二次开发

这个项目中支持二次开发的模块包括:权限管理拒绝策略,以及用户的登录校验初始化配置

img_v3_02u4_2f360650-8906-4132-92e6-72d89be6f99g.png

(1)首先就是项目的权限管理的问题大家可以看见代码里面首先需要权限校验的接口上面都会有一个

6.png
@PermissionAnnotation这个注解里面配置的就是当前接口需要用户所拥有的权限,然后这块里面底层的实现细节在aspect这个目录里面,然后里面就是对于apo的一个使用。如果大家需要给某一个接口增加新的权限大家就可以直接在接口的上面进行一个使用然后写入具体要限制的细节 比如可以写:

7.png
这块就是要求用户必须要有menu::query::list这个权限才能进入到这个接口里面进行查询操作如果大家想更进一步了解到权限管理的细节,可以去看aop的使用java里面的切面编程

(2)接下来可以看拒绝的策略,首先对于接口拒绝策略的具体控制在配置文件里面,大家可以看到

8.PNG\
这块就是一个拒绝策略的开关,如果大家想开始拒绝策略就可以直接输入true这个然后就会开启拒绝策略进行项目模式,目前是默认在演示模式里面

这个里面主要分为一个演示模式和一个项目模式,在项目模式里面大家可以自由的进行控制但是在演示模式里面,有很多的功能都被禁止了,所以大家要是不能使用的话就需要先查看是否是因为在演示模式里面导致的

(3)接下来就是用户的登录校验,大家首先要明白的一个流程就是用户首先要登录,只有登录成功以后才会将token放到redis里面,然后用户登录的校验就会先去redis里面进行查询,如果查询的到就会通过校验,如果redis里面没有当前用户人的信息就会进行一个拒绝的返回,然后就会跳转到前端的登录界面里面进行一个登录。具体就是拿一个拦截器进行拦截然后对每一个请求都进行校验只有登录过的才能进行项目的操作 (4)项目的初始化整个项目的初始化都在DataInitializer.java这个文件里面,如果后续需要进行一个项目的初始化调整,比如更改初始化的顺序以及在初始化的过程中想再加载一些资源都可以在这个文件里面进行增加

9.png

在这个run方法里面进行添加,这样项目在启动的时候就会先去加载项目里面的内容然后生成一个data文件夹的,这就标志着项目以及初始化过了,不需要再进行初始化,接下来每次的项目初始化都会先去看项目里面是否有data的目录如果存在就不走初始化的逻辑了

好了讲解完二次开发以后,接下来就要进入到docker的一个部署流程,在这个之前,大家可以更具的自己的情况去看是去买一个云服务器还是自己搭建一个虚拟机环境,然后进行配置,我在视频里面给搭建演示的就是在自己的虚拟机里面进行一个docker的部署和调用

4.docker部署

首先要了解在进行docker部署的时候,自己的容器文件里面的内容是否创建好了,以及对应的docker-compose.yml的一个配置

再检查完这些内容以后就要进入到我们的一个docker的部署流程环节,其实本质上也很简单就是进入到项目的文件夹目录里面,然后直接执行docker compose up -d这个命令以后,等待下载,但是下载的过程里面会有很多的问题比如下载过慢问题

(1)将项目的文件上传到服务器上面

10.png

然后进入当前目录大家可以看见,项目里面有两个文件一个是Dockerfile另一个是docker-compose.yml着两个文件是我们必须要的文件,进入进去看见

11.png

里面就是一些配置比如mysql的地址以及redis的地址,都是对应着我们即将启动的容器名称

(2)接下来就开始正式的启动docker-compose.yml文件,使用命令docker compose up -d启动成功以后就可以进行前端端口的配置映射到线上的docker地址,方便未来的开发
11.png

这个就是启动成功了,大家可以看映射的地址进行修改前端的配置了

5.本次参加开源之夏的感受和收获

在参加完这次的开源之夏以后,我最大的感受就是第一次有一个整齐的计划和老师还有别的学校的同学们可以一起开发一个软件,让我还没出社会的时候就已经拥有了独立开发的经验和经历。其次就是老师的辅导和社区的教导让我真的成长了很多,我特别感谢开源之夏和+OpenTiny社区对我的帮助,最后谢谢我的导师(真的很牛),他也很耐心的教我,特别感谢名字的话就不说了,不然以后有人烦他去了

谢谢大家我真的很珍惜这次机会,谢谢开源之夏,谢谢OpenTiny社区,谢谢导师,那我的这次开源之旅就结束,但是我相信只是暂时,我以后还会继续投身到开源里面,也希望可以帮助更多的人

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网:opentiny.design
OpenTiny 代码仓库:github.com/opentiny
TinyPro 源码:github.com/opentiny/ti…
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

幼儿园前端 #08:图片标签 img —— 为什么你的图片裂了?(含懒加载秘籍)

作者 Flinton
2026年1月19日 12:04

前言: 大家好,我是[小奇腾]。欢迎来到 “幼儿园前端” 第 8 集! 俗话说“一图胜千言”。一个全是文字的网页就像一本枯燥的教科书,谁也看不下去。 今天我们来学习 HTML 里颜值最高的标签: <img> 。 别看它简单,这里面藏着两个大坑:路径不对图片会裂不写 alt 会被搜索引擎扣分。今天带你完美避坑!

本期详细的视频教程bilibili:幼儿园前端 #08:图片标签 img —— 为什么你的图片裂了?(含懒加载秘籍)

一、 特立独行的“单标签”

我们之前学的标签大多是成双成对的(比如 <div>...</div>),但图片标签是独行侠。 它不需要“结束标签”,因为它肚子里不需要包文字,它自己就是一个完整的个体。

  • 语法<img src="..." />
  • 注意:在 HTML5 标准里,最后那个 / 可以不写,但我建议新手养成习惯写上,显得严谨。

二、核心属性:src (Source)

这是图片的生命线src 告诉浏览器: “去哪里找这张图?”

1. 网络图片(借别人的图)

<img src="https://img.alicdn.com/bao/upload/O1CN015FKtzh1bi8SB2Hkp5_!!6000000003498-0-yinhe.jpg_360x360q90.jpg" alt="蓝牙耳机">
  • 优点:不用存电脑里,省事。

  • 缺点:如果别人的网站挂了,你的图也就裂了。

2. 本地图片(用自己的图)

把图片存在你的项目文件夹里。 假设你的文件夹结构是这样的:

📂 demo-08
 ├── index.html
 └── logo.png  <-- 图片就在旁边
<img src="./logo.png" />
  • ./ 的意思是: “就在当前目录下找”

新手巨坑警告 🚫: 千万不要用你电脑的绝对路径! ❌ <img src="C:/Users/Administrator/Desktop/logo.png" /> 这种代码发给别人或者传到服务器上,图片百分之百会裂开(因为别人的电脑里没有 C 盘的这个文件夹)。请一定要用相对路径(./)!

三、 被忽视的神级属性:alt

很多新手写代码也是“外貌协会”,觉得图片显示出来就行了,alt 属性经常空着。 大错特错! alt (Alternative text) 是 “替代文本” 的意思。

<img src="cat.jpg" alt="一只橘色的猫在晒太阳" />

它的三大作用:

  1. 图片挂了时:如果网速慢或图片路径错了,浏览器会显示这行字,告诉用户“这里原本是一只猫”。
  2. 给盲人听的:盲人看不见图片,屏幕阅读器会读出“一只橘色的猫在晒太阳”。如果你不写,盲人只能听到冷冰冰的“图像”二字。
  3. 给百度看的:搜索引擎不知道图片里画的是猫还是狗,它全靠 alt 来判断。想让你的图在百度图片里搜到?写好 alt

四、 尺寸控制:width / height

你可以直接在标签里限制图片大小:

<img src="./downloaded-image.jpeg" width="200" height="200" />
  • 单位:默认是像素 (px),不用写单位。
  • 小技巧通常只写一个(比如只写 width),高度会自动按比例缩放。如果两个都写死,图片可能会被压扁或拉长,变得很丑。

五、 性能大招:loading="lazy" (懒加载)

如果你的网页有 100 张大图,用户还没往下划,浏览器就把 100 张全加载了,这会让网页打开巨慢,还浪费用户流量。

HTML5 现代神器

<img src="big-photo.jpg" loading="lazy" />
  • 作用:告诉浏览器“别急着加载”。只有当用户快滚动到这张图的时候,才开始加载它。
  • 好处:秒开网页,用户体验极佳!(Chrome/Edge 原生支持)

六、 总结:标准的图片写法

以后写图片,请直接复制这套**“黄金标准”**:

<img 
  src="./images/avatar.jpg" 
  alt="用户的头像" 
  width="100"
  loading="lazy"
/>

七、懒加载秘籍

  1. 打开检查工具(F12)
  2. 找到 Network (网络)
  3. 切记勾选清除缓存
  4. 刷新页面
  5. 往下滚轮,会看到需要滑动到一定距离才会加载图片。这样可以大幅度提供性能。因为一个网页一般会包含非常多的图片。
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>懒加载测试</title>
    <style>
        /* 搞一个很高的方块,把图片顶下去 */
        .spacer {
            /* 原来是 2000px 如果不生效,改成 5000px!给浏览器一点震撼 */
            height: 5000px;
            background-color: #f0f0f0;
            text-align: center;
            padding-top: 50px;
        }
    </style>
</head>

<body>
    <div class="spacer">
        <h1>往下滚动,下面有一张大图 👇</h1>
        <p>(这里全是空白,用来占位的)</p>
    </div>

    <!-- 引入图片 -->
    <img 
        src="https://img.alicdn.com/bao/upload/O1CN015FKtzh1bi8SB2Hkp5_!!6000000003498-0-yinhe.jpg_360x360q90.jpg" 
        loading="lazy" 
        width="600" 
        alt="测试图片"
    />

    <!-- 本地图片 -->
    <img src="./imgs/downloaded-image.jpeg"
        loading="lazy" width="600" alt="测试图片" />


    <p>图片终于出来啦!</p>
</body>

</html>

Galaxy比数平台功能介绍及实现原理|得物技术

作者 得物技术
2026年1月20日 14:17

一、背景

得物经过10年发展,计算任务已超10万+,数据已经超200+PB,为了降低成本,计算引擎和存储资源需要从云平台迁移到得物自建平台,计算引擎从云平台Spark迁移到自建Apache Spark集群、存储从ODPS迁移到OSS。

在迁移时,最关键的一点是需要保证迁移前后数据的一致性,同时为了更加高效地完成迁移工作(目前计算任务已超10万+,手动比数已是不可能),因此比数平台便应运而生。

二、数据比对关键挑战与目标

关键挑战一:如何更快地完成全文数据比对

现状痛点:

在前期迁移过程中,迁移同学需要手动join两张表来识别不一致数据,然后逐条、逐字段进行人工比对验证。这种方式在任务量较少时尚可应付,但当任务规模达到成千上万级别时,就无法实现并发快速分析。

核心问题:

  • 效率瓶颈:每天需要完成数千任务的比对,累计待迁移任务达10万+,涉及表数十万张。
  • 扩展性不足:传统人工比对方式无法满足大规模并发处理需求。

关键挑战二:如何精准定位异常数据

现状痛点:

迁移同学在识别出不一致数据后,需要通过肉眼观察来定位具体问题,经常导致视觉疲劳和分析效率低下。

核心问题:

  • 分析困难:在比对不通过的情况下,比对人员需要人工分析失败原因。
  • 复杂度高:面对数据量庞大、加工逻辑复杂的场景,特别是在处理大JSON数据时,肉眼根本无法有效分辨差异。
  • 耗时严重:单次比对不通过场景的平均分析时间高达1.67小时/任务。

比数核心目标

基于以上挑战,数据比对系统需要实现以下核心目标:

  • 高并发处理能力:支持每天数千任务的快速比对,能够处理10万+待迁移任务和数十万张表的规模。
  • 自动化比对机制:实现全自动化的数据比对流程,减少人工干预,提升比对效率。
  • 智能差异定位:提供精准的差异定位能力,能够快速识别并高亮显示不一致的字段和数据。
  • 可视化分析界面:构建友好的可视化分析平台,支持大JSON数据的结构化展示和差异高亮。
  • 性能优化:将用户单次比对分析时间从小时级大幅缩短至分钟级别。
  • 可扩展架构:设计可水平扩展的系统架构,能够随着业务增长灵活扩容。

三、解决方案实现原理

快速完成全文数据比对方法

比数方法调研

待比对两表数据大小:300GB,计算资源:1000c

经过调研分析比数平台采用第二种和第三种相结合的方式进行比数。

先Union再分组数据一致性校验原理

假如我们有如下a和b两表张需要进行数据比对

表a:

表b:

表行数比较:

select count(1from a ;
select count(1from b ;

针对上面的查询结果,如果数量不一致则退出比对,待修复后重新比数;数量一致则继续字段值比较。

字段值比较:

第一步:union a 和 b

select 1 as _t1_count, 0 as _t2_count, `id``name``age``score`
from a
union all
select 0 as _t1_count, 1 as _t2_count, `id``name``age``score`
from b

第二步:sum(_t1_count),sum(_t2_count) 后分组

select sum(_t1_count) as sum_t1_count, sum(_t2_count) as sum_t2_count, `id``name``age``score`
from (
select 1 as _t1_count, 0 as _t2_count, `id``name``age``score`
from a
union all
select 0 as _t1_count, 1 as _t2_count, `id``name``age``score`
from b
) as union_table
group by `id``name``age``score`

第三步:把不一致数据写入新的表中(即上面表中sum_t1_count和sum_t2_count不相等的数据)

drop table if exists a_b_diff_20240908;
create table a_b_diff_20240908 as select * from (
select sum(_t1_count) as sum_t1_count, sum(_t2_count) as sum_t2_count, `id``name``age``score`
from (
select 1 as _t1_count, 0 as _t2_count, `id``name``age``score`
from a
union all
select 0 as _t1_count, 1 as _t2_count, `id``name``age``score`
from b
) as union_table
group by `id``name``age``score`
having sum(_t1_count) <> sum(_t2_count)
) as tmp

如果a_b_diff_20240908没有数据则两张表没有差异,比数通过,如有差异如下:

第四步:读取不一致记录表,根据主键(比如id)找出不一致字段并写到结果表中。

第五步:针对不一致字段的数据进行根因分析,如 json 、数组顺序问题、浮点数精度问题等,给出不一致具体原因。

哈希值聚合实现高效一致性校验

针对上面union后sum 再 group by 方式 在数据量大的时候还是非常耗资源和时间的,考虑到比数任务毕竟有70%都是一致的,所以我们可以先采用哈希值聚合比较两表的的值是否一致,使用这种高效的方法先把两表数据一致的任务过滤掉,剩下的再采用上面方法继续比较,因为还要找出是哪个字段哪里不一致。原理如下:

SELECT count (*),SUM(xxhash64(cloum1)^xxhash64(cloum2)^...) FROM tableA 
EXCEPT 
SELECT count(*),SUM(xxhash64(cloum1)^xxhash64(cloum2)^...) FROM tableB

如果有记录为空说明数据一致,不为空说明数据不一致需要采用上面提到union 分组的方法去找出具体字段哪里不一样。

通过哈希值聚合,单个任务比数时间从500s降低到160s,节省大约70%的时间。

找到两张表不一致数据后需要对两张的数据进行分析确定不一致的点在哪里?这里就需要知道表的主键,根据主键逐个比对两张表的其他字段,因此系统会先进行主键的自动探查,以及无主键的兜底处理。

精准定位异常数据实现方法

自动探查主键:实现原理如下

刚开始我们采用的前5个字段找主键的方式,如下:

针对表a的前5个字段 循环比对
select count(distinct id) from a 与 select count(1from a 比较 ,如相等主键为id ,不相等继续往下执行
select count(distinct id,name) from a 与 select count(1from a比较,如相等主键为id,name ,不相等继续往下执行
select count(distinct id,name,age) from a 与 select count(1from a比较,如相等主键为id,name,age ,不相等继续往下执行,直到循环结束

采用上面的方法不一致任务中大约有49.6%任务自动探查主键失败:因此需重点提升主键识别能力。

针对以上主键探查成功率低的问题,后续进行了一些迭代,优化后的主键探查流程如下:

一、先采用sum(hash)高效计算方式进行探查:

1.先算出两张表每个字段的sum(hash)值  。

select sum(hash(id)),sum(hash(name)),sum(hash(age)),sum(hash(score)) from a 
union all 
select sum(hash(id)),sum(hash(name)),sum(hash(age)),sum(hash(score)) from b;

2.找出值相等的所有字段,本案例中为 id, name。

3.对id,name 可能是主键进一步确认,先进行行数校验,如 select count(distinct id,name) from a 的值等于select count(1) from a 则进一步校验,否则进入到第二种探查主键方式。

4.唯一性验证,如果值为0则表示探查主键成功,否则进入到第二种探查主键方式。

slect count(*from ((select id,name from a ) expect (select id,name from b))

二、传统distinct方式探查:

针对表a的前N(所有字段数/2或者前N、后N等)个字段 循环比对:

1.select count(distinct id) from a与select count(1) from a比较 ,如相等主键为id ,不相等继续往下执行。

2.select count(distinct id,name) from a 与 select count(1) from a比较,如相等主键为id,name ,不相等继续往下执行。

3.select count(distinct id,name,age) from a 与 select count(1) from a比较,如相等主键为id,name,age ,不相等继续往下执行,直到循环结束。

三、全字段排序模拟:

如果上面两种方式还是没有找到主键则把不一致记录表进行全字段排序然后对第一条和第二条记录挨个字段进行分析,找出不一致内容,示例如下:

slect * from a_b_diff_20240908 order by id,name,age,score asc limit 10;

通过以上结果表可以得出两表的age字段不一致 ,score不一致(但按key排序后一致)。

如果以上自动化分析还是找不到不一致字段内容,可以人工确认表的主键后到平台手动指定主键字段,然后点击后续分析即可按指定主键去找字段不一致内容。

通过多次迭代优化找主键策略,找主键成功率从最初的50.4%提升到75%,加上全字段order by排序后最前两条数据进行分析,相当于可以把找主键的成功率提升到90%以上。

根因分析:实现原理如下

当数据不一致时,平台会根据主键找出两个表哪些字段数据不一致并进行分析,具体如下:

  • 精准定位: 明确指出哪条记录、哪个字段存在差异,并展示具体的源数据和目标数据值。
  • 智能根因分析: 内置了多种差异模式识别规则,能够自动分析并提示不一致的可能原因,例如:
  • 精度问题:如浮点数计算1.0000000001与1.0的差异。
  • JSON序列化差异:如{"a":1, "b":2}与{"b":2, "a":1},在语义一致的情况下,因键值对顺序不同而被标记为差异。同时系统会提示排序后一致。
  • 空值处理差异:如NULL值与空字符串""的差异判定。
  • 日期时区转换问题:时间戳在不同时区下表示不同。

  • 比对结果统计: 提供总数据量、一致数据量、不一致数据量及不一致率百分比,为项目决策提供清晰的量化依据。
  • 比数人员根据平台分析的差异原因,决定是否手动标记通过或进行任务修复。
  • 效果展示:

四、比数平台功能介绍

数据比对基本流程

任务生成:三种比对模式

  • 两表比对: 最直接的比对方式。用户只需指定源表与目标表,平台即可启动全量数据比对。它适用于临时比对的场景。
  • 任务节点比对: 一个任务可能输出多个表,逐一配置这些表的比对任务繁琐且易遗漏,任务节点比对模式完美解决了这一问题。用户只需提供任务节点ID,平台便会自动解析该节点对应的SQL代码,提取出所有输出表,并自动生成比对任务,极大地提升任务迁移比对效率。
  • SQL查询比对: 业务在进行SDK迁移只关心某些查询在迁移后数据是否一样,因此需要对用户提交的所有查询SQL进行比对,平台会分别在ODPS和Spark引擎上执行该查询,将结果集导出到两张临时表,再生成比对任务。

前置校验:提前发现问题

在启动耗时的全量比对之前,需要对任务进行前置校验,确保比对是在表结构一致、集群环境正常的情况下进行,否则一旦启动比数会占用大量计算资源,最后结果还是比数不通过,会影响比数平台整体的运行效率。因此比数平台一般会针对如下问题进行前置拦截。

  • 元数据一致性校验: 比对双方的字段名、字段类型、字段顺序、字段个数是否一致。
  • 函数缺失校验: 针对Spark引擎,校验SQL中使用的函数是否存在、是否能被正确识别,避免因函数不支持而导致的比对失败。
  • 语法问题校验: 分析SQL语句的语法结构,确保其在目标引擎中能够被顺利解析,避免使用了某些特定写法会导致数据出现不一致情况,提前发现语法层面问题,并对任务进行改写。

更多校验点如下:

通过增加以上前置校验拦截,比数任务数从每天3000+下降到1500+, 减少50% 的无效比数,其中UDF缺失最多,有效拦截任务1238,缺少函数87个(帮比数同学快速定位,一次性解决函数缺失问题,避免多次找引擎同学陆陆续续添加,节省双方时间成本)。

破解比数瓶颈:资源分配与任务调度优化

由于比数平台刚上线的时候只有计算迁移团队在使用,后面随着更多的团队开始使用,性能遇到了如下瓶颈:

1.资源不足问题: 不同业务(计算迁移、存储迁移、SDK迁移)的任务相互影响,基本比数任务与根因分析任务相互抢占资源。

2.任务编排不合理: 没有优先级导致大任务阻塞整体比数进程。

3.引擎参数设置不合理: 并行度不够、数据分块大小等高级参数。

针对以上问题比数平台进行了如下优化:

  • 按不同业务拆分成多个队列来运行,保证各个业务之间的比数任务可以同时进行,不会相互影响。
  • 根因分析使用单独的队列,与数据比对任务的队列分开,避免相互抢占资源发生“死锁”。
  • 相同业务内部按批次分时段、分优先级运行,保障重要任务优先进行比对。
  • 针对Spark引擎默认调优了公共参数、并支持用户自主设置其他高级参数。

通过以上优化达到到了如下效果:

  • 比数任务从每天22点完成提前至18点前,同时支持比数同学自主控制高优任务优先执行,方便比数同学及时处理不一致任务。
  • 通过优化资源队列使用方式,使系统找不到主键辅助用户自主找主键接口响应时间从58.5秒降到 26.2秒。

五、比数平台收益分享

平台持续安全运行500+天,每日可完成2000+任务比对,有效比数128万+次,0误判。

  • 助力计算迁移团队节省45+人日/月,完成数据分析、离线数仓空间任务的比对、交割。
  • 助力存储迁移团队完成20%+存储数据的迁移。
  • 助力引擎团队完成800+批次任务的回归验证,确保每一次引擎发布的安全及高效。
  • 助力SDK迁移团队完成80%+应用的迁移。

六、未来演进方向

接下来,平台计划在以下方面持续改进:

智能分析引擎: 针对Json复杂嵌套类型的字段接入大模型进行数据根因分析,找出不一致内容。

比对策略优化: 针对大表自动切分进行比对,降低比数过程出现因数据量大导致异常,进一步提升比对效率。

通用方案沉淀: 将典型的比对场景和解决方案能用化,应用到更多场景及团队中去。

七、结语

比数平台是得物在迁移过程中,为了应对海量任务、大数据量、字段内容复杂多样、异常数据难定位等挑战,确保业务迁移后数据准确而专门提供的解决方案,未来它不单纯是一个服务计算迁移、存储迁移、SDK迁移、Spark版本升级等需要的数据比对工具,而是演进为数据平台中不可或缺的基础设施。

往期回顾

1.得物App智能巡检技术的探索与实践

2.深度实践:得物算法域全景可观测性从 0 到 1 的演进之路 

3.前端平台大仓应用稳定性治理之路|得物技术

4.RocketMQ高性能揭秘:承载万亿级流量的架构奥秘|得物技术

5.PAG在得物社区S级活动的落地

文 /Galaxy平台

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

[Python3/Java/C++/Go/TypeScript] 一题一解:位运算(清晰题解)

作者 lcbin
2026年1月20日 07:21

方法一:位运算

对于一个整数 $a$,满足 $a \lor (a + 1)$ 的结果一定为奇数,因此,如果 $\text{nums[i]}$ 是偶数,那么 $\text{ans}[i]$ 一定不存在,直接返回 $-1$。本题中 $\textit{nums}[i]$ 是质数,判断是否是偶数,只需要判断是否等于 $2$ 即可。

如果 $\text{nums[i]}$ 是奇数,假设 $\text{nums[i]} = \text{0b1101101}$,由于 $a \lor (a + 1) = \text{nums[i]}$,等价于将 $a$ 的最后一个为 $0$ 的二进制位变为 $1$。那么求解 $a$,就等价于将 $\text{nums[i]}$ 的最后一个 $0$ 的下一位 $1$ 变为 $0$。我们只需要从低位(下标为 $1$)开始遍历,找到第一个为 $0$ 的二进制位,如果是第 $i$ 位,那么我们就将 $\text{nums[i]}$ 的第 $i - 1$ 位变为 $1$,即 $\text{ans}[i] = \text{nums[i]} \oplus 2^{i - 1}$。

遍历所有的 $\text{nums[i]}$,即可得到答案。

###python

class Solution:
    def minBitwiseArray(self, nums: List[int]) -> List[int]:
        ans = []
        for x in nums:
            if x == 2:
                ans.append(-1)
            else:
                for i in range(1, 32):
                    if x >> i & 1 ^ 1:
                        ans.append(x ^ 1 << (i - 1))
                        break
        return ans

###java

class Solution {
    public int[] minBitwiseArray(List<Integer> nums) {
        int n = nums.size();
        int[] ans = new int[n];
        for (int i = 0; i < n; ++i) {
            int x = nums.get(i);
            if (x == 2) {
                ans[i] = -1;
            } else {
                for (int j = 1; j < 32; ++j) {
                    if ((x >> j & 1) == 0) {
                        ans[i] = x ^ 1 << (j - 1);
                        break;
                    }
                }
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    vector<int> minBitwiseArray(vector<int>& nums) {
        vector<int> ans;
        for (int x : nums) {
            if (x == 2) {
                ans.push_back(-1);
            } else {
                for (int i = 1; i < 32; ++i) {
                    if (x >> i & 1 ^ 1) {
                        ans.push_back(x ^ 1 << (i - 1));
                        break;
                    }
                }
            }
        }
        return ans;
    }
};

###go

func minBitwiseArray(nums []int) (ans []int) {
for _, x := range nums {
if x == 2 {
ans = append(ans, -1)
} else {
for i := 1; i < 32; i++ {
if x>>i&1 == 0 {
ans = append(ans, x^1<<(i-1))
break
}
}
}
}
return
}

###ts

function minBitwiseArray(nums: number[]): number[] {
    const ans: number[] = [];
    for (const x of nums) {
        if (x === 2) {
            ans.push(-1);
        } else {
            for (let i = 1; i < 32; ++i) {
                if (((x >> i) & 1) ^ 1) {
                    ans.push(x ^ (1 << (i - 1)));
                    break;
                }
            }
        }
    }
    return ans;
}

时间复杂度 $O(n \times \log M)$,其中 $n$ 和 $M$ 分别是数组 $\text{nums}$ 的长度和数组中的最大值。忽略答案数组的空间消耗,空间复杂度 $O(1)$。


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

❌
❌