普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月3日掘金 前端

分不清apply,bind,call?看这篇文章就够了

作者 左夕
2026年3月3日 12:23

我们来深入地梳理一下函数、方法、对象,以及它们之间纠缠的根源——this。这不仅仅是记住几个概念,而是理解JavaScript(以及很多面向对象语言)在运行时,代码是如何被组织、调用和执行的。

首先,我们需要建立一个核心的、动态的世界观:代码是静态的文本,而对象是存在于内存中的、活的“事物”。 函数则是一段可以执行的、静态的代码和动态的执行环境的结合体。

1. 对象:数据的容器与行为的宿主

对象,从本质上讲,是在内存中开辟的一块区域,用来存放一组相关联的数据(我们称之为属性)和操作这些数据的行为(我们称之为方法)。

可以把对象想象成一个“小王国”。这个王国里有很多“资源”(属性),比如 name: 'Alice', age: 30。同时,这个王国也有一些“法令”或“工作流程”(方法),比如 sayHello()。当这个王国要执行 sayHello 这个工作流程时,它需要知道“我”是谁?它需要访问自己的资源 name,才能说出“你好,我是Alice”。

所以,对象存在的核心意义之一,就是为行为(函数)提供一个执行的上下文(Context)。这个“上下文”就是 this 最终指向的东西——即当前这个活跃的“小王国”本身。

2. 函数:独立的执行代码单元

函数,本质上也是一段可执行的代码。但它不像方法那样,生来就“属于”某个对象。函数是独立的。你可以把它想象成一个“万能工具”或一份“公开的蓝图”。

这份蓝图(函数体)本身描述了要做什么:function greet() { console.log('Hello') }。但在它真正被执行(被调用)之前,它不知道自己属于哪个“王国”。它是自由的、无上下文的。

当你在全局环境中调用它,它就暂时地在全局“王国”里执行。你可以把这个自由的函数,通过赋值的方式,“借给”一个对象,让它成为那个对象的一个属性。从这一刻起,它在这个对象上,就扮演了“方法”的角色。

3. 方法:函数在对象中的角色

所以,方法并不是一种特殊的函数,而是函数的一种“角色”或“状态”

当你通过一个对象去调用它拥有的函数属性时,比如 alice.sayHello(),这个函数就临时获得了 alice 这个对象作为它的执行上下文。此刻,它就不再是一个孤立的“万能工具”,而是为 alice 王国服务的、有归属的“工作流程”。

关键点来了:函数本身并没有变,变的是它被调用的方式。 同一个函数,完全可以一会儿作为独立函数被调用,一会儿作为某个对象的方法被调用。它的行为会因为调用方式的不同而产生巨大的差异,这个差异的核心,就是 this

4. this:动态的执行上下文指针

this 就是连接函数、方法和对象的那根“命脉”。它是一个在函数被调用时才确定下来的指针,指向调用该函数的那个对象(即当前执行的上下文)。

  • 当函数作为独立函数被调用时: greet()。在非严格模式下,this 会指向全局对象(浏览器里的 window)。这就像这个函数在全局王国里临时客串了一下,但它找不到自己的属性,很容易出错。在严格模式下,this 则是 undefined,明确告诉你,它没有合法的上下文。

  • 当函数作为对象的方法被调用时: alice.sayHello()this 就会指向 alice 这个对象。sayHello 函数体里的 this.name,就自然而然地取到了 alice.name 的值。这就是“方法”能够操作“所属对象”数据的根本原理。

  • 当函数作为构造函数被调用时: new Person('Bob')。一个全新的、空的对象会在内存中被创建出来,然后这个函数被调用,并且函数体内的 this 会指向这个即将被创建出来的新对象。函数通过 this.name = name 这样的方式,给这个新对象初始化属性。最后,这个新对象被返回。这个过程清晰地展示了函数如何作为“蓝图”来创造新的对象“王国”。

  • 通过 .call.apply.bind 强制指定 this 这是最灵活的方式。你可以强行把一个函数“塞”进任何一个对象里去执行。比如 greet.call(alice),就是把 greet 这个独立函数,临时借给 alice 对象,让它以 alice 为上下文去执行。这完美地证明了函数和方法的本质关系——函数只是一个代码块,而通过控制 this,我们可以动态地决定它“属于”谁。

5. 内在关联:

把这些点串联起来,我们就能看到一个完整的逻辑链条:

  1. 存储与组织: 我们用对象来组织数据和逻辑。对象的属性存储数据,而它的方法则定义了对这些数据的操作。
  2. 定义与复用: 这些“方法”本质上就是函数。我们在对象外部或内部定义函数,目的是为了复用代码逻辑。函数本身不依赖于任何对象。
  3. 绑定与执行: 当程序运行时,我们需要把函数和对象动态地结合起来。这个结合点就是函数调用。调用方式(独立调用、方法调用、构造调用、间接调用)决定了函数体内的 this 指针指向哪个对象。
  4. 上下文的意义: this 指向的对象,为函数的执行提供了“上下文”数据。方法之所以能操作其所属对象的属性,就是因为 this 正确地指向了那个对象。

6. bind、apply、call:手动控制this

理解了this是动态绑定的之后,我们就掌握了一个强大的能力——我们可以主动干预这个绑定过程。JavaScript提供了三个方法:callapplybind,它们都存在于函数对象的原型上(Function.prototype),这意味着每一个函数都天生拥有这三个方法。

这三个方法的核心作用完全一致:让你手动指定函数执行时的this指向。但它们在使用方式和执行时机上有细微的差别,理解这些差别能让你在不同的场景下灵活运用。

6.1 执行并传参:call 与 apply

callapply都是立即执行函数的方法。它们的作用完全一样,唯一的区别在于传参方式不同

call:参数列表形式

function greet(greeting, punctuation) {
    console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Alice' };

// call 接收参数列表,第一个参数是 this 的指向,后面的参数依次传给函数
greet.call(person, 'Hello', '!'); 
// 输出:Hello, Alice!

apply:参数数组形式

// apply 接收两个参数:第一个是 this 的指向,第二个是参数数组
greet.apply(person, ['Hi', '!!']); 
// 输出:Hi, Alice!!

为什么需要两种形式?

这源于JavaScript的灵活性。call适用于你明确知道函数需要几个参数的情况,写起来更直观。apply的强大之处在于,它可以配合数组使用,特别是当参数数量不确定时,或者你有一个现成的参数数组时。

一个经典的例子:求数组中的最大值

const numbers = [5, 6, 2, 3, 7];

// Math.max 接收参数列表,不接收数组
const max1 = Math.max(5, 6, 2, 3, 7); // 正常用法

// 但如果我们有一个数组呢?用 apply 可以完美解决
const max2 = Math.max.apply(null, numbers); 
// 这里第一个参数传 null,是因为 Math.max 不依赖 this
console.log(max2); // 7

// 现在也可以用扩展运算符实现类似效果
const max3 = Math.max(...numbers);

6.2 绑定并等待:bind

bind和前两者有本质区别。bind不是立即执行函数,而是返回一个新的函数,这个新函数的this被永久地绑定到了你指定的对象上。

function greet(greeting, punctuation) {
    console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Alice' };

// bind 返回一个新函数,this 被绑定到 person
const greetAlice = greet.bind(person, 'Hello');

// 现在可以在任何时候调用这个新函数
greetAlice('!'); // 输出:Hello, Alice!
greetAlice('!!'); // 输出:Hello, Alice!!

注意上面的例子中,我们不仅绑定了this,还预置了第一个参数'Hello'。这叫做**柯里化(Currying)**的一种形式——固定某些参数,生成一个参数更少的新函数。

bind的核心特征:

  1. 永久绑定:一旦用bind绑定了this,即使对这个新函数再次使用callapplybind,也无法改变它的this指向。这被称为“硬绑定”。
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

const greetPerson1 = greet.bind(person1);

// 尝试用 call 改变 this,不会生效
greetPerson1.call(person2, 'Hi', '!'); // 仍然输出:Hi, Alice!
  1. 延迟执行bind生成的函数可以留到需要的时候再调用,这在实际开发中非常有用,特别是在事件处理、定时器、回调函数等场景中。

6.3 实际应用场景:为什么要手动控制this?

理解了这三个工具怎么用,更重要的是理解什么时候需要用它们

场景一:丢失的this(最经典的场景)

const person = {
    name: 'Alice',
    greet: function() {
        console.log('Hello, ' + this.name);
    }
};

// 正常调用
person.greet(); // Hello, Alice

// 把方法赋值给一个变量
const greetFunction = person.greet;
greetFunction(); // Hello, undefined (或者报错)

为什么会这样?因为greetFunction现在是独立函数调用,this指向了全局对象。解决方案就是用bind固定this

const boundGreet = person.greet.bind(person);
boundGreet(); // Hello, Alice

在React类组件中,你经常看到this.handleClick = this.handleClick.bind(this),正是为了解决这个问题。

场景二:借用方法

有时候一个对象没有某个方法,但另一个对象有。我们可以借用过来用。

const alice = {
    name: 'Alice',
    friends: ['Bob', 'Charlie']
};

const bob = {
    name: 'Bob',
    friends: ['Alice', 'David']
};

// 定义一个通用的打印方法(当然也可以定义在原型上)
function printFriends() {
    this.friends.forEach(friend => {
        console.log(this.name + ' knows ' + friend);
    });
}

// 让 alice 借用这个方法
printFriends.call(alice);
// Alice knows Bob
// Alice knows Charlie

// 让 bob 借用
printFriends.call(bob);
// Bob knows Alice
// Bob knows David

场景三:类数组对象转数组

DOM操作返回的NodeList、函数内部的arguments都是类数组对象,它们有length属性和索引,但没有数组的方法。我们可以借用数组的方法。

function listArguments() {
    // arguments 是类数组,没有 forEach 方法
    // 但我们可以借用数组的 forEach
    Array.prototype.forEach.call(arguments, arg => {
        console.log(arg);
    });
    
    // 或者转成真正的数组
    const argsArray = Array.prototype.slice.call(arguments);
    // 现代写法:const argsArray = Array.from(arguments);
}

listArguments(1, 2, 3); // 输出 1, 2, 3

场景四:保存上下文(配合箭头函数)

在异步回调中,我们常常需要保存外层的this。在箭头函数出现之前,常用的手法是var self = this,然后用bind

function Timer() {
    this.seconds = 0;
    
    // 传统写法:用 bind 绑定
    setInterval(function() {
        this.seconds++;
        console.log(this.seconds);
    }.bind(this), 1000);
    
    // 箭头函数写法(箭头函数没有自己的 this,会继承外层)
    setInterval(() => {
        this.seconds++;
        console.log(this.seconds);
    }, 1000);
}

6.4 总结三者的核心区别

方法 执行时机 返回值 参数传递 典型用途
call 立即执行 函数执行结果 参数列表 需要立即调用,参数数量明确
apply 立即执行 函数执行结果 参数数组 需要立即调用,参数是数组形式
bind 延迟执行 新函数 参数列表(可分批) 需要固定this以便后续调用

所以 : 想立即调用并指定this,用callapply(区别在于传参方式),想创建一个新函数,永久绑定this到某个对象,以便以后调用,用bind

【节点】[MetalReflectance节点]原理解析与实际应用

作者 SmalBox
2026年3月3日 11:37

【Unity Shader Graph 使用与特效实现】专栏-直达

在基于物理的渲染(PBR)工作流程中,准确模拟金属材质的光学特性是创建逼真视觉效果的关键。Unity URP的Shader Graph提供了Metal Reflectance节点,这是一个专门用于获取真实世界金属反射率值的工具节点。该节点封装了多种常见金属在可见光谱范围内的反射率数据,使着色器艺术家能够快速应用基于物理测量的准确金属属性,而无需手动查找或输入这些数值。

Metal Reflectance节点的核心价值在于其科学准确性。它提供的反射率值来源于对真实金属材料的光学测量,这些数据经过标准化处理,符合PBR渲染的理论基础。通过使用这些经过验证的物理数据,开发者可以确保他们的金属材质在不同光照条件下都能表现出正确的视觉特性,包括颜色、亮度和菲涅尔效应。

该节点特别适合需要快速原型制作和保持视觉一致性的项目。无论是创建武器系统、车辆、建筑结构还是科幻环境,只要涉及金属材质的表现,Metal Reflectance节点都能提供可靠的基础数值。节点支持从铁到铂金等多种常见金属,覆盖了从工业到珠宝等各种应用场景的需求。

描述

Metal Reflectance节点的主要功能是返回基于物理测量的金属反射率值。反射率是描述表面对光线反射能力的物理量,在PBR渲染中,它直接影响材质的视觉表现,特别是金属材质的高光颜色和强度。该节点输出的Vector 3值分别对应红色、绿色和蓝色通道的反射率系数,这些系数在0到1的范围内,表示该金属对不同波长光线的反射能力。

在Shader Graph中,Metal Reflectance节点通过Material下拉选单参数提供对多种预设金属材质的选择。每个选项对应一种特定金属的反射率特性,这些特性基于真实世界的物理测量数据。例如,黄金选项提供的反射率值体现了黄金特有的暖黄色调,而银则提供了冷色调的高反射率值。

该节点在PBR工作流程中的使用方式取决于所选的工作流程类型。当使用Specular Workflow时,应将Metal Reflectance节点的输出连接到PBR主节点的Specular输入端口。这种工作流程分离了镜面反射颜色和漫反射颜色,适合需要精确控制材质高光颜色的情况。相反,当使用Metallic Workflow时,节点的输出应连接到Albedo端口,因为在这种工作流程中,金属材质的反射颜色直接由反照率控制,而金属度贴图则用于区分金属和非金属区域。

理解这两种工作流程的区别对于正确使用Metal Reflectance节点至关重要。在Specular Workflow中,非金属材质也可以有彩色的高光,而金属材质的高光颜色则由其反射率决定。在Metallic Workflow中,非金属材质的反照率定义其漫反射颜色,而金属材质的反照率则定义其反射颜色。Metal Reflectance节点提供的值专门用于定义金属部分的反射特性,确保物理准确性。

物理基础

Metal Reflectance节点提供的值基于真实金属的光学特性。金属的反射率与其电子结构密切相关,不同金属由于电子能带结构的差异,对不同波长光线的吸收和反射特性也不同,这就导致了金属特有的颜色特征。例如,金对蓝色光谱的吸收较强,对红色和黄色光谱的反射较强,因此呈现金黄色。

这些物理测量通常是在标准照明条件下(如D65光源,代表日光),以接近垂直的入射角进行的。在实际渲染中,金属的反射率会随着观察角度的变化而变化,遵循菲涅尔方程。在掠射角(接近90度)时,几乎所有材质的反射率都会接近100%,而在垂直角度时,反射率则取决于材质本身的特性。Metal Reflectance节点提供的是在接近垂直角度下的反射率值,这是PBR材质的基础参数。

能量守恒

PBR渲染遵循能量守恒原则,即表面反射的光线能量不能超过接收到的光线能量。Metal Reflectance节点提供的值已经考虑了这一点,确保在物理合理的范围内。对于金属材质,反射率通常较高,但不会超过100%。同时,金属材质几乎不表现出次表面散射,因此其漫反射分量通常非常暗或接近黑色。

端口

Metal Reflectance节点的端口配置相对简单,仅包含一个输出端口,这反映了节点的专用性——它主要用于提供数据而非处理数据。

名称 方向 类型 绑定 描述
Out 输出 Vector 3 输出值

Out端口是Metal Reflectance节点的唯一输出,它提供一个三维向量,包含所选金属的反射率值。这个向量的三个分量分别对应红色、绿色和蓝色通道,取值范围在0到1之间。这些值表示在可见光谱范围内,该金属对相应颜色通道光线的反射能力。

输出值的精确度对于PBR渲染至关重要。即使是微小的差异也可能导致视觉上的明显变化,特别是在高动态范围(HDR)渲染和基于图像照明(IBL)的环境中。Metal Reflectance节点提供的值具有足够的精度,能够满足大多数实时渲染应用的需求。

在实际使用中,这个输出可以直接连接到PBR主节点的相应输入,也可以与其他节点组合使用,以创建更复杂的材质效果。例如,可以将Metal Reflectance节点的输出与纹理采样节点结合,通过在金属度贴图定义的金属区域使用物理准确的反射率值,而在非金属区域使用其他值,从而创建混合材质。

控件

Metal Reflectance节点提供了一个主要控件——Material下拉选单,用于选择所需的金属类型。这个控件决定了节点输出的反射率值,是节点功能的核心。

名称 类型 选项 描述
Material 下拉选单 Iron、Silver、Aluminium、Gold、Copper、Chromium、Nickel、Titanium、Cobalt、Platinum 选择要输出的材质值。

Material下拉选单包含了十种常见金属的选项,每种选项对应特定的反射率特性:

  • Iron(铁):铁是一种常见的工业金属,具有中等反射率和轻微的蓝色色调。其反射率值约为(0.560, 0.570, 0.580),表面对各颜色通道的反射较为均衡,但蓝色通道略高,赋予其微冷的视觉特性。铁材质适合用于机械零件、工具和结构元件。
  • Silver(银):银是具有最高反射率的金属之一,反射率值约为(0.972, 0.960, 0.915)。它对所有颜色通道都有很高的反射率,尤其是红色和绿色通道,这使其呈现出明亮的银白色。银材质常用于珠宝、镜面和装饰元素。
  • Aluminium(铝):铝是一种轻质金属,具有高反射率,值约为(0.913, 0.921, 0.925)。与银类似,它对各颜色通道的反射较为均衡,但整体反射率略低。铝材质广泛用于航空航天、汽车和包装行业。
  • Gold(金):金以其独特的暖黄色而闻名,反射率值约为(1.000, 0.766, 0.336)。它对红色通道的反射率极高,对绿色通道中等,对蓝色通道较低,这种不平衡的反射导致了其典型的金黄色。金材质常用于珠宝、装饰和高价值物品。
  • Copper(铜):铜具有明显的橙红色调,反射率值约为(0.955, 0.637, 0.538)。与金类似,它对红色通道的反射率远高于蓝色通道,但整体色调更偏橙红。铜材质适合用于电线、管道和装饰元素。
  • Chromium(铬):铬是一种高反射率金属,具有中性色调,反射率值约为(0.550, 0.556, 0.554)。它对各颜色通道的反射非常均衡,呈现出明亮的银灰色。铬常用于电镀、汽车零件和家电产品。
  • Nickel(镍):镍具有中等反射率和轻微的暖色调,反射率值约为(0.660, 0.609, 0.526)。它对红色通道的反射率高于蓝色通道,赋予其微暖的外观。镍材质常用于电镀和合金。
  • Titanium(钛):钛是一种强度高、重量轻的金属,反射率值约为(0.542, 0.497, 0.449)。它具有中等反射率和轻微的暖灰色调。钛材质常用于航空航天、医疗植入物和体育器材。
  • Cobalt(钴):钴具有中等反射率和中性色调,反射率值约为(0.662, 0.655, 0.634)。它对各颜色通道的反射较为均衡,呈现出中性的灰色。钴常用于合金和电池制造。
  • Platinum(铂):铂是一种贵金属,具有中等反射率和温暖的灰色调,反射率值约为(0.672, 0.637, 0.585)。它对红色通道的反射率略高于蓝色通道,赋予其微暖的外观。铂材质常用于珠宝和工业催化剂。

选择合适的金属类型对于创建视觉上准确的材质至关重要。每种金属都有其独特的光学特性,这些特性直接影响其在渲染中的表现。通过简单地从下拉选单中选择,开发者可以快速应用这些经过物理验证的值,而无需手动输入或查找参考资料。

生成的代码示例

当在Shader Graph中使用Metal Reflectance节点时,Unity会生成相应的HLSL代码,将这些反射率值嵌入到最终的着色器中。以下示例展示了选择不同金属材质时,节点可能生成的代码片段。

这些代码示例显示了Metal Reflectance节点如何将金属反射率值定义为三维向量。在实际的着色器编译过程中,这些值会被直接嵌入到生成的代码中,作为常量或内联值使用。

Iron

float3 _MetalReflectance_Out = float3(0.560, 0.570, 0.580);

铁生成的代码创建了一个三维向量,其分量值分别为0.560(红)、0.570(绿)和0.580(蓝)。这些值表明铁对蓝色光谱的反射略高于红色和绿色,赋予其轻微的冷色调。在实际渲染中,这种细微的差异在复杂照明环境下会变得更加明显。

Silver

float3 _MetalReflectance_Out = float3(0.972, 0.960, 0.915);

银的反射率值非常高,尤其是在红色和绿色通道,这表明银对暖色调光线的反射能力极强。这些高反射率值使银在渲染中呈现出明亮、闪耀的外观,特别是在高光区域。

Aluminium

float3 _MetalReflectance_Out = float3(0.913, 0.921, 0.925);

铝的反射率值在各颜色通道之间非常均衡,略偏向蓝色通道。这种均衡的反射特性使铝呈现出中性的银白色,与银相似但略微不那么明亮。

Gold

float3 _MetalReflectance_Out = float3(1.000, 0.766, 0.336);

金的代码显示了其典型的不平衡反射特性:红色通道反射率高达1.0,绿色通道为0.766,而蓝色通道仅为0.336。这种强烈的色彩偏差导致了金特有的暖黄色调,在渲染中非常醒目。

Copper

float3 _MetalReflectance_Out = float3(0.955, 0.637, 0.538);

铜的反射率模式与金类似,但对绿色和蓝色通道的反射率相对较高,这使它的色调比金更偏橙红。这种色彩特性使铜在渲染中呈现出温暖的橙红色外观。

Chromium

float3 _MetalReflectance_Out = float3(0.550, 0.556, 0.554);

铬的反射率值在各颜色通道之间几乎完全均衡,差异极小。这种高度均衡的反射使铬呈现出中性的银灰色,没有明显的色彩偏向。

Nickel

float3 _MetalReflectance_Out = float3(0.660, 0.609, 0.526);

镍的反射率显示出轻微的不平衡,红色通道最高,蓝色通道最低。这种差异赋予了镍微暖的视觉特性,在特定照明条件下尤为明显。

Titanium

float3 _MetalReflectance_Out = float3(0.542, 0.497, 0.449);

钛的反射率值相对较低,且显示出从红色到蓝色通道逐渐递减的趋势。这种特性使钛呈现出温暖的灰色外观,与其他更明亮的金属形成对比。

Cobalt

float3 _MetalReflectance_Out = float3(0.662, 0.655, 0.634);

钴的反射率值在各颜色通道之间较为均衡,略偏向红色通道。这种均衡性使钴呈现出中性的灰色,没有强烈的色彩特征。

Platinum

float3 _MetalReflectance_Out = float3(0.672, 0.637, 0.585);

铂的反射率值显示出从红色到蓝色通道递减的趋势,但不如钛明显。这种特性赋予铂微暖的灰色外观,在贵金属中独具特色。

理解这些生成的代码有助于开发者更深入地掌握Metal Reflectance节点的工作原理,并在需要时手动调整或扩展这些值。虽然节点提供了方便的预设,但在某些特殊情况下,直接使用这些数值进行自定义计算可能更为合适。

实际应用示例

Metal Reflectance节点在游戏开发和实时渲染中有广泛的应用。以下是一些典型的使用场景和示例,展示了如何充分利用这个节点的功能。

创建真实的金属材质

最基本的应用是创建各种金属表面。通过简单选择不同的金属类型,可以快速获得物理准确的反射率值:

  • 选择Iron创建机械零件和工具
  • 选择Gold和Silver制作珠宝和装饰品
  • 选择Aluminium和Titanium用于航空航天部件
  • 选择Copper和Brass用于管道和装饰元素

在这些应用中,Metal Reflectance节点确保了材质的基础反射特性符合物理规律,为后续的纹理细节和表面处理提供了准确的基础。

混合材质创作

在实际项目中,表面通常不是由单一材质组成的。Metal Reflectance节点可以与金属度贴图结合使用,创建包含金属和非金属区域的复杂表面:

  1. 使用Sample Texture 2D节点采样金属度贴图
  2. 将Metal Reflectance节点连接到PBR主节点的Albedo输入
  3. 使用金属度贴图控制哪些区域使用金属反射率,哪些区域使用其他反照率值

这种方法特别适合创建磨损表面,其中磨损区域露出底层金属,而非磨损区域则可能是油漆或其它非金属涂层。

动态材质变化

通过脚本控制Material参数,可以实现材质的动态变化效果:

  • 武器升级时从铁变为金
  • 腐蚀效果使金属表面从银变为氧化状态
  • 温度变化导致的金属颜色变化(需配合自定义函数)

虽然Metal Reflectance节点本身不提供动态变化的功能,但通过暴露Material参数给脚本,可以实现运行时材质特性的改变。

自定义金属合金

通过混合不同金属的反射率值,可以模拟合金材料:

  1. 使用多个Metal Reflectance节点,选择不同的金属类型
  2. 使用Lerp节点混合这些值
  3. 使用滑块控制混合比例

这种方法可以创建在预设选项中不存在的金属类型,扩展了节点的使用范围。

最佳实践和注意事项

为了充分利用Metal Reflectance节点,遵循一些最佳实践和注意事项是很重要的:

工作流程选择

  • 在Specular Workflow中,将节点输出连接到Specular输入
  • 在Metallic Workflow中,将节点输出连接到Albedo输入
  • 确保金属区域的漫反射颜色设置为黑色或接近黑色(在Metallic Workflow中)

性能考虑

  • Metal Reflectance节点本身性能开销极低,因为它只返回常量值
  • 在移动平台上,考虑使用更简单的金属反射率近似值以减少计算量
  • 避免每帧动态改变Metal Reflectance节点的材质类型,除非必要

物理准确性

  • 理解不同金属的反射率特性有助于创建更可信的材质
  • 注意环境光照对金属外观的影响——金属高度依赖环境反射
  • 考虑使用反射探头和基于图像的照明(IBL)来增强金属材质的真实感

局限性

  • Metal Reflectance节点提供的是理想条件下的反射率值,实际金属表面可能因氧化、污染或加工处理而有所不同
  • 节点不提供金属的粗糙度或表面纹理信息,这些需要通过其他节点添加
  • 对于非常特殊的金属或合金,可能需要手动输入反射率值

【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

Node.js 宣布重大调整,运行十年的规则要改了!

2026年3月3日 11:34

Node.js 目前的最新版是v25.7.0,每年 Node.js 都会有两次大的更新节奏,但其中的版本更新却有一个“潜规则”:偶数稳定,奇数跳过,虽然 Node 的更新频率很高,但最终大家安装的版本基本都是稳定的 LTS 版本,也就是偶数版本。

这让奇数版本的 Node.js 变得很尴尬。

而最近官方终于想通了,要对现在运行了 10 年的发布节奏做大调整!

一年只出一个精品

以前 Node.js 的规则挺让人头大的:偶数版(如 20, 22)是长期支持的“亲儿子”,奇数版(如 21, 23)则像是个临时的“试验田”,生命周期极短,大家基本都选择跳过。

现在从 2026 年(Node.js 27)开始,废弃这条规矩。

以后每年只在 4 月发布一个主版本,一年只出一个精品,而且每一个版本都会自动转为 LTS

2027 年为 27.0.0,2028 年为 28.0.0。

开发者终于不用再纠结哪个数字是稳定的,闭眼升级就完事了。

哪些没变

做了这么大的变更,所有的东西都要改吗?

答案是:并没有。对于以下内容,还是保持不变:

  • 长期支持期限保持不变:29 个月
  • 保留迁移窗口:长期支持版本之间的重叠仍然存在。
  • 质量标准不变:同样的测试方法,同样的 CITGM 流程,同样的安保流程
  • 规律的发布周期:四月发布新版本,十月进行 LTS 推广
  • V8 采用周期:Node.js 的最新版本仍将包含最多只有 6 个月左右的 V8 版本

为什么突然要改?

关于改动的原因,官方没有拐弯抹角,表现的很坦诚,主要有 2 个原因:

  • 用户不买账
  • 开发维护太累了

官方统计过,绝大多数的企业和开发者根本不碰奇数版本,大家都在用 LTS。没有 LTS 就等,等到出了 LTS 再安装。既然如此,何必浪费精力去维护那些没人用的中间版本呢?

Node.js 的核心代码主要靠社区的开发者在用“爱发电”,要同时维护 4-5 个版本的 Bug 和安全漏洞,压力实在太大。精简成一年一版,能大大减轻开发者的压力,有更多时间投入到研发中。

不盲目发布,留有缓冲

为了保证这一年一度的大版本足够稳,Node.js 官方加了一个 Alpha 阶段(每年 10 月到次年 3 月)。

对于维护开源项目的极客来说,可以在这个阶段跑 CI 测试,这个阶段允许引入破坏性更新,等到了 4 月正式发布,API 就基本定型进入稳定期了。

关键更新时间节点

Node.js 26: 依然走老路子(偶数版,LTS)。

Node.js26 依然遵循旧的发布模式,在 2026 年 10 月进入 LTS,维护到 2029 年 4 月;是旧模式的最后一个版本。

Node.js 27: 它是新时代的开端,2026 年底开启 Alpha2027 年 4 月发布,10 月直接转 LTS

等到明年,我们将正式迎来 Node.js 27.0.0

总结一下

总的来说,Node.js 这次调整,变得更像一个“成年人”了。不再盲目追求更新频率,而是开始追求更稳健、更可预测的生命周期。

对于我们开发者来说,这是一个好消息,不用再出现版本选择困难症了。

博客地址:Evolving the Node.js Release Schedule

从微信小程序 data-id 到 React 列表性能优化:少用闭包,多用 data-*

作者 兆子龙
2026年3月3日 11:32

从微信小程序 data-id 到 React 列表性能优化:少用闭包,多用 data-*

以小程序里常见的 data-* 传参为引子,讲 React 列表里「闭包 + map」对 memo/虚拟化的影响,以及用 data-* 单函数的优化写法。


一、从微信小程序 data-id 说起

写微信小程序时,列表项点击通常不会给每个 item 绑一个闭包,而是用 data-* 把 id 挂在节点上,在一个事件处理函数里从 event.currentTarget.dataset 取出来:

// 小程序 WXML 常见写法
<block wx:for="{{items}}" wx:key="id">
  <view data-id="{{item.id}}" bindtap="onItemTap">{{item.name}}</view>
</block>
// JS: 一个 onItemTap,从 event.currentTarget.dataset.id 取 id

这样做的原因之一是小程序端对「同一函数引用」更友好,列表更新时不会因为每项都绑了新函数而产生多余开销。
回到 React,我们却经常在列表里写「每个 item 一个闭包」——写法简单,但在大列表或配合 memo、虚拟化时,就会暴露出性能与优化难度问题。下面先说常见写法的问题,再给出与小程序思路一致的替代方案。


二、React 里的常见写法:.map() 中的闭包

假设你在渲染一个列表,每项可点击并需要把 item.id 传给处理函数:

{items.map((item) => (
  <button key={item.id} onClick={() => handleClick(item.id)}>
    {item.name}
  </button>
))}

这种方式简洁、好写,效果也没问题:每次点击都能拿到正确的 item.id
但背后有一个事实:每次组件渲染时,你都在为列表中的每一项创建一个新的函数——一个捕获了当前 item.id 的闭包。在大多数小列表场景下,这不会有明显影响;一旦列表变长、或你开始做「减少重渲染」的优化,这种写法就会成为障碍。


三、闭包的潜在弊端

闭包是 JavaScript 和 React 的核心概念,但在 .map() 里为每项创建一个新函数 可能带来这些问题:

1. 破坏 memo / useCallback 与虚拟化

  • 若子组件用 React.memo 包裹,或父组件用 useCallback 把回调传给子组件,优化依赖的是函数引用稳定。而 onClick={() => handleClick(item.id)} 在每次父组件渲染时都会生成新的函数引用,子组件会认为 props 变了,于是本可避免的重渲染会发生,记忆化就失效了。
  • 若使用虚拟列表(如 react-window、react-virtualized),只渲染可见项,同样依赖「回调引用稳定」或至少「不因列表数据引用变就全量更新」。每项一个闭包会导致每次父组件渲染时,所有可见项的 onClick 都是新引用,虚拟化的收益被削弱。

2. 大列表下优化难度增加

当列表很长、交互频繁时,最小化重渲染变得很重要。内联闭包让「一个列表共用一个事件处理函数」变得困难,你很难在保持可读性的前提下,既用闭包又配合 memo/虚拟化做细粒度优化。


四、替代方案:用 data-* 单函数,与小程序殊途同归

与其为每个 item 创建一个闭包,不如像小程序那样:把标识(如 id)放在 DOM 的 data 属性上,只写一个事件处理函数,在函数里从 event.currentTarget.dataset 读取。

// 单一事件处理函数,引用稳定
function handleClick(e) {
  const id = e.currentTarget.dataset.id;
  console.log("Clicked item:", id);
  // 后续用 id 做请求、跳转等
}

{items.map((item) => (
  <button key={item.id} data-id={item.id} onClick={handleClick}>
    {item.name}
  </button>
))}

优势

  • 单一函数引用 → 父组件重渲染时,handleClick 不变(若用 function 声明或配合 useCallback 无依赖,引用更稳定),React.memouseCallback 能真正生效,子组件不会因为「回调换了」而重渲染。
  • 大列表、虚拟列表 下,事件逻辑集中在一个函数里,更容易配合虚拟化做性能优化。
  • 事件逻辑集中,代码更清晰;从「每个 item 绑一个闭包」变成「一个 handleClick + data-id」,和小程序的 data-* 用法一致,跨端经验可以复用。

注意:data-id 在 DOM 上会变成 data-id(小写);在 React 里写 data-id={item.id},通过 e.currentTarget.dataset.id 读取即可。若需要传复杂数据,可只传 id,在 handler 里用 id 从 state/context/缓存中取详情,避免在 DOM 上挂大对象。


五、总结

  • 小程序 里常用 data-* 把 id 绑在节点上,用一个事件处理函数从 dataset 取 id,避免为每项创建新回调。
  • React 里在 .map() 中写 onClick={() => handleClick(item.id)} 会在每次渲染时为每项创建新闭包,容易破坏 React.memo、useCallback 和虚拟列表的优化,大列表下优化难度增加。
  • 替代方案:用 data-id(或其它 data-*)把 id 挂在 DOM 上,只写一个 handleClick(e),在内部用 e.currentTarget.dataset.id 取 id;单一函数引用,便于 memo 与虚拟化,事件逻辑更集中,与小程序写法一致。

若对你有用,欢迎点赞、收藏;你们在 React 或小程序里若有类似的列表点击优化实践,也欢迎在评论区分享。

OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用

2026年3月3日 11:30

本文由体验技术团队kagol原创。

前言

AI Agent 时代,人们已经不满足只是与 AI 进行问答交互,而是希望 AI 能直接帮人干活。
目前 AI 帮人干活的场景越来越丰富,最常见的就是 AI 帮人写代码、做视频、做 PPT、做设计稿。
你有没有想过 AI 能帮人操作网页?
这就是 OpenTiny NEXT-SDK 做的事情。

1 简介

OpenTiny NEXT‑SDK 是一套面向前端智能应用的开发工具包,核心是基于 MCP(Model Context Protocol) 协议,让前端应用快速接入 AI Agent,实现前端界面可被智能体直接操控的能力。

OpenTiny NEXT‑SDK 可以帮助开发者:

  • 把普通前端应用快速改造为 MCP Server,对外暴露界面操作能力
  • 让 AI Agent(WebAgent)通过标准 MCP 协议读取界面、调用功能、执行操作
  • 快速集成 AI 对话组件(如 TinyRobot),构建智能交互前端

2 项目优势

NEXT‑SDK 是基于 MCP 协议实现,将 MCP 的能力扩展到了 Web 端,让 Web 应用也能被 AI 操控,以下是项目优势:

  • 扩大 MCP 工具范围:为 Agent 智能体提供更多的 MCP 工具,实现当前现有的本地/云服务 MCP 工具所不具备的能力,即操控前端应用的能力。这种能力比 RPA 方案(Browser Use / Computer Use)更快、更准、更经济
  • 完全兼容 MCP 生态:所有的前端应用都采用标准的 MCP 协议声明 MCP Server,并且基于标准的 MCP 通讯方式进行连接,比如 Streamable HTTP,意味着能完全融入现有的 MCP 生态,兼容现有乃至未来的 MCP Host 应用
  • 支持智能体交互范式:当前的前端应用主要还是人机交互,即人手动操作前端界面上的 UI 组件。引入 OpenTiny NEXT-SDK 之后,Agent 智能体可以借助 MCP 工具读取前端界面的信息、调用前端界面的功能,配合生成式 UI 实现新的智能体交互范式
  • 多样的前端智能化方案:不仅支持 Web 应用的前端智能化改造,还全面覆盖 AI 应用(对话框)的多端部署场景——无论是浏览器扩展、Web 页面集成,还是各终端内置的 AI 助手,均可直接或间接调用前端应用中的 MCP 工具

3 演示动画

我们一起来看一个演示动画,直观感受下 NEXT-SDK 的能力吧!

1.gif

接入 NEXT-SDK 的前端应用,右下角会出现一个机器人图标,点击这个图标会从侧边弹出 AI 对话框,我们可以使用自然语言与 AI 对话,让 AI 帮我们操作前端应用。

比如我们可以输入以下内容:

帮我创建以下用户,用户信息如下:
邮箱:zhangsan@sina.com
密码:Abc123456
用户名:zhangsan

这时 AI 会调用页面中定义的名为 add-user 的 MCP 工具,帮我们创建 zhangsan 这个用户。

我们提供了一个 Playground 代码演练场,你可以在线体验 NEXT-SDK 的能力。

NEXT-SDK Playground:playground.opentiny.design/next-sdk

4 快速接入

使用 OpenTiny NEXT-SDK,只需要以下四步,就可以把你的前端应用变成智能应用。

第一步:安装依赖

npm install @opentiny/next-sdk

第二步:创建 MCP Client

在 Web 应用的主入口(比如:Vue 项目的 App.vue 文件)定义 WebMcpClient。

import { onMounted, provide } from 'vue'
import { WebMcpClient, createMessageChannelPairTransport } from '@opentiny/next-sdk'

onMounted(async () => {
// 创建通信通道
const [serverTransport, clientTransport] = createMessageChannelPairTransport()
provide('serverTransport', serverTransport)

// 创建 MCP Client
const client = new WebMcpClient()
await client.connect(clientTransport)
// 这个 sessionId 是 Web 应用与 WebAgent 服务建立连接后,由 WebAgent 服务生成的,用来唯一标识被操控的 Web 应用(被控端)
const { sessionId } = await client.connect({
agent: true,
url: 'https://agent.opentiny.design/api/v1/webmcp-trial/mcp'
})
})

第三步:创建 MCP Server

在 Web 应用的子页面(比如:views/page1.vue)中定义 WebMcpServer,每个页面可以定义自己的 WebMcpServer,页面切换时,MCP Client 会与当前页面的 MCP Server 建立连接,并丢弃与之前页面的连接。

import { onMounted, inject } from 'vue'
import { WebMcpServer, z } from '@opentiny/next-sdk'

onMounted(async () => {
const serverTransport = inject('serverTransport')
// 创建 MCP Server
const server = new WebMcpServer({
name: 'mcp-server-page1',
version: '1.0.0'
})

// 定义 MCP 工具
server.registerTool(
'demo-tool',
{
title: '演示工具',
description: '一个简单工具',
inputSchema: { foo: z.string() }
},
async (params) => {
console.log('params:', params)
return { content: [{ type: 'text', text: \`收到: \${params.foo}\` }] }
}
)

await server.connect(serverTransport)
})

完成!现在你的前端应用已经变成智能应用,可以被 AI 操控了,你可以通过各类 MCP Host 来操控智能应用。

第四步:添加 AI 遥控器

我们提供了一个开箱即用的 AI 对话框组件,支持 PC 端和移动端,就像一个遥控器,可以通过对话方式操控你的前端应用。

安装遥控器组件:

npm install @opentiny/next-remoter

在 Vue 项目中使用:

<script setup lang="ts">
import { TinyRemoter } from '@opentiny/next-remoter'
import '@opentiny/next-remoter/dist/style.css'

// 使用第二步获取的 sessionId
const sessionId = 'your-session-id'
</script>
<template>
  <tiny-remoter 
    :session-id="sessionId" 
    title="我的智能助手"
  />
</template>

遥控器会在你的应用右下角显示一个图标,悬浮后可以选择:

  • 弹出 AI 对话框:在应用侧边打开 AI 对话界面
  • 显示二维码:手机扫码后打开移动端遥控器

不管是 PC 端还是移动端,都可以通过自然语言对话的方式让 AI 帮你操作应用,极大提升工作效率!

如果你想了解更多 NEXT-SDK 的用法,请参考 NEXT-SDK 官网文档:docs.opentiny.design/next-sdk

5 立即行动

在 AI 技术快速迭代的今天,前端智能化不再是“高端需求”,而是提升产品竞争力、提升操作效率的核心能力和必选项。

OpenTiny NEXT-SDK 让前端 AI 集成,从“复杂踩坑”到“5分钟上手”,让你的应用瞬间拥有 AI 能力,领跑行业智能化创新!

立即行动,解锁前端智能化新可能:

  • 执行 npm install @opentiny/next-sdk 安装 OpenTiny NEXT-SDK,5分钟上手实操,快速体验 AI 操控效果
  • 前往 OpenTiny NEXT-SDK 官网:opentiny.design/next-sdk,查看详细的项目介绍、API 文档和进阶用法
  • 访问 OpenTiny NEXT-SDK 代码演练场:playground.opentiny.design/next-sdk,在线体验 AI 自动操作前端应用
  • 外部添加 OpenTiny 微信小助手:opentiny-official,加入 OpenTiny 技术交流群,获取一对一集成指导,解决实操难题,与同行交流 AI 前端集成经验

如果你有任何问题,欢迎在评论区留言交流!

编程常用模式集合

2026年3月3日 11:24

编程常用模式是指在软件设计中反复出现、经过验证的解决方案。它们不是现成的代码库,而是解决特定问题的设计模板。掌握这些模式可以让你写出更优雅、可维护、可扩展的代码。

📚 模式的三大分类

根据 GoF(Gang of Four,四人组)的经典分类,设计模式分为三大类:

类别 作用 典型模式
创建型模式 处理对象创建机制 单例、工厂、建造者
结构型模式 处理类/对象的组合 适配器、装饰器、代理
行为型模式 处理对象间的通信 观察者、策略、职责链

🏭 创建型模式 (Creational Patterns)

1. 单例模式 (Singleton)

确保一个类只有一个实例,并提供一个全局访问点。

// TypeScript 实现
class ConfigManager {
  private static instance: ConfigManager
  private settings: Map<string, string> = new Map()
  
  private constructor() {} // 私有构造函数,防止外部 new
  
  public static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager()
    }
    return ConfigManager.instance
  }
  
  public set(key: string, value: string): void {
    this.settings.set(key, value)
  }
  
  public get(key: string): string | undefined {
    return this.settings.get(key)
  }
}

// 使用
const config1 = ConfigManager.getInstance()
const config2 = ConfigManager.getInstance()
config1.set('theme', 'dark')
console.log(config2.get('theme')) // 'dark' - 同一个实例

// Vue/Pinia 中的单例
// store 实际上是单例模式的应用
const useUserStore = defineStore('user', {
  state: () => ({ name: 'John' })
})

2. 工厂模式 (Factory Method)

定义一个创建对象的接口,让子类决定实例化哪个类。

// 简单工厂
interface Button {
  render(): void
  onClick(fn: () => void): void
}

class WindowsButton implements Button {
  render() { console.log('渲染 Windows 风格按钮') }
  onClick(fn: () => void) { console.log('Windows 点击事件') }
}

class MacButton implements Button {
  render() { console.log('渲染 Mac 风格按钮') }
  onClick(fn: () => void) { console.log('Mac 点击事件') }
}

class ButtonFactory {
  static createButton(os: 'windows' | 'mac'): Button {
    switch (os) {
      case 'windows': return new WindowsButton()
      case 'mac': return new MacButton()
      default: throw new Error('不支持的操作系统')
    }
  }
}

// 使用
const button = ButtonFactory.createButton('windows')
button.render()

// Vue 中的应用:渲染函数和组件
const ButtonComponent = {
  props: ['type'],
  render() {
    switch (this.type) {
      case 'primary': return h(PrimaryButton)
      case 'danger': return h(DangerButton)
      default: return h(DefaultButton)
    }
  }
}

3. 建造者模式 (Builder)

将一个复杂对象的构建过程与其表示分离。

// 建造者模式
class HttpRequest {
  private url: string = ''
  private method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET'
  private headers: Record<string, string> = {}
  private body?: any
  private timeout: number = 5000
  
  // 私有构造函数,只能通过 Builder 创建
  private constructor() {}
  
  static get Builder() {
    class HttpRequestBuilder {
      private request: HttpRequest
      
      constructor() {
        this.request = new HttpRequest()
      }
      
      setUrl(url: string): this {
        this.request.url = url
        return this
      }
      
      setMethod(method: HttpRequest['method']): this {
        this.request.method = method
        return this
      }
      
      addHeader(key: string, value: string): this {
        this.request.headers[key] = value
        return this
      }
      
      setBody(body: any): this {
        this.request.body = body
        return this
      }
      
      setTimeout(timeout: number): this {
        this.request.timeout = timeout
        return this
      }
      
      build(): HttpRequest {
        // 可以在这里添加验证逻辑
        if (!this.request.url) {
          throw new Error('URL 是必填项')
        }
        return this.request
      }
    }
    return HttpRequestBuilder
  }
}

// 使用
const request = new HttpRequest.Builder()
  .setUrl('/api/users')
  .setMethod('POST')
  .addHeader('Content-Type', 'application/json')
  .setBody({ name: 'John' })
  .setTimeout(10000)
  .build()

🏗️ 结构型模式 (Structural Patterns)

4. 适配器模式 (Adapter)

将一个类的接口转换成客户端期望的另一个接口。

// 旧系统 API
class OldPaymentSystem {
  processPaymentInDollars(amount: number): void {
    console.log(`处理 ${amount} 美元支付`)
  }
}

// 新系统期望的接口
interface NewPaymentProcessor {
  pay(amountInCents: number): void
}

// 适配器
class PaymentAdapter implements NewPaymentProcessor {
  constructor(private oldSystem: OldPaymentSystem) {}
  
  pay(amountInCents: number): void {
    // 转换:分 -> 美元
    const dollars = amountInCents / 100
    this.oldSystem.processPaymentInDollars(dollars)
  }
}

// 使用
const oldSystem = new OldPaymentSystem()
const adapted = new PaymentAdapter(oldSystem)
adapted.pay(1299) // "处理 12.99 美元支付"

// Vue 中的应用:适配不同格式的数据
function adaptUserData(apiData: any): User {
  return {
    fullName: `${apiData.first_name} ${apiData.last_name}`,
    age: apiData.age,
    email: apiData.email_address
  }
}

5. 装饰器模式 (Decorator)

动态地给对象添加额外的职责。

// 装饰器模式
interface Coffee {
  cost(): number
  description(): string
}

class SimpleCoffee implements Coffee {
  cost(): number { return 10 }
  description(): string { return '普通咖啡' }
}

// 装饰器基类
abstract class CoffeeDecorator implements Coffee {
  constructor(protected coffee: Coffee) {}
  
  abstract cost(): number
  abstract description(): string
}

// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 2
  }
  
  description(): string {
    return `${this.coffee.description()} + 牛奶`
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 1
  }
  
  description(): string {
    return `${this.coffee.description()} + 糖`
  }
}

// 使用
let coffee: Coffee = new SimpleCoffee()
coffee = new MilkDecorator(coffee)
coffee = new SugarDecorator(coffee)

console.log(coffee.description()) // "普通咖啡 + 牛奶 + 糖"
console.log(coffee.cost()) // 13

// TypeScript 中的装饰器(提案阶段)
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value
  descriptor.value = function(...args: any[]) {
    console.log(`调用 ${propertyKey} 参数:`, args)
    return original.apply(this, args)
  }
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b
  }
}

6. 代理模式 (Proxy)

为另一个对象提供一个替身或占位符以控制对这个对象的访问。

// 代理模式
interface Image {
  display(): void
}

class RealImage implements Image {
  constructor(private filename: string) {
    this.loadFromDisk()
  }
  
  private loadFromDisk(): void {
    console.log(`加载图片: ${this.filename}`)
  }
  
  display(): void {
    console.log(`显示图片: ${this.filename}`)
  }
}

class ImageProxy implements Image {
  private realImage: RealImage | null = null
  
  constructor(private filename: string) {}
  
  display(): void {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename)
    }
    this.realImage.display()
  }
}

// 使用
const image = new ImageProxy('photo.jpg')
// 图片不会立即加载,只有在 display 时才加载
image.display() // 加载并显示
image.display() // 只显示(已加载过)

// JavaScript 原生 Proxy
const handler = {
  get: function(target: any, prop: string) {
    console.log(`访问属性: ${prop}`)
    return prop in target ? target[prop] : '默认值'
  }
}

const user = new Proxy({ name: 'John' }, handler)
console.log(user.name) // "访问属性: name" + "John"
console.log(user.age)  // "访问属性: age" + "默认值"

🔄 行为型模式 (Behavioral Patterns)

7. 观察者模式 (Observer)

定义对象间的一对多依赖关系,当一个对象改变状态时,所有依赖者都会收到通知。

// 观察者模式
interface Observer<T> {
  update(data: T): void
}

class Subject<T> {
  private observers: Observer<T>[] = []
  
  attach(observer: Observer<T>): void {
    this.observers.push(observer)
  }
  
  detach(observer: Observer<T>): void {
    const index = this.observers.indexOf(observer)
    if (index !== -1) {
      this.observers.splice(index, 1)
    }
  }
  
  notify(data: T): void {
    this.observers.forEach(observer => observer.update(data))
  }
}

// 具体观察者
class Logger implements Observer<string> {
  update(data: string): void {
    console.log(`日志记录: ${data}`)
  }
}

class EmailSender implements Observer<string> {
  update(data: string): void {
    console.log(`发送邮件: ${data}`)
  }
}

// 使用
const subject = new Subject<string>()
subject.attach(new Logger())
subject.attach(new EmailSender())
subject.notify('用户已注册')

// Vue 中的应用:响应式系统
// Vue 的 ref 和 reactive 就是观察者模式的实现
const state = ref({ count: 0 })
watchEffect(() => {
  console.log(`状态变化: ${state.value.count}`)
})

// EventBus 也是观察者模式
const eventBus = new EventEmitter()
eventBus.on('user-login', (user) => console.log(user))

8. 策略模式 (Strategy)

定义一系列算法,把它们封装起来,并使它们可以互相替换。

// 策略模式
interface PaymentStrategy {
  pay(amount: number): void
}

class CreditCardPayment implements PaymentStrategy {
  constructor(private cardNumber: string) {}
  
  pay(amount: number): void {
    console.log(`使用信用卡 ${this.cardNumber.slice(-4)} 支付 ${amount} 元`)
  }
}

class WeChatPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`使用微信支付 ${amount} 元`)
  }
}

class AlipayPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`使用支付宝支付 ${amount} 元`)
  }
}

class ShoppingCart {
  private amount = 0
  private paymentStrategy: PaymentStrategy | null = null
  
  setPaymentStrategy(strategy: PaymentStrategy): void {
    this.paymentStrategy = strategy
  }
  
  addItem(price: number): void {
    this.amount += price
  }
  
  checkout(): void {
    if (!this.paymentStrategy) {
      throw new Error('请先选择支付方式')
    }
    this.paymentStrategy.pay(this.amount)
  }
}

// 使用
const cart = new ShoppingCart()
cart.addItem(100)
cart.addItem(200)

cart.setPaymentStrategy(new CreditCardPayment('1234567890123456'))
cart.checkout() // "使用信用卡 3456 支付 300 元"

cart.setPaymentStrategy(new WeChatPayment())
cart.checkout() // "使用微信支付 300 元"

// Vue 中的应用:表单验证策略
interface ValidationStrategy {
  validate(value: string): boolean
  errorMessage: string
}

const emailStrategy: ValidationStrategy = {
  validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  errorMessage: '请输入有效的邮箱地址'
}

const phoneStrategy: ValidationStrategy = {
  validate: (value) => /^1[3-9]\d{9}$/.test(value),
  errorMessage: '请输入有效的手机号'
}

9. 职责链模式 (Chain of Responsibility)

为请求创建一条处理者链,每个处理者依次处理请求。

// 职责链模式
interface Handler {
  setNext(handler: Handler): Handler
  handle(request: string): string | null
}

abstract class AbstractHandler implements Handler {
  private nextHandler: Handler | null = null
  
  setNext(handler: Handler): Handler {
    this.nextHandler = handler
    return handler
  }
  
  handle(request: string): string | null {
    if (this.nextHandler) {
      return this.nextHandler.handle(request)
    }
    return null
  }
}

class AuthHandler extends AbstractHandler {
  handle(request: string): string | null {
    if (request.includes('token')) {
      console.log('认证通过')
      return super.handle(request)
    }
    return '认证失败'
  }
}

class ValidationHandler extends AbstractHandler {
  handle(request: string): string | null {
    if (request.length > 10) {
      console.log('数据验证通过')
      return super.handle(request)
    }
    return '数据验证失败'
  }
}

class CacheHandler extends AbstractHandler {
  handle(request: string): string | null {
    console.log('检查缓存...')
    // 模拟缓存命中
    if (Math.random() > 0.5) {
      return '返回缓存数据'
    }
    return super.handle(request)
  }
}

// 使用
const auth = new AuthHandler()
const validation = new ValidationHandler()
const cache = new CacheHandler()

auth.setNext(validation).setNext(cache)

const result = auth.handle('request-with-token-and-long-enough')
console.log(result)

// Web 开发中的应用:中间件
// Express/Koa 中间件就是职责链模式
app.use((req, res, next) => {
  console.log('中间件1')
  next()
})

app.use((req, res, next) => {
  console.log('中间件2')
  next()
})

🎯 现代前端特有模式

10. 组合式函数模式 (Composables)

Vue 3 中的逻辑复用模式。

// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event: MouseEvent) {
    x.value = event.clientX
    y.value = event.clientY
  }
  
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))
  
  return { x, y }
}

// 使用
const { x, y } = useMouse()

11. 渲染道具模式 (Render Props)

React 中的逻辑复用模式。

// Render Props 模式
interface MouseTrackerProps {
  render: (state: { x: number; y: number }) => React.ReactNode
}

class MouseTracker extends React.Component<MouseTrackerProps> {
  state = { x: 0, y: 0 }
  
  handleMouseMove = (event: React.MouseEvent) => {
    this.setState({ x: event.clientX, y: event.clientY })
  }
  
  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    )
  }
}

// 使用
<MouseTracker 
  render={({ x, y }) => (
    <h1>鼠标位置: {x}, {y}</h1>
  )}
/>

12. 容器组件模式 (Container/Presentational)

分离逻辑和展示。

// 容器组件 - 负责逻辑
// containers/UserContainer.tsx
import { useState, useEffect } from 'react'
import UserList from '../presentational/UserList'

export default function UserContainer() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data)
        setLoading(false)
      })
  }, [])
  
  return (
    <UserList 
      users={users}
      loading={loading}
    />
  )
}

// 展示组件 - 负责 UI
// presentational/UserList.tsx
interface Props {
  users: User[]
  loading: boolean
}

export default function UserList({ users, loading }: Props) {
  if (loading) return <div>加载中...</div>
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

📊 模式选择指南

场景 推荐模式 理由
需要全局唯一实例 单例模式 确保配置、连接池等只有一个实例
对象创建逻辑复杂 工厂模式 封装创建细节,便于扩展
需要动态添加功能 装饰器模式 比继承更灵活,符合开闭原则
一对多依赖关系 观察者模式 对象状态变化时自动通知依赖者
算法可以互相替换 策略模式 避免大量 if-else,便于切换算法
需要控制对象访问 代理模式 延迟加载、访问控制、日志记录
处理流程有多个步骤 职责链模式 解耦发送者和接收者
构建复杂对象 建造者模式 分步骤构建,参数可选
接口不兼容 适配器模式 让不兼容的类能一起工作

💡 模式使用原则

  1. 优先组合而非继承:大多数模式都强调组合优于继承
  2. 面向接口编程:依赖抽象而非具体实现
  3. 封装变化:找到系统中变化的部分并封装起来
  4. 开闭原则:对扩展开放,对修改封闭
  5. 单一职责:每个类只有一个改变的理由

掌握这些模式不是要你在所有地方都使用它们,而是在遇到合适的问题时,能够想到并应用对应的解决方案。模式是工具,不是教条。

文本行过滤/筛选 在线工具核心JS实现

作者 滕青山
2026年3月3日 11:24

这个工具的核心目标很直接:给定一段多行文本和多条条件,快速得到“保留匹配行”或“删除匹配行”的结果。实现上我把逻辑拆成两层:过滤引擎负责纯文本计算,页面逻辑负责参数整理、错误提示、复制与下载。

在线工具网址:see-tool.com/text-line-f…
工具截图:
工具截图.png

过滤引擎的输入约定

过滤函数接收三部分数据:原始文本、条件数组、选项对象。选项统一收口后,后续分支会非常清晰。

function filterLines(inputText, filterConditions, options) {
  const {
    filterMode = 'contains', // contains | exact
    useRegex = false,        // 是否启用正则
    ignoreCase = true,       // 是否忽略大小写
    matchAll = false,        // false=任一命中,true=全部命中
    action = 'keep'          // keep | remove
  } = options

  if (!inputText || !filterConditions || filterConditions.length === 0) {
    return []
  }

  const lines = inputText.split('\n')
  const matchedLines = []
  // 后续按行处理
}

这里有两个关键点:

  • 条件以数组传入,避免在核心函数里再做字符串切分。
  • 空输入直接返回空数组,调用方只需要处理“有结果/无结果”两种状态。

单行匹配:普通文本与正则双通道

每一行都会遍历全部条件,得到一个布尔数组 matchResults。这个数组是后面 AND/OR 逻辑的基础。

for (const line of lines) {
  const matchResults = []

  for (const condition of filterConditions) {
    let isMatch = false

    if (useRegex) {
      const flags = ignoreCase ? 'i' : ''
      const regex = new RegExp(condition, flags)
      isMatch = regex.test(line)
    } else {
      const searchText = ignoreCase ? condition.toLowerCase() : condition
      const lineText = ignoreCase ? line.toLowerCase() : line

      if (filterMode === 'contains') {
        isMatch = lineText.includes(searchText)
      } else {
        isMatch = lineText === searchText
      }
    }

    matchResults.push(isMatch)
  }
}

这段实现解决了三个常见需求:

  • 模糊匹配includes 处理“行内包含关键词”。
  • 整行匹配=== 处理“整行完全一致”。
  • 正则匹配:由用户条件直接构造 RegExp,支持复杂表达式。

多条件组合:OR 与 AND

工具支持两种条件关系:

  • matchAll = false:任意一个条件命中即视为命中(OR)
  • matchAll = true:所有条件都命中才算命中(AND)

实现非常直接:

const finalMatch = matchAll
  ? matchResults.every(result => result)
  : matchResults.some(result => result)

有了 finalMatch 后,再叠加动作类型:

if ((action === 'keep' && finalMatch) || (action === 'remove' && !finalMatch)) {
  matchedLines.push(line)
}

这一步把“保留匹配”和“删除匹配”统一到了同一套流程里,避免写两份几乎重复的过滤逻辑。

正则错误处理

正则场景最容易出现语法错误(比如括号未闭合)。引擎层统一用 try/catch 包裹,在异常时抛出可读错误:

try {
  // 过滤主流程
} catch (error) {
  throw new Error('正则表达式语法错误:' + error.message)
}

这样页面层只需要捕获一次并提示用户,不需要关心底层失败细节。

页面逻辑:参数整理与结果回填

页面层主要做四件事:

  1. 校验输入文本和条件是否为空。
  2. 把条件文本按换行拆成数组并过滤空行。
  3. 调用过滤引擎并拿到结果数组。
  4. join('\n') 回填到输出框。

核心调用方式:

const conditions = filterConditionsText
  .split('\n')
  .filter(condition => condition.trim())

const result = TextLineFilter.filterLines(inputText, conditions, {
  filterMode,
  useRegex,
  ignoreCase,
  matchAll,
  action
})

outputText = result.length > 0 ? result.join('\n') : ''

这里刻意让页面层只承担“数据进出”,把匹配规则全部留在引擎层,后续扩展新选项时改动面会更小。

复制与下载的JS闭环

过滤完成后,工具支持直接复制和导出文本。复制优先使用现代剪贴板 API,失败时降级到 textarea + execCommand('copy'),保证更多浏览器可用。

下载则通过 Blob 生成文本文件,并使用临时 <a> 标签触发保存:

const blob = new Blob([outputText], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)

const a = document.createElement('a')
a.href = url
a.download = `过滤结果-${dateStr}.txt`
a.click()

URL.revokeObjectURL(url)

到这里,文本输入、条件匹配、结果输出、复制下载就形成了完整的功能链路。

ts+vue3开发规范

2026年3月3日 11:19

针对 Vue 3 项目,TypeScript 的最佳实践会结合 Vue 的 Composition API 和响应式系统的特点,形成一套独特的规范体系。以下是专门为 Vue 3 + TypeScript 项目整理的实践指南:

🎯 项目初始化与配置

使用官方脚手架创建项目

npm create vue@latest
# 或
npm create vite@latest

在创建时务必勾选 TypeScript 选项,确保获得最佳的项目模板和配置。

环境类型声明

创建 src/env.d.ts 或扩充现有声明文件:

/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

// 环境变量类型声明
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_BASE_URL: string
  // 更多环境变量...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

📦 组件开发规范

1. 使用 <script setup> 语法

这是 Vue 3 推荐的组合式 API 写法,配合 TypeScript 体验最佳:

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// 通过泛型定义 ref 类型
const count = ref<number>(0)
const user = ref<User | null>(null)

// 类型安全的计算属性
const doubleCount = computed<number>(() => count.value * 2)

// 事件声明
const emit = defineEmits<{
  (e: 'change', value: number): void
  (e: 'update', id: string): void
}>()

// Props 定义 - 使用类型字面量
interface Props {
  title: string
  items?: string[]
  disabled?: boolean
  config?: {
    theme: 'light' | 'dark'
    size: 'small' | 'medium' | 'large'
  }
}

const props = withDefaults(defineProps<Props>(), {
  items: () => [],
  disabled: false,
  config: () => ({ theme: 'light', size: 'medium' })
})

// 暴露给父组件的属性和方法
defineExpose<{
  reset: () => void
  validate: () => boolean
}>({
  reset: () => { count.value = 0 },
  validate: () => count.value > 0
})
</script>

2. 组件 Props 的严格定义

为组件 Props 定义精确的类型,避免使用过于宽泛的类型:

// ✅ 好的做法
interface TableColumn {
  key: string
  title: string
  width?: number | string
  align?: 'left' | 'center' | 'right'
  sortable?: boolean
}

// ❌ 避免
interface TableColumn {
  key: string
  title: string
  width?: any
  align?: string
}

3. 模板引用的类型安全

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// 定义组件实例类型
const modalRef = ref<InstanceType<typeof ModalComponent> | null>(null)

// 或使用组件导出的类型
import ModalComponent, { type ModalExpose } from './ModalComponent.vue'
const modalRef = ref<ModalExpose | null>(null)

onMounted(() => {
  modalRef.value?.open() // 类型安全的方法调用
})
</script>

<template>
  <ModalComponent ref="modalRef" />
</template>

🔄 Composition API 最佳实践

1. 创建类型安全的组合式函数

// composables/useAsyncData.ts
import { ref, type Ref } from 'vue'

interface AsyncState<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<Error | null>
  execute: () => Promise<void>
}

export function useAsyncData<T>(
  fetcher: () => Promise<T>
): AsyncState<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const loading = ref(false)
  const error = ref<Error | null>(null)

  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      data.value = await fetcher()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  return {
    data,
    loading,
    error,
    execute
  }
}

// 使用示例
interface User {
  id: number
  name: string
  email: string
}

const { data, loading, error } = useAsyncData<User>(
  () => fetch('/api/user').then(res => res.json())
)

2. 类型安全的 Pinia Store

// stores/user.ts
import { defineStore } from 'pinia'

interface UserState {
  profile: User | null
  permissions: string[]
  lastLogin: Date | null
}

interface UserActions {
  login: (credentials: Credentials) => Promise<void>
  logout: () => void
  updateProfile: (data: Partial<User>) => Promise<User>
}

interface UserGetters {
  isAdmin: (state: UserState) => boolean
  fullName: (state: UserState) => string
}

export const useUserStore = defineStore<'user', UserState, UserGetters, UserActions>('user', {
  state: (): UserState => ({
    profile: null,
    permissions: [],
    lastLogin: null
  }),
  
  getters: {
    isAdmin: (state) => state.permissions.includes('admin'),
    fullName: (state) => state.profile ? 
      `${state.profile.firstName} ${state.profile.lastName}` : ''
  },
  
  actions: {
    async login(credentials: Credentials) {
      // 类型安全的登录逻辑
    },
    
    logout() {
      this.$patch({
        profile: null,
        permissions: [],
        lastLogin: null
      })
    },
    
    async updateProfile(data: Partial<User>) {
      // 部分更新,类型安全
    }
  }
})

// 在组件中使用
const userStore = useUserStore()
const { profile, isAdmin } = storeToRefs(userStore)

🛣️ Vue Router 类型安全

1. 类型化路由参数

// router/types.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    roles?: Array<'admin' | 'user'>
    title?: string
    transition?: string
    keepAlive?: boolean
  }
}

// router/index.ts
export const routes = [
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('@/views/UserDetail.vue'),
    meta: {
      requiresAuth: true,
      title: '用户详情',
      roles: ['admin']
    } as const // 使用 const assertion 确保类型精确
  }
] as const // 使路由配置成为字面量类型

2. 路由参数的类型推导

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 使用类型守卫处理路由参数
const userId = computed(() => {
  const id = route.params.id
  // 路由参数可能是 string 或 string[]
  return Array.isArray(id) ? id[0] : id
})

// 类型安全的导航
function navigateToUser(id: number) {
  router.push({
    name: 'UserDetail',
    params: { id: id.toString() }
  })
}

// 使用查询参数
const page = computed(() => {
  const page = route.query.page
  return page ? Number(page) : 1
})
</script>

📡 API 层类型安全

1. 统一的 API 响应类型

// api/types.ts
export interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
  timestamp: number
}

export interface PaginatedResponse<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
  totalPages: number
}

export interface ApiError {
  code: number
  message: string
  details?: Record<string, any>
}

2. 类型安全的请求函数

// api/user.ts
import axios, { type AxiosRequestConfig } from 'axios'
import type { ApiResponse, PaginatedResponse } from './types'

export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  role: 'admin' | 'editor' | 'viewer'
  createdAt: string
}

export interface CreateUserDto {
  name: string
  email: string
  password: string
  role?: User['role']
}

export interface UpdateUserDto extends Partial<CreateUserDto> {
  id: number
}

export class UserApi {
  private baseUrl = '/api/users'
  
  // 泛型方法,返回类型安全的数据
  async getUsers(params?: { page?: number; pageSize?: number }): Promise<PaginatedResponse<User>> {
    const response = await axios.get<ApiResponse<PaginatedResponse<User>>>(this.baseUrl, {
      params
    })
    return response.data.data
  }
  
  async getUserById(id: number): Promise<User> {
    const response = await axios.get<ApiResponse<User>>(`${this.baseUrl}/${id}`)
    return response.data.data
  }
  
  async createUser(data: CreateUserDto): Promise<User> {
    const response = await axios.post<ApiResponse<User>>(this.baseUrl, data)
    return response.data.data
  }
  
  async updateUser(data: UpdateUserDto): Promise<User> {
    const { id, ...rest } = data
    const response = await axios.put<ApiResponse<User>>(`${this.baseUrl}/${id}`, rest)
    return response.data.data
  }
  
  async deleteUser(id: number): Promise<void> {
    await axios.delete(`${this.baseUrl}/${id}`)
  }
}

export const userApi = new UserApi()

🎨 样式与 CSS Modules

类型安全的 CSS Modules

<script setup lang="ts">
// 为 CSS Modules 生成类型
import styles from './Component.module.css'

// styles 有完整的类型提示
// styles.container, styles.title, styles.highlight
</script>

<template>
  <div :class="styles.container">
    <h1 :class="styles.title">标题</h1>
  </div>
</template>

<style module="styles">
.container { /* ... */ }
.title { /* ... */ }
</style>

🔍 类型检查与工具

1. 使用 Volar 替代 Vetur

在 VS Code 中,为 Vue 3 项目推荐使用 Volar 扩展,并启用 Takeover Mode 以获得最佳的类型支持。

.vscode/settings.json 中配置:

{
  "typescript.tsdk": "node_modules/typescript/lib",
  "volar.takeoverMode.enabled": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}

2. 在 package.json 中添加类型检查脚本

{
  "scripts": {
    "type-check": "vue-tsc --noEmit",
    "type-check:watch": "vue-tsc --noEmit --watch",
    "build": "vue-tsc && vite build"
  }
}

📝 ESLint 配置示例

// .eslintrc.cjs
module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
    '@vue/eslint-config-prettier'
  ],
  rules: {
    // Vue 特定规则
    'vue/multi-word-component-names': 'error',
    'vue/component-name-in-template-casing': ['error', 'PascalCase'],
    'vue/define-props-declaration': ['error', 'type-based'],
    'vue/define-emits-declaration': ['error', 'type-based'],
    
    // TypeScript 规则
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-unused-vars': ['error', { 
      argsIgnorePattern: '^_',
      varsIgnorePattern: '^_' 
    }],
    
    // 禁止使用 console.log,除非是 warn/error
    'no-console': ['error', { allow: ['warn', 'error'] }],
  }
}

💡 项目目录结构建议

src/
├── assets/            # 静态资源
├── components/        # 公共组件
│   ├── common/       # 基础组件
│   └── layout/       # 布局组件
├── composables/       # 组合式函数
│   ├── useAuth.ts
│   ├── useAsyncData.ts
│   └── index.ts      # 统一导出
├── views/            # 页面组件
│   ├── Home/
│   │   ├── index.vue
│   │   ├── components/  # 页面专属组件
│   │   └── types.ts     # 页面专属类型
│   └── User/
├── router/           # 路由配置
│   ├── index.ts
│   ├── routes.ts
│   └── guards.ts
├── stores/           # Pinia 状态管理
│   ├── user.ts
│   └── app.ts
├── api/              # API 请求
│   ├── client.ts     # axios 实例配置
│   ├── types.ts      # API 公共类型
│   └── modules/      # 按模块划分
│       ├── user.ts
│       └── product.ts
├── types/            # 全局类型定义
│   ├── global.d.ts
│   ├── env.d.ts
│   └── models/       # 领域模型类型
├── utils/            # 工具函数
│   ├── format.ts
│   └── validation.ts
├── styles/           # 全局样式
└── main.ts           # 入口文件

🔧 进阶类型技巧

1. 为 provide/inject 提供类型

// types/context.ts
import type { InjectionKey, Ref } from 'vue'

export interface ThemeContext {
  theme: Ref<'light' | 'dark'>
  toggleTheme: () => void
}

export const themeKey: InjectionKey<ThemeContext> = Symbol('theme')

// 父组件提供
const theme = ref<'light' | 'dark'>('light')
provide(themeKey, {
  theme,
  toggleTheme: () => theme.value = theme.value === 'light' ? 'dark' : 'light'
})

// 子组件注入
const themeContext = inject(themeKey)
if (themeContext) {
  // themeContext 有完整的类型提示
  console.log(themeContext.theme.value)
}

2. 泛型组件

<!-- components/List.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
defineProps<{
  items: T[]
  renderItem: (item: T) => any
  keyExtractor?: (item: T) => string
}>()
</script>

<template>
  <div class="list">
    <div v-for="item in items" :key="keyExtractor?.(item) ?? item.id">
      <slot name="item" :item="item">
        {{ renderItem?.(item) }}
      </slot>
    </div>
  </div>
</template>

<!-- 使用示例 -->
<script setup lang="ts">
interface User {
  id: number
  name: string
}

const users = ref<User[]>([/* ... */])
</script>

<template>
  <List :items="users" :keyExtractor="(user) => `user-${user.id}`">
    <template #item="{ item }">
      {{ item.name }}
    </template>
  </List>
</template>

遵循这些实践,可以让你的 Vue 3 + TypeScript 项目更加健壮、可维护,同时提供优秀的开发体验。建议从项目一开始就建立这些规范,并在团队中推广使用。

前端JS: 跨域解决

2026年3月3日 11:16

一、什么是跨域?为什么会产生跨域?

首先,跨域的核心是浏览器的同源策略(Same-Origin Policy) 。这是一个出于安全考虑的核心机制,旨在防止不同源的网页之间进行恶意的数据交互,比如窃取用户信息、进行CSRF攻击等。

“源”(Origin)由三部分组成:协议(Protocol)、域名(Host)、端口(Port)。  只要这三者中有任何一个不同,就构成了跨域。

URL1 URL2 是否同源 原因
http://www.a.com/a.html http://www.a.com/b.html 协议、域名、端口都相同
http://www.a.com https://www.a.com 协议不同 (http vs https)
http://www.a.com http://www.b.com 域名不同
http://localhost:5000 http://localhost:7000 端口不同
http://www.a.com http://127.0.0.1:5000 域名和端口都不同(即使IP相同)

一个关键点: 跨域限制是浏览器单方面施加的安全策略。实际上,HTTP请求已经成功发送到了服务器,服务器也返回了数据,但浏览器在接收到响应后,会检查响应头信息,如果不符合同源策略,就会拦截响应体,导致前端JS无法读取数据,从而在控制台报错。

二、如何解决跨域?

解决跨域的核心思想是:让浏览器认为这次请求是“安全”的、同源的。  主要有以下几种主流方案,按推荐程度排序:

1. CORS (Cross-Origin Resource Sharing) -  【现代、标准、首选方案】

这是W3C制定的官方标准,也是目前解决跨域的根本方案。它通过在HTTP响应头中添加一系列Access-Control-*字段,来告诉浏览器哪些源有权限访问资源。

工作原理

  • 简单请求(Simple Request) : 满足以下条件的请求,浏览器会直接发送,并在响应头中检查Access-Control-Allow-Origin

    • 请求方法为:GETPOSTHEAD
    • 请求头仅包含:AcceptAccept-LanguageContent-LanguageContent-Type (且值仅限 application/x-www-form-urlencodedmultipart/form-datatext/plain)
  • 非简单请求(Preflighted Request) : 不满足简单请求条件的(如PUTDELETE方法,或Content-Type: application/json),浏览器会自动先发送一个OPTIONS方法的预检请求(Preflight Request) 。服务器通过预检请求的响应来判断是否允许后续的真实请求。

关键响应头

  • Access-Control-Allow-Origin: 必需,指定允许访问资源的源,如 http://my-app.com 或 * (表示所有源,但不安全且无法携带Cookie)。
  • Access-Control-Allow-Methods: 允许的HTTP方法,如 GET, POST, OPTIONS
  • Access-Control-Allow-Headers: 允许的请求头字段。
  • Access-Control-Allow-Credentials: 是否允许发送Cookie,若需携带cookie,后端不能使用*作为Allow-Origin的值。

实现: 主要由后端配置。例如在Node.js (Express)中:

javascript
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); // 允许的源
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204); // 预检请求直接成功返回
  }
  next();
});

Java (Spring Boot)、PHP、Python等后端框架都有相应的CORS配置方式(如注解、过滤器、中间件)。

2. 代理服务器 (Proxy) -  【开发环境常用,生产环境可选】

利用服务器之间没有同源策略限制的特点,将前端的跨域请求变为同源请求。

  • 开发环境代理: 前端开发时,配置webpack-dev-serverVite的代理功能。前端请求自己的开发服务器(如/api/users),开发服务器再将请求转发到真实的后端API(如http://api.example.com/users)。

    • 优点: 简单快捷,无需后端配合,解决开发时的跨域问题。

    • 缺点:  仅用于开发环境。

    • 示例 (Vite配置):

      javascript
      // vite.config.js
      export default {
        server: {
          proxy: {
            '/api': {
              target: 'http://localhost:8080', // 后端API地址
              changeOrigin: true,
              rewrite: (path) => path.replace(/^/api/, '')
            }
          }
        }
      }
      
  • 生产环境代理:  使用Nginx等反向代理服务器。所有请求都发往Nginx,由Nginx根据规则将请求转发给后端应用服务器。

    • 优点:  性能高,安全,可做负载均衡、日志记录等。

    • 示例 (Nginx配置):

      nginx
      location /api {
          proxy_pass http://api.example.com; # 转发到真实API服务器
          # 以下CORS头也可在Nginx层配置,作为备选方案
          add_header Access-Control-Allow-Origin *;
          add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
          add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
      }
      
3. JSONP (JSON with Padding) -  【古老、仅作了解】

利用<script>标签的src属性不受同源策略限制的“漏洞”。

  • 原理:  前端动态创建一个<script>标签,其src指向后端API,并带上一个回调函数名(如?callback=handleData)。后端将数据作为参数拼接到该函数调用中返回(如handleData({...}))。前端预先定义好handleData函数,数据返回后即可执行。

  • 缺点:

    • 只支持GET请求。
    • 安全性差,容易遭受XSS攻击。
    • 错误处理不便。
  • 现状:  在现代项目中已基本被CORS取代,仅在需要兼容非常古老的浏览器时才可能用到。

4. 其他方案(特定场景)
  • window.postMessage: 用于跨域iframe之间的通信。
  • WebSocket: 协议本身不受同源策略限制,但建立连接的握手过程仍需遵循HTTP的同源策略。一旦连接建立,后续通信是自由的。
  • document.domain: 仅适用于主域相同、子域不同的情况(如a.b.comc.b.com),通过设置document.domain = 'b.com'来实现。

三、总结与选择

在实际项目中,我的选择策略是:

  1. 首选CORS:这是最标准、最灵活的方案。前后端分离项目中,由后端配置CORS策略是最佳实践。
  2. 开发环境用代理:为了提升开发效率,避免频繁依赖后端,前端配置开发服务器代理是必须的。
  3. 生产环境可用Nginx代理:对于已部署的项目,如果不想修改后端代码,可以在Nginx层做反向代理和CORS配置。
  4. 基本不用JSONP:除非有特殊的历史遗留项目或兼容性要求。

四、常见问题排查(加分项)

如果遇到CORS报错,我会按以下步骤排查:

  1. 看网络请求:在浏览器开发者工具的Network面板,检查请求的Origin和响应的Access-Control-Allow-Origin是否匹配。
  2. 看预检请求OPTIONS请求是否成功(状态码200/204)?如果失败,可能是服务器端路由或安全策略(如防火墙、WAF)拦截了OPTIONS请求。
  3. 看响应头Access-Control-Allow-Origin是否为*或正确的源?Access-Control-Allow-Methods是否包含了请求的方法?
  4. 看凭证:如果请求需要携带Cookie,后端是否设置了Access-Control-Allow-Credentials: true,并且Access-Control-Allow-Origin不能为*
  5. 检查重复配置:响应头中Access-Control-Allow-Origin是否出现了多次?这通常是由于服务器和应用框架(如Spring Security)同时配置了CORS导致的,需要统一管理。

在线CAD开发包结构与功能说明

2026年3月3日 11:17

一、MxDraw云图开发包是什么

云图开发包是一个围绕 MxCAD 构建的完整 CAD 云化解决方案工程集合。 它不是单一 SDK,而是将 后台图纸转换、服务接口、前端项目示例、MxCAD 编辑与浏览能力 统一打包的一套工程。对新手来说,可以这样理解:云图开发包已经帮你把“一个 CAD 云系统”拆分好、放在了对应目录中。

说明: 云图开发包会根据不同操作系统(如 Windows、Linux 等)提供对应版本,开发包在可执行程序形式、部署方式及启动方式上可能存在差异。但无论运行于何种操作系统,云图开发包在功能层面与整体架构设计上保持一致,目录职责划分、核心能力以及使用方式不受平台影响,本文档中的架构与说明均适用于所有操作系统版本。


二、整体目录结构总览

MxDraw云图开发包的根目录为:

Windows:

MXDRAWCLOUDSERVER1.0_XXX_TRYVERSION (其中XXX为云图开发包版本号)
└─ MxDrawCloudServer   (云图开发包根目录)

image-20260205150120186.png Linux:

MXDRAWCLOUDSERVER1.0_XXX_xxx_TRYVERSION (其中XXX为云图开发包版本号,xxx为对应的操作系统)
└─ install
   └─ MxDrawCloudServer   (云图开发包根目录)

image-20260205153825135.png

从功能角度看,目录可以分为三大块:

  1. Bin:后台服务与核心能力

  2. SRC:前端项目与示例源码

  3. Mx3dServer.exe:启动服务和演示页面(Windows)

    start_demo.sh:启动服务和演示页面(Linux)

新手理解云图开发包,只要先理解这三块的分工,就不会迷路。


三、Bin 目录:后台服务相关目录(核心)

Windows:

MxDrawCloudServer
└─ Bin
   └─ MxCAD   (图纸转换程序目录)
   └─ MxDrawServer   (MxCAD 项目的后台服务目录)
   └─ MxServiceCode   (Node.js 服务代码目录)

image-20260205154556643.png

Linux:

MxDrawCloudServer
└─ Bin
   └─ Linux   
       └─ MxCAD   (图纸转换程序目录)
       └─ MxDrawServer   (MxCAD 项目的后台服务目录)
   └─ MxServiceCode   (Node.js 服务代码目录)

image-20260205154415049.png

image-20260205154314994.pngBin 是云图开发包中最核心的目录,承载了 CAD 云化所必需的后台能力。

1. MxCAD —— 图纸转换程序目录

  • 用于 CAD 图纸的转换处理
  • 将 DWG / DXF 等原始图纸的格式转换
  • 是云图系统能够“在线显示 CAD”的前提条件

没有这个目录下对应的转换程序,前端无法直接展示编辑 CAD 图纸。


2. MxDrawServer —— MxCAD 项目的后台服务目录

  • 提供 MxCAD 项目内部所需的后台接口服务
  • 为 CAD 图纸加载、处理、交互等能力提供服务支持
  • 属于 MxCAD 工程体系的一部分

这是连接“图纸数据”和“CAD 功能”的关键后台模块。


3. MxServiceCode —— Node.js 服务代码目录

  • 基于 Node 的 后台服务代码
  • 用于对外提供后台图纸处理服务接口
  • 常用于后台图纸处理,如后台参数化绘图,图纸数据提取,图纸拆分等

对新手而言,这是理解“云图后台如何工作”的最佳入口。


四、SRC 目录:前端项目相关目录

Winodws/Linux:

MxDrawCloudServer
└─ SRC
   └─ sample   (前端项目示例代码目录)
   └─ TsWeb   (云服务的web前端门户)
   └─ doc   (文档目录)

image-20260205160852318.png

SRC 目录是 MxDraw 云图开发包中面向开发者的核心区域,包含了所有可开放的前端示例项目源码、集成模板及配套文档。无论你是要快速体验功能,还是进行深度二次开发,都应从此目录入手。


1. doc —— 文档目录(默认为空文件夹)

  • 存放与前端项目相关的说明文档、API 手册或集成指南。
  • 开发者可在此补充自定义说明,辅助团队协作或项目交接。

2. sample —— 前端项目示例代码目录(重点)

Winodws/Linux:

MxDrawCloudServer
└─ SRC
   └─ app   (mxcad-app 在不同架构项目下的集成示例)
   └─ BrowseCAD浏览版项目源码目录)
   └─ EditCAD编辑版项目源码目录)   
   └─ GISCAD+GIS结合项目源码目录)

image-20260206142024797.png

该目录提供了多种典型应用场景的完整前端工程示例,覆盖浏览、编辑、3D、GIS 等核心能力,是新手学习和项目参考的最佳入口。

(1)app —— 集成 mxcad-app 依赖包的CAD编辑项目示例源码

Winodws/Linux:

MxDrawCloudServer
└─ SRC
   └─ sample   (前端项目示例代码目录) 
       └─ app   (mxcad-app 在不同架构项目下的集成示例源码)
          └─ MxCADAppVue2+Webpack)
          └─ plugins   (项目插件目录)
             └─ pluginAiChat   (AI模块)
          └─ sample
             └─ webapack4 
             └─ html+js
             └─ vite+vue3
             └─ webapck+react
             └─ cnd.html

image-20260206142546094.png

提供在不同前端技术栈下集成 mxcad-app 的标准方式:

  • MxCADApp:基于 Vue2 + Webpack 的完整编辑器项目
  • plugins:内置插件扩展机制,如 pluginAiChat(AI 对话模块)
  • 多框架适配示例:包含 vite+vue3webpack+reacthtml+js 及 CDN 引入方式(cnd.html

(2)Browse —— CAD 浏览版项目源码目录

Winodws/Linux:

MxDrawCloudServer
└─ SRC
   └─ sample   (前端项目示例代码目录) 
      └─ BrowseCAD 浏览版项目示例)
          └─ 2d 
             └─ Browseiframe   (iframe嵌套集成示例)
             └─ BrowseCAD浏览版项目源码目录)

image-20260206142808876.png

专注于图纸查看场景,支持轻量级部署:

  • 2d/Browse:纯 2D 图纸浏览页面
  • 2d/Browseiframe:通过 iframe 嵌套集成的浏览模式,便于嵌入第三方系统

(3)Edit—— CAD 编辑版项目源码目录

Winodws/Linux:

MxDrawCloudServer
└─ SRC
   └─ sample   (前端项目示例代码目录) 
      └─ EditCAD 编辑版项目示例)
          └─ 2d  (二维图纸项目)
             └─ dist   (MxCAD APP 的静态资源包)
             └─ MxCADMxCAD APP 中的一个插件源码目录)
             └─ MxCADiframe   (iframe嵌套集成示例)
          └─ 3d  (三维图纸项目)
             └─ dist   (3D项目的静态资源包) 
             └─ MxCAD   (3D项目中的一个插件源码目录) 

image-20260206144840098.png

image-20260206145023459.png

提供完整的在线编辑能力,包含二维与三维模式:

  • Edit/2d:2D 图纸编辑环境,含工具栏、属性面板等
  • Edit/3d:3D 模型查看与基础操作界面
  • dist 子目录:预编译的静态资源包,可直接部署到 Web 服务器

(4)GIS —— CAD+GIS项目源码目录

Winodws/Linux:

MxDrawCloudServer
└─ SRC
   └─ sample   (前端项目示例代码目录) 
       └─ GIS
          └─ MxCADMapGIS+CAD项目源码目录) 

image-20260206145149875.png

展示 MxCAD 与地理信息系统(GIS)的集成方案:

  • MxCADMap:将 CAD 图形叠加到地图底图上,实现空间数据联动分析

各子项目均采用模块化设计,开发者可按需复制、修改或组合使用,极大降低集成门槛

五、Mx3dServer.exe/start_demo.sh:Mxdraw云图启动入口

Mx3dServer.exe(Windows)与 start_demo.sh(Linux)是 MxDraw 云图开发包的统一启动入口,用于一键初始化整个 CAD 云服务环境。它们屏蔽了后台服务配置、端口绑定、依赖启动等复杂细节,让开发者或用户只需“双击”或“执行脚本”即可进入演示状态。

1. Mx3dServer.exe:梦想云图服务启动程序(图形化入口)

Mx3dServer.exe 是 MxDraw 云图开发包在 Windows 平台上的图形化启动程序,双击运行后将自动弹出“梦想云图服务启动程序”窗口。该程序集成了多模块服务的统一管理与快速访问功能,极大简化了部署流程,让开发者与用户无需手动配置即可一键开启完整的 CAD 在线演示环境。

image-20260206145602924.png

  • 开始Web服务

    当你点击 “开启Web服务” 按钮时,MxDraw 会自动启动两个关键的本地服务程序。这两个服务协同工作,共同支撑起完整的在线 CAD 功能体验。

image-20260206155506005.png

  • 第一个服务(端口 1337):CAD 核心引擎

    该服务由 Bin/MxDrawServer/Windows/app.js 脚本启动,是 MxDraw 的“大脑”。它负责处理所有与图纸相关的底层操作,例如打开 DWG 文件、解析图形数据、保存编辑结果等。虽然你看不到它的界面,但所有 CAD 功能都依赖它来完成。 image-20260206155724387.png

  • 第二个服务(端口 3000):Web 前端服务器

    该服务由 SRC/TsWeb/app.js 脚本启动,是用户的“操作窗口”。它基于 Express 框架构建,负责托管所有网页文件(如 2D 编辑器、3D 查看器、文件浏览器等),并将你的操作请求转发给 CAD 引擎。你看到的界面、按钮、工具栏,都由这个服务提供。
    image-20260206155744533.png

  • 启动浏览器查看演示

    自动调用系统默认浏览器(推荐 Chrome 或 Edge)打开首页地址 http://localhost:3000,快速进入演示环境。 image-20260206160529645.png

  • VueBrowse

    启动基于 Vue 框架的图纸浏览项目。 image-20260206160655084.png

  • Browseiframe

    iframe 嵌入模式加载 CAD 浏览页面,便于集成到第三方系统或企业门户中。

  • 启动MxCAD

    打开 2D CAD 在线编辑器,支持绘图、修改、标注、上传、保存等完整编辑功能,适用于工程设计场景。 image-20260206160845969.png

  • 启动MxCAD3D 启动 3D CAD 查看器,基于 WebGL 渲染三维模型。 image-20260206161020003.png

  • MxCAD GIS 启动 CAD 与 GIS 融合应用,将 CAD 图纸叠加至地图底图,实现空间数据联动分析。 image-20260206161958156.png

  • CAD GIS image-20260206162058664.png

  • 打开GIS DEMO目录

    直接打开本地 GIS 示例项目的文件夹,方便查看相关代码与数据资源。 image-20260206162157015.png

  • NodeJs服务测试

    打开 http://localhost:1337/serverTest 页面,提供一键调用 DWG 转换、PDF 导出、图层读取等核心 CAD 接口的可视化测试功能。 image-20260206162352393.png

  • 打开MxCAD代码开发目录

    跳转至 SRC/sample/app/MxCADApp 目录,供开发者参考完整的 Vue + TypeScript 集成项目源码。 image-20260206162440234.png

  • 打开Browse代码开发目录

    跳转至 SRC/sample/Browse 目录,查看图纸浏览类项目的前端实现逻辑。
    image-20260206162528958.png

  • 转换DWG到梦想文件格式

    启动 DWG 格式转换工具,将标准 AutoCAD DWG 文件批量转换为 MxDraw 专用的 .mxweb 格式,提升加载速度与兼容性。 image-20260206162607544.png

  • 关于

    显示软件版本号、版权信息。 image-20260208101603111.png

  • 退出

    关闭启动程序窗口。

提示:首次运行时,请在 Windows 防火墙中允许 Mx3dServer.exe 的网络访问权限,以确保服务可被正常连接。建议使用最新版 Chrome 或 Edge 浏览器获得最佳体验。

2. start_demo.sh:Linux平台云图服务启动脚本

start_demo.sh 是 MxDraw 云图开发包在 Linux 系统下的标准启动脚本,用于一键初始化完整的 Web CAD 演示环境。其功能与 Windows 平台的 Mx3dServer.exe 完全对等,确保跨平台体验一致。

核心作用

  • 同时启动两个关键服务:
    • CAD 核心服务(Node.js):运行于 1337 端口,提供 DWG 解析、绘图命令执行、格式转换等底层能力;
    • Web 前端服务(Express):运行于 3000 端口,托管所有演示页面(如 2D 编辑器、3D 查看器、文件浏览器等)。
  • 自动配置服务路径与依赖,无需手动执行多条命令。

使用步骤

  1. 提前查看LinuxDemo启动说明 参照《LinuxDemo启动说明.txt》执行权限设置运行。 image-20260208102624599.png

  2. 执行启动脚本

    ./start_demo.sh
    
  3. 访问演示页面 服务启动成功后,在浏览器中打开:

    • 首页:http://localhost:3000
    • 2D 编辑:http://localhost:3000/mxcad
    • 3D 查看:http://localhost:3000/mxweb3d.html
    • 文件浏览:http://localhost:3000/browse

注意事项

  • 脚本默认以后台方式启动服务,若需调试可修改脚本移除 & 符号以查看实时日志;
  • 若端口被占用,可编辑脚本中的 PORT 变量进行调整;

提示:尽管无图形界面,start_demo.sh 提供了与 Windows .exe 相同的功能完整性,是 Linux 开发者快速验证和集成 MxDraw 云图能力的标准入口。

typescript常用的dom 元素类型

2026年3月3日 11:10

在 TypeScript 中处理 DOM 操作时,有一整套完善的类型系统。下面我将系统地整理前端开发中最常用的 DOM 元素类型。

🎯 基础 DOM 类型体系

1. 顶层类型

// 所有 DOM 节点的基类
let node: Node = document.createElement('div')

// 所有 HTML 元素的基类
let element: Element = document.querySelector('div')!

// 所有具体的 HTML 元素都继承自 HTMLElement
let htmlElement: HTMLElement = document.createElement('div')

// 文档对象
let doc: Document = document

// 窗口对象
let win: Window = window

📦 常用具体元素类型

1. 输入控件类

// 输入框
let input: HTMLInputElement = document.createElement('input')
input.value = 'hello'
input.checked = true
input.type = 'password'
input.placeholder = '请输入'
input.files // FileList | null

// 文本域
let textarea: HTMLTextAreaElement = document.createElement('textarea')
textarea.value = '多行文本'
textarea.rows = 5
textarea.cols = 30

// 按钮
let button: HTMLButtonElement = document.createElement('button')
button.disabled = true
button.type = 'submit'

// 选择框
let select: HTMLSelectElement = document.createElement('select')
let option: HTMLOptionElement = document.createElement('option')
select.value = 'option1'
select.selectedIndex = 0
select.options // HTMLOptionsCollection

2. 容器和布局类

// 通用块级容器
let div: HTMLDivElement = document.createElement('div')
let span: HTMLSpanElement = document.createElement('span')
let section: HTMLElement = document.createElement('section')  // 直接用 HTMLElement
let article: HTMLElement = document.createElement('article')

// 列表
let ul: HTMLUListElement = document.createElement('ul')
let ol: HTMLOListElement = document.createElement('ol')
let li: HTMLLIElement = document.createElement('li')

// 表格相关
let table: HTMLTableElement = document.createElement('table')
let tr: HTMLTableRowElement = document.createElement('tr')
let td: HTMLTableCellElement = document.createElement('td')
let th: HTMLTableCellElement = document.createElement('th')
let tbody: HTMLTableSectionElement = document.createElement('tbody')
let thead: HTMLTableSectionElement = document.createElement('thead')

// 表单
let form: HTMLFormElement = document.createElement('form')
form.action = '/submit'
form.method = 'POST'
form.elements // HTMLFormControlsCollection

3. 媒体类

// 图片
let img: HTMLImageElement = document.createElement('img')
img.src = '/image.jpg'
img.alt = '描述'
img.width = 100
img.height = 100
img.complete // 图片是否加载完成

// 音频
let audio: HTMLAudioElement = document.createElement('audio')
audio.src = '/audio.mp3'
audio.volume = 0.5
audio.play()
audio.pause()

// 视频
let video: HTMLVideoElement = document.createElement('video')
video.src = '/video.mp4'
video.poster = '/poster.jpg'
video.width = 640
video.height = 360
video.playbackRate = 1.5

// 画布
let canvas: HTMLCanvasElement = document.createElement('canvas')
let ctx: CanvasRenderingContext2D | null = canvas.getContext('2d')

4. 链接和元数据类

// 链接
let a: HTMLAnchorElement = document.createElement('a')
a.href = 'https://example.com'
a.target = '_blank'
a.download = 'file.pdf'

// 图片链接
let area: HTMLAreaElement = document.createElement('area')
area.shape = 'rect'
area.coords = '0,0,100,100'

// Meta 信息
let meta: HTMLMetaElement = document.createElement('meta')
meta.name = 'description'
meta.content = '页面描述'

// 链接资源
let link: HTMLLinkElement = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/style.css'

🎨 特定功能的元素类型

1. 进度和度量

// 进度条
let progress: HTMLProgressElement = document.createElement('progress')
progress.value = 50
progress.max = 100

// 度量
let meter: HTMLMeterElement = document.createElement('meter')
meter.value = 0.6
meter.min = 0
meter.max = 1
meter.low = 0.3
meter.high = 0.8
meter.optimum = 0.5

2. 嵌入内容

// iframe
let iframe: HTMLIFrameElement = document.createElement('iframe')
iframe.src = '/other-page.html'
iframe.contentWindow // Window | null
iframe.contentDocument // Document | null

// 嵌入对象
let object: HTMLObjectElement = document.createElement('object')
object.data = '/file.pdf'
object.type = 'application/pdf'

// 嵌入脚本
let script: HTMLScriptElement = document.createElement('script')
script.src = '/main.js'
script.async = true
script.defer = true

3. 表单特有元素

// 单选/复选
let radio: HTMLInputElement = document.createElement('input')
radio.type = 'radio'
radio.name = 'gender'
radio.value = 'male'
radio.checked = true

let checkbox: HTMLInputElement = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = true
checkbox.indeterminate = false

// 文件上传
let fileInput: HTMLInputElement = document.createElement('input')
fileInput.type = 'file'
fileInput.multiple = true
fileInput.accept = 'image/*'

// 隐藏输入
let hidden: HTMLInputElement = document.createElement('input')
hidden.type = 'hidden'
hidden.value = 'secret-data'

// 颜色选择器
let color: HTMLInputElement = document.createElement('input')
color.type = 'color'
color.value = '#ff0000'

// 范围滑块
let range: HTMLInputElement = document.createElement('input')
range.type = 'range'
range.min = 0
range.max = 100
range.step = 5
range.value = '50'

🔍 类型查询和断言

1. 获取 DOM 元素

// 类型断言
const canvas = document.getElementById('canvas') as HTMLCanvasElement
const input = <HTMLInputElement>document.querySelector('input[type="text"]')

// 安全的类型检查
function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return el.tagName === 'INPUT'
}

// 使用实例
const element = document.getElementById('myElement')
if (element instanceof HTMLInputElement) {
  element.value // TypeScript 知道这是 input
} else if (element instanceof HTMLTextAreaElement) {
  element.value // 这也是 input 类型
}

// 更好的方式:使用类型守卫
function getInput(id: string): HTMLInputElement | null {
  const el = document.getElementById(id)
  return el instanceof HTMLInputElement ? el : null
}

2. 集合类型

// NodeList
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.item')
nodes.forEach(node => node.style.color = 'red')

// HTMLCollection
const forms: HTMLCollectionOf<HTMLFormElement> = document.forms
const images: HTMLCollectionOf<HTMLImageElement> = document.images

// 特定类型的集合
const allInputs = document.querySelectorAll<HTMLInputElement>('input')
// allInputs 的类型是 NodeListOf<HTMLInputElement>

// 类型化的集合
interface MyElements {
  'username': HTMLInputElement
  'submitBtn': HTMLButtonElement
  'avatar': HTMLImageElement
}

function getElement<K extends keyof MyElements>(id: K): MyElements[K] | null {
  return document.getElementById(id) as MyElements[K] | null
}

⚡ 事件对象类型

1. 常见事件类型

// 鼠标事件
function handleMouse(e: MouseEvent) {
  e.clientX, e.clientY  // 鼠标坐标
  e.button  // 按下的鼠标键
  e.ctrlKey, e.shiftKey  // 修饰键
}

// 键盘事件
function handleKey(e: KeyboardEvent) {
  e.key  // 按下的键值
  e.code  // 物理按键代码
  e.altKey  // 是否按下 Alt
  e.repeat  // 是否长按重复
}

// 焦点事件
function handleFocus(e: FocusEvent) {
  e.relatedTarget  // 上一个/下一个焦点元素
}

// 表单事件
function handleSubmit(e: Event) {
  e.preventDefault()
  const form = e.target as HTMLFormElement
}

// 拖拽事件
function handleDrag(e: DragEvent) {
  e.dataTransfer?.setData('text/plain', 'data')
}

// 剪贴板事件
function handlePaste(e: ClipboardEvent) {
  const text = e.clipboardData?.getData('text/plain')
}

2. Vue 3 中的事件类型

<script setup lang="ts">
// 原生 DOM 事件
const handleClick = (e: MouseEvent) => {
  console.log(e.clientX)
}

// Input 事件
const handleInput = (e: Event) => {
  const target = e.target as HTMLInputElement
  console.log(target.value)
}

// Change 事件
const handleChange = (e: Event) => {
  const target = e.target as HTMLSelectElement
  console.log(target.value)
}

// 键盘事件
const handleKeyDown = (e: KeyboardEvent) => {
  if (e.key === 'Enter') {
    console.log('回车键按下')
  }
}

// 自定义组件事件
interface Emits {
  (e: 'update', value: string): void
  (e: 'close', reason: 'click' | 'esc'): void
}

const emit = defineEmits<Emits>()
</script>

<template>
  <input @click="handleClick">
  <input @input="handleInput">
  <select @change="handleChange">
    <option value="1">选项1</option>
  </select>
  <input @keydown="handleKeyDown">
</template>

🛠️ 实用工具类型

1. 内置的 DOM 工具类型

// 获取元素属性的类型
type InputValue = HTMLInputElement['value']  // string
type ButtonType = HTMLButtonElement['type']  // 'button' | 'submit' | 'reset'
type ElementTag = HTMLElement['tagName']  // string

// Partial 应用于 DOM 配置
interface CanvasConfig {
  width: number
  height: number
  context: CanvasRenderingContext2D
}
const config: Partial<CanvasConfig> = { width: 800 }

// 只读 DOM 引用
const readonlyElement: Readonly<HTMLElement> = document.body
// readonlyElement.innerHTML = '' // ❌ 错误

2. 自定义 DOM 类型

// 自定义数据属性
interface DatasetMap {
  userId: string
  role: 'admin' | 'user'
  theme: 'light' | 'dark'
}

function setDataset<T extends HTMLElement>(
  el: T,
  data: Partial<{ [K in keyof DatasetMap]: string }>
) {
  Object.entries(data).forEach(([key, value]) => {
    el.dataset[key] = value
  })
}

const div = document.createElement('div')
setDataset(div, { userId: '123', role: 'admin' })
// div.dataset.userId 有类型提示

// 带状态的自定义元素
interface StatefulElement<T> extends HTMLElement {
  state: T
  setState: (newState: Partial<T>) => void
}

interface ButtonState {
  loading: boolean
  count: number
}

const btn = document.createElement('button') as StatefulElement<ButtonState>
btn.state = { loading: false, count: 0 }
btn.setState({ loading: true })

📝 实际开发模式

1. 工厂函数模式

// 类型安全的元素创建
function createElement<K extends keyof HTMLElementTagNameMap>(
  tagName: K,
  props?: Partial<HTMLElementTagNameMap[K]>
): HTMLElementTagNameMap[K] {
  const el = document.createElement(tagName)
  if (props) {
    Object.assign(el, props)
  }
  return el
}

// 使用示例
const button = createElement('button', {
  textContent: '点击',
  disabled: false,
  className: 'btn-primary'
})

const input = createElement('input', {
  type: 'email',
  placeholder: '输入邮箱',
  value: ''
})

2. 类型守卫工具

// DOM 类型守卫集合
const domGuards = {
  isInput: (el: Element | null): el is HTMLInputElement => 
    el?.tagName === 'INPUT',
  
  isButton: (el: Element | null): el is HTMLButtonElement => 
    el?.tagName === 'BUTTON',
  
  isSelect: (el: Element | null): el is HTMLSelectElement => 
    el?.tagName === 'SELECT',
  
  isTextArea: (el: Element | null): el is HTMLTextAreaElement => 
    el?.tagName === 'TEXTAREA',
  
  isForm: (el: Element | null): el is HTMLFormElement => 
    el?.tagName === 'FORM'
}

// 使用
const element = document.getElementById('myInput')
if (domGuards.isInput(element)) {
  element.value = '类型安全'
}

3. Vue 3 中的 DOM 模板引用

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// 基本元素引用
const inputRef = ref<HTMLInputElement | null>(null)
const divRef = ref<HTMLDivElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)

// 多个元素引用
const itemRefs = ref<HTMLElement[]>([])

// 组件引用
import Modal from './Modal.vue'
const modalRef = ref<InstanceType<typeof Modal> | null>(null)

onMounted(() => {
  // 类型安全的方法调用
  inputRef.value?.focus()
  canvasRef.value?.getContext('2d')
  modalRef.value?.open()
})
</script>

<template>
  <input ref="inputRef" type="text">
  <div ref="divRef" class="container"></div>
  <canvas ref="canvasRef"></canvas>
  
  <!-- 多个元素 -->
  <div 
    v-for="item in 5" 
    :ref="el => itemRefs.push(el as HTMLElement)"
    :key="item"
  >
    Item {{ item }}
  </div>
  
  <Modal ref="modalRef" />
</template>

📊 类型层次结构

EventTarget (所有事件目标的基类)
    ├── Node (所有 DOM 节点的基类)
    │   ├── Element (所有元素的基类)
    │   │   ├── HTMLElement (HTML 元素的基类)
    │   │   │   ├── HTMLInputElement
    │   │   │   ├── HTMLButtonElement
    │   │   │   ├── HTMLDivElement
    │   │   │   └── ...
    │   │   └── SVGElement (SVG 元素)
    │   ├── Text (文本节点)
    │   └── Comment (注释节点)
    ├── Window (窗口对象)
    └── Document (文档对象)

掌握这些 DOM 类型,可以让你在处理浏览器 API 时获得完整的类型支持和智能提示,减少运行时错误。

春节后,有些公司明确要求 AI 经验了

作者 张拭心
2026年3月3日 11:10

大家好,我是拭心。

昨天晚上,有个群友说:

我看 boss 直聘已经有些公司明确要求要 AI 经验了,之前是大厂先搞,现在中小开始反应过来了。

是的,这个招聘趋势已经越来越明显。不只是招聘,春节以后,很多公司推 AI 的力度也变得更强,从可选变成强制。

babaeae4cc2c5a2f392fe359f41401c8.jpg

根据 BS 招聘网站发布的数据,近 20% 的非 AI 专业岗位明确要求具备 AI 能力,这个数字在 2024 年还只有 12%。

两年时间,AI 技能从「加分项」跃升为「硬指标」

这篇文章,我们来看看 Android、前端、后端三个方向,招聘市场究竟在发生什么变化。


一、Android 岗位:AI 赋能移动端,薪资溢价明显

移动端开发曾经是一片红海,但 AI Native 应用的兴起,正在重新定义这个赛道。

大厂们不再满足于把 AI 功能“接进来用用”,而是要从架构层就以 AI 为核心来设计产品。

来看一个典型的岗位:

Android 开发工程师 - AI 创新应用

某知名电子商务公司(上海)

薪资:30-60K · 14 薪(年薪约 42-84 万)

要求:3-5 年经验,本科;

负责从 0 到 1 创造「AI Native」应用,结合 AI 模型能力打造优秀用户体验;深入掌握 Java/Kotlin,熟悉性能分析和优化方法

注意这里的关键词:AI Native。不是“会用 AI 工具”,不是“了解 LLM 概念”,而是要求从底层就以 AI 为出发点来设计应用。

这和传统 Android 岗位的差距有多大?

传统 JD 写的是「熟悉 Jetpack、掌握性能优化」,而 AI Native 岗位写的是「结合 AI 模型能力打造用户体验」。

前者考察的是工程实现能力,后者考察的是对模型能力的理解——你得知道大模型能做什么、不能做什么,才能在产品设计阶段就做出正确的判断,而不是在开发到一半时发现模型根本撑不起这个交互逻辑。

薪资上限 84 万,对应的正是这层能力溢价。同样是 3-5 年经验的 Android 工程师,懂 AI 和不懂 AI,市场给出的价格已经开始分叉


二、前端岗位:AI 产品化需求爆发,技术门槛提升

前端这个方向,变化可能是最剧烈的。

以往前端更多做展示,但 AI Agent、多模态交互的爆发,把前端工程师直接推到了 AI 应用落地的最前线。

来看三个岗位:

前端开发工程师 - AI 创新应用 P6+

某知名大型电子商务公司(上海 / 北京)

薪资:35-65K(年薪约 42-78 万)

要求:3-5 年经验,本科;了解 AI 和机器学习概念并集成到生产环境;有 webpack、React Native、小程序、后端(Python/Java)相关开发经验

「集成到生产环境」是关键——不是做个 demo,而是要真正跑在线上。

这意味着你得懂模型调用的稳定性、延迟控制、降级策略,工程能力和 AI 能力缺一不可。

AI 前端专家

某知名大型电子商务公司(北京)

薪资:50-80K · 16 薪(年薪约 80-128 万)

要求:5-10 年经验,本科;负责 AI 助理生产力平台产品迭代和技术架构精进;有 AI 相关产品研发经验,熟悉 React/Vue 等主流框架

从「集成 AI 功能」到「负责 AI 平台的技术架构」,这一级跳跃对应的是年薪从 78 万到 128 万。

最值得关注的,是 Agent 工程师这个新职位正在快速成型:

Agent 工程师

某上海知名互联网上市公司(北京)

薪资:40-70K · 15 薪(年薪约 60-105 万)

要求:3-5 年经验,本科;专注于 AI Agent / AI App 方向;有 Web 应用端到端性能优化经验,熟悉 Hybrid 容器技术者优先

从 P6+ 到 AI 前端专家,薪资从 78 万跨越到 128 万,差距的核心只有一个:有没有 AI 产品研发经验

“Agent 前端工程师”这个新职位,一年前几乎不存在,现在已经成为大厂争抢的稀缺岗位,年薪 60-105 万。

前端的边界在 AI 时代被彻底重新定义了


三、后端岗位:AI 工程化落地,架构能力成核心

后端方向的变化同样剧烈。

AI 模型本身不难调用,真正难的是把它做成稳定可靠的生产级系统——高可用、低延迟、可扩展。

这恰好是后端工程师的主场,但前提是你得先懂 AI。

AI 应用后端工程师

某大型互联网上市公司(北京)

薪资:25-50K · 16 薪(年薪约 40-80 万)

要求:3-5 年经验,本科;负责 AI Agent 核心架构设计与开发,构建高可用、低延迟的分布式系统;精通 Java/Python,熟悉 gRPC、Kafka、Redis 等中间件;熟悉 LangChain、AutoGPT 等 Agent 框架优先

高级后端开发工程师 - AI 平台开发

阿里巴巴集团(杭州余杭区)

薪资:40-70K · 16 薪(年薪约 64-112 万)

要求:1-3 年经验,本科;专注于 AI 平台开发;参与过分布式 / 高并发场景系统设计优先

注意阿里这个岗位:1-3 年经验,但年薪最高 112 万

经验要求低,薪资反而高——这说明懂 AI 平台开发的工程师太稀缺了,公司宁愿给高薪也要抢到人。


四、岗位的共同点

把这些岗位放在一起看,有几个规律非常明显:

AI 技能是涨薪的最短路径。 同样是 3-5 年经验,普通 Android 岗位薪资上限约 30K,AI Native Android 岗位上限直接到 60K,溢价接近一倍。后端方向,阿里 AI 平台岗位要求仅 1-3 年经验,但年薪最高 112 万——懂 AI 的工程师太稀缺,公司宁愿用更高的薪资来弥补经验年限的不足。

Agent 开发成为新风口。 无论是前端的 Agent 前端工程师,还是后端的 AI Agent 架构师,这类岗位一年前几乎不存在,现在已经遍布大厂的 JD,年薪普遍在 60 万以上。需求爆发的速度,远远快于市场上供给人才的速度。

不是替换,是升维。 这一点值得认真说清楚——这些岗位没有一个要求你放弃原来的技术栈。JD 里写的是「精通 Java/Kotlin」「熟悉 React/Vue」「掌握 Spring Boot」,原有的工程能力仍然是基础门槛。AI 能力是叠加在上面的新一层,而不是替代。

对于已经有 3-5 年经验的开发者来说,你的积累没有白费,缺的只是补上 AI 这一层。窗口期就在当下,越早补,溢价越大。

我转型 AI 工程师的实战笔记与心血结晶都总结到这里了:《转型 AI 工程师|提升竞争力》

好了,这篇文章到这里就结束了,感谢你的阅读,愿你平安顺遂。

TS 常用工具类型

2026年3月3日 11:06

在 TypeScript 中,常用元素类型可以从两个维度来理解:一是基础数据类型(Basic Types),二是复合/高级类型(Advanced Types)。下面我将系统地整理这些类型,并附上实际开发中的使用场景。

📋 基础数据类型

1. 原始类型

// 基本类型
let isDone: boolean = false
let count: number = 42
let name: string = 'TypeScript'
let notDefined: undefined = undefined
let empty: null = null

// 特殊类型
let anything: any = '可以是任何值'  // 尽量避免使用
let unknownValue: unknown = 4       // 类型安全的 any
let nothing: void = undefined       // 函数无返回值
let neverReturns: never             // 永远不会发生的类型

2. 数组和元组

// 数组 (Array)
let numbers: number[] = [1, 2, 3]
let strings: Array<string> = ['a', 'b', 'c']
let mixed: (string | number)[] = [1, 'two', 3]

// 元组 (Tuple) - 固定长度和类型的数组
let user: [number, string, boolean] = [1, 'Alice', true]
let httpResponse: [number, string] = [200, 'OK']

// 可选元素的元组
let optionalTuple: [string, number?] = ['hello']
optionalTuple = ['hello', 123]

// 剩余元素的元组
let restTuple: [number, ...string[]] = [1, 'a', 'b', 'c']

3. 对象类型

// 简单对象类型
let point: { x: number; y: number } = { x: 10, y: 20 }

// 可选属性
let config: {
  url: string
  method?: 'GET' | 'POST'
  timeout?: number
} = { url: '/api' }

// 只读属性
let settings: {
  readonly id: string
  name: string
} = { id: '123', name: 'app' }
// settings.id = '456' // ❌ 错误

🎯 常用复合类型

1. 接口 (Interface)

// 基础接口
interface User {
  id: number
  name: string
  email: string
  age?: number  // 可选属性
  readonly createdAt: Date  // 只读属性
}

// 接口继承
interface Employee extends User {
  department: string
  salary: number
}

// 接口合并(声明合并)
interface Window {
  title: string
}
interface Window {
  width: number
}
// Window 现在有 title 和 width 两个属性

2. 类型别名 (Type Alias)

// 基本类型别名
type UserID = number | string
type Status = 'pending' | 'success' | 'error'

// 对象类型别名
type Point = {
  x: number
  y: number
}

// 联合类型
type Shape = Circle | Square | Triangle

// 交叉类型
type Draggable = { drag: () => void }
type Resizable = { resize: () => void }
type UIElement = Draggable & Resizable

// 函数类型
type Callback = (data: string) => void
type AsyncFunction<T> = () => Promise<T>

3. 函数类型

// 函数声明
function add(x: number, y: number): number {
  return x + y
}

// 函数表达式
const multiply: (x: number, y: number) => number = (x, y) => x * y

// 函数类型别名
type MathOperation = (a: number, b: number) => number
const divide: MathOperation = (a, b) => a / b

// 可选参数和默认参数
function greet(name: string, greeting: string = 'Hello'): string {
  return `${greeting}, ${name}`
}

// 剩余参数
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, curr) => acc + curr, 0)
}

// 函数重载
function process(input: string): string[]
function process(input: number): number[]
function process(input: string | number): string[] | number[] {
  if (typeof input === 'string') {
    return input.split('')
  }
  return Array.from({ length: input }, (_, i) => i)
}

🚀 高级类型

1. 泛型 (Generics)

// 泛型函数
function identity<T>(arg: T): T {
  return arg
}
const num = identity<number>(42)
const str = identity('hello')  // 类型推断

// 泛型接口
interface Box<T> {
  value: T
  getValue: () => T
}

// 泛型类
class Stack<T> {
  private items: T[] = []
  
  push(item: T): void {
    this.items.push(item)
  }
  
  pop(): T | undefined {
    return this.items.pop()
  }
}

// 泛型约束
interface Lengthwise {
  length: number
}
function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

2. 联合类型 (Union Types) 和交叉类型 (Intersection Types)

// 联合类型 - "或"
type ID = number | string
type Result = Success | Failure

// 类型守卫
function processId(id: ID): string {
  if (typeof id === 'string') {
    return id.toUpperCase()
  }
  return id.toString()
}

// 可辨识联合(Discriminated Unions)
interface Circle {
  kind: 'circle'
  radius: number
}
interface Square {
  kind: 'square'
  sideLength: number
}
type Shape = Circle | Square

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sideLength ** 2
  }
}

// 交叉类型 - "且"
interface Person {
  name: string
  age: number
}
interface Employee {
  company: string
  role: string
}
type Worker = Person & Employee
// Worker 同时拥有 Person 和 Employee 的所有属性

3. 类型守卫和类型断言

// 类型守卫
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

// 使用类型守卫
function process(value: string | number) {
  if (isString(value)) {
    return value.toUpperCase()  // TypeScript 知道这里是 string
  }
  return value.toFixed(2)       // TypeScript 知道这里是 number
}

// 类型断言
const canvas = document.getElementById('canvas') as HTMLCanvasElement
const input = <HTMLInputElement>document.querySelector('input')

// 非空断言
const element = document.querySelector('.exists')!
element.innerHTML = 'Hello'

// 双重断言(谨慎使用)
const expr = '42' as any as number

📦 内置工具类型

1. 常用的 Utility Types

interface Todo {
  title: string
  description: string
  completed: boolean
  createdAt: Date
}

// Partial - 所有属性可选
type PartialTodo = Partial<Todo>
// { title?: string; description?: string; completed?: boolean; createdAt?: Date }

// Required - 所有属性必选
type RequiredTodo = Required<PartialTodo>
// 全部变成必选

// Readonly - 所有属性只读
type ReadonlyTodo = Readonly<Todo>
// 不能修改属性

// Pick - 选取指定属性
type TodoPreview = Pick<Todo, 'title' | 'completed'>
// { title: string; completed: boolean }

// Omit - 排除指定属性
type TodoWithoutDate = Omit<Todo, 'createdAt'>
// { title: string; description: string; completed: boolean }

// Record - 键值对映射
type PageInfo = Record<'home' | 'about' | 'contact', { title: string }>
// {
//   home: { title: string }
//   about: { title: string }
//   contact: { title: string }
// }

// Exclude - 从联合类型中排除
type T = Exclude<'a' | 'b' | 'c', 'a'>  // 'b' | 'c'

// Extract - 提取共有类型
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>  // 'a'

// NonNullable - 排除 null 和 undefined
type T1 = NonNullable<string | number | null | undefined>  // string | number

// ReturnType - 获取函数返回类型
function createUser() {
  return { id: 1, name: 'John' }
}
type UserType = ReturnType<typeof createUser>  // { id: number; name: string }

// Parameters - 获取函数参数类型
type CreateUserParams = Parameters<typeof createUser>  // []

2. 条件类型

// 基础条件类型
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'>  // true
type B = IsString<number>    // false

// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never
type StrNumArr = ToArray<string | number>  // string[] | number[]

// infer 关键字
type UnpackPromise<T> = T extends Promise<infer U> ? U : T
type PromiseType = UnpackPromise<Promise<string>>  // string

// 内置条件类型
type NonFunction<T> = T extends Function ? never : T

🎨 实际应用场景示例

1. API 响应类型

// API 通用响应结构
interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
  timestamp: number
}

// 分页数据结构
interface PaginatedData<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
  hasMore: boolean
}

// 具体业务类型
interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

// 组合使用
type UserListResponse = ApiResponse<PaginatedData<User>>

2. Vue 3 组合式函数类型

// 异步状态管理
interface AsyncState<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<Error | null>
  execute: () => Promise<void>
}

// 分页状态
interface PaginationState<T> {
  list: Ref<T[]>
  currentPage: Ref<number>
  pageSize: Ref<number>
  total: Ref<number>
  loading: Ref<boolean>
  loadMore: () => Promise<void>
  refresh: () => Promise<void>
}

// 表单验证
type ValidationRule<T> = {
  validator: (value: T) => boolean
  message: string
}

type FormRules<T> = {
  [K in keyof T]?: ValidationRule<T[K]>[]
}

3. 事件处理类型

// DOM 事件
function handleClick(event: MouseEvent): void {
  console.log(event.clientX, event.clientY)
}

function handleChange(event: Event): void {
  const input = event.target as HTMLInputElement
  console.log(input.value)
}

// 自定义事件
interface CustomEvents {
  'user-login': { userId: number; timestamp: Date }
  'user-logout': { userId: number }
  'error': { message: string; code: number }
}

type EventCallback<T> = (data: T) => void

class EventEmitter {
  private events: Map<keyof CustomEvents, EventCallback<any>[]> = new Map()
  
  on<K extends keyof CustomEvents>(
    event: K,
    callback: EventCallback<CustomEvents[K]>
  ): void {
    // 实现
  }
  
  emit<K extends keyof CustomEvents>(
    event: K,
    data: CustomEvents[K]
  ): void {
    // 实现
  }
}

📌 类型定义的最佳实践

  1. 优先使用 interface 定义对象类型,特别是需要扩展的场景
  2. 使用 type 定义联合类型、交叉类型和工具类型
  3. 为所有 API 响应定义完整的类型
  4. 使用 readonly 防止意外修改
  5. 利用泛型提高代码复用性
  6. 避免使用 any,优先使用 unknown

这些是 TypeScript 中最常用和最重要的类型元素,掌握它们可以让你在日常开发中得心应手。

大文件切片上传

作者 小怪点点
2026年3月3日 11:01

秒是不用传,快传是接着传

  • 文件切片
  • 并发控制
  • 状态记录
文件切片

File.slice() 将大文件切分为若干小文件

上传与记录

并发上传这些切片,同时在前端缓存记录下哪些已上传成功

通知合并

所有切片上传完成后,前端发送一个请求通知后端,让后端把这些切片按顺序拼回一个完整的文件

// 1、生成文件hash
const fileHash = await calculateFileHash(file); 
// 2、分片大小
const CHUNK_SIZE = 2 * 1024 *1024;

const chunks = [];
let start = 0;
// 3、分片操作
while (start < file.size) {
    const end = Math.min(start + CHUNK_SIZE, file.size);
    chunks.push({
        file: file.slice(start, end),
        index: chunks.length,
        start,
        end,
        hash: fileHash // 用于服务端校验
    });
    start = end;
}
// 4、从本地存储获取已上传的分片记录
const uploadChunks = JSON.parse(localStorage.getItem(fileHash) || '[]');

// 5. 找出待上传的分片
const pendingChunks = chunks.filter(chunk => !uploadedChunks.includes(chunk.index));

// 6. 控制并发数(比如3个并发)
const CONCURRENCY = 3;
async function uploadWithConcurrency() {
  for (let i = 0; i < pendingChunks.length; i += CONCURRENCY) {
    const batch = pendingChunks.slice(i, i + CONCURRENCY);
    // 并发上传这一批
    await Promise.all(batch.map((chunk) => uploadChunk(chunk));
  }
}

// 7. 单个分片上传函数
async function uploadChunk(chunk) {
  const formData = new FormData();
  formData.append('file', chunk.file);
  formData.append('index', chunk.index);
  formData.append('hash', chunk.hash);
  formData.append('total', chunks.length); // 总分片数

  try {
    const res = await Request({
      url: 'https://your-api.com/upload/chunk',
      method: 'POST',
      data: formData,
      header: { 'Content-Type': 'multipart/form-data' }
    });

    if (res.data.success) {
      // 上传成功,记录到本地存储
      uploadedChunks.push(chunk.index);
      localStorage.setItem(fileHash, JSON.stringify(uploadedChunks));
      
      // 计算并更新进度
      const progress = (uploadedChunks.length / chunks.length) * 100;
      console.log(`上传进度: ${progress.toFixed(2)}%`);
    }
  } catch (error) {
    console.error(`分片 ${chunk.index} 上传失败:`, error);
    // 这里可以加入重试逻辑,例如重试3次
  }
}

// 8. 开始上传
uploadWithConcurrency().then(() => {
  // 所有分片上传完成,通知服务端合并
  Request({
    url: 'https://your-api.com/upload/merge',
    method: 'POST',
    data: {
      hash: fileHash,
      fileName: file.name,
      totalChunks: chunks.length
    }
  }).then(res => {
    console.log('文件上传成功!URL:', res.data.fileUrl);
    // 上传成功,清除本地记录
    localStorage.removeItem(fileHash);
  });
});

服务端要做的事

前端的工作如上,后端需要配合提供三个接口,这也是面试中常被问到的设计点 

  1. /upload/chunk:接收单个分片。通常会以文件hash_分片索引的格式临时存储。
  2. /upload/check(可选):查询已上传的分片。前端启动时调用,快速实现“断点续传”和“秒传”。
  3. /upload/merge:所有分片完成后,后端将临时分片合并,生成最终文件。

💡 高级优化与面试加分项

当你把这套逻辑讲清楚后,能主动提出以下优化点,会让面试官眼前一亮:

  • 秒传:在初始化上传前,先根据文件hash请求后端。如果文件已存在,直接返回成功,一秒完成 
  • Web Worker:将计算文件hash这种CPU密集型任务放到Web Worker里执行,避免阻塞页面渲染 
  • 本地存储选型:小文件、少量记录用localStorage;大文件、复杂记录用IndexedDB 
  • 暂停/恢复:通过AbortController来取消进行中的请求,实现暂停功能。恢复时,只需重新调用上传逻辑,pendingChunks会自动过滤掉已上传的部分 

Vue状态管理扫盲篇:Vuex 到 Pinia | 为什么大家都在迁移?核心用法对比

作者 SuperEugene
2026年3月3日 11:00

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。

一、先搞清楚:Vuex 和 Pinia 到底是啥?

1.1 一句话认识它们

  • Vuex:Vue 2 时代的官方状态管理库,通过集中式存储管理应用的全部状态。
  • Pinia:Vue 3 时代的推荐状态管理库,作者和 Vue 核心团队同一人,被当作 Vuex 5 的正式版。

1.2 为什么大家纷纷从 Vuex 迁到 Pinia?

对比维度 Vuex Pinia
心智负担 概念多(state/mutations/actions/getters) 概念简单(一个 store,其余都是普通函数)
TypeScript 类型支持一般 原生支持好
模块化 需自己设计 modules 天然多 store,无嵌套
Composition API 需配合 useStore 天然适配 setup
打包体积 相对大 更小
官方推荐 Vue 2 主力,Vue 3 仍可用 Vue 3 推荐首选

一句话:Pinia 更简单、更贴近 Vue 3,写起来更像普通 JS。

二、Vuex 核心用法:四大金刚

2.1 整体结构回顾

Vuex 的数据流可以记成:View → Actions → Mutations → State → View

  • state:唯一数据源
  • getters:可理解为“计算属性”
  • mutations:唯一能改 state 的地方(必须同步)
  • actions:可以异步,内部再 commit mutations

2.2 完整示例:用户购物车

// store/index.js (Vuex)
import { createStore } from 'vuex'

export default createStore({
  state: {
    cartItems: [],      // 购物车商品
    user: null          // 当前用户
  },
  
  getters: {
    // 购物车商品数量
    cartCount(state) {
      return state.cartItems.reduce((sum, item) => sum + item.quantity, 0)
    },
    // 购物车总价
    cartTotal(state) {
      return state.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
    }
  },
  
  mutations: {
    addToCart(state, product) {
      const exist = state.cartItems.find(item => item.id === product.id)
      if (exist) {
        exist.quantity++
      } else {
        state.cartItems.push({ ...product, quantity: 1 })
      }
    },
    setUser(state, user) {
      state.user = user
    }
  },
  
  actions: {
    // 异步:模拟登录
    async login({ commit }, credentials) {
      const res = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials)
      })
      const user = await res.json()
      commit('setUser', user)
      return user
    }
  }
})

2.3 在组件里怎么用?

<template>
  <div>
    <p>购物车:{{ cartCount }} 件,总价:{{ cartTotal }}</p>
    <button @click="addProduct">加入购物车</button>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

const store = useStore()

// 读 state / getters
const cartCount = computed(() => store.getters.cartCount)
const cartTotal = computed(() => store.getters.cartTotal)

// 触发 mutation(同步)
function addProduct() {
  store.commit('addToCart', { id: 1, name: '商品A', price: 99 })
}

// 触发 action(异步)
async function handleLogin() {
  await store.dispatch('login', { username: 'admin', password: '123' })
}
</script>

2.4 Vuex 容易踩的坑

  1. 忘记 mutations:直接 state.xxx = xxx 在严格模式下会报错,只能通过 mutation 修改。
  2. 在 mutation 里写异步:理论上必须同步,写异步会导致难以追踪、调试困难。
  3. 命名冲突:多个 module 时,getter/mutation/action 可能重名,需要命名空间。

三、Pinia 核心用法:一个 Store 搞定

3.1 设计思路

Pinia 不再区分 mutations 和 actions,只有:

  • state:数据
  • getters:计算属性
  • actions:既可以同步也可以异步,直接改 state

3.2 完整示例:同一需求用 Pinia 写

// stores/cart.js (Pinia - Options 风格)
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    cartItems: [],
    user: null
  }),
  
  getters: {
    cartCount(state) {
      return state.cartItems.reduce((sum, item) => sum + item.quantity, 0)
    },
    cartTotal(state) {
      return state.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
    }
  },
  
  actions: {
    addToCart(product) {
      const exist = this.cartItems.find(item => item.id === product.id)
      if (exist) {
        exist.quantity++
      } else {
        this.cartItems.push({ ...product, quantity: 1 })
      }
    },
    async login(credentials) {
      const res = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials)
      })
      const user = await res.json()
      this.user = user  // 直接改 state,不需要 mutation!
      return user
    }
  }
})

3.3 Setup Store 风格(更贴近 Composition API)

// stores/cart.js (Pinia - Setup Store 风格)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // state:用 ref/reactive
  const cartItems = ref([])
  const user = ref(null)
  
  // getters:用 computed
  const cartCount = computed(() => 
    cartItems.value.reduce((sum, item) => sum + item.quantity, 0)
  )
  const cartTotal = computed(() => 
    cartItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
  
  // actions:普通函数
  function addToCart(product) {
    const exist = cartItems.value.find(item => item.id === product.id)
    if (exist) {
      exist.quantity++
    } else {
      cartItems.value.push({ ...product, quantity: 1 })
    }
  }
  
  async function login(credentials) {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
    const data = await res.json()
    user.value = data
    return data
  }
  
  // 必须 return 出去,组件才能用
  return {
    cartItems,
    user,
    cartCount,
    cartTotal,
    addToCart,
    login
  }
})

3.4 在组件里怎么用?

<template>
  <div>
    <p>购物车:{{ cartStore.cartCount }} 件,总价:{{ cartStore.cartTotal }}</p>
    <button @click="cartStore.addToCart({ id: 1, name: '商品A', price: 99 })">
      加入购物车
    </button>
  </div>
</template>

<script setup>
import { useCartStore } from '@/stores/cart'

const cartStore = useCartStore()

// 用 storeToRefs 解构,保持响应式(getters 和 state 需要)
// 如果直接解构 const { cartCount } = cartStore,会丢失响应式!
import { storeToRefs } from 'pinia'
const { cartCount, cartTotal } = storeToRefs(cartStore)

// actions 直接解构没问题
const { addToCart, login } = cartStore
</script>

四、核心概念对照表

概念 Vuex Pinia (Options) Pinia (Setup)
定义数据 state: { } state: () => ({ }) ref() / reactive()
计算属性 getters: { } getters: { } computed()
修改数据(同步) mutations actions 里直接改 this.xxx 直接改 ref.value
修改数据(异步) actions + commit actions 里直接改 在函数里直接改
组件调用 store.commit() / store.dispatch() store.xxx() store.xxx()

五、迁移时的常见坑

5.1 解构 store 丢响应式

// ❌ 错误:直接解构会丢失响应式
const { cartCount } = useCartStore()

// ✅ 正确:用 storeToRefs
const { cartCount } = storeToRefs(useCartStore())

5.2 Setup Store 忘记 return

// ❌ 错误:没 return,组件拿不到
export const useCartStore = defineStore('cart', () => {
  const count = ref(0)
  function add() { count.value++ }
  // 忘记 return!
})

// ✅ 正确
return { count, add }

5.3 多个 store 之间互相调用

关于这个坑的问题,我在初学的时候看到这个概念其实并不理解。所以我决定在这里展开的说一说。

在 Pinia 中,多个 store 互相调用是开发中很常见的需求(比如「订单 store」需要用到「购物车 store」的商品数据),但新手很容易因为调用时机不对写出“死锁代码”。

本节会用「大白话+实战代码」,教你安全调用的核心规则避坑要点,以及新手最易踩的雷,保证看完就能上手。

一、核心结论(先记重点)

多个 store 之间可以互相调用,但必须遵守一个黄金法则:

只存引用,延迟使用:在 store 的 setup 函数顶层,只能获取另一个 store 的「实例引用」;读取数据、调用方法的操作,必须放到函数(action)内部执行。

❌ 绝对禁止:在 setup 顶层直接读取另一个 store 的数据(会触发“互相等待”的死锁)。

二、安全写法(直接抄作业)

以「订单 store(order.js)」调用「购物车 store(cart.js)」为例,实现下单时获取购物车商品的功能。

步骤1:创建被调用的购物车 store(cart.js)

// stores/cart.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 购物车商品列表(state)
  const cartItems = ref([
    { id: 1, name: '新手小白入门教程', price: 99 },
    { id: 2, name: 'Pinia 避坑手册', price: 59 }
  ])

  // 简单的方法(action),方便后续被调用
  function clearCart() {
    cartItems.value = []
  }

  return { cartItems, clearCart }
})
步骤2:创建调用方订单 store(order.js)

// stores/order.js
import { defineStore } from 'pinia'
// 1. 导入购物车 store 的创建函数
import { useCartStore } from './cart'

export const useOrderStore = defineStore('order', () => {
  // ✅ 安全操作:在 setup 顶层只获取 cartStore 的「实例引用」
  // 此时只是“记下来购物车的地址”,不会读取数据、不会触发死锁
  const cartStore = useCartStore() 

  // 2. 核心:把“使用 cartStore”的逻辑,放到 action 函数内部
  function checkout() {
    // 🎯 延迟使用:只有调用 checkout 时,才会真正读取 cartStore 的数据
    // 此时两个 store 都已初始化完成,数据可正常获取
    console.log('下单商品:', cartStore.cartItems)
    
    // 也可以调用另一个 store 的方法
    if (cartStore.cartItems.length > 0) {
      console.log('下单成功,清空购物车!')
      cartStore.clearCart()
    } else {
      console.log('购物车为空,无法下单!')
    }
  }

  return { checkout }
})
步骤3:在组件中使用(验证效果)

<template>
  <button @click="handleCheckout">点击下单</button>
</template>

<script setup>
// 导入订单 store
import { useOrderStore } from '@/stores/order'

const orderStore = useOrderStore()

// 点击按钮触发下单逻辑
const handleCheckout = () => {
  orderStore.checkout()
}
</script>

点击按钮后,控制台会输出:


下单商品: [{ id: 1, ... }, { id: 2, ... }]
下单成功,清空购物车!

三、为什么要这样写?(大白话讲透“死锁”)

新手最疑惑的是:为什么不能在 setup 顶层直接读数据? 我们用“两个人出门”的例子,讲透背后的逻辑:

1. 安全写法的执行流程(无死锁)

就像两个人(orderStore 和 cartStore)先各自出门(完成初始化),再互相帮忙:

  1. 组件调用 useOrderStore() → orderStore 开始初始化:只做了一件事——“记下 cartStore 的地址”(const cartStore = useCartStore()),自己先完成初始化。

  2. 后续调用 checkout() 时:orderStore 带着“地址”去找 cartStore,此时 cartStore 早就初始化好了,能顺利拿到商品数据。

2. 新手踩坑写法(触发死锁)

如果在 setup 顶层直接读数据,就变成了两个人互相卡条件


// ❌ 错误示例:order.js(千万别这么写!)
export const useOrderStore = defineStore('order', () => {
  const cartStore = useCartStore()
  // ❌ 致命错误:setup 顶层直接读取 cartStore 的数据
  const goods = cartStore.cartItems 

  function checkout() {
    console.log(goods)
  }

  return { checkout }
})

此时的执行流程就会“僵持住”:

  1. orderStore 初始化时,要求“先拿到 cartStore 的商品数据,才能完成初始化”。

  2. 于是去调用 useCartStore(),让 cartStore 初始化。

  3. 如果 cartStore 也在顶层读 orderStore 的数据,就会变成:order 等 cart 给数据,cart 等 order 给数据,俩人都卡着不动 → 代码报错(死锁)。

四、进阶:两个 store 互相调用(依然安全)

如果购物车 store 也需要调用订单 store 的数据,只要遵守「函数内使用」的规则,完全没问题!

补全 cart.js,新增“查看订单状态”的方法:

// stores/cart.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 导入订单 store
import { useOrderStore } from './order'

export const useCartStore = defineStore('cart', () => {
  const cartItems = ref([{ id: 1, name: '新手小白入门教程', price: 99 }])
  
  // ✅ 安全:在函数内调用订单 store
  function checkOrderStatus() {
    const orderStore = useOrderStore()
    // 假设 orderStore 有一个 orderStatus 状态
    console.log('当前订单状态:', orderStore.orderStatus)
  }

  function clearCart() {
    cartItems.value = []
  }

  return { cartItems, clearCart, checkOrderStatus }
})
// stores/order.js 补充 orderStatus 状态
export const useOrderStore = defineStore('order', () => {
  const cartStore = useCartStore()
  // 新增订单状态
  const orderStatus = ref('未支付')

  function checkout() {
    console.log('下单商品:', cartStore.cartItems)
    orderStatus.value = '已支付'
  }

  return { checkout, orderStatus }
})

此时两个 store 互相调用,但因为所有“使用对方”的逻辑都在函数内,初始化阶段互不干扰,完全不会死锁!

五、汇总一下

  1. defineStore同步函数,其 setup 回调不能加 async(异步逻辑只能写在 action 里)。

  2. ✅ 跨 store 调用的核心:setup 顶层只存引用,函数内部才使用

  3. ❌ 禁止在 setup 顶层直接读取另一个 store 的 state/getters(必触发死锁)。

  4. ❌ 禁止在 setup 顶层使用 await(既不支持,也会导致初始化异常)。

5.4 Pinia 需要先挂载

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())  // 必须在 createApp 之后、mount 之前
app.mount('#app')

六、日常开发该怎么选?

  • 新项目(Vue 3):优先用 Pinia,尤其 Setup Store 风格,和 Composition API 很契合。
  • 老项目(Vue 2 + Vuex):如果项目稳定、迁移成本高,可以先不急着迁;要升级 Vue 3 时,顺带迁到 Pinia 更合适。
  • 团队习惯:如果团队已经统一用 Vuex 且运转良好,不必为了“新”而强行迁移,关键是统一和维护成本。

七、总结

维度 Vuex Pinia
概念数量 4 个(state/getters/mutations/actions) 3 个(state/getters/actions)
改数据方式 只能通过 mutation(同步) actions 直接改(同步/异步都可)
风格 偏“流程化” 更接近普通函数、Composition API
学习成本 中等 较低

一句话:Pinia 用更少的概念、更直接的方式完成同样的状态管理,而且和 Vue 3 配合更好。


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

我会如何考核一个在简历里大谈 AI 提效的高级前端?

作者 ErpanOmer
2026年3月3日 10:57

节日期间,尤其是最近掘金首页有点没法看🤷‍♂️。

满脸写着无奈.gif

点开全是 OpenClaw 怎么提效、怎么用 AI Prompt 写个 TodoList、怎么十分钟上线一个营销页。同质化严重到让人怀疑大家是不是共用了一个脑子🤯🤯。

作为面了不下百号人的老兵,我实话实说:在 2026 年,如果你简历里写熟练使用 AI 提效,对我来说几乎是废话。 现在的行情是,初级和中级前端确实正在被 AI 批量取代,而很多所谓的高级前端,只是学会了怎么更快地拉一坨更大的屎。

今天聊透一点:作为一个 9 年经验的面试者,我到底会怎么考核那些大谈 AI 提效的前端。


要警惕AI 幻觉带来的技术债!

现在的 AI 确实强,它生成的组件逻辑看起来天衣无缝。但问题就在这儿——它只负责跑通,不负责善后。

很多号称提效 50% 的候选人,本质上是在用未来的维护成本换现在的开发速度。面试时,我会拿出一个复杂的业务逻辑,让他用 AI 生成,然后我只问一个点: 这段代码里,AI 隐瞒了哪些潜在的副作用?

资深前端得能看出来:

AI 特别喜欢在 useEffect 或者最新的 Signal 监听里写闭包,层级一深,内存泄漏稳稳的。你发现了吗?

异步请求连发的时候,AI 往往不会帮你写 AbortController,它默认你的网络永远是理想状态。

为了实现一个简单功能,AI 可能会顺手给你引入一个 200KB 的第三方库,而 9 年经验的你应该知道怎么用 10 行原生代码搞定。

如果你看不出 AI 代码里的屎山,那你不是在提效,你是在给项目埋雷😒。

代码架构的坍塌

这是我最担心的。以前我们写代码,脑子里有清晰的模块边界、职责划分。现在有了 AI,大家习惯了 喂一段 Prompt,拿一段代码

后果就是,项目的熵增速度快得惊人。

我会考核候选人: 当 AI 生成的代码风格与你现有的 Monorepo 规范冲突时,你是怎么做约束的?

  • 你有没有沉淀出一套针对 AI 的 CursorRules 或者 Type Definition 约束层?
  • 你是如何保证 AI 生成的业务逻辑不会击穿你的领域模型(Domain Model)?

资深和普通人的区别在于:普通人被 AI 牵着鼻子走,架构师把 AI 关在规范的笼子里。 如果你只会复制粘贴 AI 给出的 Fragment,那你根本撑不起资深这两个字🤔。

技术底层能力

很多候选人现在离了 AI 连 Event Loop 的微任务宏任务执行顺序都讲不清楚了,更别说 WebAssembly 或者 WebGPU 的内存管理。

我会问一个很现实的问题: 当线上出现了一个 AI 无法复现、无法理解的线上 Crash(比如由于浏览器内核版本导致的渲染层级错乱),你的排查思路是什么?

这时候 AI 可帮不了你😃。

它没法帮你分析 Chrome 的 Memory Heap,也没法帮你去翻 WebKit 的源码。如果你把提效省下来的时间全用来摸鱼,而不是去深挖这些 AI 触碰不到的底层,那你很快就会被下一代更便宜的 AI 操作员取代。

提效的意义,是为了腾出时间去研究那些 AI 还没学会的硬核技术,而不是心安理得地退化成一个 Prompt 搬运工。

我平常问的三个问题

如果你的简历里写了 AI 提效,我会这么面你:

1.如果 AI 改动了底层公共组件,你如何确保它在线上环境下导致线上崩盘?

2.你的项目里,有哪些模块是你明确禁止 AI 介入的?理由是什么?(考察对业务核心逻辑的洞察力)

3.关于你的审美,在 UI 风格高度同质化的今天,如果你用的组件库和交互全是 AI 生成的,你如何通过前端工程化手段,去实现那种 AI 模拟不出来的、极致的用户交互体验?


最后

现在的掘金,吹捧技术的人太多,反思技术的人太少😖。

OpenClaw 确实是个里程碑,它让我们的双手得到了解放。但作为一个 9 年的前端,我必须提醒你:手闲下来了,脑子得转得更凶。

我面试时想看到的,不是你如何熟练地调教 AI,而是你作为一个开发,在面对复杂、混乱、不可预测的业务场景时,那份超越算法的判断力

如果你连 AI 生成的代码都 Review 不明白,那你的 9 年经验,可能真的只是 1 年经验重复了 9 次而已。

你们说是不是呢?

谢谢大家.gif

pxcharts-vue:一款专为 Vue3 打造的开源多维表格解决方案

作者 徐小夕
2026年3月3日 10:54

去年和大家分享了我的AI产品 pxcharts 超级表格的创业故事:

图片

同时我们也利用业余时间,基于国内公司最喜欢的技术栈Vue3全家桶,偷偷做了一款完全开源版的多维表格 pxcharts-vue:

图片

设计风格完全对标飞书和钉钉AI表格,大家可以基于这个方案轻松实现多维表格产品。话不多说,先上开源地址:

github.com/MrXujiang/p…

为什么要做pxcharts-vue多维表格

图片

我一直认为,在数据可视化与多维数据处理的场景中,表格始终是核心载体,但市面上多数表格组件往往局限于二维结构,难以满足复杂的多维数据展示、分析需求。

在实际的业务开发中,我们频繁遇到这类需求:

  • 电商行业的多维度经营数据(时间、地区、品类、销售额交叉分析);
  • 金融领域的多指标风控数据(客户维度、产品维度、时间维度的风险值展示);
  • 企业 BI 系统的多维报表(多维度钻取、联动、聚合)。

传统二维表格需要大量二次开发才能适配多维场景,且易出现代码冗余、性能卡顿等问题。

因此,我们决定从零开始,打造一款原生支持多维数据结构、轻量化且高度可定制的 Vue 版多维表格组件 ——pxcharts-vue。

图片

核心特性我总结如下:

  • 🎯 多维表格 - 灵活的数据视图切换(表格视图、看板视图、日历视图)
  • 🎨 低代码表单设计器 - 拖拽式表单构建,支持丰富的表单组件和自定义配置
  • 📊 数据可视化 - 集成 ECharts 图表库,支持多种图表类型和自定义配置
  • 📝 富文本编辑器 - 基于 Tiptap 的强大编辑能力,支持图片、链接、文本样式等
  • 🎭 模板市场 - 内置丰富的行业模板,快速启动项目
  • 👥 团队协作 - 支持多团队管理、成员邀请、权限控制
  • 🎪 水印编辑器 - 自定义水印样式,保护数据安全
  • 📁 文件上传 - 完善的文件管理功能
  • 🌓 响应式设计 - 适配各种屏幕尺寸,提供优质的移动端体验

下面我会和大家分享一下我们这个项目使用到的技术方案和功能亮点,供大家参考研究。

pxcharts-vue 技术架构设计和核心功能设计

先分享一下我们多维表格前端架构设计:

图片

核心技术实现

1. 多维表格系统

图片

技术方案

  • 基于 vue3-grid-layout-next 实现灵活的网格布局
  • 使用 sortablejs 实现拖拽排序功能
  • 虚拟滚动优化大数据量渲染性能

关键代码结构

src/components/DataTable/
├── GridView.vue          # 网格视图
├── KanbanView.vue        # 看板视图
├── CalendarView.vue      # 日历视图
└── TableConfig.vue       # 表格配置

2. 表单设计器

图片

技术方案

  • 自研拖拽引擎,支持组件拖拽、排序、嵌套
  • 配置化表单渲染,支持动态表单验证
  • JSON Schema 驱动的表单配置

实现特点

  • 左侧组件面板 - 组件分类、搜索、预览
  • 中间画布区域 - 实时预览、拖拽编辑
  • 右侧属性配置 - 动态表单、样式配置、事件绑定

3. 数据可视化

图片

技术方案

  • 深度集成 ECharts 6.0,封装图表组件
  • 支持图表主题定制、响应式布局
  • 提供图表二次编辑能力

支持图表类型

  • 折线图、柱状图、饼图、散点图
  • 雷达图、仪表盘、漏斗图
  • 地图、关系图、树图等高级图表

4. 富文本编辑器

图片

技术方案

  • 基于 Tiptap 构建,扩展自定义节点
  • 支持图片上传、链接插入、文本格式化
  • Markdown 快捷键支持

当然我们也实现了看板视图,大家可以开箱即用:

图片

基本上完成了多维表格70%以上的功能,大家只需要基于 pxcharts-vue 的开源版本,进行二次开发,即可实现复杂的多维表格产品。pxcharts-vue 技术栈

前端核心库:

技术 版本 说明
Vue 3 ^3.5.18 渐进式 JavaScript 框架
TypeScript ~5.8.0 JavaScript 的超集,提供类型检查
Vite ^7.0.6 下一代前端构建工具
Vue Router ^4.5.1 Vue.js 官方路由管理器
Pinia ^3.0.3 Vue 3 状态管理库

UI 与组件库:

技术 版本 说明
TDesign Vue Next ^1.16.1 企业级 UI 组件库
ECharts ^6.0.0 数据可视化图表库
Tiptap ^3.10.7 富文本编辑器框架
Lucide Vue Next ^0.548.0 精美的图标库

功能增强:

技术 版本 说明
Axios ^1.11.0 HTTP 请求库
Sortable.js ^1.15.6 拖拽排序库
Vue3 Grid Layout Next ^1.0.7 网格布局组件
Day.js ^1.11.19 轻量级日期处理库
NProgress ^0.2.0 页面加载进度条
Mitt ^3.0.1 事件总线
Lodash ^4.17.21 JavaScript 工具库

开发工具:

技术 版本 说明
ESLint ^9.31.0 代码检查工具
Prettier 3.6.2 代码格式化工具
Vue DevTools ^8.0.0 Vue 开发调试工具
unplugin-auto-import ^20.1.0 自动导入 API
unplugin-vue-components ^29.0.0 自动导入组件

快速开始

环境要求

  • Node.js >= 20.19.0 或 >= 22.12.0
  • pnpm >= 8.0.0 (推荐) / npm >= 9.0.0 / yarn >= 1.22.0

安装依赖

# 克隆项目
git clone https://github.com/MrXujiang/pxcharts-vue.git

# 进入项目目录
cd pxcharts-vue

# 安装依赖(推荐使用 pnpm)
pnpm install
# 或者
npm install

开发运行

# 启动开发服务器
pnpm dev

# 访问 http://localhost:5173

构建部署

# 生产环境构建
pnpm build

# 预览构建结果
pnpm preview

代码规范

# 代码检查
pnpm lint

# 代码格式化
pnpm format

后续我会写2篇详细的产品介绍和功能技术实现的文章,让大家更全面的了解pxcharts-vue这款开源多维表格项目,大家感兴趣可以学习研究一下。

如果你也在寻找一款开箱即用的多维表格解决方案,如果你相信数据协作还有更好的可能,欢迎来 GitHub 搜索 pxcharts-vue,或者访问我们的演示网站。你可以免费使用,可以贡献代码,也可以在留言区交流反馈。

pxcharts-vue 很多功能需要优化,欢迎大家共建。


作者:pxcharts创始人,前大厂架构师,坚信好的工具应该让人忘记工具本身的存在。

github地址:github.com/MrXujiang/p…

Vue3 中 emit 能 await 吗?事件机制里的异步陷阱

2026年3月3日 10:41

一个看起来"理所当然"的写法

某天你在写一个表单弹窗组件,子组件提交数据,父组件负责调接口保存。你顺手写下了这段代码:

// 子组件:提交按钮
const handleSubmit = async () => {
  loading.value = true
  await emit('submit', formData)  // ❌ 看似合理:等父组件保存完再关弹窗
  loading.value = false
  emit('close')
}

看起来没毛病对吧?emit 提交,等父组件处理完,关弹窗。逻辑清晰,语义明确。

然后你发现:loading 闪了一下就没了,弹窗瞬间关闭,接口还没返回。

你 await 了个寂寞。


为什么 await emit 不等于"等父组件执行完"?

很多人把 emit 理解为"调用父组件的方法"——这个理解对了一半,但恰好是错的那一半坑了你。

emit 的本质:同步的函数调用

Vue 的事件机制不是浏览器的 EventEmitter,也不是 Node.js 的事件循环。它的底层实现极其简单:

// Vue3 emit 的核心逻辑(简化版)
function emit(instance, event, ...args) {
  const props = instance.vnode.props || {}

  // 'submit' → 'onSubmit'
  const handlerName = `on${event[0].toUpperCase()}${event.slice(1)}`
  const handler = props[handlerName]

  if (handler) {
    // 就是直接调用,没有任何异步包装
    callWithAsyncErrorHandling(handler, instance, args)
  }
}

emit 就是从 props 里找到对应的回调函数,直接调用。 没有事件队列,没有微任务,没有 Promise 包装。本质上等价于:

// emit('submit', data) 就是:
props.onSubmit(data)

就这么朴素。像你在对象上调方法一样朴素。


那 await emit(...) 到底 await 到了什么?

JavaScript 里 await 一个非 Promise 的值会立即返回:

const result = await 42          // result === 42,立即返回
const result2 = await undefined  // result2 === undefined,立即返回
const result3 = await emit('submit', data) // 取决于父组件回调的返回值

所以关键问题是:父组件的事件处理函数返回了什么?

场景一:父组件返回普通值(await 无效)

// ❌ 父组件:没有 return,也没有 await
const onSubmit = (data) => {
  api.save(data)
  console.log('已发送请求')
}
// 子组件
await emit('submit', formData)
// ↑ onSubmit 返回 undefined → await undefined → 立即继续
// 此时接口还在飞,弹窗已经关了

场景二:父组件返回 Promise(await 碰巧生效)

// 父组件:async 函数自动返回 Promise
const onSubmit = async (data) => {
  await api.save(data)
  message.success('保存成功')
}
// 子组件
await emit('submit', formData)
// ↑ onSubmit 是 async 函数,返回 Promise → await 真正等待了
loading.value = false  // 时机正确

等等,这不是能 await 吗?!

能。但这是一个危险的巧合,不是一个可靠的契约


为什么说"能用"不等于"该用"?

问题一:隐式契约,没有类型保障

const emit = defineEmits<{
  submit: [data: FormData]  // 返回值类型?不存在的
}>()

defineEmits 的类型系统只约束参数,不约束返回值。子组件根本不知道父组件会返回什么。

今天父组件的同事写了 async,明天换个人维护去掉了 async,你的子组件就悄悄坏了。没有编译错误,没有运行时报错,只有一个"偶尔弹窗关太快"的玄学 bug。

问题二:多个监听器时行为不可预测

一个监听器还好。但如果事件通过 v-on="$attrs" 透传,或组件被包了一层 wrapper,监听器可能不止一个。这时候 emit 的返回值是哪个处理器的?没人说得清。

问题三:违反单向数据流

Vue 的设计哲学是:props down, events up。 数据从父到子,事件从子到父。

await emit() 的潜台词是:"子组件等待父组件的处理结果"——这相当于子组件在反向依赖父组件的执行逻辑。

正常的数据流:
  父 —— props ——→ 子
  子 —— emit ——→ 父(通知一下就走,不等回信)

await emit 的数据流:
  父 —— props ——→ 子
  子 —— emit ——→ 父 —— Promise ——→ 子(等回信才走)

这不是 emit,这是 RPC 调用。


那正确的做法是什么?

方案一:props 控制状态(最直接)

不要让子组件等父组件,让父组件主动控制子组件的状态:

// 子组件:只负责发信号,不管后续
const props = defineProps<{
  loading: boolean
}>()

const emit = defineEmits<{
  submit: [data: FormData]
  close: []
}>()

const handleSubmit = () => {
  emit('submit', formData) // ✅ 不 await,发完就完事
}
<!-- 父组件:掌握全部控制权 -->
<MyForm
  :loading="saving"
  @submit="onSubmit"
  @close="visible = false"
/>
// 父组件
const saving = ref(false)

const onSubmit = async (data: FormData) => {
  saving.value = true
  try {
    await api.save(data)
    message.success('保存成功')
    visible.value = false  // ✅ 父组件决定什么时候关弹窗
  } finally {
    saving.value = false
  }
}

子组件只管发信号,父组件全权处理。 清晰,可控,可维护。

方案二:传入异步回调 prop(需要子组件控制流程时)

有些场景子组件内部有复杂的多步骤流程,确实需要等异步结果:

// 子组件
const props = defineProps<{
  onSubmit: (data: FormData) => Promise<boolean> // ✅ 类型明确,契约清晰
}>()

const handleSubmit = async () => {
  loading.value = true
  try {
    const success = await props.onSubmit(formData) // ✅ 类型系统保证返回 Promise<boolean>
    if (success) {
      emit('close')
    }
  } finally {
    loading.value = false
  }
}
<!-- 父组件 -->
<MyForm :on-submit="handleSave" @close="visible = false" />
// 父组件
const handleSave = async (data: FormData): Promise<boolean> => {
  try {
    await api.save(data)
    return true
  } catch {
    message.error('保存失败')
    return false   // 子组件收到 false,不关弹窗
  }
}

await emit 的区别在哪?类型安全。 defineProps 明确声明了返回 Promise<boolean>,父子组件之间有了白纸黑字的契约。谁改了返回类型,TypeScript 立刻报错。

方案三:expose + ref 模式(命令式控制)

适合弹窗、抽屉这类"父组件全权控制生命周期"的场景:

// 子组件:暴露内部状态和方法
const loading = ref(false)
const reset = () => { /* 重置表单 */ }

defineExpose({ loading, reset })
// 父组件:直接操作子组件
const formRef = ref<InstanceType<typeof MyForm>>()

const onSubmit = async (data: FormData) => {
  formRef.value!.loading = true
  try {
    await api.save(data)
    formRef.value!.reset()
    visible.value = false
  } finally {
    formRef.value!.loading = false
  }
}

直接,粗暴,但某些场景下最高效。适合团队内部组件,不适合对外暴露的公共组件。


三种方案怎么选?

维度 方案一:props 控制 方案二:异步 prop 回调 方案三:expose
类型安全 ✅ 好 ✅ 最好 🟡 一般
组件耦合度 ✅ 低 🟡 中 ❌ 高
子组件自治能力 ❌ 低 ✅ 高 ❌ 低
复用性 ✅ 好 ✅ 好 🟡 差
适用场景 简单交互 复杂多步流程 命令式弹窗
  • 80% 的场景用方案一就够了——别过度设计
  • 子组件有复杂流程(多步表单、条件跳转)用方案二
  • 内部工具组件、弹窗管理器用方案三

其他框架怎么处理 emit 的?

不是所有框架都像 Vue 这样:

// Node.js EventEmitter — 返回 boolean(是否有监听器)
emitter.emit('data', payload)  // → true / false

// Svelte createEventDispatcher — 返回 boolean
dispatch('submit', data)  // → true(未被 preventDefault)/ false

// Angular EventEmitter — 基于 RxJS,没有返回值
this.submit.emit(data)  // → void

Vue 的 emit 返回父组件回调的返回值,这在框架中其实是个异类。它不是设计出来让你 await 的——只是 JavaScript 函数调用的自然结果:你调了一个函数,它当然有返回值。

就像 Array.forEach 回调里能 return,但那个返回值没人接收。能用,但不是给你用的。


项目里已经大量 await emit 了怎么办?

别慌,渐进式修复:

// Step 1:加一层防御,避免父组件忘写 async 导致的静默失败
const handleSubmit = async () => {
  loading.value = true
  try {
    const result = emit('submit', formData)
    if (result instanceof Promise) {
      await result  // 只有真正返回 Promise 时才等待
    }
  } finally {
    loading.value = false
  }
}

// Step 2:新组件直接用方案一或方案二,老组件排期重构

最后

emit 是单向通知——我告诉你发生了什么,至于你怎么处理,跟我无关。

await emit 把它强行变成了请求-响应——我不仅要告诉你,还要等你的回复。

就像你不会对着对讲机喊完话之后,傻站在原地等回复——对讲机是单工通信,要双向通话得打电话。

在 Vue 里,"电话"就是 prop 回调expose。选对工具,问题自然消失。

想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍

作者 Moment
2026年3月3日 10:34

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

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。 很多人第一次打开 OpenClaw,会下意识把它当成"接在微信或 Slack 上的聊天机器人"。这种理解只对了一半。从架构上看,OpenClaw 更像一个网关:它站在你和一堆能力之间,负责路由、鉴权、记忆和工具调用。真正决定你能做多少事的,不是对话框有多好看,而是背后接了多少"身体"——也就是 Skills。

2025 年很多人说是 vibe coding 的元年,用自然语言描述需求、让 AI 帮你写代码和改代码,从极客玩具变成了日常开发方式。到了 2026 年,一个更直白的问题浮出水面:是不是人人都会有一个自己的 Agent?不再只是"问一问答一答"的聊天窗口,而是一个真的能替你操作电脑、完成任务、且完全听你指挥的智能体。OpenClaw 就是这条路上一个绕不开的名字,它把「大模型 + 电脑」做成开源框架,让任何人都有机会在自家机器上部署一个通用 Agent。这篇文章先说清 Chatbot 和 Agent 到底差在哪,再介绍通用 Agent 是什么,最后落在 OpenClaw 的定位、优劣和可能走向。

豆包、元宝、千问这类产品大家都很熟悉,它们背后是「大语言模型」,能帮你分析、推导问题,最后给出一段文字答案。但在执行层面,始终需要人类参与。比如你可以问豆包"回老家最近的高铁车次是哪趟",具体的付款、买票依然要你自己登录 12306 完成。这类我们习惯称之为 Chatbot。

AI Agent 和 Chatbot 的差别在于,前者是「大语言模型 + 工具」的结合体。模型负责出思路,工具负责落地,最终交付的是用户要的"成品",而不只是一段话。程序员常用的 CursorCodeBuddy 就是典型:大模型给编程思路,编辑器当工具写代码,还能调浏览器做测试、发布。春节期间"让千问帮你点奶茶"、更早的豆包手机,都是传统 Chatbot 往 Agent 方向升级的信号。

20260303091822

上图从左到右概括了 Chatbot 与 Agent 的差别。Chatbot 只产出文字答案,执行仍要你自己动手。Agent 则多了"工具"这一环,能直接操作电脑或外部服务,把"成品"交到你手上。

那有没有一种 Agent,能干的事情特别多、甚至接近"什么都行"?有,这类产品叫「通用 Agent」。它的核心工具是一台完整的电脑,能用电脑上的一切软硬件来完成你的需求,相当于你请了专人,用你的电脑帮你办事。交付物自然也只能是电脑能生产的东西,你不能跟它说"给我一百万",但凡是电脑能做的,它理论上都能参与。比如你可以说:"找一下回老家最近的高铁车次,有票就帮我买,没票就对比交通工具的时间和开销,做成报告发我邮箱。" 通用 Agent 里比较出名的是去年年底被 Meta 收购的 Manus

20260303091927

图中概括了通用 Agent 的工作方式:用户用自然语言下指令,大模型理解并拆解成步骤,把"电脑"当作统一工具,调用上面的软件和网络完成操作,最后把电脑能产出的结果(订单、报告、邮件等)交回给你。

OpenClawManus 在技术本质上是一致的,都是大语言模型配合电脑作为工具的通用 Agent。区别在于:Manus 的模型和电脑由服务方提供,你按月付费使用。OpenClaw 则由开发者自己找电脑或云服务器部署,代码开源。很多人因此把 OpenClaw 当成 Manus 的平替,一上来就丢语义不清的长任务,比如"每天用 rss 拉取最新资讯做成简报",结果抱怨效果差、费钱、费 Token。问题不在技术路线,而在预期,开源带来的两面性下面会细说。

开源为什么看起来"能力弱"

OpenClaw 最初是创始人 Peter Steinberger 用来连接 WhatsApp 和本地 Claude 的工具,方便在 WhatsApp 里给 Claude 下编程指令,所以早期叫 Clawbot(Claw 和 Claude 同音),即"Claude 机器人"。后来这套「大模型控制电脑」的 Agent 框架被正式开源,并定名 OpenClaw

开源的特性决定了它的两面性。一方面,谁都可以改代码,运行逻辑可以高度定制。另一方面,内置逻辑相对"单薄"。早期版本的上下文切换、记忆能力都偏简单,自带的工具也只有读写文件、执行命令等基础操作。而 Manus 在工具层面就覆盖了 Office、图表、浏览器交互等一整套打工人常用能力,去年内测时已经在做"上下文溢出后的无缝切换"。所以如果你追求开箱即用、不想折腾,更适合每月花 199 美元订阅 Manus。如果你愿意花时间教 OpenClaw 更多技能,它可以变成你专属的、完全自主可控的智能助理。

20260303092032

上图左边是开源带来的"看起来能力弱":内置工具少、逻辑薄,和商业版 Manus 一对比尤其明显。右边是开源带来的真正价值:可定制、数据在自己手里、还有社区一起迭代。

开源的优势:自主可控

OpenClaw 的核心价值,恰恰来自开源。

你可以按自己的需求扩展:想用什么模型就用什么模型,觉得文件存记忆不够就自己接向量数据库,觉得太费 Token 就给它设定分步执行、克制的规则。代码完全透明,不用担心偷偷收集数据或乱传数据,所有上下文和敏感信息都留在你自己的设备上。Sam Altman 也提过,OpenAI 不做这类产品,核心原因之一就是隐私。个人建议不要开外网端口,牺牲一点便利,能明显提高安全性。

开源也带来了社区。OpenClawGitHub 上星标接近 20 万,有全球开发者一起修能力、扩展性和安全问题。从今年 1 月初定名 OpenClaw 到现在,已经发布了 41 个版本,基本一两天就有一次更新,社区还有日常直播,方便交流使用和开发心得。只要你愿意投入时间调教,它可以成为只属于你的"白金之星"。即便你目前只有"紫色隐者"级别的需求,它也能做一个完全不依赖外界、完全在你掌控下的本地助理。

两个方向上的预测

第一,OpenClaw 的未来会往本地化部署走。很多人聊安全时会说"要么独立电脑,要么云服务器"。现阶段云上部署性价比高,能力和本地差不太多,但可玩性差很多。比如你没法让云上的 OpenClaw 帮你放音乐,而本地部署可以(有人就教会了 OpenClaw 用 QQ 音乐)。本地电脑能接各种硬件,摄像头当"眼睛",音响当"声带",甚至接机械臂让它动起来。当大模型能操控更多实体硬件时,Agent 的想象空间会大很多。

这里顺带避个坑:最近有些"包装 OpenClaw 的云端产品"打着"无需买服务器和算力、开箱即用"的旗号收月费。这类内容多数可以当作软文看待。真想低成本体验,可以在阿里、腾讯、华为等云厂商买一台内存型服务器、部署 OpenClaw 镜像,最便宜每小时几毛钱,模型用 MinimaxKimi 或千问的免费额度就够试用了,不用了释放即可。不熟悉技术的可以问豆包要具体操作步骤,并不复杂。

第二,算力也会逐渐本地化。不仅是运行环境,大模型本身也会更多地在本地跑。当前"模型在云端"的方案下,理论上模型方是有办法接触到你的数据的。随着模型变小、消费级硬件变强,大模型完全可以在本地完成推理,到时候断网只要通电,OpenClaw 也能正常用。有人会说本地模型能力不如云端,可以换个角度想:你请的是私人助理,他不需要上知天文下知地理,只要能理解常识、能帮你对接专业能力就行。电视坏了,他帮你联系工程师(比如 Claude)。身体不舒服,他帮你问医生(比如蚂蚁阿福)。更重要的是,和这个"助理"的所有对话都只存在本地硬盘上,你可以聊任何隐私话题,他也能持续、不中断地辅助你,这才是真正意义上的私人助理。

20260303092919

图中上排是部署本地化:云端 Agent 受限于不能碰你本地的硬件,本地部署则可以接摄像头、音响、机械臂,可玩性高很多。下排是算力本地化:从"模型在云端"走向"模型在本地",断网可用,对话只留在自己硬盘上,更像真正的私人助理。

人人都有一个 Agent?2026 年的两条路

所以回到开头那个问题,现在是不是人人都会有一个自己的 Agent?从趋势上看,是的,但"有一个"的方式会分化。2025 年 vibe coding 把"用自然语言写代码"普及了,2026 年大家要的是"用自然语言让 AI 替自己干活"。这条路上有两条很清晰的路径。一条是付费订阅商业通用 Agent,比如 Manus,模型和电脑都由服务方提供,开箱即用,适合不想折腾的人。另一条就是自己部署开源框架,比如 OpenClaw,机器和模型自己选,代码自己控,数据不出本机,适合愿意花时间调教、把 Agent 当成长期资产的人。两条路技术本质相同,都是「大模型 + 电脑」的通用 Agent,差别只在于你要的是省心,还是主权。人人都有一个 Agent,可能指的是人人都有一个"能用"的窗口,但那个窗口是别人家的云端,还是你家电脑上的开源实例,选择权在你。

结语

通用机器人可以承担营救、探索等高危场景,比如去鳌太线执行救援。但私人助理这个场景,无论是 OpenClaw 还是以后出现的其他形态,更可能的方向都是开源且本地化,不被单一厂商垄断,完全由用户自己掌控。2025 年 vibe coding 让"人人都会用 AI 写代码"往前迈了一大步,2026 年"人人都有一个 Agent"不再是一句口号,而是两条可选的路径。OpenClaw 的价值不在于替代 Manus,而在于给愿意折腾的人一条路,用开源和本地换来自主可控和隐私,再通过社区和迭代,把能力一点点打磨成适合自己的样子。选哪条路,取决于你更在意省心,还是更在意主权。

❌
❌