阅读视图

发现新文章,点击刷新页面。

基于微信小程序实现幸运大转盘页面

基于微信小程序实现幸运大转盘页面,以下是搭建教程:

1. 创建项目结构

首先,确保你已经安装了微信开发者工具,然后创建一个新的微信小程序项目。在项目目录下,会有 pages 文件夹,我们在其中创建一个新的页面文件夹,例如 lucky-wheel,并在该文件夹下创建四个文件:lucky-wheel.js、lucky-wheel.json、lucky-wheel.wxml 和 lucky-wheel.wxss。

2. 编写 lucky-wheel.json 文件

这个文件用于配置页面的相关信息,以下是示例代码:

{
  "navigationBarTitleText": "幸运大转盘"
}

3. 编写 lucky-wheel.wxml 文件

该文件用于构建页面的结构,包含一个大转盘和一个开始按钮,示例代码如下:

<view class="container">
  <view class="wheel" style="transform: rotate({{rotateDeg}}deg); transition: transform 3s ease-out;">
    <!-- 这里可以使用图片或者自定义样式绘制转盘的每一个扇形 -->
    <view class="sector" style="transform: rotate(0deg);">奖品1</view>
    <view class="sector" style="transform: rotate(60deg);">奖品2</view>
    <view class="sector" style="transform: rotate(120deg);">奖品3</view>
    <view class="sector" style="transform: rotate(180deg);">奖品4</view>
    <view class="sector" style="transform: rotate(240deg);">奖品5</view>
    <view class="sector" style="transform: rotate(300deg);">奖品6</view>
  </view>
  <button bindtap="startRotate">开始抽奖</button>
</view>

4. 编写 lucky-wheel.wxss 文件

此文件用于设置页面的样式,包括大转盘和扇形的样式,示例代码如下:

.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

.wheel {
  position: relative;
  width: 300px;
  height: 300px;
  border-radius: 50%;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}

.sector {
  position: absolute;
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 150px solid #f0f0f0;
  transform-origin: 50px 150px;
  text-align: center;
  line-height: 200px;
}

5. 编写 lucky-wheel.js 文件

该文件用于实现页面的逻辑,包括开始抽奖和控制转盘旋转的功能,示例代码如下:

Page({
  data: {
    rotateDeg: 0,
    prizeList: [0, 60, 120, 180, 240, 300], // 每个奖品对应的角度
    isRotating: false
  },

  startRotate() {
    if (this.data.isRotating) return;
    this.setData({ isRotating: true });

    // 随机选择一个奖品
    const randomIndex = Math.floor(Math.random() * this.data.prizeList.length);
    const targetDeg = this.data.prizeList[randomIndex] + 360 * 5; // 旋转5圈后停在目标奖品处

    this.setData({
      rotateDeg: targetDeg
    });

    setTimeout(() => {
      wx.showToast({
        title: `恭喜你获得奖品${randomIndex + 1}`,
        icon: 'none'
      });
      this.setData({ isRotating: false });
    }, 3000); // 旋转动画时间为3秒
  }
});

运行项目

将上述代码保存后,在微信开发者工具中运行项目,即可看到幸运大转盘页面。点击“开始抽奖”按钮,大转盘会开始旋转,旋转停止后会弹出提示框显示中奖信息。添加更多奖品、美化界面等。

前沿多模态模型开发与应用实战3:DeepSeek-VL2多模态理解大模型算法解析与功能抢先体验

多模态理解大模型,是一类可以同时处理和理解多种数据形式(如图像、文本、视频等)的人工智能大模型,可以应用于图文理解、视觉问答、文档理解、场景描述等任务。本文将介绍目前热门的 DeepSeek-VL2多模态大模型。DeepSeek-VL2是一款基于混合专家(MoE,Mixture of Experts)架构的多模态大模型,结合了混合专家架构和多模态数据处理能力,通过稀疏计算和专家分工的方式高效处理多种模态(如文本、图像、音频等)的数据,推理时只激活部分网络参数。而前两期课程介绍的 Qwen2.5VL、Janus-Pro 以及 DeepSeek-VL第一代模型,则是经典的 Dense 类的多模态理解大模型,会对所有模型参数进行计算和更新。MoE(Mixture of Experts)混合专家模型的核心思想是将模型划分为多个专家子网络(experts),并通过路由机制(router)动态选择合适的专家来处理输入数据。MoE 的最大优势就是是稀疏激活,只有少数几个专家网络模块会被激活,这意味着计算量可以显著减少,计算效率得到提升,同时精度指标远远超出相同激活参数量的 Dense 类模型。

图片

DeepSeek-VL2在视觉理解上的效果展示

接下来,本篇文章内容将包括模型结构、训练流程、模型能力的展示,并以飞桨多模态开发套件 PaddleMIX 中 DeepSeek-VL2的实现为例,对代码进行逐步解读。

01 模型架构

DeepSeek-VL2的前身是去年发布的 DeepSeek-VL,其模型结构设计是经典的 Dense 模型结构,也就是有参数都会进行计算和更新。DeepSeek-VL 由三个主要模块组成:

  • Hybrid Vision Encoder:**混合视觉编码器,采用 SigLIP-L 作为视觉编码器,结合 SAM-B 和 SigLIP-L 编码器,能够高效处理高分辨率图像(1024×1024),同时保留语义和细节信息。高分辨率特征图经过插值和卷积处理后,与低分辨率特征图连接,生成具有2048个维度的视觉 token。

  • VL Adaptor:**视觉语言适配器,使用两层混合 MLP 桥接视觉编码器和语言模型。高分辨率和低分辨率特征分别经过单层 MLP 处理后沿维度连接,再通过另一层 MLP 转换到语言模型的输入空间。

  • DeepSeek LLM:**语言模型是 DeepSeek-LLM,其设计遵循 LLaMA,采用 Pre-Norm 结构和 SwiGLU 激活函数,使用旋转嵌入进行位置编码。

图片

DeepSeek-VL 架构

而近期发布的 DeepSeek-VL2尽管是 MoE 架构,但它也是由三部分核心模块组成:视觉编码器 Vision Encoder、视觉-语言适配器 VL Adaptor 和 DeepSeek-MoE 语言模型。与其前身 DeepSeek-VL 相比,DeepSeek-VL2在视觉编码器和语言建模部分都有了显著的提升,这主要是因为 DeepSeek-VL2引入了两项重大改进:动态切片策略,以及采用多头隐变量注意力(Multi-head Latent Attention,MLA)机制的 DeepSeek-MoE 语言模型。这些创新使得 DeepSeek-VL2能够更高效地处理高分辨率视觉输入和文本数据。

图片

DeepSeek-VL2架构

  • Vision Encoder:**DeepSeek-VL2采用的也是 SigLIP,同时引入了动态切片策略(Dynamic Tiling Strategy),能够处理不同分辨率和长宽比的高分辨率图像。传统的图像编码方法往往固定分辨率,导致在处理较大或不规则图像时性能下降。动态切片策略通过将高分辨率图像分割成多个小块进行处理,减少了计算成本,同时保留了详细的视觉特征。该方法避免了传统视觉编码器的固定分辨率限制,使得模型在处理复杂图像任务(如视觉引导、文档分析等)时具有更好的性能。

  • VL Adaptor:**DeepSeek-VL2采用两层多层感知器(MLP),然后再使用2×2 pixel shuffle 操作压缩每个图像块的 token 数目,用于视觉特征映射到文本空间。

  • DeepSeek-MoE LLM:**语言模型采用了 DeepSeek-MoE(Mixture of Experts)架构,并结合了多头潜在注意力机制(Multi-head Latent Attention,MLA)。MLA 机制能够有效压缩键值缓存(KV Cache),提升推理效率。MoE 架构则通过稀疏计算进一步提升了效率,使得模型在处理大规模数据时能够实现更高的吞吐量。

在模型尺寸上,DeepSeek-VL2系列目前有以下3个参数版本:DeepSeek-VL2-Tiny、DeepSeek-VL2-Small 和 DeepSeek-VL2,分别拥有1B、2.8B 和4.5B 的激活参数。具体的结构设置如下表所示:

图片

DeepSeek-VL2三种参数量的模型设置

02 创新点

2.1 动态图像切片编码策略

动态切片策略

DeepSeek-VL2将一张高分辨率图像切片,为了适应不同长宽比,首先定义了一组候选分辨率:CR={(m⋅384,n⋅384) ∣ m∈N,n∈N,1≤m,n,mn≤9}, m:n表示宽高比。对于一张(H,W)图像,在保证宽高比不变下调整图像分辨率,计算以长边对其到候选分辨率所需要的填充的区域面积。选择面积最小的分辨率 (mi⋅384,ni⋅384),然后将调整大小后的图像划分成 mi×ni 个384×384分辨率的局部图块以及一个全局缩略图块。出于计算效率和上下文长度管理的考虑,在处理多于2张图像时,禁用动态图块策略。

图片

DeepSeek-VL2中的动态切片策略

在将图片切片后,再使用2×2 pixel shuffle 操作压缩每个图像块的 token 数目,从27×27压缩至14×14=196 tokens 对于全局缩略图像块(14×14),在每一行的末尾添加14个标记,从而总共得到14×15=210个 tokens。当处理 mi×ni 个局部图像块时,在每一行的局部块末尾新增,共新增 mi⋅14个 tokens,完整的 Visual Token 包含210+1+mi⋅14×(ni⋅14+1) 个视觉标记,这些 Tokens 随后使用两层多层感知器(MLP)投影到语言模型的 Embedding 空间中。

2.2 DeepSeek-MoE语言模型

在语言模型部分,DeepSeek-VL2使用了 DeepSeek-MoE 语言模型,该模型结合了混合专家(Mixture of Experts, MoE)架构和多头潜在注意力(Multi-head Latent Attention,MLA)机制。MoE 架构通过选择性激活不同的专家网络,实现了计算资源的高效利用和模型性能的提升。而 MLA 机制 MLA 机制通过将键值缓存压缩为潜在向量,增强了推理效率,从而提升了吞吐量,且能够在处理多模态信息时,更好地捕捉到视觉和语言之间的复杂关系,进而提升模型在图文理解、问答等任务中的表现。

在 MoE 训练过程中,为每个专家引入了一个全局偏置项,以经济高效的方式改善专家之间的负载均衡。现有的 MoE 架构可能存在知识混杂(Knowledge Hybridity)和知识冗余(Knowledge Redundancy)的问题,限制了专家的专业化。在实现思想上,DeepSeek-MoE 采用两个主要策略:

  • Fine-Grained Expert Segmentation-细粒度的专家分割,通过细化 FFN 中间隐藏维度,维持参数数量不变的同时激活更多细粒度的专家,使得激活的专家更加灵活和适应性更强;

  • Shared Expert Isolation-共享专家隔离,将某些专家隔离为共享专家,始终激活,旨在捕捉和巩固不同上下文中的共同知识。

图片

DeepSeek-MOE 的架构

2.3 高校的推理速度与吞吐量

为了提升模型的推理速度,DeepSeek-VL2在语言部分的处理上引入了键值缓存压缩技术。这项技术能够有效减少计算中的冗余操作,从而提高推理过程的效率,尤其在处理大规模数据时表现出色。通过这种优化,DeepSeek-VL2在多个任务上不仅表现出了更高的准确率,也大大提升了计算效率。

03 训练方法

3.1 训练数据

DeepSeek-VL2从多种来源构建了一个综合性的视觉-语言数据集。训练过程分为三个阶段:(1)视觉-语言对齐(VL alignment);(2)视觉-语言预训练(VL pretraining);(3)监督微调(Supervised Fine-Tuning)。

1.VL alignment 数据

对齐阶段专注于训练多层感知机(MLP)VL Adaptor,以桥接预训练的视觉编码器和大型语言模型。这一阶段使用了 ShareGPT4V 数据集,该数据集包含大约120万个描述和对话样本。

2.VL-Pretrain 数据

VL-Pretrain 数据结合了视觉-语言数据和纯文本数据,以保持 VL 能力和纯文本性能之间的平衡。对于 DeepSeek-VL2,作者保持了大约70%的 VL 数据和30%的纯文本数据的比例,后者直接来源于作者基础大型语言模型(LLM)的预训练语料库。

Image-Text 混合数据

数据收集始于几个开源数据集,包括 WIT、WikiHow 和 OBELICS 中的30%随机样本。这一特定的混合比例是通过使用 DeepSeek-VL2-Tiny 进行初步实验确定的。为了增强多语言能力,在主要以英语为主的数据集中补充了从 Wanjuan 中提取的中文内容。此外,DeepSeek-VL2还开发了一个内部数据集,以扩大对一般现实世界知识的覆盖范围。

Image Caption 数据

图像描述是视觉语言模型(VLM)训练中的基础数据,提供了视觉信息和文本信息之间的直接对齐。因为开源数据集质量差异很大,为了解决这些质量不一致的问题,DeepSeek-VL2开发了一个全面的图像描述流程,该流程考虑了:(1)光学字符识别(OCR)提示;(2)元信息(例如位置、相机设置);(3)原始描述作为提示。DeepSeek-VL2使用内部 Captioner,使用 类似于 PixelProse 的提示策略重新为图像添加描述,采用不同的指令来指导 VLM 生成描述。尽管 Catpion 整体质量有所提高,在大规模标注流程中观察到了重复问题。为了缓解这一问题,DeepSeek-VL2采用一个质量控制流程,使用 DeepSeek Chat 仅根据 Caption 的写作质量进行评分。

OCR 数据

LaTex OCR 和12M RenderedText、包括不同文档类型的大规模内部数据集

VQA 数据

  • DeepSeek-VL 通用的 VQA 数据。

  • 表格、图表和文档理解数据。PubTabNet、FinTabNet 和 Docmatix。

  • Web-to-code 和 plot-to-Python 生成。Websight,并遵循 DeepSeek-VL 的方法,使用公开的 Jupyter 笔记本中的 Python 图表。通过使用 DeepSeek V2.5对 Websight 部分数据增强。作者还利用 DeepSeek V2.5生成的 Python 图表代码来减少 plot-to-code 中的噪声。

  • 包括视觉提示的 QA 数据:参考 Vip-llava 构建具有不同视觉提示(箭头、方框、圆圈和涂鸦)的数据。

Visual grounding 数据

基于 Kosmos-2和 Objects365构建 视觉定位数据,并采用以下模版构建

  • Prompt: \texttt{Locate <|ref|><|/ref|> in the given image.}

  • Response: \texttt{<|ref|><|/ref|><|det|>[[x1, y1, x2, y2],\ldots]<|/det|>}

Grounded 对话数据

基于 Kosmos-2构建视觉定位对话数据并采用以下模版构建

  • Prompt: \texttt{<|grounding|>Can you describe the content of the image?}
  • Response: $\texttt{Two <|ref|>dogs<|/ref|><|det|>[[x1, y1, x2, y2],\ldots]<|/det|> are running on the grass.}

3.SFT 数据

DeepSeek-VL2的 SFT 数据结合了多种开源数据集与高质量的内部 QA 对。

General visual question-answering

虽然 VQA 数据集种类繁多,但它们通常存在三大局限:(1)回答简短;(2)光学字符识别(OCR)质量不佳;(3)内容虚幻。为解决这些问题,DeepSeek-VL2综合考虑原始问题、图像和 OCR 信息来重新生成回答。作者的实验表明,这种方法能产生更全面、更准确的结果。在 DeepSeek-VL2的开发过程中早期版本,尤其是 Tiny 变体,偶尔会在中文回答中不恰当地插入英文单词。这一问题在 DeepSeek-VL2大型模型中并不存在,这表明它源于模型容量有限以及视觉-语言预训练阶段中英文数据的不平衡。为解决小型模型中的这一局限,DeepSeek-VL2团队开发了一个包含多样图像描述和单轮/多轮对话的内部中文问答数据集。该数据集有助于缓解语言混合问题。此外还创建了补充现实世界的和文化相关的视觉知识,包括动漫、网络梗、美食和艺术的内部数据集。

OCR and document understanding

得益于 DeepSeek-VL2先进的 Caption Pipeline,DeepSeek-VL2已经展现出比其他最先进的视觉语言模型(VLM)更优越的 OCR 能力。因此,在 SFT 阶段未进一步提升 OCR 性能,而是专注于清理现有的开源数据集,通过移除 OCR 质量不佳的样本。对于文档理解,DeepSeek-VL2团队从内部数据中筛选了一个多样化的文档页面子集。然后针对文档理解生成了多轮对话式问答对。

Table and chart understanding

通过对除 Cauldron(其已展现出高质量)外的所有公共数据集基于其原始问题重新生成回答,从而增强了基于表格的问答数据。与在视觉语言预训练阶段开发的 OCR 能力类似,的模型在图表理解方面也表现出色,且无需额外努力。

Textbook and academic questions

从文档集合中构建了一个专注于教科书的内部数据集。该数据集主要强调多个学科领域的大学水平内容。

Web-to-code and plot-to-Python generation

网页到代码与图表到 Python 代码生成。扩展了内部关于网页代码和 Python 图表代码的数据集,这些数据集超出了预训练期间所使用的范围。对于开源数据集,通过重新生成答案来提高其质量。

纯文本数据

为了保持模型的语言能力,在 SFT 阶段,还使用了纯文本指令调整数据集。

3.2 训练阶段

DeepSeek-VL2通过三阶段的流程进行训练:

  • 初始阶段:使用3.1.1节中详细描述的图文配对数据,训练视觉编码器和视觉-语言适配器 MLP,同时保持语言模型固定。
  • 预训练阶段:使用3.1.2节描述的数据进行视觉-语言预训练。在此阶段,所有模型参数,包括视觉编码器、视觉-语言适配器和语言模型,都会解锁并同时训练。
  • 微调阶段:使用第3.1.3节概述的数据进行有监督的微调,进一步优化模型性能。

在预训练和微调阶段,强调视觉理解能力,并仅在文本标记上计算下一个标记预测损失。

视觉-语言对齐

基于预训练的语言模型(DeepSeekMoE 3B/16B/27B),的主要目标是建立视觉特征和语言特征之间的稳固连接。这种对齐使得预训练的语言模型能够有效地处理视觉输入。与之前的方法不同,这些方法保持预训练的视觉编码器和语言模型固定,调整固定分辨率的视觉编码器以适应动态高分辨率图像。在这个阶段,优化视觉编码器和视觉-语言适配器,同时保持语言模型冻结。

视觉-语言预训练 在嵌入空间中建立视觉-语言对齐之后,将大部分计算资源用于视觉-语言预训练。这个阶段的重点是开发跨多种任务的综合性联合视觉-语言知识。解锁所有参数,包括视觉编码器、视觉-语言适配器和语言模型,并同时进行训练。通过这些阶段的系统训练,DeepSeek-VL2不仅能够处理高分辨率的视觉输入,还能够在多模态任务中表现出色。这种训练方法使得模型在多样化的任务中提高了视觉和语言理解能力。

有监督微调 在最后阶段,通过有监督的微调来增强预训练模型的指令跟随能力和对话能力。利用内部的视觉语言 SFT 数据,优化所有参数,但仅对答案和特殊标记进行监督,同时屏蔽系统和用户提示。为了加强对话理解,将多模态数据与来自 DeepSeek-V2的纯文本对话数据结合使用。这种方法确保了在各种视觉语言任务中具有强大的性能,包括密集图像描述、通用视觉问答(VQA)、光学字符识别(OCR)、表格/图表/文档/图形理解、视觉到代码、视觉推理、视觉定位和语言理解等。

图片

DeepSeek-VL2的训练超参数

3.3 结果评估

DeepSeek-VL2在多个常用的多模态基准数据集上进行了评估,包括 DocVQA、ChartQA、InfoVQA、TextVQA 等。这些基准涵盖了从文档理解到逻辑推理等多种任务,全面评估了 DeepSeek-VL2在不同任务上的表现。

视觉引导能力

DeepSeek-VL2在视觉引导任务上展现了强大的能力,能够根据图像中的描述性信息准确定位物体,并生成相应的回答。

多图像对话能力

DeepSeek-VL2在处理多图像对话任务时表现突出,能够分析多张图片之间的关系,并基于这些信息进行简单的推理。

视觉故事生成能力

在视觉故事生成任务中,DeepSeek-VL2能够根据图片创作出创意十足的故事,并且能够有效结合图像中的细节,如地标识别和 OCR 结果。

图片

DeepSeek-VL2 OCR 相关能力指标结果

图片

DeepSeek-VL2用 VQA 和数学相关能力指标结果

图片

DeepSeek-VL2视觉故事生成能力展示

04 代码解读

下面以 PaddleMIX 中 DeepSeek-VL2的实现为例,对关键创新点的代码实现进行讲解。

PaddleMIX

github.com/PaddlePaddl…

4.1 标动态切片策略

功能

该函数 select_best_resolution 的目的是在给定的候选分辨率列表中找到最适合原始图像大小的分辨率。

步骤实现

DeepSeek-VL2在处理多图像对话任务时表现突出,能够分析多张图片之间的关系,并基于这些信息进行简单的推理。

  • 计算缩放比例:对于每个候选分辨率,计算其相对于原始图像尺寸的缩放比例(取宽度和高度缩放比例中的最小值)。

  • 计算缩放后的尺寸:使用上述缩放比例计算缩放后的图像宽度和高度。

  • 计算有效分辨率:有效分辨率是缩放后的图像分辨率与原始图像分辨率中较小的一个。这是为了确保缩放后的图像不会比原始图像具有更高的分辨率。

  • 计算浪费的分辨率:浪费的分辨率是候选分辨率的面积减去有效分辨率的面积。

  • 选择最佳匹配:遍历所有候选分辨率,找到有效分辨率最大且浪费分辨率最小的那个作为最佳匹配。如果两个候选分辨率的有效分辨率相同,则选择浪费分辨率较小的那个。

输出

返回一个元组,代表最佳匹配的分辨率(宽度和高度)。如果没有找到任何合适的分辨率,理论上应该返回 None(尽管在当前的实现中,如果至少有一个候选分辨率,它总是会返回一个结果)。

def select_best_resolution(image_size, candidate_resolutions):
    original_width, original_height = image_size
    best_fit = None
    max_effective_resolution = 0
    min_wasted_resolution = float("inf")

    for width, height in candidate_resolutions:
        scale = min(width / original_width, height / original_height)
        downscaled_width, downscaled_height = int(original_width * scale), int(original_height * scale)
        effective_resolution = min(downscaled_width * downscaled_height, original_width * original_height)
        wasted_resolution = width * height - effective_resolution

        if (
            effective_resolution > max_effective_resolution
            or effective_resolution == max_effective_resolution
            and wasted_resolution < min_wasted_resolution
        ):
            max_effective_resolution = effective_resolution
            min_wasted_resolution = wasted_resolution
            best_fit = width, height

    return best_fit


4.2 VL Adapter

方法

tokenize_with_images

功能

该函数 tokenize_with_images 的目的是将包含 texttt\\texttt{} 标签的文本对话进行分词处理,并同时处理与文本对话相关联的图像。它将文本和图像转换为适合模型处理的格式,包括图像的分辨率调整、裁剪、以及将文本和图像转换为一系列 tokens。

参数

  • conversation:包含 texttt\\texttt{} 标签的原始文本对话。

  • images:与文本对话中的 texttt\\texttt{} 标签相对应的图像列表。

  • bos:布尔值,指定是否在分词结果的开头添加开始序列(Begin Of Sequence, BOS) token。默认为 True。

  • eos:布尔值,指定是否在分词结果的末尾添加结束序列(End Of Sequence, EOS)token。默认为 True。

  • cropping:布尔值,指定是否对图像进行裁剪以适应特定的分辨率。默认为 True。

步骤实现

  • 断言检查:确保文本对话中的 texttt\\texttt{} 标签数量与提供的图像数量相匹配。

  • 文本分割:使用 texttt\\texttt{} 标签将文本对话分割成多个部分。

  • 初始化列表:用于存储处理后的图像、图像序列掩码、图像空间裁剪信息、图像 token 数量以及分词后的字符串。

  • 遍历文本和图像:对于每个文本部分和对应的图像,执行以下操作:

  • 文本分词:将文本部分分词,但不添加 BOS 和 EOS token。

  • 图像分辨率选择:根据裁剪标志选择最佳图像分辨率。

  • 全局视图处理:将图像调整为固定大小(self.image_size),并填充背景色。

  • 局部视图处理:根据最佳分辨率将图像裁剪成多个小块,并对每个小块进行处理。

  • 记录裁剪信息:记录每个图像在宽度和高度上被裁剪成的小块数量。

  • 添加图像 token:为每个图像(全局和局部视图)生成一系列图像 token,并添加到分词后的字符串中。

  • 更新掩码和 token 数量:更新图像序列掩码和图像 token 数量列表。

  • 处理最后一个文本部分:对最后一个文本部分进行分词处理(但不添加 BOS 和 EOS token),并更新分词后的字符串和图像序列掩码。

  • 添加 BOS 和 EOS token:根据参数设置,在分词结果的开头和末尾添加 BOS 和 EOS token。

  • 断言检查:确保分词后的字符串长度与图像序列掩码的长度相匹配。

输出

返回一个元组,包含以下内容:

  • tokenized_str:分词后的字符串,包含文本和图像 token。

  • images_list:处理后的图像列表,包括全局视图和局部视图。

  • images_seq_mask:图像序列掩码,用于指示哪些 token 是图像 token。

  • images_spatial_crop:图像空间裁剪信息,记录每个图像在宽度和高度上的裁剪小块数量。

  • num_image_tokens:每个图像对应的 token 数量列表。

def tokenize_with_images(
        self, conversation: str, images: List[Image.Image], bos: bool = True, eos: bool = True, cropping: bool = True
    ):
        """Tokenize text with <image> tags."""
        assert conversation.count(self.image_token) == len(images)
        text_splits = conversation.split(self.image_token)
        images_list, images_seq_mask, images_spatial_crop = [], [], []
        num_image_tokens = []
        tokenized_str = []
        for text_sep, image in zip(text_splits, images):
            """encode text_sep"""
            tokenized_sep = self.encode(text_sep, bos=False, eos=False)
            tokenized_str += tokenized_sep
            images_seq_mask += [False] * len(tokenized_sep)
            """select best resolution for anyres"""
            if cropping:
                best_width, best_height = select_best_resolution(image.size, self.candidate_resolutions)
            else:
                best_width, best_height = self.image_size, self.image_size

            """process the global view"""
            global_view = ImageOps.pad(
                image, (self.image_size, self.image_size), color=tuple(int(x * 255for x in self.image_transform.mean)
            )
            images_list.append(self.image_transform(global_view))

            """process the local views"""
            local_view = ImageOps.pad(
                image, (best_width, best_height), color=tuple(int(x * 255for x in self.image_transform.mean)
            )

            for i in range(0, best_height, self.image_size):
                for j in range(0, best_width, self.image_size):
                    images_list.append(
                        self.image_transform(local_view.crop((j, i, j + self.image_size, i + self.image_size)))
                    )

            """record height / width crop num"""
            num_width_tiles, num_height_tiles = (best_width // self.image_size, best_height // self.image_size)
            images_spatial_crop.append([num_width_tiles, num_height_tiles])

            """add image tokens"""
            h = w = math.ceil(self.image_size // self.patch_size / self.downsample_ratio)
            tokenized_image = [self.image_token_id] * h * (w + 1)
            tokenized_image += [self.image_token_id]
            tokenized_image += [self.image_token_id] * (num_height_tiles * h) * (num_width_tiles * w + 1)
            tokenized_str += tokenized_image
            images_seq_mask += [True] * len(tokenized_image)
            num_image_tokens.append(len(tokenized_image))

        """process the last text split"""
        tokenized_sep = self.encode(text_splits[-1], bos=False, eos=False)
        tokenized_str += tokenized_sep
        images_seq_mask += [False] * len(tokenized_sep)

        """add the bos and eos tokens"""
        if bos:
            tokenized_str = [self.bos_id] + tokenized_str
            images_seq_mask = [False] + images_seq_mask
        if eos:
            tokenized_str = tokenized_str + [self.eos_id]
            images_seq_mask = images_seq_mask + [False]
        assert len(tokenized_str) == len(
            images_seq_mask
        ), f"tokenize_with_images func: tokenized_str's length {len(tokenized_str)} is not equal to imags_seq_mask's length {len(images_seq_mask)}"
        return (tokenized_str, images_list, images_seq_mask, images_spatial_crop, num_image_tokens)


4.3 MLA(Multi-head Latent Attention)

类名

DeepseekV2Attention

主要功能

实现多头注意力机制,用于处理序列数据,支持缓存机制和不同的 RoPE(Rotary Position Embedding)缩放策略。

初始化参数 (init)

  • config:DeepseekV2Config 类型的配置对象,包含模型的各种配置参数。

  • layer_idx:可选参数,表示当前层的索引,用于缓存机制。

前向传播参数 (forward)

  • hidden_states:paddle.Tensor 类型的输入张量,表示隐藏状态。

  • attention_mask:可选参数,paddle.Tensor 类型的注意力掩码,用于屏蔽不需要关注的位置。

  • position_ids:可选参数,paddle.Tensor 类型的位置编码,用于 RoPE。

  • past_key_value:可选参数,Tuple[paddle.Tensor] 类型的缓存键值对,用于加速推理。

  • output_attentions:布尔类型,表示是否输出注意力权重。

  • use_cache:布尔类型,表示是否使用缓存机制。

  • kwargs:其他可选参数。

前向传播 (forward)

实现多头注意力机制,用于处理序列数据,支持缓存机制和不同的 RoPE(Rotary Position Embedding)缩放策略。

1.查询投影(Query Projection):

  • 如果 q_lora_rank 为 None,则使用 q_proj 对查询进行投影。

  • 否则,使用基于 LoRA 的投影(q_a_proj、q_a_layernorm 和 q_b_proj)。

2.键值投影(Key-Value Projection):

  • 使用 kv_a_proj_with_mqa 对键和值进行投影。

  • 将结果拆分为 LoRA 和 RoPE 组件。

  • RoPE 应用(RoPE Application)。

  • 计算 RoPE 的余弦和正弦值。

  • 将 RoPE 应用于查询和键。

3.缓存(Caching):

  • 如果 use_cache 为 True,则更新缓存的键和值。

注意力权重(Attention Weights)

实现多头注意力机制,用于处理序列数据,支持缓存机制和不同的 RoPE(Rotary Position Embedding)缩放策略。

  • 使用缩放点积注意力计算注意力分数。

  • 应用注意力掩码和 softmax。

  • 输出投影(Output Projection)。

  • 使用注意力权重和投影后的值计算注意力输出。

  • 应用输出投影(o_proj)。

class DeepseekV2Attention(paddle.nn.Layer):
    """Multi-headed attention from 'Attention Is All You Need' paper"""

    def __init__(self, config: DeepseekV2Config, layer_idx: Optional[int] = None):
        super().__init__()
        self.config = config
        """
        ..............
        """
        if self.q_lora_rank is None:
            self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.q_head_dim, bias_attr=False)
        else:
            self.q_a_proj = nn.Linear(self.hidden_size, config.q_lora_rank, bias_attr=config.attention_bias)
            self.q_a_layernorm = DeepseekV2RMSNorm(config=config, hidden_size=config.q_lora_rank)
            self.q_b_proj = nn.Linear(config.q_lora_rank, self.num_heads * self.q_head_dim, bias_attr=False)

        self.kv_a_proj_with_mqa = nn.Linear(self.hidden_size, config.kv_lora_rank + config.qk_rope_head_dim, bias_attr=config.attention_bias)
        self.kv_a_layernorm = DeepseekV2RMSNorm(config=config, hidden_size=config.kv_lora_rank)
        self.kv_b_proj = nn.Linear(config.kv_lora_rank, self.num_heads * (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim), bias_attr=False)

        self.o_proj = nn.Linear(self.num_heads * self.v_head_dim, self.hidden_size, bias_attr=config.attention_bias)

        self._init_rope()

        self.softmax_scale = self.q_head_dim**-0.5
        if self.config.rope_scaling is not None:
            mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim"0)
            scaling_factor = self.config.rope_scaling["factor"]
            if mscale_all_dim:
                mscale = yarn_get_mscale(scaling_factor, mscale_all_dim)
                self.softmax_scale = self.softmax_scale * mscale * mscale

    def forward(
        self,
        hidden_states: paddle.Tensor,
        attention_mask: Optional[paddle.Tensor] = None,
        position_ids: Optional[paddle.Tensor] = None,
        past_key_value: Optional[Tuple[paddle.Tensor]] = None,
        output_attentions: bool = False,
        use_cache: bool = False,
        **kwargs,
    ) -> Tuple[paddle.Tensor, Optional[paddle.Tensor], Optional[Tuple[paddle.Tensor]]]:

        bsz, q_len, _ = tuple(hidden_states.shape)
        if self.q_lora_rank is None:
            q = self.q_proj(hidden_states)
        else:
            q = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(hidden_states)))

        q = q.reshape([bsz, q_len, self.num_heads, self.q_head_dim]).transpose(perm=[0213])
        q_nope, q_pe = paddle.split(q, [self.qk_nope_head_dim, self.qk_rope_head_dim], axis=-1)

        compressed_kv = self.kv_a_proj_with_mqa(hidden_states)
        compressed_kv, k_pe = paddle.split(compressed_kv, [self.kv_lora_rank, self.qk_rope_head_dim], axis=-1)
        compressed_kv = self.kv_a_layernorm(compressed_kv)
        k_pe = k_pe.reshape([bsz, q_len, 1, self.qk_rope_head_dim]).transpose(perm=[0213])

        kv_seq_len = tuple(k_pe.shape)[-2]
        if past_key_value is not None:
            kv_seq_len += past_key_value[0].shape[1]
        cos, sin = self.rotary_emb(q_pe, seq_len=kv_seq_len)
        q_pe, k_pe = apply_rotary_pos_emb(q_pe, k_pe, cos, sin, position_ids)

        if use_cache and past_key_value is not None:
            compressed_kv = compressed_kv.unsqueeze(axis=2)
            k_pe = k_pe.transpose(perm=[0213])  # (b h l d) to (b l h d)
            k_pe = paddle.concat([past_key_value[0], k_pe], axis=1)
            compressed_kv = paddle.concat([past_key_value[1], compressed_kv], axis=1)

            past_key_value = (k_pe, compressed_kv)

            k_pe = k_pe.transpose(perm=[0213])  # go back to (b l h d)
            compressed_kv = compressed_kv.squeeze(2)
        elif use_cache:
            past_key_value = (k_pe.transpose([0213]), compressed_kv.unsqueeze(axis=2))
        else:
            past_key_value = None

        # shit tranpose liner weight
        kv_b_proj = self.kv_b_proj.weight.T.reshape([self.num_heads, -1, self.kv_lora_rank])
        q_absorb = kv_b_proj[:, :self.qk_nope_head_dim, :]
        out_absorb = kv_b_proj[:, self.qk_nope_head_dim:, :]

        q_nope = paddle.matmul(q_nope, q_absorb)
        attn_weights = (
            paddle.matmul(q_pe, k_pe.transpose([0132])) # [1, 16, 1304, 64] * [1, 1, 1304, 64]
            + paddle.matmul(q_nope, compressed_kv.unsqueeze(axis=-3).transpose([0132])) #  [1, 16, 1304, 512] * [1, 1, 1304, 512]
        ) * self.softmax_scale

        if tuple(attn_weights.shape) != (bsz, self.num_heads, q_len, kv_seq_len):
            raise ValueError(
                f"Attention weights should be of size {bsz, self.num_heads, q_len, kv_seq_len}, but is {tuple(attn_weights.shape)}"
            )
        assert attention_mask is not None
        if attention_mask is not None:
            if tuple(attention_mask.shape) != (bsz, 1, q_len, kv_seq_len):
                raise ValueError(
                    f"Attention mask should be of size {bsz, 1, q_len, kv_seq_len}, but is {tuple(attention_mask.shape)}"
                )
            attn_weights = attn_weights + attention_mask

        # upcast attention to fp32
        attn_weights = F.softmax(attn_weights, axis=-1, dtype="float32").to(q_pe.dtype)
        attn_weights = F.dropout(attn_weights, self.attention_dropout, training=self.training)
        attn_output = paddle.einsum("bhql,blc->bhqc", attn_weights, compressed_kv)
        attn_output = paddle.matmul(attn_output, out_absorb.transpose([021]))

        if tuple(attn_output.shape) != (bsz, self.num_heads, q_len, self.v_head_dim):
            raise ValueError(
                f"`attn_output` should be of size {bsz, self.num_heads, q_len, self.v_head_dim}, but is {tuple(attn_output.shape)}"
            )
        attn_output = attn_output.transpose([0213])
        attn_output = attn_output.reshape([bsz, q_len, self.num_heads * self.v_head_dim])
        attn_output = self.o_proj(attn_output)
        if not output_attentions:
            attn_weights = None
        return attn_output, attn_weights, past_key_value


4.4 DeepSeekV2-MoE

类名

DeepseekV2MoE

主要功能

实现混合专家机制,通过路由机制将输入分配给多个专家网络,并将结果加权组合。

初始化参数 (init)

config:配置对象,包含模型的各种参数,如专家数量、共享专家数量、中间层大小等。

步骤实现

1.初始化 (init):

  • 从配置对象中读取参数,如专家数量、共享专家数量、中间层大小等。

  • 根据分布式环境(ep_size)分配专家网络到不同的设备上。

  • 初始化专家网络列表(self.experts)和共享专家网络(self.shared_experts)。

  • 初始化门控机制(self.gate)。

2.前向传播 (forward):

  • 保存输入张量的原始形状和值(identity 和 orig_shape)。

  • 使用门控机制(self.gate)计算路由索引(topk_idx)、路由权重(topk_weight)和辅助损失(aux_loss)。

  • 将输入张量展平以便处理。

  • 训练模式:

    ·将输入张量复制多次以匹配每个专家的输入。

    ·根据路由索引将输入分配给对应的专家网络,并计算输出。

    ·对专家输出进行加权求和,并恢复原始形状。

    ·添加辅助损失(AddAuxiliaryLoss.apply)。

  • 推理模式:

    ·调用 moe_infer 方法处理输入,并恢复原始形状。

    ·如果存在共享专家网络,将其输出与专家网络的输出相加。

    ·返回最终的输出张量。

class DeepseekV2MoE(paddle.nn.Layer):
    """
    A mixed expert module containing shared experts.
    """
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.num_experts_per_tok = config.num_experts_per_tok
        if hasattr(config, "ep_size") and config.ep_size > 1:
            assert config.ep_size == dist.get_world_size()
            self.ep_size = config.ep_size
            self.experts_per_rank = config.n_routed_experts // config.ep_size
            self.ep_rank = dist.get_rank()
            self.experts = nn.ModuleList(
                [
                    (
                        DeepseekV2MLP(
                            config, intermediate_size=config.moe_intermediate_size
                        )
                        if i >= self.ep_rank * self.experts_per_rank
                        and i < (self.ep_rank + 1) * self.experts_per_rank
                        else None
                    )
                    for i in range(config.n_routed_experts)
                ]
            )
        else:
            self.ep_size1
            self.experts_per_rank = config.n_routed_experts
            self.ep_rank0
            self.experts = nn.LayerList(
                [
                    DeepseekV2MLP(config, intermediate_size=config.moe_intermediate_size)
                    for i in range(config.n_routed_experts)
                ]
            )
        self.gate = MoEGate(config)
        if config.n_shared_experts is not None:
            intermediate_size = config.moe_intermediate_size * config.n_shared_experts
            self.shared_experts = DeepseekV2MLP(config=config, intermediate_size=intermediate_size)

    def forward(self, hidden_states):
        identity = hidden_states
        orig_shape = hidden_states.shape
        topk_idx, topk_weight, aux_loss = self.gate(hidden_states)
        hidden_states = hidden_states.reshape([-1, hidden_states.shape[-1]])
        flat_topk_idx = topk_idx.reshape([-1])
        # remove the infer method
        if self.training:
            hidden_states = hidden_states.repeat_interleave(self.num_experts_per_tok, axis=0)
            y = paddle.empty_like(hidden_states)
            for i, expert in enumerate(self.experts):
                # y[flat_topk_idx == i] = expert(hidden_states[flat_topk_idx == i])
                if paddle.any(flat_topk_idx == i):
                    y[flat_topk_idx == i] = expert(hidden_states[flat_topk_idx == i])

            y = (y.reshape([*topk_weight.shape, -1]) * topk_weight.unsqueeze(-1)).sum(axis=1)
            y = paddle.cast(y, hidden_states.dtype).reshape([*orig_shape])
            if self.gate.alpha > 0.0:
                y = AddAuxiliaryLoss.apply(y, aux_loss)
        else:
            y = self.moe_infer(hidden_states, topk_idx, topk_weight).reshape([*orig_shape])
        if self.config.n_shared_experts is not None:
            y = y + self.shared_experts(identity)
        return y


4.5 MoEGate

类名

MoEGate

主要功能

实现混合专家机制的门控逻辑,包括路由权重计算、专家选择和辅助损失计算。

初始化参数 (init)

config:配置对象,包含模型的各种参数,如专家数量、路由缩放因子、评分函数等。

步骤实现 1.初始化 (init):

  • 从配置对象中读取参数,如专家数量、路由缩放因子、评分函数等。

  • 初始化门控权重(self.weight)和路由策略相关参数。

  • 如果使用 noaux_tc 路由策略,初始化专家评分校正偏置(self.e_score_correction_bias)。

  • 调用 reset_parameters 方法初始化权重。

2.权重初始化 (reset_parameters):

  • 使用 Kaiming 均匀分布初始化门控权重。

3.前向传播 (forward):

  • 将输入张量展平以便处理。

  • 使用线性变换计算路由得分(logits)。

  • 根据评分函数(如 softmax 或 sigmoid)计算路由权重(scores)。

  • 如果 top_k > 1 且 norm_topk_prob 为 True,对路由权重进行归一化。

  • 在训练模式下,计算辅助损失(aux_loss)以优化路由机制。

  • 返回路由索引(topk_idx)、路由权重(topk_weight)和辅助损失(aux_loss)。

class MoEGate(paddle.nn.Layer):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.top_k = config.num_experts_per_tok
        self.n_routed_experts = config.n_routed_experts
        self.routed_scaling_factor = config.routed_scaling_factor
        self.scoring_func = config.scoring_func
        self.alpha = config.aux_loss_alpha
        self.seq_aux = config.seq_aux
        self.topk_method = config.topk_method
        self.n_group = config.n_group
        self.topk_group = config.topk_group
        # topk selection algorithm
        self.norm_topk_prob = config.norm_topk_prob
        self.gating_dim = config.hidden_size
        self.weight = paddle.base.framework.EagerParamBase.from_tensor(
            tensor=paddle.empty(shape=(self.gating_dim, self.n_routed_experts))
        )
        if self.topk_method == "noaux_tc":
            self.e_score_correction_bias = paddle.base.framework.EagerParamBase.from_tensor(
                tensor=paddle.empty(shape=[self.n_routed_experts])
            )

    def forward(self, hidden_states):
        bsz, seq_len, h = tuple(hidden_states.shape)
        hidden_states = hidden_states.reshape([-1, h])
        logits = paddle.nn.functional.linear(
            x=hidden_states.astype("float32"), weight=self.weight.astype("float32"), bias=None
        )
        if self.scoring_func == "softmax":
            scores = paddle.nn.functional.softmax(logits, axis=-1, dtype="float32")
        elif self.scoring_func == "sigmoid":
            scores = logits.sigmoid()
        else:
            raise NotImplementedError(f"insupportable scoring function for MoE gating: {self.scoring_func}")
        if self.topk_method == "greedy":
            topk_weight, topk_idx = paddle.topk(k=self.top_k, sorted=False, x=scores, axis=-1)
        elif self.topk_method == "group_limited_greedy":
            group_scores = scores.reshape(bsz * seq_len, self.n_group, -1).max(dim=-1).values

            group_idx = paddle.topk(k=self.topk_group, sorted=False, x=group_scores, axis=-1)[1]
            group_mask = paddle.zeros_like(x=group_scores)
            group_mask.put_along_axis_(axis=1, indices=group_idx, values=1, broadcast=False)
            score_mask = (
                group_mask.unsqueeze(axis=-1)
                .expand(shape=[bsz * seq_len, self.n_group, self.n_routed_experts // self.n_group])
                .reshape([bsz * seq_len, -1])
            )
            tmp_scores = scores.masked_fill(mask=~score_mask.astype(dtype="bool"), value=0.0)
            topk_weight, topk_idx = paddle.topk(k=self.top_k, sorted=False, x=tmp_scores, axis=-1)
        elif self.topk_method == "noaux_tc":
            assert not self.training
            scores_for_choice = scores.reshape([bsz * seq_len, -1]) + self.e_score_correction_bias.unsqueeze(axis=0)
            group_scores = scores_for_choice.reshape([bsz * seq_len, self.n_group, -1]).topk(k=2, axis=-1)[0].sum(axis=-1)

            group_idx = paddle.topk(k=self.topk_group, sorted=False, x=group_scores, axis=-1)[1]
            group_mask = paddle.zeros_like(x=group_scores)
            group_mask.put_along_axis_(axis=1, indices=group_idx, values=1, broadcast=False)
            # todo
            score_mask = (
                group_mask.unsqueeze(axis=-1)
                .expand(shape=[bsz * seq_len, self.n_group, self.n_routed_experts // self.n_group])
                .reshape([bsz * seq_len, -1])
            )
            tmp_scores = scores_for_choice.masked_fill(mask=~score_mask.astype(dtype="bool"), value=0.0)
            _, topk_idx = paddle.topk(k=self.top_k, sorted=False, x=tmp_scores, axis=-1)
            topk_weight = scores.take_along_axis(axis=1, indices=topk_idx, broadcast=False)

        if self.top_k > 1 and self.norm_topk_prob:
            denominator = topk_weight.sum(axis=-1, keepdim=True) + 1e-20
            topk_weight = topk_weight / denominator * self.routed_scaling_factor
        else:
            topk_weight = topk_weight * self.routed_scaling_factor
        if self.training and self.alpha > 0.0:
            scores_for_aux = scores
            aux_topk = self.top_k
            topk_idx_for_aux_loss = topk_idx.reshape([bsz, -1])
            if self.seq_aux:
                scores_for_seq_aux = scores_for_aux.reshape([bsz, seq_len, -1])
                ce = paddle.zeros(shape=[bsz, self.n_routed_experts])
                ce.put_along_axis_(
                    axis=1,
                    indices=topk_idx_for_aux_loss,
                    values=paddle.ones(shape=[bsz, seq_len * aux_topk]),
                    reduce="add",
                ).divide_(y=paddle.to_tensor(seq_len * aux_topk / self.n_routed_experts))
                aux_loss = (ce * scores_for_seq_aux.mean(axis=1)).sum(axis=1).mean() * self.alpha
            else:
                mask_ce = paddle.nn.functional.one_hot(
                    num_classes=self.n_routed_experts, x=topk_idx_for_aux_loss.reshape([-1])
                ).astype("int64")
                ce = mask_ce.astype(dtype="float32").mean(axis=0)
                Pi = scores_for_aux.mean(axis=0)
                fi = ce * self.n_routed_experts
                aux_loss = (Pi * fi).sum() * self.alpha
        else:
            aux_loss = None
        return topk_idx, topk_weight, aux_loss


05 上手教程

5.1 DeepSeek-VL2在 PaddleMIX 里快速体验

通过解析代码我们也更深入地理解模型的实现细节和技术创新,快跟着我们的 aistudio 教程一起来动手实践一下吧!

  • AI Studio 教程链接:

aistudio.baidu.com/projectdeta…

我们以 DeepSeek-VL2-tiny 为例,在单卡 V100上需23G 显存可推理完成图像理解。 首先下载 PaddleMIX 代码库:

# clone PaddleMIX代码库
git clone https://github.com/PaddlePaddle/PaddleMIX.git

cd PaddleMIX


安装 PaddlePaddle:

# 提供三种 PaddlePaddle 安装命令示例,也可参考PaddleMIX主页的安装教程进行安装

# 3.0.0b2版本安装示例 (CUDA 11.8)
python -m pip install paddlepaddle-gpu==3.0.0b2 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/

# Develop 版本安装示例
python -m pip install paddlepaddle-gpu==0.0.0.post118 -f https://www.paddlepaddle.org.cn/whl/linux/gpu/develop.html

# sh 脚本快速安装
sh build_paddle_env.sh


安装 PaddleMIX 环境依赖包:

# 提供两种 PaddleMIX 依赖安装命令示例

# pip 安装示例,安装paddlemix、ppdiffusers、项目依赖、paddlenlp
python -m pip install -e . --user
python -m pip install -e ppdiffusers --user
python -m pip install -r requirements.txt --user
python -m pip install paddlenlp==3.0.0b3 --user

# sh 脚本快速安装
sh build_env.sh


5.2图像理解

运行以下命令即可:

# Deepseek-vl2-tiny multi image understanding
python paddlemix/examples/deepseek_vl2/multi_image_infer.py \
    --model_path="deepseek-ai/deepseek-vl2-tiny" \
    --image_file_1="paddlemix/demo_images/examples_image1.jpg" \
    --image_file_2="paddlemix/demo_images/examples_image2.jpg" \
    --image_file_3="paddlemix/demo_images/twitter3.jpeg" \
    --question="Can you tell me what are in the images?" \
    --dtype="bfloat16"


输出结果:

<|User|>: This is image_1:

This is image_2:

This is image_3:

Can you tell me what are in the images?

<|Assistant|>: The first image shows a red panda resting on a wooden platform. The second image features a giant panda sitting among bamboo plants. The third image captures a rocket launch at night, with the bright trail of the rocket illuminating the sky.<|end▁of▁sentence|>

06 总结

DeepSeek-VL2是一个基于 MoE 架构的前沿多模态大模型。通过引入动态图像切片编码策略,高效处理不同长宽比的高分辨率图像,大幅提升了视觉理解、视觉问答等任务的表现;其语言模型部分 DeepSeek-MoE 也通过压缩键值缓存的方式优化了推理速度和吞吐量。

百度飞桨团队推出的 PaddleMIX 套件现已完整实现这个热门模型的推理训练全流程支持,通过深入解析其代码实现,研究人员和开发者能够更透彻地理解模型的核心技术细节与创新突破。我们诚挚推荐您访问 AI Studio 平台的专项教程(点击以下链接🔗),通过实践演练掌握前沿多模态模型的开发与应用技巧。

▎论文链接

DeepSeek-VL: Towards Real-World Vision-Language Understanding

arxiv.org/pdf/2403.05…

DeepSeek-VL2: Mixture-of-Experts Vision-Language Models for Advanced Multimodal Understanding arxiv.org/pdf/2412.10…

▎项目地址

DeepSeek-VL2:github.com/PaddlePaddl…

▎AI Studio 教程链接

aistudio.baidu.com/projectdeta…

END

推荐阅读

秒哒首发即爆发!上线首日吸引2万用户,打造3万应用!

秒哒,全面开放!

图灵数据洞察平台-TDF(Turing Data Finder)

两连发!文心大模型4.5及X1,上线千帆!

百度百舸万卡集群的训练稳定性系统设计和实践

鸿蒙ArkUI框架中的状态管理

在ArkUI框架中,状态管理是构建动态应用的核心。以下是组件级别应用级别状态管理装饰器的分类、用途及区别的总结,结合了思考过程中的关键点:

一、组件级别状态管理

1. @State

  • 用途:组件内部私有状态,变化触发UI更新。
  • 示例:按钮的点击状态、计数器数值。
  • 特点:只能初始化一次,单向数据流(组件内修改)。

代码示例

@Component
struct CounterButton {
  @State count: number = 0; // 组件内部状态

  build() {
    Button(`点击次数:${this.count}`)
      .onClick(() => {
        this.count++; // 修改@State变量自动更新UI
      })
  }
}

2. @Prop

  • 用途:父组件向子组件传递数据(单向)。
  • 特点:需通过父组件回调更新数据。
  • 示例:显示父组件传递的文本,子组件不可直接修改。

代码示例

// 父组件
@Component
struct ParentComponent {
  @State parentCount: number = 0;

  build() {
    Column() {
      ChildComponent({ countProp: this.parentCount }) // 传递数据
      Button("父组件增加").onClick(() => this.parentCount++)
    }
  }
}

// 子组件
@Component
struct ChildComponent {
  @Prop countProp: number; // 单向接收父组件数据

  build() {
    Text(`来自父组件的值:${this.countProp}`)
  }
}

3. @Link

  • 用途:父子组件双向数据绑定。
  • 特点:双向同步,类似Vue的v-model
  • 示例:共享开关状态,子组件直接修改影响父组件。

代码示例

// 父组件
@Component
struct ParentComponent {
  @State sharedCount: number = 0;

  build() {
    Column() {
      ChildComponent({ countLink: $sharedCount }) // 双向绑定
      Text(`父组件值:${this.sharedCount}`)
    }
  }
}

// 子组件
@Component
struct ChildComponent {
  @Link countLink: number; // 双向绑定变量

  build() {
    Button("子组件修改").onClick(() => {
      this.countLink++; // 修改会同步到父组件
    })
  }
}

4. @Provide / @Consume

  • 用途:跨层级组件数据共享(祖先→后代)。
  • 示例:主题颜色全局设置。
  • 特点:避免逐层传递,类似React Context。

代码示例

// 祖先组件
@Component
struct AncestorComponent {
  @Provide themeColor: string = 'blue'; // 提供数据

  build() {
    Column() {
      ChildComponent()
    }
  }
}

// 后代组件
@Component
struct ChildComponent {
  @Consume themeColor: string; // 消费数据

  build() {
    Text(`当前主题色:${this.themeColor}`)
      .fontColor(this.themeColor)
  }
}

5. @Observed / @ObjectLink

  • 用途:观察嵌套对象属性变化。
  • 特点@Observed装饰类,@ObjectLink引用实例。
  • 示例:用户对象({name: string})属性更新。

代码示例

@Observed // 装饰类
class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

@Component
struct UserProfile {
  @ObjectLink user: User; // 引用被观察对象

  build() {
    Column() {
      Text(`姓名:${this.user.name}`)
      Button("修改年龄").onClick(() => {
        this.user.age++; // 修改会触发UI更新
      })
    }
  }
}

6. @Style

  • 用途:定义可复用的组件样式。
  • 示例:统一按钮样式(颜色、边距)。

代码示例

@Styles function customButtonStyle() {
  .width(120)
  .height(40)
  .backgroundColor(Color.Blue)
  .fontColor(Color.White)
}

@Component
struct StyledButton {
  build() {
    Button("样式按钮")
      .useStyle(customButtonStyle) // 应用样式
  }
}

7. @Builder / @BuilderParam

  • 用途:构建可复用的UI片段或动态插入布局。
  • 示例:自定义卡片布局,父组件传递头部Builder。
  • 区别@Builder定义结构,@BuilderParam接收结构作为参数。

代码示例

@Component
struct CustomCard {
  @BuilderParam header: () => void; // 接收Builder参数

  build() {
    Column() {
      this.header() // 插入自定义头部
      Text("卡片内容...")
    }
  }
}

// 使用组件时传递Builder
CustomCard({
  header: () => {
    Text("自定义标题")
      .fontSize(20)
      .fontColor(Color.Red)
  }
})

8. @Extend

  • 用途:扩展组件样式(如全局字体)。
  • 示例:统一所有文本的字体大小和颜色。

代码示例

@Extend(Text) function boldText() {
  .fontWeight(FontWeight.Bold)
  .fontColor('#333')
}

@Component
struct ExtendedText {
  build() {
    Column() {
      Text("普通文本")
      Text("加粗文本").useStyle(boldText) // 应用扩展样式
    }
  }
}

二、应用级别状态管理

1. @LocalStorage

  • 用途:页面级临时存储(页面关闭后可能保留)。
  • 示例:表单草稿保存。

补充知识点: @LocalStorageProp / @LocalStorageLink

区别:前者单向同步,后者双向绑定页面存储数据。

代码示例

// 页面A
@Entry
@Component
struct PageA {
  @LocalStorage('formData') formData: string = '';

  build() {
    TextInput(this.formData)
      .onChange((value) => {
        this.formData = value; // 数据保存到LocalStorage
      })
  }
}

// 页面B可读取同一LocalStorage
@Component
struct PageB {
  @LocalStorage('formData') formData: string;

  build() {
    Text(`保存的数据:${this.formData}`)
  }
}

2. @AppStorage

  • 用途:全局状态存储(应用生命周期内有效)。
  • 示例:用户登录Token。

补充知识点: @StorageProp / @StorageLink

用途:绑定到AppStorage中的具体键。

区别@StorageProp单向,@StorageLink双向。

代码示例

// 全局存储用户Token
@AppStorage.setOrCreate('userToken', '') // 初始化

@Component
struct LoginComponent {
  @StorageLink('userToken') token: string; // 双向绑定

  build() {
    Button("登录").onClick(() => {
      this.token = 'abc123'; // 修改全局状态
    })
  }
}

// 其他页面读取
@Component
struct ProfilePage {
  @StorageProp('userToken') token: string; // 单向读取

  build() {
    Text(`Token: ${this.token}`)
  }
}

3. @PersistentStorage

  • 用途:持久化存储(应用重启保留)。
  • 示例:用户偏好设置(语言、主题)。

代码示例

@PersistentStorage.setOrCreate('settings', { theme: 'light', fontSize: 16 })

@Component
struct SettingsPage {
  @StorageLink('theme') theme: string;
  @StorageLink('fontSize') fontSize: number;

  build() {
    Column() {
      Button("切换主题").onClick(() => {
        this.theme = this.theme === 'light' ? 'dark' : 'light';
      })
      Slider({ min: 12, max: 24 })
        .value(this.fontSize)
        .onChange((value) => {
          this.fontSize = value;
        })
    }
  }
}

4. @Environment

  • 用途:访问环境变量(如主题、语言)。
  • 示例:根据系统主题切换应用外观。

代码示例

@Component
struct ThemeAwareComponent {
  @Environment('currentTheme') theme: string;

  build() {
    Text("主题敏感文本")
      .fontColor(this.theme === 'dark' ? Color.White : Color.Black)
      .backgroundColor(this.theme === 'dark' ? Color.Black : Color.White)
  }
}

三、关键区别与选择

  • 作用域

    • 组件级:@State@Prop等用于组件或父子通信。
    • 应用级:@AppStorage@PersistentStorage等跨页面共享。
  • 数据流

    • 单向:@Prop@LocalStorageProp(父→子/存储→组件)。
    • 双向:@Link@LocalStorageLink(父子/组件与存储同步)。
  • 持久性

    • 临时:@State@LocalStorage(页面级)。
    • 持久:@PersistentStorage(设备存储)。
  • 使用场景

    • 简单组件状态 → @State
    • 跨组件共享 → @Provide/@Consume
    • 全局配置 → @AppStorage + @StorageLink
    • 复杂对象监听 → @Observed + @ObjectLink


四、最佳实践总结

  1. 简单交互优先使用@State:适合按钮状态、临时计数等
  2. 父子通信选择@Prop/@Link:单向传递用@Prop,双向同步用@Link
  3. 跨层级共享数据用@Provide/@Consume:避免多级传递的麻烦
  4. 全局状态使用@AppStorage:如用户登录状态、主题配置
  5. 复杂对象监听用@Observed+@ObjectLink:确保嵌套属性变化触发更新
  6. 持久化数据用@PersistentStorage:用户设置、历史记录等需要长期保存的数据

官方文档是更全面的参考:
HarmonyOS ArkUI 文档

面试官问我React组件和state的关系,我指了指路口的红绿灯…

🚥 马路边的面试奇遇

面试官:(突然指向路口的红绿灯)你看这个红灯变绿,像不像React组件的重新渲染?

:(战术挑眉)您这红绿灯要是用React实现,isGreen这个state一变,整个组件就得重绘。不过嘛...(突然掏出手电筒)要是只换灯泡不换灯罩,可能不用整个拆了重建?

面试官:(突然打开手电筒照我眼睛)说人话!


🔋 一、React组件的能量守恒定律

1. 基本法则:触发渲染的三原色

// 组件重渲染的三种触发方式
const 触发重渲染 = () => {
  1. setState(newValue) // 原生state变化
  2. props变更 // 父组件传值变化
  3. 父组件重渲染 // 上级组件更新
};

2. 红绿灯案例解析

function TrafficLight() {
  const [isGreen, setIsGreen] = useState(false);

  useEffect(() => {
    const timer = setInterval(() => {
      setIsGreen(prev => !prev); // ✅ 每次切换触发重渲染
    }, 3000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div className={`light ${isGreen ? 'green' : 'red'}`}>
      {/* 每次isGreen变化都会重绘整个div */}
    </div>
  );
}

🚨 二、触发重渲染的四大天王

Case 1:useState值变化

const [count, setCount] = useState(0);

// 🚀 触发重渲染
setCount(1); 

// ❌ 不会触发(React使用Object.is比较)
setCount(0); 

Case 2:props对象引用变化

// 父组件
<Child items={[...items]} /> // 每次都是新数组,必触发

<Child items={items} /> // 数组引用不变不触发

Case 3:Context变更

const ThemeContext = createContext();

// 只要Provider的value变化
<ThemeContext.Provider value={newTheme}>
  <App />
</ThemeContext.Provider>

即使组件用memo包裹,只要消费了该Context的子组件都会重渲染

Case 4:祖传染色体攻击

// 祖父组件
const Grandfather = () => {
  const [state] = useState();
  return <Father />; // 👉 只要祖父重渲染,父亲不优化的话...
};

// 父亲组件
const Father = () => <Child />; // 👉 孩子也会被迫重渲染

🕶️ 三、金钟罩铁布衫:不触发重渲染的玄学时刻

1. 对象原地变性术

const [user, setUser] = useState({ name: '老王' });

// ❌ 不会触发
user.name = '隔壁老王'; 
setUser(user); // 引用地址没变!

// ✅ 正确做法
setUser({ ...user, name: '隔壁老王' });

2. 数组索引戏法

const [list, setList] = useState(['A', 'B', 'C']);

// ❌ 不会触发(React认为数组没变)
list.push('D');
setList(list);

// ✅ 正确做法
setList([...list, 'D']);

3. 函数式更新隐身术

const [count, setCount] = useState(0);

// ✅ 触发
setCount(1); 

// ❌ 不会触发(相同值)
setCount(prev => prev); 

👻 四、幽灵state现形记

实验:未使用的state会触发渲染吗?

function GhostComponent() {
  const [usedState] = useState('显形state'); 
  const [ghostState, setGhostState] = useState('幽灵state'); 

  return (
    <div>
      <button onClick={() => setGhostState(Math.random())}>
        触发幽灵state变化
      </button>
      <p>{usedState}</p> {/* 只展示usedState */}
    </div>
  );
}

现象
点击按钮时:

  • ✅ 组件重新渲染(控制台打印执行)
  • ❌ UI纹丝不动(ghostState从未被使用)

结论
所有state变化都会触发重渲染,哪怕它是个"幽灵"!


驱魔三式:让幽灵state安息

招式1:组件分家术

// 父组件(无state)
function Parent() {
  return <Child />; // 免疫幽灵攻击
}

// 子组件(独自承受)
const Child = React.memo(() => {
  const [ghostState, setGhostState] = useState();
  // ...处理state
});

招式2:useRef封印大法

function StealthComponent() {
  const ghostRef = useRef();
  
  const updateGhost = () => {
    ghostRef.current = Math.random(); // ✅ 无渲染触发
  };

  return <button onClick={updateGhost}>秘密行动</button>;
}

招式3:Context选择器狙击

const useGhostSelector = () => {
  const context = useContext(GhostContext);
  return useSelector(context, state => state.usedPart); // 精确打击
};

🧙 性能优化法典(新增条款)

闹鬼场景 驱魔方案 效果
未使用的UI state游荡 组件拆分 隔离在子组件内
纯逻辑state(如计时器ID) useRef镇压 完全隐形
全局state的幽灵扩散 Context选择器 精准狙杀
高频无用state波动 移出React生态 彻底驱散

🔍 五、组件渲染的量子纠缠实验

实验1:Memo的薛定谔防护

const ExpensiveComponent = memo(({ data }) => {
  // 只有data变化时才重渲染
});

// 父组件
const Parent = () => {
  const [state] = useState();
  return <ExpensiveComponent data={state} />; 
  // 👆父组件重渲染时,子组件不会跟着渲染
};

实验2:useMemo的时间结界

const heavyData = useMemo(() => {
  return computeHeavyData(); // 依赖项不变时缓存结果
}, [deps]);

return <Chart data={heavyData} />; 

实验3:useCallback的克隆人军团

const onClick = useCallback(() => {
  // 依赖项不变时保持函数引用
}, [deps]);

return <Button onClick={onClick} />;

💣 六、高频作死案例现场

作死案例1:在渲染中创建新对象

// 每次渲染都创建新style对象
<div style={{ color: 'red' }}> 
  <ChildComponent /> // 即使Child是memo也会重渲染
</div>

解法:将style提升到组件外或使用useMemo

作死案例2:匿名函数轰炸机

// 每次渲染都生成新函数
<Button onClick={() => handleClick()} />

// 正确做法
const handleClick = useCallback(() => {...}, []);
<Button onClick={handleClick} />

作死案例3:无脑Context

// 把整个state对象放入Context
<AppContext.Provider value={{ state, setState }}>
  {/* 任何state变化都会触发所有消费者重渲染 */}
</AppContext.Provider>

// 正确做法:拆分Context
<UserContext.Provider value={user}>
<CartContext.Provider value={cart}>

🚀 七、性能优化九阳神功

第一式:组件记忆术

// 用memo包裹组件
const UserCard = memo(({ user }) => {
  return <div>{user.name}</div>;
});

第二式:道具稳定符

// 用useMemo稳定props
const userData = useMemo(() => transformData(rawData), [rawData]);
return <Profile data={userData} />;

第三式:时间切片大法

// 用startTransition标记非紧急更新
const [tab, setTab] = useState('home');

function switchTab(nextTab) {
  startTransition(() => {
    setTab(nextTab); // 低优先级更新
  });
}

🎙️ 面试官の终极大招

面试官:(突然掏出三个iPhone)如果这三个手机同时运行React应用,分别出现:

  1. 疯狂重渲染
  2. 状态不同步
  3. 性能雪崩 要怎么快速定位问题?

:(战术擦汗)可能需要:

  1. 用React DevTools的Profiler抓重渲染元凶
  2. 检查state是否被意外篡改
  3. 上memo、useMemo、虚拟列表三连

不过...(突然抢过手机)您这三个都是模型机啊!


后记:后来发现,真正的"幽灵state"其实是产品经理半夜改需求时偷偷加的那些...(逃)

AntV X6 常用方法

AntV X6 常用方法

1. Graph 相关方法

1.1 创建 Graph

使用场景:初始化画布,设置基本参数。

import { Graph } from '@antv/x6';
const graph = new Graph({
  container: document.getElementById('container'),
  width: 800,
  height: 600,
  grid: true,
});

1.2 添加节点

使用场景:在画布上添加新的图形节点。

const node = graph.addNode({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  label: 'Hello',
});

1.3 添加边

使用场景:创建节点之间的连接关系。

const edge = graph.addEdge({
  source: node1,
  target: node2,
  label: 'Edge',
});

1.4 获取所有节点

使用场景:用于遍历所有节点,例如批量更新节点样式。

const nodes = graph.getNodes();

1.5 获取所有边

使用场景:获取当前所有连接关系,例如调整样式或删除特定边。

const edges = graph.getEdges();

1.6 删除元素

使用场景:当用户进行编辑操作时,删除选定的节点或边。

graph.removeNode(node);
graph.removeEdge(edge);

1.7 清空画布

使用场景:用于重置整个画布。

graph.clear();

2. Node 相关方法

2.1 设置/获取节点数据

使用场景:存储或获取节点的业务数据,例如状态信息。

node.setData({ key: 'value' });
const data = node.getData();
console.log(data.key);

2.2 更新节点数据

使用场景:在不覆盖原有数据的情况下,增量更新节点数据。

node.setData({ key: 'newValue' }, { overwrite: false });

2.3 设置/获取节点位置

使用场景:调整节点在画布中的位置。

node.position(100, 100);
const { x, y } = node.getPosition();

2.4 设置/获取节点大小

使用场景:改变节点尺寸,例如放大或缩小。

node.resize(120, 50);
const { width, height } = node.getSize();

2.5 设置/获取节点旋转角度

使用场景:适用于旋转特定类型的节点,如箭头或图标。

node.rotate(45);
const angle = node.getAngle();

2.6 设置/获取节点文本

使用场景:修改节点的显示文本。

node.setLabel('New Label');
const label = node.getLabel();

2.7 设置节点样式

使用场景:修改节点外观,例如颜色、字体大小。

node.attr('body/fill', 'blue');
node.attr('label/fontSize', 14);

3. Edge 相关方法

3.1 设置/获取边的文本

使用场景:给边添加描述信息。

edge.setLabel('New Edge Label');
const label = edge.getLabel();

3.2 设置/获取边的样式

使用场景:修改边的外观,例如颜色、宽度。

edge.attr('line/stroke', 'red');

3.3 设置/获取边的连接点

使用场景:动态调整边的起点和终点。

edge.setSource(node1);
edge.setTarget(node2);
const source = edge.getSource();
const target = edge.getTarget();

4. 事件监听

4.1 监听节点添加事件

使用场景:当用户添加新节点时触发特定操作。

graph.on('node:added', ({ node }) => {
  console.log('Node added:', node);
});

4.2 监听节点点击事件

使用场景:用户点击节点时,显示详细信息或执行操作。

graph.on('node:click', ({ node }) => {
  console.log('Node clicked:', node.id);
});

4.3 监听边点击事件

使用场景:点击边时高亮、删除或修改连接信息。

graph.on('edge:click', ({ edge }) => {
  console.log('Edge clicked:', edge.id);
});

4.4 监听画布点击事件

使用场景:用户点击空白区域时,取消选中所有元素。

graph.on('blank:click', () => {
  console.log('Canvas clicked');
});

5. 画布操作

5.1 放大/缩小

使用场景:调整画布的缩放级别。

graph.zoom(1.2); // 放大 1.2 倍
graph.zoom(0.8); // 缩小 0.8 倍

5.2 适应画布

使用场景:自动缩放图形以适应视图。

graph.zoomToFit();

5.3 平移画布

使用场景:调整视图中心点。

graph.translate(100, 50);

6. 自定义节点与边

6.1 自定义节点

使用场景:创建特定类型的节点,如流程图或组织结构图。

import { Shape } from '@antv/x6';

graph.addNode(
  new Shape.Rect({
    width: 100,
    height: 40,
    attrs: {
      body: { fill: 'blue' },
      label: { text: 'Custom Node', fill: 'white' },
    },
  })
);

6.2 自定义边

使用场景:创建具有特定样式的边,例如虚线或曲线连接。

import { Shape } from '@antv/x6';

graph.addEdge(
  new Shape.Edge({
    source: node1,
    target: node2,
    attrs: {
      line: { stroke: 'blue', strokeWidth: 2 },
    },
  })
);

以上是 AntV X6 的常用方法,包括对 GraphNodeEdge 以及 Data 的操作,并添加了使用场景,希望对你有所帮助!

轿车3D展示

本文将会以three.js 官网的一个轿车3D展示demo为例,进行讲解。示例具体查看地址:www.yanhuangxueyuan.com/threejs/exa…

车.gif

一、主要开发流程

  1. 搭建3D渲染场景
  2. 使用 GridHelper 对象,生成网格地板
  3. 使用 GLTFLoader 加载轿车模型,并自定义模型材质,可通过颜色选择器操控材质样式
  4. 将四个车轮模型保存在wheels对象中,通过改变车轮模型的 rotation.x 属性,让车轮旋转起来,模拟汽车奔跑。

二、查看3D模型

可以使用3D软件或者在线工具,预览轿车模型。这里推荐一个在线地址,用于浏览模型: gltf.nsdt.cloud/

image.png

三、绘制网格地板

GridHelper 是 Three.js 里的一个实用工具,用于创建网格辅助线,能在场景中直观地显示网格,辅助你理解和定位物体的位置。
该demo中使用 GridHelper 来模拟地板。

grid = new THREE.GridHelper( 20, 40, 0xffffff, 0xffffff );
grid.material.opacity = 0.2;
grid.material.depthWrite = false;
grid.material.transparent = true;
scene.add( grid );

代码解读:

1. 实例化GridHelper对象

grid = new THREE.GridHelper( 20, 40, 0xffffff, 0xffffff );

参数说明:

  • 第一个参数 20:表示网格的大小(边长),这里意味着创建的网格是一个边长为 20 个单位的正方形区域。
  • 第二个参数 40:表示网格的分割数量,即把整个网格区域在每个方向上平均分割成 40 份,这样会形成更密集的网格线。
  • 第三个参数 0xffffff:指定网格中轴线(穿过网格中心的线)的颜色,0xffffff 代表白色。
  • 第四个参数 0xffffff:指定网格线的颜色,同样是白色。

2. 设置材质透明度

grid.material.opacity = 0.2;

opacity 属性用于设置材质的透明度,取值范围是 0 到 1,其中 0 表示完全透明,1 表示完全不透明。这里将透明度设置为 0.2,意味着网格线会呈现出半透明的效果。

3. 禁用深度写入

grid.material.depthWrite = false;
  • depthWrite 是材质的一个属性,用于控制是否将该材质所渲染的物体的深度信息写入深度缓冲区。
  • 当设置为 false 时,意味着该材质渲染的物体不会影响深度缓冲区,这样可能会使得该物体在渲染时不会被其他物体遮挡,即使从深度上看它应该被遮挡。

4. 启用材质透明效果

grid.material.transparent = true;

transparent 属性用于启用材质的透明效果。当设置为 true 时,材质会根据 opacity 属性的值来呈现透明效果。

四、加载轿车模型,并自定义材质(核心)

下面将会介绍如何使用 Three.js 加载一个 GLTF 格式的汽车模型,并为模型的不同部分(车身、细节、玻璃等)设置不同的材质。同时,允许用户通过改变颜色值来动态改变这些部分的颜色。此外,还为汽车模型添加了底部阴影效果。

1. 定义材质

// 车身材质
const bodyMaterial = new THREE.MeshPhysicalMaterial( {
    color: 0xff0000, // 默认颜色
    metalness: 1.0, // 车外壳金属都
    roughness: 0.5, // 车外壳粗糙度
    clearcoat: 1.0, // 清漆层强度为 1.0,模拟清漆效果
    clearcoatRoughness: 0.03 //清漆层的粗糙度为 0.03
});

// 细节部分(如轮毂、装饰条等)的材质
const detailsMaterial = new THREE.MeshStandardMaterial( {
    color: 0xffffff, 
    metalness: 1.0, 
    roughness: 0.5
});

// 玻璃材质
const glassMaterial = new THREE.MeshPhysicalMaterial( {
    color: 0xffffff, 
    metalness: 0.25, 
    roughness: 0, 
    transmission: 1.0
});
(1) MeshPhysicalMaterial
  • MeshPhysicalMaterial 是具有有金属度metalness、粗糙度roughness属性的PBR材质。
  • MeshPhysicalMaterial是基于物理的材质,能够模拟真实世界中的光照和材质交互效果。对于车身材质,使用这种材质可以让车身在不同光照条件下表现出更加逼真的反射、折射、阴影等效果,使车身看起来更有质感和真实感。
(2) MeshStandardMaterial

MeshStandardMaterial也是一种常用的材质,它在计算光照时采用了标准的 PBR(基于物理的渲染)模型,能够提供较为真实的光照效果,同时性能相对较好。对于汽车的细节部分,如轮辋(rim)和装饰条(trim)等,使用MeshStandardMaterial可以在保证视觉效果的同时,减少计算量,提高渲染性能。

2. 模型加载

// 车底部阴影图
const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );

// 车3D模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );

const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );

loader.load( 'models/gltf/ferrari.glb', function ( gltf ) {
const carModel = gltf.scene.children[ 0 ];
})
  • 汽车底部阴影纹理图: 使用 THREE.TextureLoader 加载 ferrari_ao.png 图片
  • Draco 解码器设置:创建 DRACOLoader 对象并设置解码器路径,用于处理压缩的 GLTF 模型。
  • GLTF 模型加载:创建 GLTFLoader 对象并设置 Draco 解码器,然后使用 load 方法加载 ferrari.glb 模型。

3. 替换汽车材质,收集车轮

carModel.getObjectByName( 'body' ).material = bodyMaterial;

carModel.getObjectByName( 'rim_fl' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_fr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rl' ).material = detailsMaterial;
carModel.getObjectByName( 'trim' ).material = detailsMaterial;

carModel.getObjectByName( 'glass' ).material = glassMaterial;

wheels.push(
carModel.getObjectByName( 'wheel_fl' ),
carModel.getObjectByName( 'wheel_fr' ),
carModel.getObjectByName( 'wheel_rl' ),
carModel.getObjectByName( 'wheel_rr' )
);

4. 汽车底部阴影

ferrari_ao.png

const mesh = new THREE.Mesh(
    new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
    new THREE.MeshBasicMaterial( {
        map: shadow, 
        blending: THREE.MultiplyBlending, 
        toneMapped: false, 
        transparent: true 
    } )
);
mesh.rotation.x = - Math.PI / 2;
mesh.renderOrder = 2;
carModel.add( mesh );

scene.add( carModel );
  • 定义一个网格对象,并将之前加载好的阴影纹理应用到该材质上。
  • 对mesh 沿x轴旋转90度,使其平行于地面

5. 使车轮和地面动起来

function render() {
controls.update();
const time = - performance.now() / 1000;
for ( let i = 0; i < wheels.length; i ++ ) {
wheels[ i ].rotation.x = time * Math.PI * 2;
}
grid.position.z = - ( time ) % 1;
renderer.render( scene, camera );
stats.update();
}
  • 旋转车轮: for循环遍历四个车轮对象,wheels[i].rotation.x 表示第 i 个车轮绕 X 轴的旋转角度
  • 移动网格辅助线:( time ) % 1 计算出 time 的小数部分,取负号后将其赋值给 grid.position.z,使得网格辅助线在 Z 轴上以 1 个单位为周期循环移动,从而产生网格滚动的动画效果

6. 动态更改车模型材质颜色

const bodyColorInput = document.getElementById( 'body-color' );
bodyColorInput.addEventListener( 'input', function () {
bodyMaterial.color.set( this.value );
});

const detailsColorInput = document.getElementById( 'details-color' );
detailsColorInput.addEventListener( 'input', function () {
detailsMaterial.color.set( this.value );
});

const glassColorInput = document.getElementById( 'glass-color' );
glassColorInput.addEventListener( 'input', function () {
glassMaterial.color.set( this.value );
});

通过 color.set方法,修改材质颜色

四、完整代码

<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - materials - car</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {
color: #bbbbbb;
background: #333333;
}
a {
color: #08f;
}
.colorPicker {
display: inline-block;
margin: 0 10px
}
</style>
</head>

<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> car materials<br/>
Ferrari 458 Italia model by <a href="https://sketchfab.com/models/57bf6cc56931426e87494f554df1dab6" target="_blank" rel="noopener">vicent091036</a>
<br><br>
<span class="colorPicker"><input id="body-color" type="color" value="#ff0000"></input><br/>Body</span>
<span class="colorPicker"><input id="details-color" type="color" value="#ffffff"></input><br/>Details</span>
<span class="colorPicker"><input id="glass-color" type="color" value="#ffffff"></input><br/>Glass</span>
</div>

<div id="container"></div>

<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>

<script type="module">

import * as THREE from 'three';

import Stats from 'three/addons/libs/stats.module.js';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

let camera, scene, renderer;
let stats;

let grid;
let controls;

const wheels = [];

function init() {

const container = document.getElementById( 'container' );

renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
// setAnimationLoop: 每个可用帧都会调用的函数。 如果传入“null",所有正在进行的动画都会停止。
renderer.setAnimationLoop( render );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.85; // 色调映射的曝光级别。默认是1
container.appendChild( renderer.domElement );

window.addEventListener( 'resize', onWindowResize );

stats = new Stats();
container.appendChild( stats.dom );

//

camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 4.25, 1.4, - 4.5 );

// OrbitControls: 轨道控制器
controls = new OrbitControls( camera, container );
controls.maxDistance = 9; // 能够将相机向外移动多少, 其默认值为Infinity
controls.maxPolarAngle = THREE.MathUtils.degToRad( 90 ); // 你能够垂直旋转的角度的上限,范围是0到Math.PI,其默认值为Math.PI。
controls.target.set( 0, 0.5, 0 );
controls.update();

scene = new THREE.Scene();
scene.background = new THREE.Color( 0x333333 );
// environment: 若该值不为null,则该纹理贴图将会被设为场景中所有物理材质的环境贴图。 然而,该属性不能够覆盖已存在的、已分配给 MeshStandardMaterial.envMap 的贴图。默认为null。
scene.environment = new RGBELoader().load( 'textures/equirectangular/venice_sunset_1k.hdr' );
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
scene.fog = new THREE.Fog( 0x333333, 10, 15 );

// 网格地板
grid = new THREE.GridHelper( 20, 40, 0xffffff, 0xffffff );
grid.material.opacity = 0.2;
grid.material.depthWrite = false;
grid.material.transparent = true;
scene.add( grid );

// materials

const bodyMaterial = new THREE.MeshPhysicalMaterial( {
color: 0xff0000, 
metalness: 1.0, 
roughness: 0.5, 
clearcoat: 1.0, // 清漆层
clearcoatRoughness: 0.03
} );

const detailsMaterial = new THREE.MeshStandardMaterial( {
color: 0xffffff, metalness: 1.0, roughness: 0.5
} );

const glassMaterial = new THREE.MeshPhysicalMaterial( {
color: 0xffffff, metalness: 0.25, roughness: 0, transmission: 1.0
} );

const bodyColorInput = document.getElementById( 'body-color' );
bodyColorInput.addEventListener( 'input', function () {

bodyMaterial.color.set( this.value );

} );

const detailsColorInput = document.getElementById( 'details-color' );
detailsColorInput.addEventListener( 'input', function () {

detailsMaterial.color.set( this.value );

} );

const glassColorInput = document.getElementById( 'glass-color' );
glassColorInput.addEventListener( 'input', function () {

glassMaterial.color.set( this.value );

} );

// Car
// 车底部阴影图
const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );

// 车3D模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );

const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );

loader.load( 'models/gltf/ferrari.glb', function ( gltf ) {

const carModel = gltf.scene.children[ 0 ];

carModel.getObjectByName( 'body' ).material = bodyMaterial;

carModel.getObjectByName( 'rim_fl' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_fr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rl' ).material = detailsMaterial;
carModel.getObjectByName( 'trim' ).material = detailsMaterial;

carModel.getObjectByName( 'glass' ).material = glassMaterial;

wheels.push(
carModel.getObjectByName( 'wheel_fl' ),
carModel.getObjectByName( 'wheel_fr' ),
carModel.getObjectByName( 'wheel_rl' ),
carModel.getObjectByName( 'wheel_rr' )
);

// shadow  车底部阴影
const mesh = new THREE.Mesh(
new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
new THREE.MeshBasicMaterial( {
map: shadow, 
blending: THREE.MultiplyBlending, 
toneMapped: false, // 定义这个材质是否会被渲染器的toneMapping设置所影响,默认为 true 。
transparent: true // 定义此材质是否透明。这对渲染有影响,因为透明对象需要特殊处理,并在非透明对象之后渲染。设置为true时,通过设置材质的opacity属性来控制材质透明的程度。默认值为false。
} )
);
mesh.rotation.x = - Math.PI / 2;
// renderOrder: 这个值将使得scene graph(场景图)中默认的的渲染顺序被覆盖, 即使不透明对象和透明对象保持独立顺序。 渲染顺序是由低到高来排序的,默认值为0。
mesh.renderOrder = 2;
carModel.add( mesh );

scene.add( carModel );

// 坐标轴
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);

} );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

}

function render() {

controls.update();

const time = - performance.now() / 1000;

for ( let i = 0; i < wheels.length; i ++ ) {

wheels[ i ].rotation.x = time * Math.PI * 2;

}

grid.position.z = - ( time ) % 1;

renderer.render( scene, camera );

stats.update();

}

init();

</script>

</body>
</html>

vite构建工具和webpack构建工具有什么共同点和不同处

Webpack 和 Vite 都是现代前端开发中常用的构建工具,但它们在设计理念、性能和使用方式上有显著的区别。以下是它们的对比以及如何使用它们的简要说明:

1. Webpack

特点

  • 成熟稳定:Webpack 是前端生态中历史最悠久、最成熟的构建工具之一,拥有庞大的社区和丰富的插件生态。
  • 模块化打包:Webpack 的核心功能是将项目中的所有资源(如 JavaScript、CSS、图片等)视为模块,并通过依赖关系将它们打包成一个或多个文件。
  • 高度可配置:Webpack 的配置非常灵活,支持通过配置文件(如 webpack.config.js)自定义打包行为。
  • 支持多种功能:Webpack 支持代码分割、懒加载、热更新(HMR)、Tree Shaking 等高级功能。

适用场景

  • 大型项目,尤其是需要复杂配置和自定义打包逻辑的项目。
  • 需要兼容旧版浏览器或处理复杂资源加载的项目。

如何使用

  1. 安装 Webpack
npm install webpack webpack-cli --save-dev

2. 创建配置文件webpack.config.js):

const path = require('path');
module.exports = {
  entry: './src/index.js', //入口文件
  output: {
    filename: 'bundle.js', //入口文件
    path: path.resolve(__dirname,'dist'), //输出目录
  },
  module: {
    rules: [
      { 
        test: /\.css$/, //处理css文件
        use: ['style-loader', 'css-loader'],
      },
      ],
  },
};



3. 运行 Webpack

npx webpack

2. Vite

特点

  • 极速开发体验:Vite 利用现代浏览器的原生 ES 模块支持,在开发环境下无需打包,直接按需加载模块,启动速度极快。
  • 基于 ES Modules:Vite 在开发模式下使用浏览器原生的 ES Modules,生产模式下使用 Rollup 进行打包。
  • 开箱即用:Vite 提供了默认配置,支持 TypeScript、CSS 预处理器、热更新等功能,无需复杂配置。
  • 面向现代浏览器:Vite 更适合现代浏览器,对旧版浏览器的支持需要通过插件实现。

适用场景

  • 中小型项目,尤其是需要快速启动和开发的场景。
  • 使用现代前端框架(如 Vue 3、React)的项目。

如何使用

  1. 安装 Vite

按照提示选择项目模板(如 Vue、React、Vanilla JS 等)。

2. 启动开发服务器

npm run dev

3. 构建生产环境代码

npm run build

3:Webpack和Vite的主要区别

1743643881186.png

4. 如何选择?

  • 选择 Webpack
    • 项目需要高度自定义的打包配置。
    • 需要兼容旧版浏览器。
    • 项目规模较大,依赖复杂。
  • 选择 Vite
    • 追求极速的开发体验。
    • 项目基于现代前端框架(如 Vue 3、React)。
    • 项目规模较小,配置简单。

总结

  • Webpack 是前端构建工具的“老大哥”,功能强大但配置复杂,适合大型项目。
  • Vite 是新一代构建工具,以极速开发体验著称,适合中小型项目和现代前端框架。

根据项目需求选择合适的工具,可以显著提升开发效率和体验!

需求:对表格操作列中的操作进行局部刷新

需求:对表格操作列中的操作进行局部刷新,如action列中有勾号,叉号,来回切换按钮能够改变数据状态,同时不希望使用整体调接口查询数据来刷新,这里用“局部刷新(缓存方式)”实现

如下图:

image.png

  1. 表格结构
<el-table-column
                  prop="address"
                  label="Action"
                  width="120"
                  align="center"
                >
                  <template #default="scope">
                    <div class="tableTools">
                      <template v-if="scope.row.show">
                              <el-button
                                @click="
                                  fileStatusBtn(
                                    scope.row.id,
                                    0,
                                    scope.row,
                                    scope.$index
                                  )
                                "
                                link
                                title="Set the file status (hide or show) for new reviewer and author"
                                type="primary"
                              >
                                <el-icon><Select /></el-icon>
                              </el-button>
                            </template>
                            <template v-else>
                              <el-button
                                @click="
                                  fileStatusBtn(
                                    scope.row.id,
                                    1,
                                    scope.row,
                                    scope.$index
                                  )
                                "
                                link
                                title="Set the file status (hide or show) for new reviewer and author"
                                type="primary"
                              >
                                <el-icon>
                                  <CloseBold />
                                </el-icon>
                              </el-button>
                            </template>

                  
                    </div>
                  </template>
                </el-table-column>
  1. 方法
const fileStatusBtn = (id, status, row, index) => {
    row.show = !row.show; // show是用来控制勾号,叉号的交替显示
    // 局部更新某一条数据的show状态,不要重新调用接口,造成服务器压力
    updateRowData(index, "show", row.show);
    
    // 原先的调整个表格接口查询新结果
    // emit("uploadDatas"); 
};
  1. updateRowData方法
// 修改数据并同步更新缓存的方法
const updateRowData = (rowIndex, key, value) => {
  // 修改当前数据
  const cacheKey = `tableSubmissionData_${pageId}`;
  const cachedData = JSON.parse(localStorage.getItem(cacheKey));

  if (cachedData) {
    // 修改缓存中的数据
    cachedData.data.content.preReview.articleFiles[rowIndex][key] = value;

    // 同步到组件的内存数据
    aeInfor.value.reviewFiles[rowIndex][key] = value;

    // // 更新缓存
    updateCache(cacheKey, cachedData);
  }
};

const updateCache = (key, data) => {
  localStorage.setItem(key, JSON.stringify(data));
};
  1. 联调接口的方法中添加缓存数据判断
const cacheKey = ref("");

const getSubmissionTabDatas = async () => {
  // 使用 localStorage 缓存数据,避免重复请求
  cacheKey.value = `tableSubmissionData_${pageId}`;
  const cachedData = localStorage.getItem(cacheKey.value);

  if (cachedData) {
    const res = JSON.parse(cachedData);

    allDatas.value = res.data.content;
    participent.value = res.data.content.summary.participantList;
    lastRoundId.value = res.data.content.summary.roundid;
    tabButtons.value = res.data.content.tabButtons;
    isachive.value = res.data.content.isachive ? true : false;
    aeInfor.value.reviewFiles = res.data.content.preReview.articleFiles;
    allDatas.value.showBlackList = res.data.content.showBlackList;

    console.log("====缓存===", aeInfor.value.reviewFiles);

    return; // 从缓存中获取数据,不需要再进行 API 请求
  }

  await getSubmissionTab({
    id: pageId,
    type: pageType ? "editorialAssistant" : undefined,
    accessArticle: route.query.isOnlyShowSE ? 1 : undefined,
  }).then((res) => {
    localStorage.setItem(cacheKey.value, JSON.stringify(res));

    allDatas.value = res.data.content;
    participent.value = res.data.content.summary.participantList;
    lastRoundId.value = res.data.content.summary.roundid;
    tabButtons.value = res.data.content.tabButtons;
    isachive.value = res.data.content.isachive ? true : false;
    aeInfor.value.reviewFiles = res.data.content.preReview.articleFiles;
    allDatas.value.showBlackList = res.data.content.showBlackList;

    console.log("====接口===", aeInfor.value.reviewFiles);
  });
};
  1. 缓存数据清除
const handleBeforeUnload = (event) => {
  localStorage.clear();
  localStorage.removeItem(cacheKey.value);
};

onMounted(async () => {
  window.addEventListener("beforeunload", handleBeforeUnload);
  localStorage.clear();
  localStorage.removeItem(cacheKey.value);
  await getSubmissionTabDatas();
  
  ----------------------------------------------------------------
  await initializeCollapseState(); // 初始化折叠状态
  nextTick(() => {
    const el = document.getElementById("submissionId");
    if (!route.query.roundIndex) return;
    el?.scrollIntoView({
      behavior: "smooth", //smooth:平滑,auto:直接定位
      block: "start",
      inline: "start",
    });
  });
});

onBeforeUnmount(() => {
  window.removeEventListener("beforeunload", handleBeforeUnload);
  localStorage.clear();
  localStorage.removeItem(cacheKey.value);
});

git subtree 最佳实践

目录

1背景

1.1 痛点

目前业务主要有A端和B端两个系统,这两个系统技术栈是完全相同的,许多功能也相同。所以在日常的开发过程中,产生了大量的重复工作,一个需求在A端完成后,还需要复制到B端,这样往往容易出现疏漏。

1.2 解决思路

实现代码复用目前,有下面两种方法:

  • 抽象成NPM包进行复用

  • 使用Git的子仓库对代码进行复用

由于本项目要实现业务代码复用,抽成 npm 包的方式就不太合适。

1.3 什么是git子仓库

通俗上的理解, 一个Git仓库下面放了多个其他的Git仓库,其他的Git仓库就是我们父级仓库的子仓库。

通过使用git子仓库将公共的组件抽离出来,实现在一端更改后,另一端通过git去合并代码,将我们从繁重的复制粘贴中解放出来。同时,可以在后续的需求中放入公共组件,通过增量的方式去应用这个技术,不会影响以前的代码。

1.4 git的两种子仓库方案

目前git实现子仓库有下面两种方案:

  1. git submodule。 tdesign 使用的就是这种方案。

  2. git subtree

两种方案的对比如下:

维度 subtree submodule 优劣对比
空间占用 subtree 在初始化 add 时,会将子仓库 copy 到父仓库中,并产生至少一次 merge 记录。所以会占用大量父仓库空间 submodule 在初始化 add 时,会在父仓库新建一个 .gitmodules 文件,用于保存子仓库的 commit hash 引用。所以不会占用父仓库空间 submodule 更优
clone subtree add 至父仓库之后,后续的 clone 操作与单一仓库操作相同 后续 clone 时 submodule 还需要 init/update 操作,且 submodule 子仓库有自己的分支。 流水线部署时需要更改配置。 subtree 更优
update 子仓库更新后,父仓库需要 subtree pull 操作,且命令行略长,需要指定 --prefix 参数。由于无法感知子仓库的存在,可能会产生 merge 冲突需要处理 子仓库更新后,父仓库需要 submodule update 操作。父仓库只需变动子仓库 hash 引用,不会出现冲突 submodule 更优
commit 父仓库直接提交父子仓库目录里的变动。若修改了子仓库的文件,则需要执行 subtree push 父子仓库的变动需要单独分别提交。且注意先提交子仓库再提交父仓库 subtree 更优

用一句话来描述 Git Subtree 的优势就是:

经由 Git Subtree 来维护的子项目代码,对于父项目来说是透明的,所有的开发人员看到的就是一个普通的目录,原来怎么做现在依旧那么做,只需要维护这个 Subtree 的人在合适的时候去做同步代码的操作。

1.5 git subtree 对现有项目的影响

使用git subtree 无需改变现有工程结构,可以只在新需求中使用它去复用代码,相当于它只是一个复制粘贴的工具。

2方案设计

2.1 创建子仓库

建立一个单独的git仓库命名为 common , 可以创建如下的目录结构:

-common
  -utils 公共的工具函数
  -services 接口
  -components 公共的组件
  -hooks 公共的hooks

2.2 关联子仓库

然后在A端和B端添加common的远程仓库:

 git remote add common [common仓库地址]

建立父仓库和子仓库的依赖关系:

git subtree add --prefix=src/common common master

将common远程仓库的master分支拷贝到父仓库的 src/common 目录下, 这时在两个项目的src目录多一个 common 的文件夹,我们可以像一个本地目录一样去使用里面的代码。

--prefix 可以用 -P 来代替,见下文。

2.3 拉取子仓库更新

git subtree pull -P src/common common master

2.4 推送更改到子仓库

方法一 直接提交

git subtree push -P src/common common master

subtree push实际上是遍历本工程每一次提交,把提交文件涉及到subtree目录的挑出来,同步到subtree工程,如果提交有很多,速度会非常慢。

方法二 拆分代码再push[推荐]

git subtree split --rejoin -P src/common
git subtree push -P src/common common master

如果想要split成功,一定要去除 commit msg 的校验。

方法三 拆分代码到单独分支

git subtree split --rejoin -P src/common -b split-common
git push common split-common

首先将 common 拆分到父仓库的 split-common 分支,可以通过 checkout 到这个分支查看内容。

2.5 删除子仓库

git rm -r src/common

2.5 细节

在开发一个需求的时候, A端更改了 common 后,其他人只需要向以前一样在父仓库拉取代码。而当想在B端使用 common 代码,则需要将A端的代码同步到common 仓库,B拉取一下就行。

问题

git subtree split 无效

我们项目是基于 umi 脚手架开发的项目,这个脚手架自带了一个 gitHooks 会对 commit 的msg进行校验,而git subtree split 的原理就是通过 msg 进行判断。 解决方法:去掉 package.json 中的 commit 校验

{
  "gitHooks": {
  }
}

修改后没有同步

问题描述

修改一个后,没有push代码,慢慢导致后面两端的子仓库出现差异, 出现代码冲突。

解决方法

每次修改公共的代码都要 push 和 pull, 手动保持一致。

git subtree pull 冲突

错误信息如下

fatal: refusing to merge unrelated histories

解决方法, 在 git subtree pull 时添加 --squash 参数, 类似于 git push 的 --allow-unrelated-historie参数。

git subtree push 不上去

git push using:  common feature/20221214
cache for f1156335aca1314ff75ba328a850cbdd13affb5a already exists!

stackoverflow.com/questions/6…

暂时无法解决

参考文章

# 为什么你的公司不应该使用git submodule # Git subtree用法与常见问题分析 # 用 Git Subtree 在多个 Git 项目间双向同步子项目 # Git subtree 要不要使用 –squash 参数 # 掌握Git的subtree[译]

🔥《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(15)-Charles如何配置反向代理

1.简介

在App开发的过程当中,抓包是一个很常见的需求,而有些app的请求不会在网络设置代理时被抓到数据包,这里若是需要抓包就需要搭建反向代理。

2.什么是代理?

什么是代理,来一张图了解一下。

代理又分为正向代理和反向代理。

3.什么是正向代理?

先来看张图~

【再举个栗子】

某同学喜欢面向搜索引擎编程,想通过 百度 搜索引擎查找一些学习资料,但是有些网站直接访问可能不太安全,会暴露自己的IP,同学比较苦恼,想着怎样才能使用百度 搜索自己想要的学习资料,又不会暴露自己的IP在网站上呢?

这时我告诉该同学,我呢手上刚好有一台代理服务器,这台代理服务器通过nginx配置了正向代理转发http和https请求,你呢,只需要在自己的Windows本地电脑的网关配置一下这台代理服务器的IP和端口号,就能正常通过代理服务器访问到百度 并搜索相关的学习资料了,还不会暴露自己真实的IP~

4.什么是反向代理?

先来一张图了解下~

和正向代理相应的,正向代理代理客户端,反向代理代理服务端。

反向代理(reverse proxy):是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

我们在租房子的过程中,除了有些房源需要通过中介以外,还有一些是可以直接通过房东来租的。用户直接找到房东租房的这种情况就是我们不使用代理直接访问国内的网站的情况。

还有一种情况,就是我们以为我们接触的是房东,其实有时候也有可能并非房主本人,有可能是他的亲戚、朋友,甚至是二房东。但是我们并不知道和我们沟通的并不是真正的房东。这种帮助真正的房主租房的二房东其实就是反向代理服务器。这个过程就是反向代理。

对于常用的场景,就是我们在Web开发中用到的负载均衡服务器(二房东),客户端(租客)发送请求到负载均衡服务器(二房东)上,负载均衡服务器(二房东)再把请求转发给一台真正的服务器(房东)来执行,再把执行结果返回给客户端(租客)。
反向代理,其实是"代理服务器"代理了"目标服务器",去和"客户端"进行交互。

通过反向代理服务器访问目标服务器时,客户端是不知道真正的目标服务器是谁的,甚至不知道自己访问的是一个代理。

5.正向代理和反向代理的区别

虽然正向代理服务器和反向代理服务器所处的位置都是客户端和真实服务器之间,所做的事情也都是把客户端的请求转发给服务器,再把服务器的响应转发给客户端,但是二者之间还是有一定的差异的。

1、正向代理其实是客户端的代理,帮助客户端访问其无法访问的服务器资源。反向代理则是服务器的代理,帮助服务器做负载均衡,安全防护等。

2、正向代理一般是客户端假设的,比如在自己的机器上安装一个代理软件。而反向代理一般是服务器假设的,比如在自己的机器集群中部署一个反向代理服务器。

3、正向代理中,服务器不知道真正的客户端到底是谁,以为访问自己的就是真实的客户端。而在反向代理中,客户端不知道真正的服务器是谁,以为自己访问的就是真实的服务器。

4、正向代理和反向代理的作用和目的不同。正向代理主要是用来解决访问限制问题。而反向代理则是提供负载均衡、安全防护等作用。二者均能提高访问速度。

6.须要准备的工做

  • 在本身电脑上面搭建一个可用的Charles
  • 须要抓包的远端服务的端口号和Host地址
  • 在本身电脑上面搭建一个本地DNS解析服务

7.具体步骤 (Windows下的操作,Mac也同理)

1.确保手机可以连接上Charles,本身电脑上面可以看到正常请求出来的数据包(具体抓包可以看宏哥前边的教程)

2.打开Charles,勾选Proxy --> Reverse proxise...,进入反向代理设置界面。如下图所示:

3.进入Reverse Proxies Settings(反向代理设置)页面,勾选 Enable Reverse Proxies 。如下图所示:

4.在【Add】新增。如下图所示:

Edit Reverse Proxy 视图中的选项含义:

local port:本地端口
本地主机上的端口创建反向代理。该字段可能会自动填充一个可用的端口。如果有另一个应用程序使用该端口,则在反向代理启动时将收到一条警告消息

Remote host:远程主机
作为反向代理的目的地的远程主机的主机名或IP地址

Remote port:远程端口
远程端口默认为80,这是HTTP的默认端口。

Rewrite redirects:重写重定向
重定向远程服务器的响应将被重写与反向代理源地址相匹配,默认为开
远程服务器的重定向响应是完全限定的URL,即使它们在同一网站内
如果重定向到远程服务器地址,则需要将其重写为反向代理本地地址,否则客户端将使用重定向URL到远程主机,因此不再通过反向代理连接

Preserve host in header fields:保留主机头
Host HTTP标头从传入请求不变地传递,而不是正常重写主机头以匹配反向代理远程主机,默认为关闭
仅当您具有特定要求时,才需要保留主机头;普通使用的时候没有必要使用的

Listen on a specific address:监听特定地址
指定本地地址以侦听反向代理,可以启用此选项并在此处输入IP地址

8.Charles反向代理实战

Charles反向代理是提供一个端口转发的功能,用于除IE外发出的HTTP请求,例如需要跟踪Smartbi服务器与XMLA服务器之间的通信、Smartbi SDK与服务器之间通信等。

宏哥在Apache服务器安装在A计算机上,IP地址为:10.11.53.180,并开启服务,端口号为:80(默认)。然后宏哥简单部署一个HTML页面,在浏览器中访问服务。如下图所示:

现移动端访问A服务器部署的HTML页面出现错误,但是需要录制移动端的HTTP请求。这时候就需要Charles的反向代理帮助我们解决这个问题。具体操作步骤如下:

1.找一台其他计算机,如计算机B,其IP地址为10.11.53.193,宏哥这里演示的就是宏哥的本地计算机,如下图所示:

2.在计算机B上安装Charles,并启动,这里宏哥已经安装就不做演示了。

3.选中charles上的"Proxy"-》"Reverse Proxies",进入反向代理设置界面,如下图所示:

4.反向代理设置界面如下,点击"Add"按钮,新建反向代理设置,如下图所示:

5.设置反向代理的端口号,IP地址等信息。

其中 Local Port是指计算机B的一个空闲端口,如本例中使用8080;
Remote Host是指HTML页面服务的IP,即计算机A的IP: 10.11.55.182;
Remote Port是指HTML页面服务的端口号,在本例中访问HTML页面的端口号为80(Apache默认端口)
点击OK保存反向代理设置,如下图所示:

6.上一步点击OK之后会出现反向代理列表窗口,勾选我们上一步设置的反向代理,点击ok启用,如下图所示:

7.在任意一台计算机或者移动端上,通过**http://计算机B的IP:反向代理中设置的Loal Port端口/inde.html**,可以访问到HTML页面服务。本例中通过在浏览器或者移动端的服务器设置上输入**http://10.11.53.193:8080/index.html**访问,如下图所示:

*注:访问是需要写IP,不能写localhost。
*

8.在charles中会监测到反向代理访问,首次会弹出是否允许访问,选择'Allow'按钮,允许访问。没有设置代理之前是访问不到的,如下图所示:

9.在计算机B上的charles就可以录制到HTTP请求,如下图所示:

9.小结

反向代理位于用户和应用服务器之间,是连接用户和服务器的中介。

于是我们可以

1.缓存,将服务器的响应缓存在自己的内存中,减少服务器的压力。

2.负载均衡,将用户请求分配给多个服务器。

3.访问控制

4.加上一些特殊的东西做特殊的事情(如IPS—入侵防御系统、web应用防火墙等)

好了,今天时间也不早了,宏哥就讲解和分享到这里,感谢您耐心的阅读,希望对您有所帮助。

Flask学习笔记 - 视图函数

前言

继续自习...

Flask 视图函数

视图函数是Flask应用中的核心部分,它负责处理请求并生成响应

  1. 定义视图函数:视图函数是处理请求并返回响应的核心功能。
  2. 接收请求数据:使用 request 对象获取 URL 参数、表单数据、查询参数等。
  3. 返回响应:可以返回字符串、HTML、JSON 或自定义响应对象。
  4. 处理请求和响应:使用 request 对象和 make_response 来处理请求和生成自定义响应。
  5. 处理错误:视图函数内处理异常或使用 Flask 的错误处理机制。
  6. 视图函数的装饰器:使用 @app.before_request、@app.after_request 等装饰器处理请求前后逻辑。
  7. 视图函数返回的状态码:可以指定 HTTP 状态码来表示请求的处理结果。

定义视图函数/接收请求数据/返回响应/处理请求和响应

from flask import request

# 接收 - URL参数
@app.route('/greet/<name>')  
def greet(name):
    return f'Hello, {name}!'

# 接收 - 表单数据
@app.route('/submit', methods=['POST'])  
def submit():
    username = request.form.get('username')  
    return f'Form submitted by {username}!'

# 接收 - GET请求中query
@app.route('/search')
def search():
    query = request.args.get('query')
    return f'Search results for: {query}'
    
# 返回 - 字符串
@app.route('/message')
def message():
    return 'This is a simple message.'

# 返回 - HTML模板
@app.route('/hello/<name>')
def html_hello(name):
    return render_template('hello.html', name=name)

# 返回 - JSON
@app.route('/api/data')
def api_data():
    data = {'key': 'value'}
    return jsonify(data)

# 返回 - 自定义响应对象
@app.route('/custom')
def custom_response():
    response = Response('Custom response with headers', status=200)
    response.headers['X-Custom-Header'] = 'Value'
    return response

# 处理请求和响应
@app.route('/info')
def info():
    user_agent = request.headers.get('User-Agent')
    return f'Your user agent is {user_agent}'

处理错误

可以在视图函数中处理异常或错误,或者通过 Flask 提供的错误处理机制来处理应用中的错误

# try-except
# /divide/3/0
@app.route('/divide/<int:x>/<int:y>')
def divide(x, y):
    try:
        result = x / y
        return f'Result: {result}'
    except ZeroDivisionError:
        return 'Error: Division by zero', 400

# 全局错误
@app.errorhandler(404)
def not_found(error):
    return '消失了', 404

抛异常 01.png

正常 02.png

全局错误码 03.png

装饰器

  • @app.before_request:在每个请求处理之前运行的函数。
  • @app.after_request:在每个请求处理之后运行的函数。
  • @app.teardown_request:在请求结束后运行的函数,用于清理工作。
@app.before_request
def before_request():
    print('请求前')

@app.after_request
def after_request(response):
    print('请求后')
    return response

@app.teardown_request
def teardown_request(exception):
    print('请求结束,清理')

看到终端有输出对应的日志 04.png

视图函数返回的状态码

视图函数不仅可以返回内容,还可以指定 HTTP 状态码。

# 返回
@app.route('/status')
def status():
    return 'Everything is OK', 200

@app.route('/error')
def error():
    return Response('An error occurred', status=500)

工具栏 - 帮助 - 切换开发人员工具,可以打开开发工具,检查网络,确认确实返回了500的错误码

06.png

Demo

Flask

参考

  1. Flask 视图函数

前端图片技术深度解析:格式选择、渲染原理与性能优化

前言

在Web开发中,图片处理是影响用户体验和网站性能的关键因素。前端开发者每天都要面对一系列图片相关的技术决策:

  • 静态图片格式:选择PNG还是JPG追求更小体积?
  • 动态图像选择:GIF还是APNG?
  • 压缩策略:如何在保持视觉质量的前提下减小文件大小?
  • 格式应用:web端图片是否要转化成 WebP 还是 AVIF?

这些决策直接影响着首屏加载时间、带宽消耗和用户交互体验等核心指标,甚至将影响业务的商业转化率。图片处理其实也考验着一个前端的基本功,本文将深入解析常见图片格式的编码原理,结合浏览器渲染机制与现代Web标准,提供一套科学、可落地的图片优化实践方案。


一、图片格式

在选择图片格式之前首先要清楚这些图片格式的特点和区别

1. 类型

图片格式主要分为两种类型:位图和矢量图。我们常见的图片除了 SVG 都是位图

位图

位图是由像素点组成的,适合照片和复杂图像,其清晰度由分辨率和像素总量共同决定。

矢量图

矢量图是通过代码或者说是用数学公式描述的

  1. 优点是在任何缩放比例下都可以展示得很清晰而不会失真
  2. 缺点是细节的展示效果不够丰富,对于复杂图像来说,比如要达到照片的效果,若通过SVG进行矢量图绘制,需要大量的代码描述,生成文件就会非常大,并且浏览器解析和渲染对很耗性能,所以svg不适合细节丰富的图像,只适合图标和简单图形。

2. 压缩方式

图片格式不同的压缩方法由图像编码标准决定:

  1. JPG(有损压缩)

    • 原理:通过丢弃人眼不敏感的细节(如高频色彩信息)来减少文件体积。
    • 特点
      • 压缩率极高,适合照片类图像(色彩丰富、渐变多)。
      • 不支持透明度,多次保存会累积质量损失(“代际损失”)。
  2. PNG(无损压缩)

    • 原理:使用 DEFLATE 算法无损压缩像素数据,保留所有原始信息。
    • 特点
      • 适合图标、线条图等需要保留锐利边缘的图像。
      • 支持透明度,文件体积通常比 JPEG 大。
  3. WebP:集 JPEG 和 PNG 优点于一身。无损压缩比 PNG 小 26%,有损压缩比 JPEG 小 25-34%,并且支持透明通道。

  4. AVIF:更先进的压缩算法,压缩率比 WebP 还高 30%。但兼容性较差,目前浏览器支持率只有 86%。

而我们开发过程中经常对图片进行二次压缩(同格式),比如借助 TinyPng 工具,实际上是对图片进行有损压缩

  1. 量化减少颜色数量:将 1600 万色(24-bit)缩减到 256 色(8-bit)。
  2. 丢弃元数据:删除 EXIF、ICC 配置等非必要数据。
  3. 优化压缩算法:使用改进的 DEFLATE 算法更高效压缩数据。

虽然 PNG 本身是无损格式,但 TinyPNG 通过有损预处理+无损压缩的组合,实现了“视觉无损”的高压缩率。

3. 图片格式总结

格式 类型 压缩方式 透明度支持 动画支持 特点 适用场景
JPEG 位图 有损 高压缩率,适合照片,但压缩过度会失真 照片、复杂色彩图像
PNG 位图 无损 ✅ (Alpha) 支持透明,无损压缩,文件较大 透明图标、精确图形(如截图)
GIF 位图 无损(256色) ✅ (1-bit) 支持简单动画,但色彩有限 表情包、简单动画
WebP 位图 有损/无损 ✅ (Alpha) 现代格式,压缩效率高,支持动画和透明度 全能替代(JPEG/PNG/GIF)
AVIF 位图 有损/无损 ✅ (Alpha) 下一代压缩,比WebP更高效,但兼容性差 高质量图片、未来项目
APNG 位图 无损 ✅ (Alpha) PNG的动画扩展,支持全透明动画 高质量动画(替代GIF)
SVG 矢量图 无损 无限缩放,通过代码描述图形,支持CSS/JS控制 图标、LOGO、UI元素

二、浏览器渲染机制与性能影响

1. 关键渲染路径中的图片处理

graph TD
    A[HTML解析] --> B[发现<img>标签]
    B --> C{是否可见视口?}
    C -->|是| D[立即加载]
    C -->|否| E[延迟加载]
    D --> F[解码线程处理]
    F --> G[主线程布局计算]
    G --> H[绘制合成]

浏览器的图片渲染流程可以分为以下阶段:

1. 解析与加载

  • HTML 解析:浏览器解析 HTML 时遇到 <img> 标签,立即发起图片资源的网络请求(除非使用 loading="lazy" 延迟加载)。
  • 优先级控制:图片的加载优先级通常低于关键资源(如 CSS、JS),但可通过 fetchpriority="high" 调整。
  • 预加载优化:使用 <link rel="preload"> 提前加载关键图片。
2. 解码(Decoding)
  • 主线程阻塞:图片解码(将二进制数据转换为像素)默认在主线程执行,大图片可能导致主线程卡顿。

  • 异步解码:通过 img.decode() API 异步解码图片,避免阻塞主线程。

  • 格式影响

    • JPEG/PNG:解码复杂度较高。
    • WebP/AVIF:现代格式解码效率更高。
3. 布局(Layout)与绘制(Paint)
  • 布局计算:图片尺寸变化(未指定 width/height)会导致布局重排(Reflow)。
  • 图层分离:使用 will-change: transform 或 transform: translateZ(0) 将图片提升到独立图层,减少绘制区域。
4. 合成(Composite)
  • GPU 加速:某些 CSS 属性(如 transformopacity)触发 GPU 合成,跳过主线程直接由合成器处理。
  • 减少重绘:避免频繁修改图片尺寸或位置,触发不必要的重绘(Repaint)。

2. 性能瓶颈分析

  • 解码耗时:高分辨率图片消耗CPU资源(尤其是AVIF/WebP)
  • 布局抖动:未设置尺寸的图片引发回流(CLS问题)
  • 内存占用:4096x4096的RGBA图片占用67MB内存
  • 网络竞争:过多图片请求阻塞关键资源加载

实测数据

  • 未优化的2MB JPEG:加载时间≈800ms(4G网络)
  • 优化后的200KB WebP:加载时间≈150ms
  • LCP(最大内容绘制)提升300ms可使转化率提高5%

三、性能优化全方案

1. 格式选择策略

前面可以得出结论,avif压缩率最高,性能最好,但要考虑兼容性问题,而webp是当前最佳平衡选择,最佳实践我们可以通过代码检测图片格式兼容性。

服务端:浏览器端在发起图片请求时会带上当前浏览器支持的图片格式,可以在服务端判断后返回对应的图片格式。

image.png

// 动态格式检测
const acceptHeader = req.headers.accept || '';
const supportsWebP = acceptHeader.includes('webp');
const supportsAvif = acceptHeader.includes('avif');

function getBestFormat() {
  if (supportsAvif) return 'avif';
  if (supportsWebP) return 'webp';
  return 'jpg';
}

决策树

graph TD
    A[选择图片] --> B{是否需要动画?}
    B -->|是| C[GIF/WebP/AVIF]
    B -->|否| D{是否为复杂图像?}
    D -->|是| E[是否需要透明度]
    D -->|否| F[SVG]
    E -->|是| G[PNG/WebP/AVIF]
    E -->|否| H[JPEG/WebP/AVIF]
    C --> I[检测浏览器兼容性]
    F --> I
    G --> I
    H --> I

2. 响应式图片技术

响应式图片是现代Web开发的必备技术,可以根据设备屏幕大小和分辨率提供最合适的图片资源。

srcset 和 sizes 属性

<img
  src="image-400w.jpg"
  srcset="image-400w.jpg 400w, image-800w.jpg 800w, image-1200w.jpg 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  alt="响应式图片示例" />

picture 元素实现格式回退

<picture>
  <source srcset="image.avif" type="image/avif" />
  <source srcset="image.webp" type="image/webp" />
  <img src="image.jpg" alt="图片描述" />
</picture>

3. 懒加载策略

原生懒加载

<img src="image.jpg" loading="lazy" alt="懒加载图片" />

交叉观察器实现

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach((img) => {
  observer.observe(img);
});

4. CDN与缓存优化

  • 使用CDN分发图片资源,减少网络延迟
  • 设置合理的缓存策略,常见图片可设置长期缓存
  • 使用内容哈希命名,便于缓存更新
# Nginx缓存配置示例
location ~* \.(jpg|jpeg|png|gif|webp|avif)$ {
    expires 30d;
    add_header Cache-Control "public, no-transform";
}

5. 图片预处理与自动化

构建时优化

使用webpack、gulp等工具在构建阶段自动处理图片:

// webpack配置示例
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65,
              },
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: [0.65, 0.9],
                speed: 4,
              },
              webp: {
                quality: 75,
              },
            },
          },
        ],
      },
    ],
  },
};

图片服务API

使用图片处理服务动态调整图片大小和格式: 例如通过阿里云OSS服务通过x-oss-process图片处理参数,将图片进行裁剪、质量变化、格式转化等操作

<!-- 使用OSS等服务 -->
<img src="https://oss-console-img-demo-cn-hangzhou-3az.oss-cn-hangzhou.aliyuncs.com/example.gif?x-oss-process=image/format,png" alt="优化图片" />

四、未来趋势展望

1. HTTP/3多路复用

HTTP/3基于QUIC协议,采用UDP传输,解决了HTTP/2中的队头阻塞问题。对于图片密集型应用,多路复用能显著提升并行下载效率,减少等待时间。

graph LR
    A[HTTP/2] --> B[单TCP连接]
    B --> C[队头阻塞]
    D[HTTP/3] --> E[多UDP连接]
    E --> F[避免队头阻塞]

2. 机器学习驱动的图像压缩

Google RAISR技术

RAISR (Rapid and Accurate Image Super Resolution) 使用机器学习算法,能在低分辨率图像基础上重建高质量图像,大幅减少传输数据量。

神经网络压缩

基于神经网络的压缩算法如Facebook的DLVC (Deep Learning Video Coding),可将压缩率提高30-50%,同时保持视觉质量。

3. WebCodecs API与GPU加速

WebCodecs API允许Web应用直接访问底层媒体编解码器,实现硬件加速:

// WebCodecs API示例
async function decodeImage(imageData) {
  const decoder = new ImageDecoder({
    data: imageData,
    type: 'image/avif',
  });

  const result = await decoder.decode();
  return result.image;
}

4. 新一代图像格式

JPEG XL

JPEG XL旨在替代JPEG,提供更高压缩率和更丰富的功能:

  • 比JPEG小60%,同时提高视觉质量
  • 支持无损转换现有JPEG
  • 支持动画、透明度和HDR

WebP 2

Google正在开发WebP的下一代版本,预计将进一步提高压缩效率,并增强动画支持。

结语

图片优化是性能工程的艺术,开发需要平衡视觉质量、加载速度与兼容性需求。通过深入理解格式特性、浏览器工作原理,并结合本文提供的全方位优化策略,可以显著提升Web应用的用户体验和业务指标。

随着新技术的不断涌现,图片优化的方法也在持续演进。作为前端开发者,我们需要不断学习和实践,将最佳图片处理方案融入日常开发流程中。

URL参数传递的两种方式:查询参数与路径参数详解

在现代Web开发中,URL设计是前后端交互的重要桥梁。当我们需要在URL中传递数据时,主要有两种方式:查询参数(Query Parameters)和路径参数(Path Parameters)。这两种方式各有特点,适用于不同场景。本文将通过清晰的对比和具体案例,帮助开发者理解它们的区别,并在实际项目中做出合理选择。

第一部分:基础概念与语法

1. 查询参数 (Query Parameters)

定义:查询参数是附加在URL末尾的键值对,以问号?开始,多个参数间用&连接。

语法格式http://example.com/path?参数1=值1&参数2=值2

实际示例http://localhost:3000/files?category=documents

在这个例子中:

  • 基本URL:http://localhost:3000/files
  • 查询参数:category=documents(表示文件类别为"documents")

2. 路径参数 (Path Parameters)

定义:路径参数是直接嵌入在URL路径中的变量部分,成为URL路径结构的一部分。

语法格式http://example.com/资源类型/资源ID/子资源

实际示例http://localhost:3000/chat/session-1743569839440

在这个例子中:

  • 基本URL:http://localhost:3000/chat
  • 路径参数:session-1743569839440(表示特定的聊天会话ID)

第二部分:技术实现与代码示例

1. 在不同框架中获取查询参数

Next.js实现

// app/files/page.tsx
"use client"
import { useSearchParams } from 'next/navigation';

export default function FilesPage() {
  const searchParams = useSearchParams();
  const category = searchParams.get('category') || 'all';
  
  return <div>显示{category}类别的文件</div>;
}

Express.js实现

// server.js
app.get('/files', (req, res) => {
  const category = req.query.category || 'all';
  res.send(`显示${category}类别的文件`);
});

2. 在不同框架中获取路径参数

Next.js实现

// app/chat/[id]/page.tsx
export default function ChatPage({ params }) {
  const sessionId = params.id; // 例如 "session-1743569839440"
  
  return <div>显示会话ID为{sessionId}的聊天记录</div>;
}

Express.js实现

// server.js
app.get('/chat/:id', (req, res) => {
  const sessionId = req.params.id;
  res.send(`显示会话ID为${sessionId}的聊天记录`);
});

第三部分:系统性对比分析

1. 功能与特性对比

特性 查询参数 (Query Parameters) 路径参数 (Path Parameters)
位置 URL末尾,?之后 嵌入在URL路径中
格式 键值对:?key=value 路径片段:/value
可选性 通常是可选的 通常是必需的
多值支持 支持(如?tags=js&tags=react 需要特殊处理
可见性 显式的键值关系,易于理解 隐含在路径中,需要了解API规则
URL长度 可能导致URL较长 通常产生更简洁的URL

2. 技术层面对比

技术方面 查询参数 (Query Parameters) 路径参数 (Path Parameters)
路由定义 单一路由可处理多种参数组合 通常需要为不同参数值定义路由模式
缓存影响 不同参数可能被视为同一资源的不同表示 不同路径通常被视为完全不同的资源
编码处理 需要正确处理URL编码(如空格、特殊字符) 通常需要符合URL路径规则,可能限制某些字符
安全性 容易在日志、历史记录中暴露 较不明显,但仍可见
状态保存 适合保存在书签中 同样适合保存在书签中

3. 业务与用户体验对比

业务方面 查询参数 (Query Parameters) 路径参数 (Path Parameters)
SEO优化 搜索引擎可能将带不同参数的URL视为同一页面 被视为不同的URL,可能有更好的SEO表现
用户友好性 参数意义较为直观 需要理解站点结构,但URL更简洁
分享便利性 长参数列表可能导致复制/分享问题 通常更简短,便于分享
语义清晰度 明确表示是参数 作为资源路径的一部分,语义更强

第四部分:适用场景分析

1. 查询参数的最佳使用场景

查询参数最适合以下情况:

  1. 筛选与排序

    • 示例:/products?category=electronics&sort=price&order=asc
    • 优势:不改变资源本身,只改变资源的查看方式
  2. 分页功能

    • 示例:/articles?page=2&limit=10
    • 优势:保持URL基础路径不变,便于实现分页控制
  3. 搜索操作

    • 示例:/search?q=javascript&type=blog
    • 优势:可以传递复杂的搜索条件
  4. 非必需参数

    • 示例:/dashboard?view=grid&theme=dark
    • 优势:参数可省略,使用默认值
  5. 状态保持

    • 示例:/map?lat=37.7749&lng=-122.4194&zoom=12
    • 优势:便于保存和恢复用户界面状态

2. 路径参数的最佳使用场景

路径参数最适合以下情况:

  1. 资源标识

    • 示例:/users/12345
    • 优势:明确表示访问特定资源
  2. 分层资源

    • 示例:/departments/sales/employees/john
    • 优势:表达资源的层次结构关系
  3. RESTful API设计

    • 示例:/api/v1/products/567
    • 优势:符合REST架构风格
  4. 必需参数

    • 示例:/invoices/INV-2021-001/download
    • 优势:参数是资源定位的必要部分
  5. 多语言支持

    • 示例:/zh-CN/docs/getting-started
    • 优势:使URL结构更有语义

第五部分:实际案例解析

1. 电子商务网站案例

产品列表页

  • 使用查询参数:/products?category=shoes&size=42&color=black&sort=price_asc
  • 原因:这些都是筛选条件,不改变我们正在查看的资源类型(产品)

产品详情页

  • 使用路径参数:/products/nike-air-max-2023
  • 原因:标识特定产品,是访问该资源的核心标识

2. 内容管理系统案例

文章列表

  • 使用查询参数:/articles?author=zhang&tag=technology&published_after=2023-01-01
  • 原因:这些参数用于筛选文章列表,不改变资源类型

特定文章

  • 使用路径参数:/articles/understanding-url-parameters
  • 原因:直接标识特定文章资源

3. 社交媒体应用案例

用户资料

  • 使用路径参数:/users/johndoe
  • 原因:标识特定用户资源

用户内容过滤

  • 混合使用:/users/johndoe/posts?year=2023&type=photo
  • 原因:路径参数标识资源,查询参数进行筛选

第六部分:设计决策指南

1. 选择查询参数的指标

  • 参数是可选的
  • 参数用于筛选现有资源集合
  • 参数可能有多个值
  • 参数不会改变所请求的资源基本类型
  • 需要向后兼容性(可以轻松添加新参数)

2. 选择路径参数的指标

  • 参数是必需的
  • 参数直接标识资源
  • 参数具有层次关系
  • URL需要对SEO友好
  • 构建RESTful API

3. 混合策略最佳实践

在许多情况下,同时使用两种参数类型是最佳选择:

/users/123/albums/456/photos?size=large&download=true
  • 路径参数:/users/123/albums/456/photos - 标识资源层次结构
  • 查询参数:?size=large&download=true - 控制资源的呈现和行为

第七部分:技术实现最佳实践

1. 查询参数实现建议

  1. 默认值处理

    // 不要在URL中显示默认值
    const page = parseInt(req.query.page || '1', 10);
    const limit = parseInt(req.query.limit || '20', 10);
    
  2. 参数验证

    const validSortFields = ['date', 'name', 'price'];
    const sort = validSortFields.includes(req.query.sort) 
      ? req.query.sort 
      : 'date';
    
  3. 保持URL简洁

    • 使用简短但有意义的参数名
    • 省略具有默认值的参数

2. 路径参数实现建议

  1. 使用有意义的标识符

    // 优先使用:
    app.get('/articles/:slug', ...);  // /articles/how-to-design-urls
    
    // 而不是:
    app.get('/articles/:id', ...);    // /articles/12345
    
  2. 处理参数验证

    app.get('/users/:userId', (req, res) => {
      const userId = req.params.userId;
      
      if (!/^[0-9a-f]{24}$/.test(userId)) {
        return res.status(400).send('无效的用户ID格式');
      }
      
      // 继续处理...
    });
    
  3. 考虑URL截断问题

    • 避免过长的路径参数
    • 重要资源使用短标识符

总结

URL参数设计是Web开发中的基础环节,直接影响用户体验、SEO表现和API可用性。查询参数和路径参数各有所长:

  • 查询参数灵活且可选,适合筛选、排序和状态保存,但可能导致冗长的URL。
  • 路径参数简洁且语义化,适合资源标识和层次结构,但相对固定且必需。

在实际应用中,我们常常需要结合使用这两种方式,以创建既直观又实用的URL结构。理解它们的区别和适用场景,是设计高质量Web应用的重要基础。

选择正确的URL参数传递方式,不仅关乎技术实现,更关乎产品体验和业务发展。希望本文的分析能够帮助您在下一个项目中做出更合理的URL设计决策。

svg按钮渐变边框

共用css

body {
    padding: 50px;
    background-color: black;
    color: white;
}

svg {
    --text_fill: orange;
    --svg_width: 120px;
    --svg_height: 40px;
    width: var(--svg_width);
    height: var(--svg_height);
    cursor: pointer;
    /* 创建图层 */
    will-change: transform;

    &:hover {
        --text_fill: #fed71a;
    }

    text {
        fill: var(--text_fill);
        font-size: 1rem;
        transform: translate(50%, 50%);
        text-anchor: middle;
        dominant-baseline: middle;
        stroke: yellowgreen;
        stroke-width: .5px;
        cursor: pointer;
    }

    rect {
        --stroke_width: 4px;
        width: calc(var(--svg_width) - var(--stroke_width));
        height: calc(var(--svg_height) - var(--stroke_width));
        stroke-width: var(--stroke_width);
        rx: calc(var(--svg_height)/2);
        x: calc(var(--stroke_width)/2);
        y: calc(var(--stroke_width)/2);
        fill: none;
        cursor: pointer;
    }
}

移入执行、移出暂停

<body>
    <svg style='position:absolute;left:-9999px;width:0;height:0;visibility:hidden;'>
        <defs>
            <linearGradient id='strokeColor1' x1='0%' y1='0%' x2='100%' y2='0%'>
                <stop offset='0%' stop-color="#00ccff" stop-opacity="1" />
                <stop offset='50%' stop-color="#d400d4" stop-opacity="1" />
                <stop offset='100%' stop-color="#ff00ff" stop-opacity=".7" />
            </linearGradient>
        </defs>
    </svg>

    <svg id="svg1">
        <text>渐变按钮</text>
        <rect stroke='url(#strokeColor1)' />
        <animateTransform id="ani1" href="#strokeColor1" attributeName='gradientTransform' dur='5s' type="rotate"
            form="0,.5,.5" to="360,.5,.5" repeatCount='indefinite' begin="indefinite" />
    </svg>
</body> 
<script>
    svg1.addEventListener('mouseover', function () {
        if (!this.beginMark) {
            ani1.beginElement();
            this.beginMark = true;
            return;
        }
        this.unpauseAnimations();
    })

    svg1.addEventListener('mouseleave', function () {
        this.pauseAnimations();
    })
</script>

svg1效果图

svg1.gif

移入暂停、移出执行

<body>
    <svg style='position:absolute;left:-9999px;width:0;height:0;visibility:hidden;'>
        <defs>
            <linearGradient id='strokeColor2' x1='0%' y1='0%' x2='100%' y2='0%'>
                <stop offset='0%' stop-color="#ec261b" />
                <stop offset='50%' stop-color="#ff9f43" />
                <stop offset='100%' stop-color="#ffe66d" stop-opacity="1" />
            </linearGradient>
        </defs>
    </svg>

    <svg id="svg2">
        <text>渐变按钮</text>
        <rect stroke='url(#strokeColor2)' />
        <animateTransform id="ani2" href="#strokeColor2" attributeName='gradientTransform' dur='5s' type="rotate"
            form="0,.5,.5" to="360,.5,.5" repeatCount='indefinite' begin="0s" />
    </svg>
</body>

<script>
    svg2.addEventListener('mouseover', function () {
        this.pauseAnimations();
    })
    svg2.addEventListener('mouseleave', function () {
        this.unpauseAnimations();
    })
</script>

sv2效果图

svg2.gif

总结

个人感觉svg实现渐变边框相比较css的实现来说,相对代码量更大一些,但是svg其实还有很多好玩的地方。 用svg来做渐变边框也是另外的一种思路,也许以后能够用的上。

10分钟教你用高德MCP搭建你的私人导游 🗺️

引言 🎯

曾经假期临近,但却因各种原因不想做旅行攻略?
曾经想去一个地方,但却不知道怎么安排行程?

现在,让我们一起来解决这个问题!


什么是MCP? 🤔

官方给出的解释:

The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context and tools to LLMs. Think of MCP as a plugin system for Cursor - it allows you to extend the Agent's capabilities by connecting it to various data sources and tools through standardized interfaces.

简单来说:MCP是一个能够让AI调用各种工具的协议


实现步骤 📝

1. 准备支持MCP协议的客户端 💻

想要实现这个功能,我们需要一个既支持AI又支持MCP协议的客户端(例如:Cursor、Claude、Cline)。这里我们选择 Cursor编辑器

cursor.jpg

2. 环境搭建 🛠️

由于需要调用高德提供的MCP server,我们需要安装 Node.js

⚠️ 重要提示:

  • 请尽量下载最新版本
  • 如之前已安装Node.js,建议升级至v22.14.0版本及以上

nodejs.jpg

3. 注册账号并获取Key 🔑

  1. 首先访问高德开放平台并注册登录账号

    gaode.jpg

    gaode2.jpg

  2. 进入控制台创建新应用

gaode3.jpg

  1. 添加Key
    • 点击添加Key
    • 输入Key名称
    • 选择Web应用
    • 提交

gaode4.jpg

  1. 保存生成的Key(后续需要使用)

gaode5.jpg

4. 接入高德MCP 🔌

  1. 配置MCP server
    • 在当前目录下创建 .cursor/mcp.json 文件
    • 或在全局目录下创建(不同系统目录位置不同)
    • 复制以下配置代码:
{
  "mcpServers": {
    "amap-maps": {
      "command": "npx",
      "args": ["-y", "@amap/amap-maps-mcp-server"],
      "env": {
        "AMAP_MAPS_API_KEY": "!!!这里替换成第三步最后获取到的Key!!!"
      }
    }
  }
}
  1. 在控制台执行安装命令:
npm i @amap/amap-maps-mcp-server -g

配置成功后,Cursor Setting中MCP项应显示如下:

amap.jpg

💡 小提示:如果遇到问题,重启可以解决80%的问题


开始使用 🚀

让我们以清明假期杭州3天2晚旅行攻略为例:

⚠️ 注意:需要使用Agent模式,而不是Ask模式

amap2.jpg


结语 📌

虽然AI能给出不错的建议,但推荐西湖醋鱼这种事,还是要靠人类的味蕾来判断啊!😉

JS拖动的原理

在 JavaScript 中实现元素的拖动效果,核心原理是通过监听鼠标事件(或触摸事件)来计算元素的位置变化。以下是详细的实现原理和步骤:


1. 核心事件

拖动需要处理三个关键事件:

  • mousedown(按下鼠标):标记拖动开始,记录初始位置。
  • mousemove(移动鼠标):实时计算元素新位置并更新。
  • mouseup(松开鼠标):结束拖动,移除事件监听。

如果是移动端,对应的事件为 touchstarttouchmovetouchend


2. 实现步骤

2.1 绑定 mousedown 事件

当用户在目标元素上按下鼠标时,记录:

  • 鼠标的初始位置clientX, clientY)。
  • 元素的初始位置offsetLeft, offsetTop)。
  • 鼠标相对于元素左上角的偏移量(用于保持拖动时的相对位置)。
element.addEventListener('mousedown', function(e) {
  // 1. 记录初始数据
  const startX = e.clientX;
  const startY = e.clientY;
  const elemLeft = element.offsetLeft;
  const elemTop = element.offsetTop;

  // 2. 计算鼠标在元素内的偏移量
  const offsetX = startX - elemLeft;
  const offsetY = startY - elemTop;

  // 3. 绑定 mousemove 和 mouseup 事件
  document.addEventListener('mousemove', onMouseMove);
  document.addEventListener('mouseup', onMouseUp);

  function onMouseMove(e) {
    // 计算新位置
    const newX = e.clientX - offsetX;
    const newY = e.clientY - offsetY;
    
    // 更新元素位置
    element.style.left = newX + 'px';
    element.style.top = newY + 'px';
  }

  function onMouseUp() {
    // 移除事件监听
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
  }
});

2.2 关键细节

  • 事件委托到 document
    mousemovemouseup 绑定到 document,而非元素本身。这样即使鼠标快速移动超出元素区域,仍能正常触发事件。

  • 性能优化
    避免在 mousemove 中频繁触发重排(如读取 offsetLeft),提前缓存初始值。

  • 边界限制(可选)
    可添加逻辑限制元素在容器内移动:

    const maxX = container.offsetWidth - element.offsetWidth;
    const maxY = container.offsetHeight - element.offsetHeight;
    newX = Math.max(0, Math.min(newX, maxX));
    newY = Math.max(0, Math.min(newY, maxY));
    

2.3 处理 CSS 定位

  • 元素必须设置为 position: absoluteposition: fixed,才能通过 lefttop 修改位置。
  • 使用 transform: translate() 实现位置变化(性能更优):
    element.style.transform = `translate(${newX}px, ${newY}px)`;
    

3. 完整代码示例

<div id="draggable" style="position: absolute; left: 0; top: 0;">拖动我</div>

<script>
  const element = document.getElementById('draggable');

  element.addEventListener('mousedown', startDrag);

  function startDrag(e) {
    e.preventDefault();
    
    const startX = e.clientX;
    const startY = e.clientY;
    const elemX = element.offsetLeft;
    const elemY = element.offsetTop;
    const offsetX = startX - elemX;
    const offsetY = startY - elemY;

    document.addEventListener('mousemove', onDrag);
    document.addEventListener('mouseup', stopDrag);

    function onDrag(e) {
      const newX = e.clientX - offsetX;
      const newY = e.clientY - offsetY;
      element.style.left = newX + 'px';
      element.style.top = newY + 'px';
    }

    function stopDrag() {
      document.removeEventListener('mousemove', onDrag);
      document.removeEventListener('mouseup', stopDrag);
    }
  }
</script>

4. 高级优化

  • 防抖(Debounce):减少 mousemove 事件的触发频率。
  • 请求动画帧(RAF):使用 requestAnimationFrame 优化动画流畅度。
  • 触摸事件支持:通过 touchstart/touchmove 兼容移动端。
  • 拖拽反馈:添加半透明效果或占位符提升用户体验。

5. 原生拖拽 API 对比

HTML5 提供了原生拖放 API(draggable 属性 + dragstart/dragover 事件),但:

  • 优点:支持跨元素拖放、文件拖拽上传。
  • 缺点:定制性较差,默认会显示半透明图像。

总结

通过监听鼠标事件、计算偏移量并更新元素位置,可以灵活实现自定义拖拽效果。相比原生 API,手动控制更适用于需要高度定制的场景(如游戏、复杂 UI 组件)。

Threejs绘制小兩伞快拿去送给你的女神

大家好!我是 [数擎 AI],一位热爱探索新技术的前端开发者,在这里分享前端和 Web3D、AI 技术的干货与实战经验。如果你对技术有热情,欢迎关注我的文章,我们一起成长、进步! 开发领域:前端开发 | AI 应用 | Web3D | 元宇宙
技术栈:JavaScript、React、ThreeJs、WebGL、Go
经验经验:6 年+ 前端开发经验,专注于图形渲染和 AI 技术
开源项目AI 简历元宇宙数字孪生

chrome-capture-2025-4-2.gif

1. SDF 函数(Signed Distance Functions)

SDF 是一种通过数学公式定义形状的方式,常用于计算距离场。我们使用了几个 SDF 函数来构建图形:

  • sdfCircle: 用于绘制圆形。
  • sdfEllipse: 用于绘制椭圆形。
  • sdfLine: 用于绘制线段。

每个 SDF 函数返回一个值,表示当前像素到形状的距离。如果这个距离小于某个阈值,则表示像素在形状内部。

float sdfCircle(vec2 center, float radius, vec2 coord) {
  vec2 offset = coord - center;
  return sqrt((offset.x * offset.x) + (offset.y * offset.y)) - radius;
}

2. 布尔操作函数

SDF 可以通过布尔运算进行组合,例如求并集、差集和交集。我们在代码中使用了以下几种操作:

  • sdfUnion: 返回两个形状的并集。
  • sdfDifference: 返回两个形状的差集。
  • sdfIntersection: 返回两个形状的交集。
float sdfUnion(float a, float b) { return min(a, b); }
float sdfDifference(float a, float b) { return max(a, -b); }
float sdfIntersection(float a, float b) { return max(a, b); }

这些运算让我们能够通过数学方式灵活地合成复杂的图形。

3. 渲染函数

render 函数负责将计算出的形状绘制到屏幕上。它通过 smoothstep 函数实现抗锯齿效果,并根据距离来调整颜色的透明度。

vec4 render(float d, vec3 color, float stroke) {
float anti = fwidth(d) * 1.0;
vec4 strokeLayer = vec4(vec3(0.05), 1.0 - smoothstep(-anti, anti, d - stroke));
vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d));
return stroke < 0.000001 ? colorLayer : vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}

这个函数通过逐层混合不同颜色和透明度来呈现复杂的视觉效果。

4.动态条纹

我们还使用了正弦函数 sin() 来生成动态条纹效果。sin(uv.x * 40.0) 使得图案随时间变化,创造出条纹的动感效果。

vec2 sinuv = vec2(uv.x, (sin(uv.x _ 40.0) _ 0.02 + 1.0) * uv.y);

通过改变 time 参数,这些条纹会在场景中随着时间不断变化,增强动画效果的表现力。

5. 背景与图层混合

为了让图形与背景更好地融合,我们使用了图层混合和背景颜色的处理。每个图层根据其透明度逐渐与背景颜色混合,最终得出渲染结果。

vec3 bcol = vec3(1.0, 0.8, 0.7 - 0.07 _ p.y) _ (1.0 - 0.25 * length(p));
fragColor.rgb = mix(fragColor.rgb, layer0.rgb, layer0.a);
fragColor.rgb = mix(fragColor.rgb, layer1.rgb, layer1.a);
fragColor.rgb = mix(fragColor.rgb, layer2.rgb, layer2.a);

6. Gamma 校正

为了调整最终的颜色输出并确保其符合人眼的感知,采用了 Gamma 校正。通过将颜色值提升到 1.0 / 2.2 的幂次方,我们可以得到更为自然的视觉效果。

fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));

7. 完整代码

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 1. 初始化Three.js基础场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000,
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 顶点着色器
const vertexShader = `
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelMatrix * viewMatrix * vec4(position, 1.0);
}
`;

// 片元着色器
const fragmentShader = `
uniform vec2 resolution;
uniform float time;
varying vec2 vUv;

float sdfCircle(vec2 center, float radius, vec2 coord) {
  vec2 offset = coord - center;
  return sqrt((offset.x * offset.x) + (offset.y * offset.y)) - radius;
}

float sdfEllipse(vec2 center, float a, float b, vec2 coord) {
  float a2 = a * a;
  float b2 = b * b;
  return (b2 * (coord.x - center.x) * (coord.x - center.x) +
         a2 * (coord.y - center.y) * (coord.y - center.y) - a2 * b2)/(a2 * b2);
}

float sdfLine(vec2 p0, vec2 p1, float width, vec2 coord) {
  vec2 dir0 = p1 - p0;
  vec2 dir1 = coord - p0;
  float h = clamp(dot(dir0, dir1)/dot(dir0, dir0), 0.0, 1.0);
  return (length(dir1 - dir0 * h) - width * 0.5);
}

float sdfUnion(float a, float b) { return min(a, b); }
float sdfDifference(float a, float b) { return max(a, -b); }
float sdfIntersection(float a, float b) { return max(a, b); }

vec4 render(float d, vec3 color, float stroke) {
  float anti = fwidth(d) * 1.0;
  vec4 strokeLayer = vec4(vec3(0.05), 1.0-smoothstep(-anti, anti, d - stroke));
  vec4 colorLayer = vec4(color, 1.0-smoothstep(-anti, anti, d));
  return stroke < 0.000001 ? colorLayer : 
    vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}

void main() {
  float size = min(resolution.x, resolution.y);
  float pixSize = 1.0 / size;
  vec2 uv = vUv;
  float stroke = pixSize * 1.5;
  
  // 适配宽高比
  float aspect = resolution.y / resolution.x;
  vec2 center = vec2(0.5, 0.5 * aspect);

  // 主要形状
  float a = sdfEllipse(vec2(0.5, center.y*2.0-0.34), 0.25, 0.25, uv);
  float b = sdfEllipse(vec2(0.5, center.y*2.0+0.03), 0.8, 0.35, uv);
  b = sdfIntersection(a, b);
  vec4 layer1 = render(b, vec3(0.32, 0.56, 0.53), fwidth(b) * 2.0);

  // 动态条纹
  vec4 layer2 = layer1;
  vec2 sinuv = vec2(uv.x, (sin(uv.x*40.0)*0.02 + 1.0)*uv.y);
  for (float i = 0.0; i < 10.0; i++) {
    float t = mod(time + 0.3 * i, 3.0) * 0.2;
    float r0 = (t - 0.15)/0.2 * 0.9 + 0.1;
    float r1 = (t - 0.15)/0.2 * 0.1 + 0.9;
    float r2 = (t - 0.15)/0.2 * 0.15 + 0.85;
    
    float e = sdfEllipse(vec2(0.5, center.y*2.0+0.37-t*r2), 0.7*r0, 0.35*r1, sinuv);
    float f = sdfEllipse(vec2(0.5, center.y*2.0+0.41-t), 0.7*r0, 0.35*r1, sinuv);
    f = sdfDifference(e, f);
    f = sdfIntersection(f, b);
    vec4 layer = render(f, vec3(1.0, 0.81, 0.27), 0.0);
    layer2 = mix(layer2, layer, layer.a);
  }

  // 手柄绘制
  float bottom = 0.08;
  float handleWidth = 0.01;
  float handleRadius = 0.04;
  float d = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius, uv);
  float c = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius-handleWidth, uv);
  d = sdfDifference(d, c);
  c = uv.y - bottom;
  d = sdfIntersection(d, c);
  c = sdfLine(vec2(0.5, center.y*2.0-0.05), vec2(0.5, bottom), handleWidth, uv);
  d = sdfUnion(d, c);
  c = sdfCircle(vec2(0.5, center.y*2.0-0.05), 0.01, uv);
  d = sdfUnion(c, d);
  c = sdfCircle(vec2(0.5-handleRadius*2.0+handleWidth, bottom), handleWidth*0.5, uv);
  d = sdfUnion(c, d);
  vec4 layer0 = render(d, vec3(0.404, 0.298, 0.278), stroke);

  // 背景混合
  vec2 p = (2.0*gl_FragCoord.xy-resolution.xy)/min(resolution.y, resolution.x);
  vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-0.25*length(p));
  vec4 fragColor = vec4(bcol, 1.0);
  
  // 图层混合
  fragColor.rgb = mix(fragColor.rgb, layer0.rgb, layer0.a);
  fragColor.rgb = mix(fragColor.rgb, layer1.rgb, layer1.a);
  fragColor.rgb = mix(fragColor.rgb, layer2.rgb, layer2.a);
  
  // Gamma 校正
  fragColor.rgb = pow(fragColor.rgb, vec3(1.0/2.2));
  gl_FragColor = fragColor;
}
`;

// Three.js 材质创建
const material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: {
    resolution: {
      value: new THREE.Vector2(window.innerWidth, window.innerHeight),
    },
    time: { value: 0 },
  },
});

// 3. 创建全屏平面并应用材质
const geometry = new THREE.PlaneGeometry(10, 10);
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);

// 4. 相机位置和控制器
camera.position.z = 10;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;

// 5. 响应窗口大小变化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  shaderMaterial.uniforms.iResolution.value.set(
    renderer.domElement.width,
    renderer.domElement.height,
  );
});

// 6. 动画循环
function animate() {
  requestAnimationFrame(animate);
  material.uniforms.time.value = performance.now() / 1000;
  renderer.render(scene, camera);
}
animate();

总结

利用了 SDF 技术绘制了多个形状,并通过布尔运算组合它们,进一步通过动态条纹和动画效果增加了复杂度。通过理解这些 Shader 代码,你将能更好地掌控图形渲染的细节,并应用到更复杂的 Three.js 项目中。

useDateFormat源码解析

深入解析 VueUse 的 useDateFormat:源码解读与使用示例

在前端开发中,日期格式化是一个常见的任务,无论是展示时间戳、格式化用户输入的日期,还是处理国际化时间显示,开发者都需要一个简单而灵活的工具。VueUse 是一个专为 Vue 3 设计的实用工具库,其中的 useDateFormat 提供了一种响应式的日期格式化解决方案。本文将深入分析 useDateFormat 的源码实现,并结合实际使用示例,帮助你更好地理解和应用这一工具。


useDateFormat 简介

useDateFormat 是一个基于 Vue 3 组合式 API 的 Hook,用于将日期对象或时间戳格式化为指定的字符串。它基于 JavaScript 的 Intl.DateTimeFormat API,支持灵活的格式化选项和国际化需求。基本用法如下:

import { useDateFormat } from '@vueuse/core'

const date = new Date()
const formatted = useDateFormat(date, 'YYYY-MM-DD HH:mm:ss')
console.log(formatted.value) // 输出类似 "2025-04-02 14:30:00"

在这个例子中,我们将当前日期格式化为 YYYY-MM-DD HH:mm:ss 的字符串。


源码解读

以下是 useDateFormat 的简化源码实现(基于 VueUse v10.x,具体实现请参考官方仓库):

import { ref, computed, watch, isRef } from 'vue'
import { isString, isDate } from '@vueuse/shared'

export function useDateFormat(date, format = 'YYYY-MM-DD', options = {}) {
  // 处理输入的日期
  const dateRef = isRef(date) ? date : ref(date)

  // 默认选项
  const {
    locales = 'en-US', // 默认语言环境
    ...intlOptions // 其他 Intl.DateTimeFormat 选项
  } = options

  // 格式化函数
  const formatter = (value, fmt) => {
    if (!value) return ''

    let dateValue = value
    if (isString(value)) {
      dateValue = new Date(value)
    } else if (!isDate(value)) {
      dateValue = new Date(value)
    }

    if (isNaN(dateValue.getTime())) return ''

    // 如果是自定义格式字符串
    if (fmt.includes('Y') || fmt.includes('M') || fmt.includes('D')) {
      return formatCustom(dateValue, fmt)
    }

    // 使用 Intl.DateTimeFormat 格式化
    return new Intl.DateTimeFormat(locales, intlOptions).format(dateValue)
  }

  // 自定义格式化逻辑
  const formatCustom = (date, fmt) => {
    const year = date.getFullYear()
    const month = String(date.getMonth() + 1).padStart(2, '0')
    const day = String(date.getDate()).padStart(2, '0')
    const hours = String(date.getHours()).padStart(2, '0')
    const minutes = String(date.getMinutes()).padStart(2, '0')
    const seconds = String(date.getSeconds()).padStart(2, '0')

    return fmt
      .replace('YYYY', year)
      .replace('MM', month)
      .replace('DD', day)
      .replace('HH', hours)
      .replace('mm', minutes)
      .replace('ss', seconds)
  }

  // 响应式格式化结果
  const output = computed(() => formatter(dateRef.value, format))

  return output
}

源码关键点解析

  1. 输入处理

    • date 参数可以是 Date 对象、时间戳、字符串或响应式 ref
    • 使用 isRef 判断输入是否为 ref,如果是则直接使用,否则包装为 ref
  2. 格式化逻辑

    • 如果 format 是自定义字符串(如 YYYY-MM-DD),调用 formatCustom 进行替换。
    • 如果 format 未包含自定义占位符,则使用 Intl.DateTimeFormat 进行格式化,支持国际化。
  3. 自定义格式化

    • formatCustom 通过 getFullYeargetMonth 等方法提取日期组件,并用 padStart 补齐两位数。
    • 支持的占位符包括 YYYY(年)、MM(月)、DD(日)、HH(小时)、mm(分钟)、ss(秒)。
  4. 响应式支持

    • 使用 computed 返回格式化结果,确保当 dateRefformat 变化时,输出自动更新。
  5. 国际化支持

    • 通过 localesintlOptions 参数,可以利用 Intl.DateTimeFormat 实现不同语言环境下的格式化。

使用示例

下面是一个实际的 Vue 组件示例,展示如何使用 useDateFormat 实现动态日期格式化:

<template>
  <div>
    <p>当前时间:{{ formattedDate }}</p>
    <input v-model="format" placeholder="输入格式,如 YYYY-MM-DD HH:mm:ss" />
    <button @click="updateDate">刷新时间</button>
  </div>
</template>

<script setup>
  import { ref } from 'vue'
  import { useDateFormat } from '@vueuse/core'

  const date = ref(new Date())
  const format = ref('YYYY-MM-DD HH:mm:ss')

  const formattedDate = useDateFormat(date, format)

  const updateDate = () => {
    date.value = new Date() // 更新日期,触发重新格式化
  }
</script>

<style scoped>
  input {
    margin: 10px;
    padding: 5px;
  }
  button {
    padding: 5px 10px;
  }
</style>

示例说明

  1. 功能

    • 显示当前时间的格式化结果。
    • 用户可以输入自定义格式(如 YYYY-MM-DDHH:mm:ss),实时更新显示。
    • 点击“刷新时间”按钮更新日期。
  2. 效果

    • dateformat 变化时,formattedDate 会自动重新计算并更新。
    • 输入 YYYY-MM-DD 会显示类似 2025-04-02,输入 HH:mm:ss 会显示类似 14:30:45

扩展与优化

  1. 国际化格式化: 使用 locales 参数支持不同语言环境:

    const formatted = useDateFormat(new Date(), 'YYYY-MM-DD', { locales: 'zh-CN' })
    console.log(formatted.value) // 输出类似 "2025-04-02"
    
  2. 使用 Intl 标准选项: 如果不需要自定义格式,可以直接使用 Intl.DateTimeFormat 的选项:

    const formatted = useDateFormat(new Date(), '', {
      locales: 'en-US',
      dateStyle: 'full',
    })
    console.log(formatted.value) // 输出类似 "Wednesday, April 2, 2025"
    
  3. 动态切换格式: 将 format 定义为响应式变量,动态切换格式:

    const format = ref('YYYY-MM-DD')
    const formatted = useDateFormat(new Date(), format)
    format.value = 'HH:mm:ss' // 切换为时间格式
    
  4. 错误处理: 如果传入无效日期,useDateFormat 会返回空字符串:

    const formatted = useDateFormat('invalid-date', 'YYYY-MM-DD')
    console.log(formatted.value) // 输出 ""
    

与其他工具的对比

相比传统的日期格式化库(如 moment.jsday.js),useDateFormat 有以下优势:

  • 轻量:无需引入额外的依赖,直接基于原生 API。
  • 响应式:与 Vue 的响应式系统无缝集成。
  • 灵活性:支持自定义格式和国际化。

但它也有局限性,例如不支持过于复杂的格式化规则(如 moment.jsfromNow),适合轻量级场景。


总结

useDateFormat 是 VueUse 中一个简单而实用的工具,通过结合自定义格式化和 Intl.DateTimeFormat,它为开发者提供了灵活的日期格式化能力。本文的源码分析和示例展示了它的核心原理和应用场景,希望能帮助你在项目中更高效地处理日期相关需求。

❌