普通视图

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

每日一题-构造最小位运算数组 II🟡

2026年1月21日 00:00

给你一个长度为 n 的质数数组 nums 。你的任务是返回一个长度为 n 的数组 ans ,对于每个下标 i ,以下 条件 均成立:

  • ans[i] OR (ans[i] + 1) == nums[i]

除此以外,你需要 最小化 结果数组里每一个 ans[i] 。

如果没法找到符合 条件 的 ans[i] ,那么 ans[i] = -1 。

质数 指的是一个大于 1 的自然数,且它只有 1 和自己两个因数。

 

示例 1:

输入:nums = [2,3,5,7]

输出:[-1,1,4,3]

解释:

  • 对于 i = 0 ,不存在 ans[0] 满足 ans[0] OR (ans[0] + 1) = 2 ,所以 ans[0] = -1 。
  • 对于 i = 1 ,满足 ans[1] OR (ans[1] + 1) = 3 的最小 ans[1] 为 1 ,因为 1 OR (1 + 1) = 3 。
  • 对于 i = 2 ,满足 ans[2] OR (ans[2] + 1) = 5 的最小 ans[2] 为 4 ,因为 4 OR (4 + 1) = 5 。
  • 对于 i = 3 ,满足 ans[3] OR (ans[3] + 1) = 7 的最小 ans[3] 为 3 ,因为 3 OR (3 + 1) = 7 。

示例 2:

输入:nums = [11,13,31]

输出:[9,12,15]

解释:

  • 对于 i = 0 ,满足 ans[0] OR (ans[0] + 1) = 11 的最小 ans[0] 为 9 ,因为 9 OR (9 + 1) = 11 。
  • 对于 i = 1 ,满足 ans[1] OR (ans[1] + 1) = 13 的最小 ans[1] 为 12 ,因为 12 OR (12 + 1) = 13 。
  • 对于 i = 2 ,满足 ans[2] OR (ans[2] + 1) = 31 的最小 ans[2] 为 15 ,因为 15 OR (15 + 1) = 31 。

 

提示:

  • 1 <= nums.length <= 100
  • 2 <= nums[i] <= 109
  • nums[i] 是一个质数。

GDAL 实现影像合并

作者 GIS之路
2026年1月20日 23:42

^ 关注我,带你一起学GIS ^

前言

GDAL作为地理空间数据处理的核心工具,其影像合并功能为多源栅格数据的集成与分析提供了高效、灵活的解决方案。无论是遥感影像镶嵌、地图瓦片拼接,还是时间序列数据的融合,该功能能够帮助用户将分散的影像片段整合为具有统一地理参考和连贯信息表达的完整数据集,为后续的空间分析、可视化及应用构建可靠的数据基础。

由于本文由一些前置知识,在正式开始之前,需要你掌握一定的Python开发基础和GDAL的基本概念。在之前的文章中讲解了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,可以作为基础入门学习。本篇教程在之前一系列文章的基础上讲解如何使用GDAL 实现影像合并

如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2025年

系统:Windows 11

Python:3.11.7

GDAL:3.11.1

2. 数据准备

俗话说巧妇难为无米之炊,数据就是软件的基石,没有数据,再美好的设想都是空中楼阁。因此,第一步需要下载遥感影像数据。

但是,影像数据在哪里下载呢?别着急,本文都给你整理好了。

数据下载可参考文章:GIS 影像数据源介绍

如下,这是我在【地理空间数据云】平台下载的landsat8遥感影像。

3. 导入依赖

GeoTIFF作为一种栅格数据格式,可以使用GDAL直接进行处理,以实现影像数据的合并操作。影像合并涉及到大量数学运算,所以还需要导入numpy模块进行处理。

from osgeo import gdal
import numpy as np

如果你没有安装numpy模块的话,可执行命令pip install numpy进行安装。

4. 影像合并

定义一个方法merge_raster(input_files, output_file, target_resolution=None)用于实现栅格数据的合并。

"""
说明:GDAL 影像合并
参数:
    -input_files:输入合并的影像文件列表
    -output_file:输出影像文件
    -target_resolution:影像合并分辨率
"""
def merge_raster(input_files, output_file, target_resolution=None):

在获取到影像数据后,需要对数据范围进行合并。

"""
需要合并图像尺寸后进行输出
"""

# 获取所有影像的边界和分辨率
min_x, max_x, min_y, max_y = float('inf'), -float('inf'), float('inf'), -float('inf')
res_x, res_y = None, None

for file in input_files:
    # 打开数据集
    ds = gdal.Open(file)
    # 获取地理变换信息
    gt = ds.GetGeoTransform()

    # 获取行、列数
    x_size = ds.RasterXSize
    y_size = ds.RasterYSize

    # 计算实际边界
    x_min = gt[0]
    y_max = gt[3]
    x_max = gt[0] + x_size * gt[1]
    y_min = gt[3] + y_size * gt[5]

    min_x = min(min_x, x_min)
    max_x = max(max_x, x_max)
    min_y = min(min_y, y_min)
    max_y = max(max_y, y_max)

    if res_x is None or abs(gt[1]) < abs(res_x):
        res_x = abs(gt[1])
        res_y = abs(gt[5])

    # 关闭数据源
    ds = None

计算影像合并分辨率。

# 使用目标分辨率或计算出的分辨率
if target_resolution:
    res_x = res_y = target_resolution

获取影像大小,输出合并影像的总行数和总列数。

# 计算输出尺寸
out_cols = int((max_x - min_x) / res_x + 0.5)
out_rows = int((max_y - min_y) / res_y + 0.5)

调用gdal对象方法Warp进行影像合并,该函数第一个参数destNameOrDestDS 为输出数据集名称或者数据源,第二个参数srcDSOrSrcDSTab为源数据,第三个参数options为可选项描述,用于定义合并影像信息。

# 设置Warp参数
warp_options = gdal.WarpOptions(
    format='GTiff',
    outputBounds=[min_x, min_y, max_x, max_y],
    xRes=res_x,
    yRes=res_y,
    resampleAlg='bilinear',  # 根据需要选择:near, bilinear, cubic, lanczos等
    dstNodata=0,
    targetAlignedPixels=True,  # 确保像素对齐
    multithread=True,
    creationOptions=['COMPRESS=LZW''TILED=YES''BIGTIFF=IF_SAFER']
)

# 执行合并
gdal.Warp(output_file, input_files, options=warp_options)

main函数中调用合并方法。

if __name__ == "__main__":

    # 输入影像文件
    input_files = [ 
        "E:\ArcGIS\bandmerge\lc8_band_432.tif",
        "E:\ArcGIS\bandmerge\lc8_band_432_d.tif"
    ]
    # 输出影像文件
    output_file"E:\ArcGIS\bandmerge\merge_result.tif"

    merge_raster(input_files, output_file, target_resolution=10)

图片效果

OpenLayers示例数据下载,请在公众号后台回复:vector

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集

小小声说一下GDAL的官方API接口

《云南省加快构建现代化产业体系推进产业强省建设行动计划》发布

ArcGIS Pro 添加底图的方式

为什么每次打开 ArcGIS Pro 页面加载都如此缓慢?

ArcGIS Pro 实现影像波段合成

自然资源部党组关于苗泽等4名同志职务任免的通知

GDAL 创建矢量图层的两种方式

GDAL 实现矢量数据转换处理(全)

GDAL 实现投影转换

国产版的Google Earth,吉林一号卫星App“共生地球”来了

2026年全国自然资源工作会议召开

日本欲打造“本土版”星链系统

O(1) 计算每个数(Python/Java/C++/Go)

作者 endlesscheng
2024年10月13日 08:11

例如 $x=100111$,那么 $x\ |\ (x+1) = 100111\ |\ 101000 = 101111$。

可以发现,$x\ |\ (x+1)$ 的本质是把二进制最右边的 $0$ 置为 $1$。

反过来,如果已知 $x\ |\ (x+1) = 101111$,那么倒推 $x$,需要把 $101111$ 中的某个 $1$ 变成 $0$。满足要求的 $x$ 有:

$$
\begin{aligned}
100111 \
101011 \
101101 \
101110 \
\end{aligned}
$$

其中最小的是 $100111$,也就是把 $101111$ 最右边的 $0$ 的右边的 $1$ 置为 $0$。

无解的情况:由于 $x\ |\ (x+1)$ 最低位一定是 $1$(因为 $x$ 和 $x+1$ 中必有一奇数),所以如果 $\textit{nums}[i]$ 是偶数(质数中只有 $2$),那么无解。

写法一

举例说明:把 $101111$ 取反,得 $010000$,其 $\text{lowbit}=10000$,右移一位得 $1000$。把 $101111$ 与 $1000$ 异或,即可得到 $100111$。

关于 $\text{lowbit}$ 的原理,请看 从集合论到位运算,常见位运算技巧分类总结!

本题视频讲解,欢迎点赞关注~

###py

class Solution:
    def minBitwiseArray(self, nums: List[int]) -> List[int]:
        for i, x in enumerate(nums):
            if x == 2:
                nums[i] = -1
            else:
                t = ~x
                nums[i] ^= (t & -t) >> 1
        return nums

###java

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

###cpp

class Solution {
public:
    vector<int> minBitwiseArray(vector<int>& nums) {
        for (int& x : nums) { // 注意这里是引用
            if (x == 2) {
                x = -1;
            } else {
                int t = ~x;
                x ^= (t & -t) >> 1;
            }
        }
        return nums;
    }
};

###go

func minBitwiseArray(nums []int) []int {
for i, x := range nums {
if x == 2 {
nums[i] = -1
} else {
t := ^x
nums[i] ^= t & -t >> 1
}
}
return nums
}

写法二

把 $101111$ 加一,得到 $110000$,再 AND $101111$ 取反后的值 $010000$,可以得到方法一中的 $\text{lowbit}=10000$。

###py

class Solution:
    def minBitwiseArray(self, nums: List[int]) -> List[int]:
        for i, x in enumerate(nums):
            if x == 2:
                nums[i] = -1
            else:
                nums[i] ^= ((x + 1) & ~x) >> 1
        return nums

###java

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

###cpp

class Solution {
public:
    vector<int> minBitwiseArray(vector<int>& nums) {
        for (int& x : nums) { // 注意这里是引用
            if (x == 2) {
                x = -1;
            } else {
                x ^= ((x + 1) & ~x) >> 1;
            }
        }
        return nums;
    }
};

###go

func minBitwiseArray(nums []int) []int {
for i, x := range nums {
if x == 2 {
nums[i] = -1
} else {
nums[i] ^= (x + 1) &^ x >> 1
}
}
return nums
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面位运算题单的「八、思维题」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Polyfill方式解决前端兼容性问题:core-js包结构与各种配置策略

作者 漂流瓶jz
2026年1月20日 23:57

简介

在之前我介绍过Babel:解锁Babel核心功能:从转义语法到插件开发,Babel是一个使用AST转义JavaScript语法,提高代码在浏览器兼容性的工具。但有些ECMAScript并不是新的语法,而是一些新对象,新方法等等,这些并不能使用AST抽象语法树来转义。因此Babel利用core-js实现这些代码的兼容性。

core-js是一个知名的前端工具库,里面包含了ECMAScript标准中提供的新对象/新方法等,而且是使用旧版本支持的语法来实现这些新的API。这样即使浏览器没有实现标准中的新API,也能通过注入core-js代码来提供对应的功能。

像这种通过注入代码实现浏览器没有提供的API特性,叫做Polyfill。这个单词的本意是填充材料,在JavaScript领域中,这些注入的代码就类似“填充材料”一样,帮助我们提高代码的兼容性。另外core-js还提供了一些还在提议中的API的实现。

core-js使用方式

使用前后对比

要想看到core-js使用前后的效果对比,首先需要确定某个特性和对应的执行环境,在这个环境中对应的特性不存在。我本地是Node.js v18.19.1版本,这个版本并没有实现Promise.try这个方法,因此我们就用这个方法进行实验。首先是没有引入core-js的场景:

Promise.try(() => {
  console.log('jzplp!')
})

/* 执行结果
Promise.try(() => {
           ^
TypeError: Promise.try is not a function
*/

可以看到没有引入core-js,直接使用Promise.try时,会因为没有该方法而报错。然后再试试引入core-js的效果:

require('core-js')
Promise.try(() => {
  console.log('jzplp!')
})

/* 输出结果
jzplp!
*/

可以看到引入core-js后,原本不存在的API被填充了,我们的代码可以正常执行并拿到结果了。这就是core-js提高兼容性的效果。

单个API引入

core-js不仅可以直接引入全部语法,还可以仅引入单个API,比如某个对象或某个方法。首先看下只引入Promise对象:

// require('core-js/full') 等于 require('core-js')
require('core-js/full/promise')
Promise.try(() => {
  console.log('jzplp!')
})

/* 输出结果
jzplp!
*/

然后再看下直接引入对象中的某个方法:

require('core-js/full/promise/try')
Promise.try(() => {
  console.log('jzplp!')
})

/* 输出结果
jzplp!
*/

不注入全局对象

前面展示的场景,core-js都是将API直接注入到全局,这样使用这些API就如环境本身支持一样,基本感受不到区别。但如果我们不希望直接注入到全局时,core-js也提供了使用方式:

const promise = require('core-js-pure/full/promise');
promise.try(() => {
  console.log('jzplp!')
})
Promise.try(() => {
  console.log('jzplp!2')
})
/* 输出结果
jzplp!
Promise.try(() => {
           ^
TypeError: Promise.try is not a function
*/

可以看到,使用core-js-pure这个包之后,可以直接导出我们希望要的API,而不直接注入到全局。此时直接使用全局对象方法依然报错。而core-js这个包虽然也能导出,但它还是会直接注入全局,我们看下例子:

const promise = require('core-js/full/promise');
promise.try(() => {
  console.log('jzplp!')
})
Promise.try(() => {
  console.log('jzplp!2')
})

/* 输出结果
jzplp!
jzplp!2
*/

因此,如果希望仅使用导出对象,还是需要使用core-js-pure这个包。core-js-pure也可以仅导出对象方法:

const try2 = require("core-js-pure/full/promise/try");
Promise.try = try2;
Promise.try(() => {
  console.log("jzplp!");
});

/* 输出结果
jzplp!
*/

因为导出的对象方法不能独立使用,因此在例子中我们还是将其注入到Promise对象后使用。

特性分类引入

core-js中包含非常多API特性的兼容代码,有些是已经稳定的特性,有些是还处在提议阶段的,不稳定的特性。我们直接引入core-js会把这些特性全部引入,但如果不需要那些不稳定特性,core-js也提供了多种引入方式:

  • core-js 引入所有特性,包括早期的提议
  • core-js/full 等于引入core-js
  • core-js/actual 包含稳定的ES和Web标准特性,以及stage3的特性
  • core-js/stable 包含稳定的ES和Web标准特性
  • core-js/es 包含稳定的ES特性

这里我们举两个例子尝试下。首先由于ECMAScript标准一直在更新中,有些特性现在是提议,未来可能就已经被列入正式特性了。因此这里的例子需要明确环境和core-js版本。这里我们使用Node.js v18.19.1和core-js@3.47.0版本,以写这篇文章的时间为准。

首先第一个特性是:数组的lastIndex属性,这是一个stage1阶段的API,这里针对不同的引入方式进行尝试:

// 不引入core-js尝试
const arr = ["jz", "plp"];
console.log(arr.lastIndex);
/* 输出结果
undefined
*/

// 引入core-js/full
require("core-js/full");
const arr = ["jz", "plp"];
console.log(arr.lastIndex);
/* 输出结果
1
*/

// 引入core-js/actual
require("core-js/actual");
const arr = ["jz", "plp"];
console.log(arr.lastIndex);

/* 输出结果
undefined
*/

首先当不引入core-js时,因为不支持这个API,所以输出undefined。core-js/full支持stage1阶段的API,可以正确输出结果。但core-js/actual仅支持stage3阶段的API,因此还是不支持这个API。

然后我们再看下另外一个API,数组的groupBy方法。这是一个stage3阶段的API:

// 不引入core-js尝试
const arr = [
  { group: 1, value: "jz" },
  { group: 2, value: "jz2" },
  { group: 1, value: "plp" },
];
const arrNew = arr.groupBy(item => item.group);
console.log(arrNew)
/* 输出结果
const arrNew = arr.groupBy(item => item.group);
                   ^
TypeError: arr.groupBy is not a function
*/

// 引入core-js/actual
require("core-js/actual");
const arr = [
  { group: 1, value: "jz" },
  { group: 2, value: "jz2" },
  { group: 1, value: "plp" },
];
const arrNew = arr.groupBy(item => item.group);
console.log(arrNew)
/* 输出结果
[Object: null prototype] {
  '1': [ { group: 1, value: 'jz' }, { group: 1, value: 'plp' } ],
  '2': [ { group: 2, value: 'jz2' } ]
}
*/

// 引入core-js/stable
require("core-js/stable");
const arr = [
  { group: 1, value: "jz" },
  { group: 2, value: "jz2" },
  { group: 1, value: "plp" },
];
const arrNew = arr.groupBy(item => item.group);
console.log(arrNew)
/* 输出结果
const arrNew = arr.groupBy(item => item.group);
                   ^
TypeError: arr.groupBy is not a function
*/

可以看到,不引入core-js时不支持,引入了core-js/actual(包含stage3阶段的API)后支持并能输出正确的结果。core-js/stable中不支持又报错了。

core-js源码结构

前面描述了很多core-js的引入方式,这里我们看一下源码结构,看看core-js内部是如何组织的。

core-js源码目录

core-js
├─actual
│   ├─array
│   │  ├─at.js
│   │  ├─concat.js
│   │  └─...
│   ├─set
│   │  └─...
│   └─...
├─es
│   └─...
├─features
│   └─...
├─index.js
└─...

首先列出core-js源码目录的示意图,可以看到core-js内部有很多目录,对应前面的各种引入方式。这里我们列出每个目录的内容:

  • actual 包含稳定的ES和Web标准特性,以及stage3的特性
  • es 包含稳定的ES特性
  • features 没有说明,猜测和full类似
  • full 所以特性包括早期提议
  • internals 包内部使用的逻辑
  • modules 实际特性的代码实现
  • proposals 包含提议的特性
  • stable 包含稳定的ES和Web标准特性
  • stage 按照stage阶段列出提议特性
  • web 包含Web标准特性
  • configurator.js 是否强制引入逻辑,后面会描述
  • index.js 内容为导出full目录,因此导入core-js等于导入core-js/full

层层引用

在目录中actual, es, full, stable, es是我们已经介绍过的。另外还有web目录仅包含web标准的特性,features和full类似(index.js中直接导出full目录)。

proposals目录包含提议的特性,以特性名来命名文件名。而stage目录中包含0.js, 1.js, 2.js等等,是根据stage阶段来整理的,方便整理和引入对应阶段的特性。

这样整理目录虽然清晰,但这些目录中的特性都是重复的,不可能在每个目录中把特性都实现一遍。因此上面这些目录的文件中,存放的都是实现的引用,并不是特性代码实现本身。真正的实现在modules目录中。modules目录中是以特性名作为命名的文件,文件有固定的前缀名:es.表示ES标准;esnext.表示提议中的标准;web.表示web标准。

这里以我们上面提到过的两个特性为例,看看引用路径,首先是Promise.try:

  • 使用者引入 core-js/full/promise/try.js
  • 引入 actual/promise/try.js
  • 引入 actual/promise/try.js
  • 引入 stable/promise/try.js
  • 引入 es/promise/try.js
  • 最终引入 modules/es.promise.try.js

然后是groupBy方法:

  • 使用者引入 core-js/actual/array/group-by.js
  • 最终引入 modules/esnext.array.group-by.js

可以看到,core-js内部的特性是经过层层引入,最终引入具体的实现代码的。

core-js-pure与core-js-bundle

除了core-js之外,core-js-pure与core-js-bundle这两个包也提供了兼容性。core-js-pure内部的目录结构与core-js一致,只不过core-js-pure不将特性注入到全局。core-js-bundle比较特殊,它是将core-js代码经过打包后再提供,它的结构如下:

core-js-bundle
├─index.js
├─minified.js
├─minified.js.map
└─...

其中index.js是打包过后的特性集合代码,minified.js是经过压缩混淆后的代码。core-js-bundle只能全部引入并注入到全局,不能引入部分目录或者导出某个属性。

打包和浏览器效果

创建Webpack示例

首先创建一个Webapck项目,方便后续打包查看效果。首先执行:

# 创建项目
npm init -y
# 安装webpack依赖
npm add webpack webpack-cli html-webpack-plugin
# 安装core-js依赖
npm add core-js core-js-pure core-js-bundle

创建src/index.js,内容如下:

const arr = ["jz", "plp"];
console.log(arr.lastIndex);

在package.json文件的scripts中增加命令:"build": "webpack"。最后是Webpack配置文件webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production', // 生产模式
  entry: './src/index.js', // 源码入口
  plugins: [
    new HtmlWebpackPlugin({ // 生成HTML页面入口
      title: 'jzplp的core-js实验', // 页面标题
    }),
  ],
  output: {
    filename: 'main.js', // 生成文件名
    path: path.resolve(__dirname, 'dist'),  // 生成文件目录
    clean: true, // 生成前删除dist目录内容
  },
};

命令行运行npm run build,即可使用Webpack打包。在dist目录中生成了两个文件,一个是main.js,里面是打包后的js代码;index.html可以让我们在浏览器查看效果。由于我们没有引入core-js,浏览器没有预置lastIndex这个提议中的特性,因此输出undefined。

core-js打包

这里引入core-js,然后打包查看效果。首先是全量引入:

require("core-js");
const arr = ["jz", "plp"];
console.log(arr.lastIndex);

此时浏览器输出1,表示core-js注入成功,lastIndex特性生效了。但是我们查看main.js,发现居然有267KB。这是因为它把所有特性都引入了。

如果引入require("core-js/full/array"),此时新特性也可以生效。因为只引入了数组相关特性,因此main.js的大小为59.3KB,比全量引入小很多。

如果引入require("core-js/full/array/last-index"),此时新特性也可以生效。因为只引入了这一个特性,因此main.js的大小为12.2KB。

Babel与core-js

从前面打包的例子中可以看到,core-js整个打包进项目中是非常巨大的,可能比你正常项目的大小还要更大。这样明显会造成占用资源更多,页面加载时间变慢等问题。一个解决办法是,只引入我们代码中使用到的特性,以及我们要适配的浏览器版本中不兼容的特性,用不到的特性不打包进代码中。Babel就提供了这样的功能。

创建Babel示例

# 创建项目
npm init -y
# 安装webpack依赖
npm add @babel/core @babel/cli @babel/preset-env
# 安装core-js依赖
npm add core-js core-js-pure core-js-bundle

创建src/index.js,内容如下:

require('core-js');
const jzplp = 1;

在package.json文件的scripts中增加命令:"babel": "babel src --out-dir lib"。最后是Babel配置文件babel.config.json:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "100"
        }
      }
    ]
  ]
}

targets中表示我们需要兼容的浏览器版本。执行npm run babel,生成结果再lib/index.js中,内容如下。可以看到未对core-js做任何处理。

"use strict";

require('core-js');
const jzplp = 1;

preset-env配置entry

@babel/preset-env是一个Babel预设,可以根据配置为代码增加兼容性处理。前面创建Babel示例时已经增加了这个预设,但是没有增加core-js配置。这里我们加一下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "100"
        },
        "useBuiltIns": "entry",
        "corejs": "3.47.0"
      }
    ]
  ]
}

这里增加了corejs版本和useBuiltIns配置,值为entry。配置这个值,会使得@babel/preset-env根据配置的浏览器版本兼容性,选择引入哪些core-js中的特性。这里再执行命令行,结果如下:

"use strict";

require("core-js/modules/es.symbol.async-dispose.js");
require("core-js/modules/es.symbol.dispose.js");
// ... 更多es特性省略
require("core-js/modules/esnext.array.filter-out.js");
require("core-js/modules/esnext.array.filter-reject.js");
// ... 更多esnext特性省略
require("core-js/modules/web.dom-exception.stack.js");
require("core-js/modules/web.immediate.js");
// ... 更多web特性省略
const jzplp = 1;

可以看到core-js被拆开,直接引入了特性本身。在配置chrome: 100版本时,引入的特性为215个。我们修改配置chrome: 140版本时,再重新生成代码,此时引入的特性为150个。可以看到确实时根据浏览器版本选择不同的特性引入。这对于其它core-js的引入方式也生效:

// 源代码
require('core-js/stable');
const jzplp = 1;

// 生成代码
"use strict";

require("core-js/modules/es.symbol.async-dispose.js");
require("core-js/modules/es.symbol.dispose.js");
// ... 更多es特性省略
require("core-js/modules/web.dom-exception.stack.js");
require("core-js/modules/web.immediate.js");
// ... 更多web特性省略
const jzplp = 1;

我们引入core-js/stable,可以看到生成代码中不引入esnext特性了。在配置chrome: 100版本时,引入的特性为71个,配置chrome: 100版本时,引入的特性为6个。同样的,如果引入换成core-js/full/array,就会只引入数组相关特性,而且也是根据浏览器兼容版本引入。

preset-env配置usage

@babel/preset-env的useBuiltIns配置值为usage时,Babel不仅会跟根据配置的浏览器版本兼容性,还会根据代码中实际使用的特性来选择引入哪些core-js中的特性。首先是Babel配置:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "100"
        },
        "useBuiltIns": "usage",
        "corejs": "3.47.0"
      }
    ]
  ]
}

然后是要处理的代码,注意配置usage时是不需要手动引入core-js的。我们配置不同的Chrome浏览器版本,看看输出结果如何:

// 源代码
const jzplp = new Promise();
const b = new Map();

// chrome 50 生成代码 
"use strict";

require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/web.dom-collections.iterator.js");
const jzplp = new Promise();
const b = new Map();

// chrome 100 生成代码 
"use strict";

const jzplp = new Promise();
const b = new Map();

首先可以看到,引入core-js中的特性数量变得非常少了,代码中没有用到的特性不再引入。其次不同的浏览器版本引入的特性不一样,因此还是会根据浏览器兼容性引入特性。我们再修改一下源代码试试:

// 源代码
const jzplp = new Promise();
const b = new Map();
Promise.try(() =>{});

// chrome 50 生成代码 
"use strict";

require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/es.promise.try.js");
require("core-js/modules/web.dom-collections.iterator.js");
const jzplp = new Promise();
const b = new Map();
Promise.try(() => {});

// chrome 100 生成代码 
"use strict";

require("core-js/modules/es.promise.try.js");
const jzplp = new Promise();
const b = new Map();
Promise.try(() => {});

可以看到,源代码中增加了Promise.try,引入的特性也随之增加了对应的core-js特性引入。因此,使用@babel/preset-env的usage配置,可以保证兼容性的同时,最小化引入core-js特性。另外这个配置并不会自动引入提议特性,如果需要则额外配置proposals为true。

@babel/polyfill

@babel/polyfill是一个已经被弃用的包,推荐直接使用core-js/stable。查看@babel/polyfill源码,发现他就是引入了core-js特性与regenerator-runtime这个包。regenerator-runtime也是一个兼容性相关的包,可以帮助添加generatore和async/await相关语法。作为替代可以这样引入:

import 'core-js/stable';
import 'regenerator-runtime/runtime';

@babel/runtime

@babel/runtime就像自动引入版的core-js-pure。它还是根据代码实际使用的特性来注入core-js特性,但它不注入到全局,而是引入这些API再调用。这里我们使用@babel/plugin-transform-runtime插件,里面包含了@babel/runtime相关逻辑。首先看下Babel配置:

{
  "plugins": [["@babel/plugin-transform-runtime", { "corejs": 3 }]]
}

再转义上一节中的代码,结果如下:

// 源代码
const jzplp = new Promise();
const b = new Map();
Promise.try(() =>{});

// 生成代码
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
import _Map from "@babel/runtime-corejs3/core-js-stable/map";
const jzplp = new _Promise();
const b = new _Map();
_Promise.try(() => {});

可以看到,虽然没有直接引入core-js-pure,但效果是一样的。打开@babel/runtime-corejs3这个包查看,里面实际上就是导出了core-js-pure中的特性。例如:

// @babel/runtime-corejs3/core-js-stable/map.js 文件内容
module.exports = require("core-js-pure/stable/map");

core-js/configurator强制控制

如果希望在正常引入core-js时,对于部分特殊属性进行引入或者不引入的控制,就需要用到core-js/configurator。这个工具可以配置三种选项:

  • useNative: 当环境中有这个特性时不引入,当确定没有时才引入
  • usePolyfill: 明确引入这个特性
  • useFeatureDetection: 默认行为,和不使用core-js/configurator一致

useNative不引入

首先试试不引入特性,这里我们使用Promise这个特性为例。首先是不引入core-js的效果,可以看到全局Promise对象被我们改掉了。

const jzplp = {};
Promise = jzplp;
console.log(Promise, Promise === jzplp);

/* 输出结果
{} true
*/

然后在中间引入core-js试试。可以看到我们改掉的Promise,被core-js给改回去了。

const jzplp = {};
Promise = jzplp;
require("core-js/actual");
console.log(Promise, Promise === jzplp);

/* 输出结果
[Function: Promise] false
*/

这时候,如果不希望core-js改掉我们自定义的Promise,可以利用useNative配置,强制core-js不引入这个特性。看结果core-js引入之后,我们自定义的Promise依然存在。

const configurator = require("core-js/configurator");
configurator({
  useNative: ["Promise"],
});
const jzplp = {};
Promise = jzplp;
require("core-js/actual");
console.log(Promise, Promise === jzplp);

/* 输出结果
{} true
*/

usePolyfill强制引入

想要验证usePolyfill的效果,需要找一个环境中本来存在的特性,core-js即使引入也不会修改的特性。Promise不行,因为core-js引入时会对这个Promise增加子特性。Promise.try也不行,因为原来环境中不存在。这里试一下Promise.any,这是环境中本来就存在的特性:

console.log(Promise.any);
const jzplp = () => {};
Promise.any = jzplp;
console.log(Promise.any, Promise.any === jzplp);

/* 输出结果
[Function: any]
[Function: jzplp] true
*/

可以看到,Promise.any原来就存在,但是被我们修改成了新函数。再引入core-js试试:

console.log(Promise.any);
const jzplp = () => {};
Promise.any = jzplp;
require('core-js');
console.log(Promise.any, Promise.any === jzplp);

/* 输出结果
[Function: any]
[Function: jzplp] true
*/

引入了core-js之后,结果没有变化。这说明core-js并不会修改我们自定义的函数。这时候就可以试一下usePolyfill的效果了:

const configurator = require("core-js/configurator");
configurator({
  usePolyfill: ["Promise.any"],
});
console.log(Promise.any);
const jzplp = () => {};
Promise.any = jzplp;
require('core-js');
console.log(Promise.any, Promise.any === jzplp);

/* 输出结果
[Function: any]
[Function: any] false
*/

可以看到,Promise.any又被改为了真正起效果的函数,这说明usePolyfill的强制引入特性是有效的。

core-js中的特性选择

前面我们体验了Babel根据浏览器兼容性,选择不同的core-js特性引入,那么不同浏览器兼容哪些特性的数据是从哪里获取呢?core-js本身就提供了这个功能。

core-js-compat

core-js-compat提供了不同浏览器对应特性的兼容性数据。它有好几个参数,这里先列举一下含义:

  • targets: Browserslist格式的浏览器兼容配置
  • modules: 需要设置兼容性配置的模块,可以是core-js/full,也可以是某个特性,甚至是正则
  • exclude: 需要排除的模块
  • version: 使用的core-js版本
  • inverse: 反向输出,即输出不需要兼容的特性列表

这里举几个例子试一下:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 10%",
  modules: ["core-js/actual"],
  version: "3.47",
});
console.log(data);

/* 输出结果
{
  list: [
    'es.iterator.concat',
    'es.math.sum-precise',
    'es.async-iterator.async-dispose',
    'esnext.array.group',
    'esnext.array.group-by',
    ...其它特性
  ],
  targets: {
    'es.iterator.concat': { 'chrome-android': '143' },
    'es.math.sum-precise': { 'chrome-android': '143' },
    'es.async-iterator.async-dispose': { 'chrome-android': '143' },
    'esnext.array.group': { 'chrome-android': '143' },
    'esnext.array.group-by': { 'chrome-android': '143' },
    ...其它特性
  }
}
*/

compat会根据我们设置的浏览器兼容性配置,输出特性列表,包含两个字段:list是一个特性名称列表;targets是一个Map结构,key为特性名,值为可以兼容的浏览器。假设我们把上面的 targets改成 > 50%,此时会输出空值:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 50%",
  modules: ["core-js/actual"],
  version: "3.47",
});
console.log(data);

/* 输出结果
{ list: [], targets: {} }
*/

我们增加exclude,排除部分属性,可以看到特性数量大大减少:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 10%",
  modules: ["core-js/actual"],
  exclude: ["esnext"],
  version: "3.47",
});
console.log(data);

/* 输出结果
{
  list: [
    'es.iterator.concat',
    'es.math.sum-precise',
    'es.async-iterator.async-dispose',
    'web.dom-exception.stack',
    'web.immediate',
    'web.structured-clone'
  ],
  targets: {
    'es.iterator.concat': { 'chrome-android': '143' },
    'es.math.sum-precise': { 'chrome-android': '143' },
    'es.async-iterator.async-dispose': { 'chrome-android': '143' },
    'web.dom-exception.stack': { 'chrome-android': '143' },
    'web.immediate': { 'chrome-android': '143' },
    'web.structured-clone': { 'chrome-android': '143' }
  }
}
*/

再试一下inverse的效果:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 10%",
  modules: ["core-js/actual"],
  version: "3.47",
  inverse: true
});
console.log(data);

/* 输出结果
{
  list: [
    'es.symbol',
    'es.symbol.description',
    ...其它特性
  ],
  targets: {
    'es.symbol': {},
    'es.symbol.description': {},
    ...其它特性
  }
}
*/

因为输出的是不需要引入core-js兼容的特性,所以特性数量非常多,而且targets中没有列出支持的浏览器版本。

core-js-builder

前面介绍的core-js-compat是接收参数之后,输出core-js的特性列表数组。而core-js-builder接收类似的参数,直接输出引用core-js的代码。我们首先列举一下参数:

  • targets: Browserslist格式的浏览器兼容配置
  • modules: 需要设置兼容性配置的模块,可以是core-js/full,也可以是某个特性,甚至是正则
  • exclude: 需要排除的模块
  • format: 'bundle'输出打包后的源码;'cjs'和'esm'输出对应格式的引用代码
  • filename: 输出的文件名

我们先试一下例子。首先是format格式的:

const builder = require("core-js-builder");
async function funJzplp() {
  const data = await builder({
    targets: "> 30%",
    modules: ["core-js/actual"],
    format: 'bundle',
  });
  console.log(data);
}
funJzplp();

/* 输出结果
...代码很长,这里节选部分
 (function(module, exports, __webpack_require__) {
"use strict";
var NATIVE_BIND = __webpack_require__(8);
var FunctionPrototype = Function.prototype;
*/

可以看到,builder函数输出了非常长的代码,内容实际为输出的特性经过打包之后的结果代码。再试一下'cjs'和'esm',输出的是对应木块的引用代码:

// format: 'cjs' 输出结果
...代码很长,这里节选部分
require('core-js/modules/es.iterator.concat');
require('core-js/modules/es.math.sum-precise');
require('core-js/modules/es.async-iterator.async-dispose');
*/

// format: 'esm' 输出结果
...代码很长,这里节选部分
import 'core-js/modules/es.iterator.concat.js';
import 'core-js/modules/es.math.sum-precise.js';
import 'core-js/modules/es.async-iterator.async-dispose.js';
*/

如果设置了filename,core-js-builder会创建该名称的文件,并将代码写入到文件中。

总结

这篇文章描述了core-js相关包的代码内容和使用方式。core-js实际上就是提供了JavaScript中一些API特性的兼容实现方式。它与实现语法兼容的Babel一起,可以做到大部分JavaScript的兼容性。当然core-js和Babel也不是万能的,它们都有各自无法转义和兼容的语法和特性。

core-js这个包名字起的非常好,一听就是JavaScript的“核心”包。由于它实现了很多API特性,而且引用数量非常非常大,因此叫“核心”也不为过。虽然这个包引用量很大,但不如React/Vue或者一些其它包出名。因为这个包是处理的更底层的兼容性有问题,因此用户感知不强。core-js包的作者还因为没有钱而遇到很多问题,这个包并没有让他变的富有。

参考

昨天 — 2026年1月20日技术

Three.js 实战:使用 DOM/CSS 打造高性能 3D 文字

作者 烛阴
2026年1月20日 22:21

Three.js 实战:使用 DOM/CSS 打造高性能 3D 文字

在 Three.js 中渲染文字有多种方案,本文介绍一种高性能且灵活的方案:CSS2DRenderer。它能将 DOM 元素无缝嵌入 3D 场景,同时保留 CSS 的全部能力。

为什么选择 DOM/CSS 方案?

方案 优点 缺点
TextGeometry 真正的 3D 几何体 性能开销大,需加载字体文件
CSS2DRenderer 清晰锐利、CSS 全特性、高性能 无法被 3D 物体遮挡

CSS2DRenderer 的核心优势:

  • 文字永远清晰:浏览器原生渲染,不受 3D 缩放影响
  • CSS 全特性:阴影、渐变、动画、backdrop-filter 磨砂玻璃效果
  • 性能优异:DOM 渲染与 WebGL 渲染分离,互不干扰

核心实现

1. 初始化双渲染器

import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

// WebGL 渲染器(渲染 3D 场景)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// CSS2D 渲染器(渲染 DOM 标签)
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
Object.assign(labelRenderer.domElement.style, {
  position: 'absolute',
  top: '0',
  pointerEvents: 'none' // 关键:让鼠标事件穿透
});
document.body.appendChild(labelRenderer.domElement);

关键点pointerEvents: 'none' 让鼠标事件穿透 DOM 层,否则无法拖拽 3D 场景。

2. 创建 CSS2D 标签

const createLabel = (text, position) => {
  const div = document.createElement('div');
  div.className = 'label';
  div.textContent = text;
  const label = new CSS2DObject(div);
  label.position.copy(position);
  return label;
};

// 将标签添加到 3D 物体上
earth.add(createLabel('Earth', new THREE.Vector3(0, 1.5, 0)));

标签添加到 earth 网格后,会自动跟随地球的旋转和位移。

3. 双渲染器同步渲染

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);      // 渲染 3D 场景
  labelRenderer.render(scene, camera); // 渲染 DOM 标签
}

4. CSS 样式

.label {
  color: #FFF;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: bold;
  padding: 5px 10px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 4px;
  backdrop-filter: blur(4px); /* 磨砂玻璃效果 */
}

backdrop-filter: blur() 实现的磨砂玻璃效果,在纯 WebGL 中需要复杂的后处理才能实现,而 CSS 一行代码搞定。

完整代码

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

// 场景、相机
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.z = 5;

// WebGL 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);

// CSS2D 渲染器
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
Object.assign(labelRenderer.domElement.style, {
  position: 'absolute',
  top: '0',
  pointerEvents: 'none'
});
document.body.appendChild(labelRenderer.domElement);

// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 创建地球
const earth = new THREE.Mesh(
  new THREE.SphereGeometry(1, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0x2233ff, roughness: 0.5 })
);
scene.add(earth);

// 生成地球纹理
const canvas = Object.assign(document.createElement('canvas'), { width: 512, height: 512 });
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#1e90ff';
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = '#228b22';
for (let i = 0; i < 20; i++) {
  ctx.beginPath();
  ctx.arc(Math.random() * 512, Math.random() * 512, Math.random() * 50 + 20, 0, Math.PI * 2);
  ctx.fill();
}
earth.material.map = new THREE.CanvasTexture(canvas);

// 创建标签工厂函数
const createLabel = (text, position) => {
  const div = document.createElement('div');
  div.className = 'label';
  div.textContent = text;
  const label = new CSS2DObject(div);
  label.position.copy(position);
  return label;
};

earth.add(createLabel('Earth', new THREE.Vector3(0, 1.5, 0)));

// 光源
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 3, 5);
scene.add(dirLight);

// 动画循环
(function animate() {
  requestAnimationFrame(animate);
  earth.rotation.y += 0.005;
  controls.update();
  renderer.render(scene, camera);
  labelRenderer.render(scene, camera);
})();

// 响应窗口变化
addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
  labelRenderer.setSize(innerWidth, innerHeight);
});

📂 核心代码与完整示例:     my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

2026 年,值得前端全栈尝试的 NestJS 技术栈组合 😍😍😍

作者 Moment
2026年1月20日 22:08

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

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。

对很多想从前端转向全栈的同学来说,NestJS 是一个非常友好的选择:语法风格接近前端熟悉的 TypeScript,又借鉴了后端常见的模块化与依赖注入模式,可以在保留前端开发舒适度的同时,比较轻松地搭起一套“像样的后端服务”。

如果目标不仅是写几个简单接口,而是要扛起鉴权、实时协同、AI 能力,还要自己搭建和维护数据库、缓存、搜索、对象存储、监控这些基础设施,那么就需要一套偏“自托管、自运维”的 NestJS 技术栈组合。

这里推荐的技术栈选择标准主要有三点:

  1. NestJS 生态高度契合,有成熟的官方或社区集成;
  2. 能够支撑中大型文档、知识类应用的性能和复杂度,比如协同编辑、全文检索、RAG、任务队列等;
  3. 个人或小团队也能在合理成本内自行部署和维护,比如使用 Prisma + MySqlRedisElasticsearchMinIO 这类开源组件。

接下来就按照从框架、运行时到数据层、搜索、队列、AI 的顺序,分享一套适合前端转全栈使用的 NestJS 核心技术栈,代码也尽量贴近实战,方便直接改造复用。

NestJSTypeScript

NestJS 是整个后端的“框架壳子”,负责模块划分、依赖注入、装饰器等基础能力;TypeScript 则是地基,把很多原本要靠经验避免的错误提前到编译期发现,例如 Controller 入参、Service 返回值、配置对象等。

实际项目中,一般会开启严格模式,再结合全局的 ValidationPipeclass-transformer,把请求里的原始 JSON 自动转换成带类型的 DTO 实例,从而减少 any 的使用、避免脏数据流入业务层。

基本使用案例:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      transform: true,
    }),
  );
  await app.listen(3000);
}

bootstrap();

Fastify@nestjs/platform-fastify

Fastify 是一个高性能 HTTP 引擎,相比 Express 更轻量、更适合高并发场景;@nestjs/platform-fastify 负责把 Fastify 接进 Nest,让你在业务层依然只写标准的 Controller、Guard、Interceptor 等。

搭配 @fastify/helmet@fastify/rate-limit@fastify/cookie@fastify/secure-session@fastify/multipart@fastify/static,可以在统一框架里完成安全头、限流、会话、上传和静态资源托管。

基本使用案例:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );
  await app.listen(3000, '0.0.0.0');
}

bootstrap();

@nestjs/config

@nestjs/config 是 Nest 官方的配置模块,用来统一管理多环境配置。思路很简单:所有数据库地址、第三方秘钥、开关配置等,都通过 ConfigService 读取,而不是在代码里到处散落 process.env

推荐的做法是为不同领域写独立的配置工厂,然后在对应模块中注入使用。

基本使用案例:

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
  ],
})
export class AppModule {}
// some.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class SomeService {
  constructor(private readonly configService: ConfigService) {}

  getDbUrl() {
    return this.configService.get<string>('DATABASE_URL');
  }
}

PassportJWT

@nestjs/passport@nestjs/jwt 提供了一整套认证基础设施:Passport 统一管理各种认证策略(本地、JWT、OAuth 等),JWT 提供无状态令牌,让前后端分离、多端访问更容易管理。

常见流程是:通过本地策略验证用户名密码,登录成功后签发 JWT,后续请求通过 AuthGuard('jwt') 验证;接入第三方登录(如 GitHub)时,仅需增加新的 Passport 策略。

基本使用案例:

// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: 'secret', // 实际项目请从 ConfigService 读取
    }),
  ],
  providers: [JwtStrategy],
  exports: [PassportModule, JwtModule],
})
export class AuthModule {}
// some.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('profile')
export class ProfileController {
  @UseGuards(AuthGuard('jwt'))
  @Get()
  getProfile() {
    return { ok: true };
  }
}

WebSocketSocket.IO

当系统需要即时通知、在线状态、协同编辑时,可以使用 @nestjs/websockets 搭配 @nestjs/platform-socket.iosocket.ioGateway 充当“长连接入口”,负责管理连接、房间和事件。

更高级的协同场景中,还可以引入 hocuspocusyjsy-prosemirror 来负责文档协同算法,Nest 只需要负责连接管理和权限校验。

基本使用案例:

// chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
} from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: string) {
    this.server.emit('message', data);
  }
}

Prisma

Prisma 通过独立的 schema.prisma 文件定义模型,并生成强类型的 PrismaClient,非常适合在 Nest 的 Service 层中使用。它把数据库迁移、数据建模、类型安全绑在一起,能明显降低 SQL 错误和字段拼写错误的概率。

在 Service 中,直接注入封装好的 PrismaService,就可以用类型安全的方式进行增删改查。

基本使用案例:

// user.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/common/prisma/prisma.service';

@Injectable()
export class UserService {
  constructor(private readonly prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany();
  }
}

Redisioredis

Redis 是常见的缓存与中间层,而 ioredis 是稳定好用的 Node 客户端组合。它通常用于三个方向:缓存(加速读取)、分布式协调(锁、限流、防重复)、短期数据存储(会话、任务状态等)。

在 Nest 中一般会封装一个 RedisService,对外暴露 getsetincr 等方法,避免直接在业务里使用底层客户端。

基本使用案例:

// redis.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Redis } from 'ioredis';

@Injectable()
export class RedisService implements OnModuleInit {
  private client: Redis;

  onModuleInit() {
    this.client = new Redis('redis://localhost:6379');
  }

  async get(key: string) {
    return this.client.get(key);
  }

  async set(key: string, value: string, ttlSeconds?: number) {
    if (ttlSeconds) {
      await this.client.set(key, value, 'EX', ttlSeconds);
    } else {
      await this.client.set(key, value);
    }
  }
}

BullMQ

BullMQ 是基于 Redis 的任务队列,@nestjs/bullmq 让你可以用装饰器方式定义队列和消费者。它适合承载各种耗时任务,例如大文件解析、批量导入导出、调用外部 AI 接口等。

这样可以把重任务从 HTTP 请求中剥离,避免接口超时,用户只需拿到一个“任务已受理”的 ID。

基本使用案例:

消费者(处理任务):

// task.processor.ts
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';

@Processor('tasks')
export class TaskProcessor extends WorkerHost {
  async process(job: Job) {
    // 这里处理耗时任务
    console.log('processing job', job.id, job.data);
  }
}

生产者(投递任务):

// task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';

@Injectable()
export class TaskService {
  constructor(@InjectQueue('tasks') private readonly queue: Queue) {}

  async createTask(payload: any) {
    const job = await this.queue.add('process', payload);
    return { jobId: job.id };
  }
}

Elasticsearch

Elasticsearch 在文档和知识类系统中通常用作结构化搜索与日志索引引擎。通过 @elastic/elasticsearch 客户端可以在 Nest 的 Service 里封装搜索接口,对外统一暴露“搜索文档”“搜索日志”等能力。

相对数据库原生查询,Elasticsearch 更擅长复杂查询、聚合统计、模糊搜索与搜索结果排序,是提升搜索体验的关键组件。

基本使用案例:

封装搜索 Service:

// search.service.ts
import { Injectable } from '@nestjs/common';
import { Client } from '@elastic/elasticsearch';

@Injectable()
export class SearchService {
  private client = new Client({ node: 'http://localhost:9200' });

  async searchDocs(keyword: string) {
    const res = await this.client.search({
      index: 'documents',
      query: {
        multi_match: {
          query: keyword,
          fields: ['title', 'content'],
        },
      },
    });
    return res.hits.hits;
  }
}

对象存储(MinIO

当系统有大量文件(如 PDFWord、图片、音频)时,本地磁盘很快就会吃不消,这时可以用自建的 MinIO 集群来做对象存储。它负责长期保存大文件,后端只需要关心对象名和访问地址,不必再直接管理磁盘。

在 Nest 中通常会封装一个存储 Service,对上层暴露“上传文件”“生成下载地址”等方法;同时配合 imagekitsharpexiftool-vendoredpdf-parsemammoth 等,对文件做压缩、预览、元信息与文本提取等处理。

基本使用案例:

// storage.service.ts
import { Injectable } from '@nestjs/common';
import { Client } from 'minio';

@Injectable()
export class StorageService {
  private client = new Client({
    endPoint: 'localhost',
    port: 9000,
    useSSL: false,
    accessKey: 'minio',
    secretKey: 'minio123',
  });

  async upload(bucket: string, objectName: string, buffer: Buffer) {
    await this.client.putObject(bucket, objectName, buffer);
    return { bucket, objectName };
  }
}

@nestjs/swagger

@nestjs/swagger 负责从 Controller 和 DTO 的装饰器中生成 OpenAPI 文档,让接口定义和代码实现保持同步,不再需要单独维护一份容易过期的接口文档。

在前后端分离项目中,Swagger 文档可以同时服务前端、测试与产品:前端对齐请求和响应结构,测试做接口验证,产品了解后端已有能力。

基本使用案例:

// main.ts 片段
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('API 文档')
    .setDescription('服务接口说明')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(3000);
}

class-validatorclass-transformer

class-validator 通过在 DTO 类属性上添加装饰器(如 IsStringIsIntIsOptional 等)定义字段的合法规则,class-transformer 负责把原始请求 JSON 转换成 DTO 实例。配合全局 ValidationPipe,可以保证进入 Controller 的数据已经过校验和转换。

这一体系大大减少了手写 if 校验的重复劳动,同时确保错误请求在统一入口被拦截并抛出合适的 HTTP 异常。

基本使用案例:

// create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(6)
  password: string;
}
// user.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UserController {
  @Post()
  create(@Body() dto: CreateUserDto) {
    return dto;
  }
}

PrometheusTerminus

@willsoto/nestjs-prometheus 搭配 prom-client 可以方便地暴露各种指标端点,例如 HTTP 延迟、错误率、队列堆积情况等;@nestjs/terminus 则专注于健康检查,通过多种 HealthIndicator 检查数据库、RedisElasticsearchQdrant 等依赖服务是否可用。

在生产环境下,这两者为“可观测性”打基础,使运维和开发可以快速感知和定位问题。

基本使用案例:

// health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
  HealthCheck,
  HealthCheckService,
  TypeOrmHealthIndicator,
} from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }
}

OpenAILangChain

openai 提供与大模型交互的基础接口,而 langchain@langchain/core@langchain/community@langchain/openai@langchain/textsplitters 则把模型调用、提示模板、工具调用、长文档切片等复杂逻辑抽象成可组合的模块。对于文档工作流类项目,这一层就是从“普通文档系统”升级为“智能文档系统”的关键。

在 Nest 中,通常会拆出一个 AI 模块,把“向量检索 + RAG + 模型调用”封装在 Service 里,再通过少量 HTTP 接口暴露出问答、总结、润色等能力。

基本使用案例:

// ai.service.ts
import { Injectable } from '@nestjs/common';
import OpenAI from 'openai';

@Injectable()
export class AiService {
  private client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

  async chat(prompt: string) {
    const res = await this.client.chat.completions.create({
      model: 'gpt-4.1-mini',
      messages: [{ role: 'user', content: prompt }],
    });
    return res.choices[0]?.message?.content ?? '';
  }
}

总结

如果只是想把接口“跑起来”,也许只要 NestJS 加上一两个库就够用;但一旦要扛起鉴权、实时协同、AI、文件与搜索这些完整能力,就很容易演变成这篇文章前面列出来的这一整套技术栈:NestJS + TypeScript 打基础,Fastify 提供高性能 HTTP 入口,Prisma + MySql 管数据,Redis + BullMQ 管缓存和队列,Elasticsearch 管搜索,MinIO 管文件,再加上 @nestjs/swaggerclass-validatorPrometheusTerminusOpenAILangChain 这些周边,让项目从“能跑”变成“好用、可维护、可扩展”。

对前端转全栈来说,这套组合有两个现实的好处:一是语法和思路都围绕 TypeScript 展开,上手成本可控;二是每一个环节都有成熟的 Nest 集成(模块、装饰器、示例代码),可以按需逐步引入,而不必一口气吃下全部。你可以先用 NestJS + Prisma + Redis 起一个简单项目,再慢慢把队列、搜索、对象存储和 AI 能力补上,最终搭出一套适合自己长期维护的后端脚手架。

让 AI 学会"问一嘴":assistant-ui 前端工具的人机交互实践

作者 红尘散仙
2026年1月20日 22:02

让 AI 学会"问一嘴":assistant-ui 前端工具的人机交互实践

🤖 当 AI 助手要帮用户执行敏感操作时,总不能闷头就干吧?得先问问人家确不确认啊!

背景:一个"莽撞"的 AI 助手

我在做一个工单管理系统,里面有个 AI 助手功能。用户可以说"帮我创建一个工单,数量 50",AI 就会调用工具创建记录。

听起来很美好,但问题来了:

AI 太"自信"了。

它收到指令就直接执行,万一用户说错了呢?万一 AI 理解错了呢?50 变成 500,那可就麻烦了。

所以我需要一个机制:AI 调用工具前,先让用户确认一下。

用户: 帮我创建一个工单,数量 50

AI: 好的,我来帮你创建记录
    ┌─────────────────────────┐
    │ 确认创建记录             │
    │ 名称: A 款               │
    │ 数量: 50                 │
    │ [确认]  [取消]           │
    └─────────────────────────┘

用户: *点击确认*

AI: ✅ 已创建记录:Ax 50

这就是所谓的 Human-in-the-Loop(人机协作)模式。

技术选型

  • 前端:React + @assistant-ui/react
  • 后端:Rust + Axum + Rig(AI 框架)
  • AI:OpenAI GPT-4

assistant-ui 是一个专门为 AI 聊天界面设计的 React 组件库,它有个很棒的特性:前端工具(Frontend Tools)

架构概览

flowchart TB
    subgraph Frontend["前端 (React)"]
        Runtime[useChatRuntime]
        Tool[CreateRecordTool<br/>前端工具]
        UI[确认 UI]
        Runtime --> Tool --> UI
        UI -->|resume| Tool
        Tool -->|sendAutomatically| Runtime
    end

    subgraph Backend["后端 (Rust)"]
        Handler[Chat Handler]
        FTool[FrontendTool<br/>工具定义转发]
        AI[AI Model]
        Handler --> FTool --> AI
    end

    Runtime <-->|HTTP| Handler

核心概念:前端工具 vs 后端工具

前端工具 后端工具
执行位置 浏览器 服务器
能否与用户交互 ✅ 可以 ❌ 不行
适用场景 需要确认的操作 查询、计算

前端工具的精髓在于:工具定义发给 AI,但执行在前端

AI 知道有这个工具可以用,当它决定调用时,前端拦截执行,可以弹个确认框、让用户填个表单,用户操作完再把结果告诉 AI。

实现步骤

1. 定义工具参数

// schema.ts
import { z } from "zod";

export const CreateRecordSchema = z.object({
  name: z.string().describe("名称"),
  amount: z.number().describe("数量"),
});

2. 创建前端工具

这是最关键的部分,使用 makeAssistantTool

import { makeAssistantTool } from "@assistant-ui/react";

export const CreateRecordTool = makeAssistantTool({
  toolName: "create-record",
  type: "frontend",  // 🔑 关键:标记为前端工具
  parameters: CreateRecordSchema,
  description: "创建记录",

  // execute 在前端执行
  execute: async (args, ctx) => {
    const { human } = ctx;

    // human() 会暂停执行,等待用户确认
    const response = await human("请确认创建记录");

    if (response === "confirmed") {
      // 调用实际 API
      await api.createRecord(args);
      return { success: true };
    }
    return { success: false };
  },

  // render 渲染确认 UI
  render: ({ args, status, result, resume }) => {
    // 等待确认状态
    if (status.type === "requires-action") {
      return (
        <div className="rounded-lg border p-4">
          <div>确认创建记录</div>
          <div>名称: {args.name} | 数量: {args.amount}</div>
          <button onClick={() => resume("confirmed")}>确认</button>
          <button onClick={() => resume("cancelled")}>取消</button>
        </div>
      );
    }

    // 完成状态
    return <div>{result?.success ? "✅" : "❌"} 已处理</div>;
  },
});

核心 API 解释:

  • human(message): 暂停执行,等待用户操作
  • resume(value): 用户操作后恢复执行,value 会作为 human() 的返回值

3. 注册工具

function ChatPage() {
  const processedToolCalls = useRef(new Set<string>());

  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({
      api: "/api/chat",
    }),
    // 工具完成后自动发送结果给后端
    sendAutomaticallyWhen: (options) => {
      if (!lastAssistantMessageIsCompleteWithToolCalls(options)) {
        return false;
      }
      const lastMsg = options.messages.at(-1);
      const toolPart = lastMsg?.parts.find(
        (p) => p.type === "tool-create-record" && p.state === "output-available"
      ) as { toolCallId: string } | undefined;

      if (toolPart && !processedToolCalls.current.has(toolPart.toolCallId)) {
        processedToolCalls.current.add(toolPart.toolCallId);
        return true;
      }
      return false;
    },
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <Thread />
      <CreateRecordTool />
    </AssistantRuntimeProvider>
  );
}

4. 后端接收工具定义

前端会把工具定义发给后端,后端需要转发给 AI:

// Rust 后端
pub struct FrontendTool {
    pub name: String,
    pub description: String,
    pub parameters: Value,
}

impl ToolDyn for FrontendTool {
    fn name(&self) -> String { self.name.clone() }

    fn definition(&self, _: String) -> ToolDefinition {
        ToolDefinition {
            name: self.name.clone(),
            description: self.description.clone(),
            parameters: self.parameters.clone(),
        }
    }

    fn call(&self, _: String) -> Result<String, ToolError> {
        // 前端工具不在后端执行!
        Err(ToolError::ToolCallError("Frontend tool".into()))
    }
}

完整流程

sequenceDiagram
    participant 用户
    participant 前端
    participant 后端
    participant AI

    用户->>前端: "创建工单,数量 50"
    前端->>后端: 发送消息 + 工具定义
    后端->>AI: 转发
    AI->>后端: 调用 create-record
    后端-->>前端: 返回工具调用
    前端->>前端: execute() → human()
    前端->>用户: 显示确认框
    用户->>前端: 点击确认
    前端->>前端: resume("confirmed")
    前端->>后端: 发送工具结果
    后端->>AI: 转发结果
    AI->>后端: "好的,已记录"
    后端-->>前端: 返回回复
    前端->>用户: 显示完成

踩坑记录

坑 1:Tool call is not waiting for human input

原因:没在 execute 里调用 human(),或者在错误状态下调用了 resume()

解决:确保 execute 里有 await human(),且只在 status.type === "requires-action" 时调用 resume()

坑 2:工具完成后消息无限发送

原因sendAutomaticallyWhen 对同一个工具调用重复返回 true

解决:用 useRef 记录已处理的 toolCallId

const processedToolCalls = useRef(new Set<string>());

sendAutomaticallyWhen: (options) => {
  // ... 找到完成的工具调用
  if (!processedToolCalls.current.has(toolCallId)) {
    processedToolCalls.current.add(toolCallId);
    return true;
  }
  return false;
}

坑 3:用错了 makeAssistantToolUI

makeAssistantToolUI 只渲染 UI,不会把工具定义发给后端。如果需要 AI 能调用,必须用 makeAssistantTool

总结

通过 assistant-ui 的前端工具机制,我们实现了:

  1. AI 能力不打折:AI 仍然可以决定何时调用工具
  2. 用户有控制权:敏感操作必须经过用户确认
  3. 体验很自然:确认框嵌入在对话流中,不突兀

这套方案已经在生产环境稳定运行,用户再也不用担心 AI "手滑"了 😄


技术栈:React + Rust + Tauri + assistant-ui

如果你也在做 AI 应用,需要人机协作的场景,希望这篇文章对你有帮助!

欢迎评论区交流 👇

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

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

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

至今已经过去了 20 年!

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

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

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

这个数字意味着什么?

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

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

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

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

1. 混沌时代

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

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

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

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

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

2. 横空出世

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

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

Write Less,Do More

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

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

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

看看这个对比:

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

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

差距一目了然!

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

3. 辉煌岁月

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

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

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

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

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

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

4. 影响深远

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

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

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

5. 价值动摇

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

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

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

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

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

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

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

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

6. 框架打击

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

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

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

7. 惨遭背叛

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

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

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

8. 瘦死骆驼

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

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

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

因为现实远比理想复杂:

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

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

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

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

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

9. 拥抱现代

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

前言

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

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


一、3个核心概念的定义

1.1 大模型应用(AI Application)

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

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

1.2 Agent(智能代理)

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

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

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

1.3 MCP(Model Context Protocol)

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

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

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


二、三者的包含关系

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

三、工作流程详解

3.1 初始化阶段

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

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

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

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

代码示例:

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

3.2 处理请求阶段

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

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

3.3 循环机制的关键

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

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

      const toolResults = [ ]

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

关键点:

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

四、MCP 的泛化调用设计

4.1 为什么需要泛化?

不泛化的方式(混乱):

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

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

泛化的方式(MCP):

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

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

4.2 泛化的实现原理

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

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

4.3 MCP 规范的约束

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

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

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

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

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

四、结尾

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

评论区欢迎讨论。

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

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

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

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

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

滚动触发动画示例

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

初始加载动画示例

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

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


🔧 核心原理概览

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

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

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


🧱 HTML 结构(简化版)

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

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

💡 类名与属性说明

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

🎨 CSS 动画定义

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

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

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

✨ 参数说明

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

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


🕵️ JavaScript:Intersection Observer 监听逻辑

为什么不用 scroll 事件?

传统方式:

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

现代方案:

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

完整监听逻辑

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

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

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

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

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

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

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

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

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

🎯 关键设计细节

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

📊 两种场景下的行为对比

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

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


💡 总结:这套方案的优势

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

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

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


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

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

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

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

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

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

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

01. 场景复现

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

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

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

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

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

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

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

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

解析:

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

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

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

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

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

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

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

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

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

✅ 优化方案:使用 Transform: Scale

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

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

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

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

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

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

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

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

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

最终代码清单

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

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

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

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

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

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

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

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

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

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

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

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

</html>

总结

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

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

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

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

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

2026年1月20日 17:22

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

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

VCR

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

第一:模式选择

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

第二:积木扩展

在这里插入图片描述

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

第三:代码解析

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

总结

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

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

2026年1月20日 17:12

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

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

  const array = [];

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

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

但如果改为Float64Array:

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

占用内存如下: image.png

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


一、什么是 Backing Store?

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

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

可以理解为:

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


二、为什么需要 Backing Store?

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

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

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

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


三、结构示意(简化)

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

四、V8 中 Backing Store 的生命周期

1️. 创建阶段

当你在 JS 层执行:

const buf = new ArrayBuffer(1024 * 1024);

V8 会在内部:

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

2. 使用阶段

访问 TypedArray 时,例如:

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

3️. 销毁阶段

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

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

五、JS Heap 和 Native Heap

V8 内存总体结构

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

  1. JS Heap(JavaScript 堆)

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

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

Native Heap 与 JS Heap 的关系

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

例子:

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

此时:

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

六、ArrayBuffer到底占不占用JS Heap

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

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

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

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

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

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

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

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

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

run();


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

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

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

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

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

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

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

具体参见 Chromium bug 1203451 讨论:

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

也就是说:

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

runtime chunk 到底是什么?

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

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

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

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

1. 准备极简项目结构

plaintext

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

2. 编写业务代码

javascript

运行

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

javascript

运行

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

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

javascript

运行

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

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

javascript

运行

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

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

4. 执行打包 & 查看输出

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

plaintext

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

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

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

javascript

运行

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

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

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

对比:禁用 runtime chunk 的效果

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

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

为什么要单独提取 runtime chunk?

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

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

用户浏览器缓存中:

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

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


总结

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

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

2026年1月20日 16:49

前言

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

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

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


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

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

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

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


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

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

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

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

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

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

codeBash

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

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

2. YAML 驱动的自动化告警

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

codeYaml

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

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

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

codeBash

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

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

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

codeBash

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

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

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


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

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

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

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

六、 结语

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

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

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


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

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

2026年1月20日 16:44

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

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


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

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

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

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

学习曲线对比

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

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

生态圈对比

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


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

Nuxt 3入门示例

项目初始化

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

创建页面和组件

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

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

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

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

Next.js入门示例

项目初始化

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

创建页面和组件

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

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

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

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

数据获取方式对比

Nuxt 3的数据获取

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

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

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

Next.js的数据获取

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

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

渲染策略对比

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

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

04 实际应用场景分析

何时选择Nuxt 3?

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

Nuxt 3优势场景示例

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

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

何时选择Next.js?

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

Next.js优势场景示例

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

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

05 开发体验与工具链对比

Nuxt 3的开发体验

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

Next.js的开发体验

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

06 性能与优化对比

性能特征

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

优化技巧对比

Nuxt 3优化示例

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

Next.js优化示例

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

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

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

07 新手选择建议

根据背景选择

  1. 完全零基础

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

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

根据项目类型选择

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

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

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

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

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

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

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


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

❌
❌