普通视图

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

mac电脑安装nvm

2025年11月29日 18:03

方案一、常规安装

  1. 下载安装脚本:在终端中执行以下命令来下载并运行 NVM 的安装脚本3:

    bash

    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.5/install.sh | bash
    

  2. 配置环境变量:安装完成后,需要配置环境变量。如果你的终端使用的是 bash,打开或创建~/.bash_profile文件,添加以下内容3:

    bash

    export NVM_DIR="$HOME/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # 加载nvm
    [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # 加载bash自动补全(可选)
    

    如果使用的是 zsh,则打开或创建~/.zshrc文件,添加相同内容。然后执行source ~/.bash_profilesource ~/.zshrc使配置生效。

方案二、解决网络问题的安装

如果因为网络原因无法直接访问官方源,可以尝试以下方法:

  1. 通过国内镜像下载安装脚本:可以从 gitee 等国内代码托管平台的镜像下载安装脚本,例如:

    bash

    curl -o- https://gitee.com/cunkai/nvm-cn/raw/master/install.sh | bash
    

  2. 配置 NVM 使用国内镜像:安装完成后,编辑~/.zshrc(或~/.bashrc),添加以下内容来配置 NVM 使用国内的 Node.js 镜像源:

    bash

    export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
    export NVM_IOJS_ORG_MIRROR=https://npmmirror.com/mirrors/iojs
    

    保存后执行source ~/.zshrcsource ~/.bashrc使配置生效。

安装完成后,可以通过nvm -v命令查看 NVM 的版本,以确认是否安装成功。

nvm常用命令

;安装node18.16.0
nvm install 18.16.0

;查看nvm安装的node版本
nvm list

;通过nvm list查看电脑已有的版本号,设置默认的版本
nvm alias default v22.16.0

5 分钟把 Coze 智能体嵌入网页:原生 JS + Vite 极简方案

作者 烟袅
2025年11月29日 17:49

你已经创建好了 Coze 智能体,现在想快速把它接入一个网页?不用 React、不用 Vue,甚至不用手敲 npm create —— 本文教你用 trae 的 AI 助手 + 原生 HTML/JS,5 分钟搭建一个可运行、可部署、安全调用 Coze OpenAPI 的前端 Demo。

我们将实现:

  • 通过 trae AI 一键生成项目并初始化 Vite
  • 安全注入 Bot ID 和 API Token
  • 调用 Coze 接口实现问答交互

一、用 trae AI 快速搭建项目(无需手动命令)

告别 npm init 和配置文件!我们借助 trae 的右侧 AI 对话栏,全自动完成项目创建。

操作步骤如下:

  1. 打开 trae 平台,进入任意工作区

  2. 在右侧 AI 对话框 中输入:

    创建一个通用的原生HTML/CSS/JS 项目
    
  3. 等待 AI 生成基础结构(通常包含 index.htmlmain.jsstyle.css

  4. 接着在同一对话中继续输入:

    帮我初始化vite配置
    
  5. AI 会自动为你:

    • 创建 vite.config.js
    • 添加 package.json 脚本(如 devbuild
    • 安装 vite 依赖(或提示你运行 npm install

✅ 此时你已拥有一个标准的 Vite 原生 JS 项目,无需任何手动配置!

将项目同步到本地后,执行:

npm run dev

确保页面能正常打开,接下来我们集成 Coze。


二、获取 Coze 智能体凭证

  1. 复制两个关键信息:

    • Bot ID 进入你的智能体,在链接最后那一串就是你的ID,选择复制

    • API Key 点击Web SDK 将其中的token复制下来

image.png

⚠️ 这个 API Key 具有调用权限,请务必保密!

关于智能体具体的创建 juejin.cn/post/757769… 这篇文章里面有,当然智能体发布的时候一定要选择API选项


三、安全注入环境变量

在项目根目录创建 .env.local 文件:

VITE_BOT_ID=your_actual_bot_id
VITE_API_KEY=your_actual_api_key

🔒 Vite 只会暴露以 VITE_ 开头的变量到客户端代码,这是官方推荐的安全做法。


四、编写前端交互逻辑

1. index.html

可以把trae生成的代码删掉用下面这份

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Coze API Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/lib/marked.umd.min.js"></script>
</head>
<body>
  <h1>Coze API Demo 随处智能</h1>
  <input type="text" id="ipt" placeholder="请输入问题">
  <div id="reply">think...</div>
  <script type="module" src="./script.js"></script>
</body>
</html>

在这段代码看起有点不一样

<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/lib/marked.umd.min.js"></script>

是哪里冒出来的呢?

其实加上这个主要是为了待会我们从智能体那里获取图片展示到网页上,如果不加的话我们只会获得图片的链接,这还要结合待会的js一起使用

2. main.js

const ipt = document.getElementById('ipt');
const reply = document.getElementById('reply');
const endpoint = 'https://api.coze.cn/open_api/v2/chat';
// DOM 2 
ipt.addEventListener('change',async function(event) {
  const prompt = event.target.value;
  console.log(prompt);
  const payload = {
    bot_id: import.meta.env.VITE_BOT_ID,
    user: 'yvo',
    query: prompt,
    chat_history:[],
    stream: false,
    custom_variables: {
      prompt: '你是一个AI助手'
    }
  }
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${import.meta.env.VITE_API_KEY}`
    },
    body: JSON.stringify(payload)
  })
  const data = await response.json();
  console.log(data, '////');
  // reply.innerHTML = data.messages[1].content;
  reply.innerHTML=marked.parse(data.messages.find(item => item.type === 'answer').content);
})

代码分析

  reply.innerHTML=marked.parse(data.messages.find(item => item.type === 'answer').content);

这段代码可能看起来有点突兀,那我们拆开来看首先我们看吧

data.messages.find(item => item.type === 'answer').content

这主要是获取智能体的回答,这时就有人问了一般获取信息不都是使用 .choices[0].message.content来获取吗?

但是coze的智能体返回的结构是不一样的

image.png

看这个结构很容易观察到其实coze智能体返回的结构需要在messages[1].content或type:"answer"才能拿到结果,这就是coze与我们调用一般的llm不一样的地方。

接下来我们继续分析

marked.parse()

将 Markdown 格式的字符串 → 转换成 HTML 字符串

这样浏览器才能正确显示标题、列表、链接、图片等内容。

这也就实现了我们能在页面上获取智能体给我们的图片了。 我们可以删去试试看效果

image.png

我们并没有得到我们想要的只获得了https地址

那加上试试呢?

image.png

成功将照片拿到。

 const payload = {
    bot_id: import.meta.env.VITE_BOT_ID,
    user: 'yvo',
    query: prompt,
    chat_history:[],
    stream: false,
    custom_variables: {
      prompt: '你是一个AI助手'
    }

这段代码好像见的也不多,这段其实就要根据www.coze.cn/open/docs/d… coze的官方文档去使用了


五、启动 & 验证

npm run dev

在浏览器输入问题(如“JavaScript 如何判断数组?”),即可看到 Coze 智能体的实时回复!


七、常见问题

Q:返回 {"code":4101,"msg":"The token you entered is incorrect"}
A:请检查:

  • .env.local 是否命名正确
  • Token 是否正确或过期

结语

通过 trae AI + Vite + Coze OpenAPI,我们用最轻量的方式实现了智能体前端集成。整个过程:

  • 无框架负担
  • 无复杂构建
  • 环境变量安全隔离
  • 代码清晰可维护

一个输入框,一行 API 调用,背后是千行训练数据与万亿参数的智能体在为你思考。
而你,只用了 5 分钟,就把它请进了自己的网页。
这不是魔法——这是新时代前端工程师的日常。

从「似懂非懂」到「了如指掌」:Promise 与原型链全维度拆解

2025年11月29日 17:36

前言

在前端世界里,Promise原型链(Prototype) 是两个看似毫不相干,却又互相影响、甚至能相互解释的重要概念。

很多人学习 Promise 时,会关注它的使用:thencatchfinallyPromise.all 等;
学习原型链时,又会关注 __proto__prototype、构造函数与实例之间的关系。

但鲜有人把 Promise 本身也是一个对象,它也依赖原型链运作 这件事真正联系起来讲透。

本文将以一次完整的 Promise 异步流程为主线,把“原型链 + 状态机 + 微任务”融合讲解,让你完全理解 Promise 到底是怎么在底层“跑”起来的。


一、Promise 为什么是“对象”?

我们常常写:

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve('OK'), 1000)
})

很多人知道 Promise 是“异步解决方案”,但忽略了一个基本事实:

Promise 是一个构造函数(类),你创建的是它的实例。

也就是说:

  • Promise —— 构造函数(带 prototype
  • p —— 实例对象(带 __proto__

打开控制台试试:

console.log(p.__proto__ === Promise.prototype) // true

这里马上就把原型链扯进来了。

🔍 Promise.prototype 上都有啥?

输入:

console.log(Promise.prototype)

你会看到:

then: ƒ then()
catch: ƒ catch()
finally: ƒ finally()
constructor: ƒ Promise()
...

这说明:

所有 Promise 实例都是通过原型链访问 then/catch/finally 的。

也就是说 p.then() 并不是实例自身有,而是:

p ---> Promise.prototype ---> Object.prototype ---> null

这为后文理解 Promise “链式调用”机制奠定基础。


二、原型链视角下,看懂 Promise 的执行流

我们直接看一个你提供的代码精简版:

const p = new Promise((resolve, reject) => {
  console.log(111)
  setTimeout(() => {
    reject('失败1')
  }, 1000)
})

console.log(222)

p.then(data => {
  console.log(data)
}).catch(err => {
  console.log(err)
}).finally(() => {
  console.log('finally')
})

输出顺序:

111
222
失败1
finally

要理解为什么 Promise 能这样执行,必须从两个角度讲:

  • (1)Promise 内部是状态机(pending → fulfilled / rejected)
  • (2)then/catch/finally 是通过原型链挂载的“回调注册器”

我们分开看看。


1)Promise 内部是一个状态机

内部状态(无法手动修改):

状态 描述 何时出现
pending 初始状态 执行 executor 期间
fulfilled resolve 被调用 成功
rejected reject 被调用 失败

也就是说:

new Promise(executor)

执行后:

  • 立即执行 executor
  • executor 只在同步阶段运行
  • 真正的 resolve/reject 回调是“挂起来”,等事件循环驱动

所以你看到:

111(executor 同步执行)
222(外部同步执行)
失败1(异步到点后 reject)
finally(状态 settled 后触发)

2)then/catch/finally:它们不是魔法,是原型链的方法

看看这段链式调用:

p.then(...).catch(...).finally(...)

为什么可以一直“链式”?

因为每次调用 then 都 返回一个新的 Promise 实例

p.then(...) → p2
p2.catch(...) → p3
p3.finally(...) → p4

这几个实例的原型链依然是:

p2.__proto__ === Promise.prototype
p3.__proto__ === Promise.prototype
...

因此:

链式本质 = 每次链式都返回一个新的 Promise 实例,然后继续在原型链上查找 then/catch/finally。

这就是原型链在 Promise 底层的重要性。


三、原型链的类比:Promise 就像“火车头 + 车厢”系统

你提到的类比非常棒,我把它整理成完整模型:

✨ Promise = 火车系统

  • 构造函数(Promise) = 火车制造厂
  • 原型对象(Promise.prototype) = “火车车厢模板”
  • 实例(p) = 火车头
  • then/catch/finally = 可以接在车头后的“车厢类型”

于是我们看到:

p(车头).then(挂一个车厢)
         .then(再挂一节)
         .catch(挂一个处理失败的车厢)
         .finally(挂尾部的清理车厢)

每次挂车厢(调用 then/catch)时,都会生成 新的火车车头(新的 Promise 实例)

整个火车最终沿着轨道(事件循环)开动到终点。

⚠️ 注意:为什么 finally 一定执行?

因为 finally 不关心结果,只关心火车是否开到终点(settled)。


四、Promise 与普通对象原型链的对比

你提供了一个经典例子:

function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.speci = '人类'

let zhen = new Person('白兰地空瓶', 18)
console.log(zhen.speci)

const kong = {
  name: '空瓶',
  hobbies: ['读书', '喝酒']
}

zhen.__proto__ = kong

console.log(zhen.hobbies, zhen.speci)

输出:

人类
['读书','喝酒'] undefined

这个例子非常适合用来对比 Promise 的原型链逻辑。

对比 1:实例可以动态改原型(不推荐)

zhen.__proto__ = kong 改掉了原来的 Person.prototype

所以:

  • 能访问 hobbies:因为来自 kong
  • 不能访问 speci:因为已脱离 Person.prototype

Promise 则不能做这种事

你不能这样做:

p.__proto__ = {}

否则:

p.then is not a function

因为 then/catch/finally 都来自 Promise.prototype。

这反而让我们更清楚地理解:

Promise 的能力几乎全部来自原型链。


五、Promise.all 的底层逻辑:一辆多车头的“联挂火车”

提到 Promise.all,这里正好顺便讲讲它的底层设计。

Promise.all([p1, p2, p3])

机制可以用一个形象类比解释:

  • 假设有三辆火车(p1/p2/p3)
  • Promise.all 创建一辆“总火车头” pAll
  • pAll 盯着三个火车头,只要全部变成 fulfilled,就把所有结果一次性返回
  • 如果有一个 reject,则整个 pAll 变成 rejected(列车脱轨)

也就是说:

Promise.all = 多个 Promise 状态机的并联 + 一个新的总状态机。

为什么它能做到?

答案依旧在原型链:

  • Promise.all 本质是一个静态方法,返回新的 Promise 实例
  • 新的 Promise 实例依然沿用同一套路(prototype → then/catch)

六、用真实工程场景收尾:Promise 原型链为什么重要?

在真实项目里,理解 Promise 的原型机制有三个实际价值:

① debugger 时能看清原型链,定位异步回调来源

你能区分:

  • then 回调从哪里来的?(Promise.prototype.then)
  • promise 链断在哪一层?

② 手写 Promise 时必须实现 then/catch/finally

如果你手写 Promise A+:

MyPromise.prototype.then = function(onFulfilled, onRejected) {}

这里你就必须自己处理链式、状态机、回调队列。

③ 能理解 async/await 的底层依赖 Promise 链式调度

await 会把后续步骤注册到 promise.then 中。

理解 then 的原型链,就能理解 async/await 的机制本质。


七、总结:Promise + 原型链的全景图

// 创建实例
const p = new Promise(executor)

// 原型链:调用能力来自这里
p.__proto__ = Promise.prototype

// 状态机:内部维护 pending → fulfilled/rejected

// then/catch/finally:注册微任务

// 链式调用:每次都返回一个新的 Promise 实例

// Promise.all:多个状态机的并联

一句话总结:

Promise 本质是一个基于“原型链 + 状态机 + 微任务队列”的异步调度框架。

它既是面向对象设计(通过原型链复用方法),又是异步控制核心工具(内部状态机)。

理解二者的融合,你就真正吃透了 Promise。

🧠 深入理解 JavaScript Promise 与 `Promise.all`:从原型链到异步编程实战

2025年11月29日 17:32

在现代 JavaScript 开发中,Promise 是处理异步操作的核心机制之一。ES6 引入的 Promise 极大地简化了“回调地狱”(Callback Hell)问题,并为后续的 async/await 语法奠定了基础。而 Promise.all 则是并发执行多个异步任务并统一处理结果的强大工具。

本文将结合 原型链原理Promise 基础用法实际示例代码,带你系统掌握 Promise 及其静态方法 Promise.all 的使用与底层逻辑。


🔗 一、JavaScript 的面向对象:原型链而非“血缘”

在深入 Promise 之前,我们先厘清一个关键概念:JavaScript 的继承不是基于“类”的血缘关系,而是基于原型(prototype)的链式查找机制

1.1 🏗️ 构造函数与原型对象

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.speci = '人类';

let zhen = new Person('张三', 18);
console.log(zhen.speci); // 输出: "人类"
  • Person 是构造函数。
  • Person.prototype 是所有 Person 实例共享的原型对象。
  • zhen.__proto__ 指向 Person.prototype
  • Person.prototype.constructor 又指回 Person,形成闭环。

🚂 小比喻:可以把 constructor 看作“车头”,prototype 是“车身”。实例通过 __proto__ 连接到车身,而车身知道自己的车头是谁。

1.2 ⚡ 动态修改原型链(不推荐)

const kong = {
    name: '孔子',
    hobbies: ['读书', '喝酒']
};

zhen.__proto__ = kong;
console.log(zhen.hobbies);     // ✅ 输出: ['读书', '喝酒']
console.log(kong.prototype);   // ❌ undefined!普通对象没有 prototype 属性

⚠️ 注意:

  • 只有函数才有 prototype 属性;
  • 普通对象(如 kong)只有 __proto__,没有 prototype
  • 在这里kong是object的一个实例kong.__prpto__ == object.prototype

💡 虽然可以动态修改 __proto__,但会破坏代码可预测性,影响性能,应避免使用。


⏳ 二、Promise:ES6 的异步解决方案

2.1 🧩 Promise 基本结构

<script>
const p = new Promise((resolve, reject) => {
    console.log(111); // 同步执行
    setTimeout(() => {
        console.log(333);
        // resolve('结果1');  // 成功
        reject('失败1');      // 失败
    }, 1000);
});

console.log(222);
console.log(p, '////////'); // 此时 p 状态仍是 pending
console.log(p.__proto__ == Promise.prototype); // true
</script>

📋 执行顺序分析:

  1. 111 立即输出(executor 函数同步执行)✅
  2. 222 紧接着输出 ✅
  3. p 此时处于 pending(等待) 状态 ⏳
  4. 1 秒后,333 输出,调用 reject('失败1'),状态变为 rejected
  5. .catch() 捕获错误,.finally() 无论成功失败都会执行 🔁

2.2 🎯 Promise 的三种状态

  • ⏳ pending:初始状态,既不是成功也不是失败。
  • ✅ fulfilled:操作成功完成(通过 resolve 触发)。
  • ❌ rejected:操作失败(通过 reject 触发)。

🔒 核心特性:一旦状态改变,就不可逆。这是 Promise 的设计基石。

2.3 🔍 原型关系验证

console.log(p.__proto__ === Promise.prototype); // ✅ true
  • pPromise 的实例。
  • 所有 Promise 实例的 __proto__ 都指向 Promise.prototype
  • Promise.prototype 上定义了 .then(), .catch(), .finally() 等方法。
  • Promise.prototype.__proto__ == object.prototype

🚀 三、Promise.all:并发处理多个异步任务

3.1 ❓ 什么是 Promise.all

Promise.all(iterable) 接收一个可迭代对象(如数组),其中包含多个 Promise。它返回一个新的 Promise:

  • ✅ 全部成功 → 返回一个包含所有结果的数组(顺序与输入一致)。
  • ❌ 任一失败 → 立即 rejected,返回第一个失败的原因。

3.2 💻 使用示例

const task1 = fetch('/api/user');       // 假设返回 { id: 1, name: 'Alice' }
const task2 = fetch('/api/posts');      // 假设返回 [{ title: 'JS' }]
const task3 = new Promise(resolve => setTimeout(() => resolve('done'), 500));

Promise.all([task1, task2, task3])
  .then(([user, posts, msg]) => {
    console.log('全部完成:', user, posts, msg);
  })
  .catch(err => {
    console.error('某个任务失败:', err);
  });

🌐 适用场景:需要同时加载用户信息、文章列表、配置数据等,全部就绪后再渲染页面。

3.3 ⚠️ 错误处理演示

const p1 = Promise.resolve('成功1');
const p2 = Promise.reject('失败2');
const p3 = Promise.resolve('成功3');

Promise.all([p1, p2, p3])
  .then(results => console.log('不会执行'))
  .catch(err => console.log('捕获错误:', err)); // 输出: "失败2"

关键点:只要有一个失败,整个 Promise.all 就失败,其余成功的 Promise 结果会被丢弃。

3.4 🛡️ 替代方案:Promise.allSettled(ES2020)

如果你希望无论成功失败都等待所有任务完成,可以使用 Promise.allSettled

Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach((res, i) => {
      if (res.status === 'fulfilled') {
        console.log(`✅ 任务${i} 成功:`, res.value);
      } else {
        console.log(`❌ 任务${i} 失败:`, res.reason);
      }
    });
  });

✅ 适用于:批量上传、日志收集、非关键资源加载等场景。


📚 四、总结:从原型到实践

概念 说明
🔗 原型链 JS 对象通过 __proto__ 查找属性,constructor 指回构造函数
Promise 表示异步操作的最终完成或失败,具有 pending/fulfilled/rejected 三种状态
🧩 Promise.prototype 所有 Promise 实例的方法来源(.then, .catch 等)
🚀 Promise.all 并发执行多个 Promise,全成功则成功,任一失败则整体失败
🛡️ 最佳实践 使用 Promise.all 提升性能;用 allSettled 处理非关键任务

💭 五、思考题

  1. 🤔 为什么 console.log(p)setTimeout 之前打印时,状态是 pending
  2. 🛠️ 能否通过修改 Promise.prototype.then 来全局拦截所有 Promise 的成功回调?这样做有什么风险?
  3. 📦 如果 Promise.all 中传入空数组 [],结果会是什么?

💡 答案提示

  1. 因为异步任务尚未执行,状态未改变。
  2. 技术上可行,但会破坏封装性、可测试性和团队协作,强烈不推荐
  3. 立即 resolved,返回空数组 [] —— 这是符合规范的!

通过本文,你不仅掌握了 PromisePromise.all 的用法,还理解了其背后的 原型机制异步执行模型。这将为你编写健壮、高效的异步代码打下坚实基础。🌟

Happy Coding! 💻✨

从摄影新手到三维光影师:Three.js 核心要素的故事

作者 一千柯橘
2025年11月29日 16:43

当我第一次学习摄影时,老师告诉我一句话:

“你不是在拍东西,而是在拍光。”

后来我学习 Three.js 时突然意识到:
这句话原来依旧成立。

Three.js 不只是一个 3D 引擎,更像是一台虚拟相机。要拍好这张“虚拟的照片”,我们必须掌握三个核心要素:

场景(Scene)

相机(Camera)
灯光与材质(Light & Material)

于是,我把学习过程想象成一个摄影新手成长为三维光影师的故事。

空无一物的影棚 —— Scene 场景

故事从一个空影棚开始。

当我第一次打开 Three.js 时,教程告诉我:

const scene = new THREE.Scene();

这就像摄影师走进了一个空旷的工作室:
没有布景、没有模特、没有灯光,甚至连相机都还没架好, 在影棚这个场景中,摄影师可以在这个场景中放任何的东西:

  • 架好摄像机(Camera 📹)
  • 拍照的物体(Mesh 网格物体)、物体拥有着自己的形状(Geometry几何体)和材质(Material)
  • 摆设好灯光(Light)
  • 也可以是任意的对象 (Object3D)

摄影师往 Scene 里布置道具,而程序员的你往 Scene 里添加各种对象,因此 场景就是一个可以放任何东西的容器

找到你要观看的角度 —— Camera 相机

刚学摄影时,我最常做的事情,就是移动、蹲下、趴着、绕圈……
只为了找到一个“对的角度”。

Three.js 的相机就是你的眼睛。创建相机就像准备拍摄时拿起单反:

const camera = new THREE.PerspectiveCamera(const camera = new THREE.PerspectiveCamera(
  50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
  1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);

// 2.1 设置相机的位置,放在不同的位置看到的风景当然不一样
camera.position.set(5, 10, 10); // x, y, z

camera.lookAt(0, 0, 0); // 设置相机方向(这就是你女朋友让你找最佳角度的原因)

摄影师会说:“我走两步,让模特在背景中更突出。”
程序员会说:

camera.position.z = 3;
camera.lookAt(0, 0, 0)

本质完全一样:
都是在调整观察世界的方式。

让世界真正亮起来 —— Light & Material 灯光与材质

你可以有再漂亮的模特、再好的相机,如果没有光——
一切都会变成漆黑一片。

Three.js 也是如此。你搭了一个完美的 3D 模型,如果没有光,它看起来只是纯黑。

于是我制作“虚拟布光”:

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 5);
scene.add(light);

摄影师打灯,而我在代码里放置光源:

  • DirectionalLight(平行光)= 太阳光
  • PointLight(点光源)= 想象灯泡发光,由点向八方发射
  • SpotLight(聚光灯)= 舞台灯,从上打下来,呈现圆锥体,它离光越远,它的尺寸就越大。这种光源会产生阴影
  • AmbientLight(环境光)= 影棚柔光,环境光没有特定的来源方向,不会产生阴影

同时材质(Material)也等同于现实世界的“被光击中时的反应”:

  • 皮肤 = standard material
  • 金属 = metalness 高
  • 塑料 = roughness 较高
  • 玻璃 = transparent=True + envMap

想要一个皮肤质感的物体?
那么你就得给材质加入 roughness、metalness、normalMap 就像摄影师在打柔光,为人物皮肤创造质感。

光与材质的搭配,就是 Three.js 里的“布光艺术”。

最终章:按下快门 —— Renderer 渲染器

当场景布好、相机调好、灯光到位后——
摄影师要做的就是按下快门。

在 Three.js 里:

renderer.render(scene, camera);

渲染器就是那个“快门”,
真正把世界投射到屏幕上。

摄影师用快门把现实世界的光记录下来;
Three.js 用 GPU 把虚拟世界的光影计算出来。

本质上,两者做的是同一件事:

把真实或虚拟的三维世界,投射成一张二维图像。

import * as THREE from "three";

// 1. 创建场景
const scene = new THREE.Scene();

// 2. 创建相机(透视投影相机)
const camera = new THREE.PerspectiveCamera(
  50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
  1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);

// 2.1 设置相机的位置
camera.position.set(5, 10, 10); // x, y, z

camera.lookAt(0, 0, 0); // 设置相机方向(默认看向场景原点)

// 3. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿,使边缘更平滑
// 3.1 设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 3.2 将渲染器的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// 4. 创建一个立方体几何体
const geometry = new THREE.BoxGeometry(4, 4, 4); // 宽、高、深

// 为了让光源有效果,我们使用 MeshLambertMaterial 或 MeshPhongMaterial
//  创建材质 MeshLambertMaterial (兰伯特材质) 是一种非光泽材质,会受光照影响,但没有镜面高光
const material = new THREE.MeshLambertMaterial({
  color: 0x00ff00, // 颜色
  // wireframe: true, // 如果需要线框效果可以加上
});

// 6. 创建一个网格模型(网格模型由几何体和材质组成)
// Mesh 构造函数通常只接受一个材质。如果需要多材质,Three.js 有专门的 MultiMaterial 或 Group 来处理
const cube = new THREE.Mesh(geometry, material); // 使用 MeshLambertMaterial

// 6.1 将几何模型添加到场景中
scene.add(cube);

// 6.2 设置相机看向物体(拍摄对象)的位置(默认状态下相机看向的是场景的原点(0,0,0))
camera.lookAt(cube.position);

// 7. 创建光源
const spotLight = new THREE.SpotLight(0xffffff); // 创建聚光灯,颜色为白色
// 7.1 设置光源的位置
spotLight.position.set(0, 20, 20); // 调整光源位置,使其能够照亮立方体
// 7.2 设置光源照射的强度,默认值为1, 越大越亮
spotLight.intensity = 2;
// 7.3 将光源添加到场景中
scene.add(spotLight);

// 8. 为了方便观察 3D 图像,添加三维坐标系对象
const axesHelper = new THREE.AxesHelper(6); // 参数表示坐标系的大小 (x轴红色, y轴绿色, z轴蓝色)
scene.add(axesHelper); // 将坐标系添加到场景中

// 9. 渲染函数
function animate() {
  requestAnimationFrame(animate); // 请求再次执行渲染函数animate,形成循环

  // 让立方体动起来
  cube.rotation.x += 0.01; // 沿x轴旋转
  cube.rotation.y += 0.01; // 沿y轴旋转
  cube.rotation.z += 0.01;

  renderer.render(scene, camera); // 使用渲染器,通过相机将场景渲染出来
}

animate(); // 执行渲染函数,进入无限循环,完成渲染

2025年CSS新特性大盘点

作者 Immerse
2025年11月29日 16:41

大家好,我是 Immerse,一名独立开发者、内容创作者、AGI 实践者。

关注公众号:沉浸式趣谈,获取最新文章(更多内容只在公众号更新)

个人网站:yaolifeng.com 也同步更新。

转载请在文章开头注明出处和版权信息。

我会在这里分享关于编程独立开发AI干货开源个人思考等内容。

如果本文对您有所帮助,欢迎动动小手指一键三连(点赞评论转发),给我一些支持和鼓励,谢谢!


2025年了,CSS又进化了

去年写过一篇 CSS 新特性盘点,本来以为今年不会有太大变化。结果一看,新东西比去年还多。

这次整理了几个我觉得特别实用的功能,浏览器支持也都不错,可以用起来了。

终于可以动画到 auto 了

之前我们做高度展开动画,基本都是靠 max-height 硬撑。

比如从 0 展开到实际高度,只能写个超大的值,体验很差。

现在可以直接动画到 auto 了:

html {
  interpolate-size: allow-keywords;
}

加上这一行,所有 height: 0 到 height: auto 的过渡都能生效。

或者你也可以用 calc-size() 函数,不需要全局设置:

.content {
  height: 3lh;
  overflow: hidden;
  transition: height 0.2s;

  &.expanded {
    height: calc-size(auto, size);
  }
}

这个功能总算来了。

而且不只是 height,任何接受尺寸的属性都能用,不只是 auto,min-content 这些关键字也行。

目前 Chrome 已经支持,其他浏览器应该也快了。

Popover 和 Invoker

Popover 是个 HTML 属性,给任意元素加上就有开关功能。

配合 Invoker 用起来更爽,不用写 JavaScript 就能控制弹窗。

<button commandfor="menu" command="toggle">
  打开菜单
</button>

<div id="menu" popover>
  菜单内容
</div>

这样就够了,按钮点击自动控制弹窗显示隐藏。

浏览器会自动处理无障碍访问、键盘操作、焦点管理这些细节。

而且还能配合 Anchor Positioning 用,让弹窗自动定位到触发元素旁边。

Popover 已经全浏览器支持,Invoker 目前只有 Chrome,不过有 polyfill 可以用。

CSS 里可以写函数了

CSS 有 calc()、clamp() 这些内置函数,现在我们可以自己写了:

@function --titleBuilder(--name) {
  result: var(--name) " is cool.";
}

然后就能在任何地方调用:

.title::after {
  content: --titleBuilder("CSS");
}

这个功能让 CSS 更像编程语言了。

把复杂逻辑封装到函数里,代码更清爽,也更好维护。

不过目前只有 Chrome 支持,可以先用着,不支持的浏览器会回退到默认值。

if() 函数也来了

CSS 本来就有很多条件逻辑,比如选择器匹配、媒体查询。

但这次的 if() 函数是第一个专门做条件分支的:

.grid {
  display: grid;
  grid-template-columns:
    if(
      media(max-width > 300px): repeat(2, 1fr);
      media(max-width > 600px): repeat(3, 1fr);
      media(max-width > 900px): repeat(auto-fit, minmax(250px, 1fr));
      else: 1fr;
    );
}

看起来像不像 switch 语句?第一个匹配的条件会生效。

条件可以是 media()、supports()、style() 这几种。

把所有逻辑都写在一个属性里,代码可读性好很多。

目前 Chrome 独占,其他浏览器还在路上。

表单输入框自动调整大小

field-sizing 这个属性专门解决表单输入框的问题。

textarea {
  field-sizing: content;
}

加上这一行,textarea 会自动根据内容调整高度。

用户输入多少内容,输入框就有多高,不用手动拖拽了。

在手机上体验特别好,拖拽调整大小本来就很难操作。

这个功能之前都是用 JavaScript 实现,现在 CSS 原生支持了。

Chrome 和 Safari 都能用,Firefox 估计也快了。

select 下拉框终于能自定义样式了

select 元素的外观一直很难自定义,打开后显示的选项更是完全没法控制。

现在可以完全自定义了,只要先开启:

select,
::picker(select) {
  appearance: base-select;
}

然后想怎么改就怎么改,选项的样式、布局、动画都能控制。

目前 Chrome 独占,不过不支持的浏览器会回退到原生样式,完全不影响使用。

text-wrap 让排版更好看

text-wrap: balance 可以让每行文字长度尽量接近:

h1 {
  text-wrap: balance;
}

用在标题上效果特别好,不会出现最后一行只有一个词的情况。

还有个 text-wrap: pretty,专门优化正文排版:

p {
  text-wrap: pretty;
}

浏览器会自动调整断行,避免孤词,让文字看起来更舒服。

balance 已经全浏览器支持,pretty 在 Chrome 和 Safari 能用。

这种优化对用户体验很重要,而且完全不影响功能,可以直接加上。

linear() 实现复杂缓动效果

CSS 的 linear 关键字之前就是匀速动画,很无聊。

但 linear() 函数可以实现超复杂的缓动,比如弹跳效果:

.bounce {
  animation-timing-function: linear(
    0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765,
    1, 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785,
    0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953,
    0.973, 1, 0.988, 0.984, 0.988, 1
  );
}

这种效果用 cubic-bezier() 根本做不出来。

而且已经全浏览器支持了,可以放心用。

有在线工具可以生成这些值,不用自己手写。

shape() 函数画任意图形

CSS 之前有 path() 函数,但语法很难写,而且只能用像素。

shape() 是专门为 CSS 设计的,支持所有单位和自定义属性:

.arrow {
  clip-path: shape(
    evenodd from 97.788201% 41.50201%,
    line by -30.839077% -41.50201%,
    curve by -10.419412% 0% with -2.841275% -3.823154% / -7.578137% -3.823154%,
    smooth by 0% 14.020119% with -2.841275% 10.196965%,
    close
  );
}

可以用在 clip-path 裁剪元素,也能用在 offset-path 做路径动画。

而且可以响应式调整,配合媒体查询和容器查询都没问题。

Chrome 和 Safari 已经支持,Firefox 也在开发中。

attr() 变强了

之前 attr() 只能取字符串,现在可以指定类型了:

<div data-count="42" data-color="#ff0000">
div {
  --count: attr(data-count type(<number>));
  --color: attr(data-color type(<color>));
}

这样可以直接把 HTML 属性当数字或颜色用,方便多了。

目前 Chrome 独占,不过对于不支持的浏览器,可以设置回退值。

reading-flow 解决 Tab 顺序问题

用 Grid 或 Flexbox 重新排列元素后,Tab 键的焦点顺序会乱。

现在可以用 reading-flow 告诉浏览器按照视觉顺序来:

.grid {
  reading-flow: grid-rows;
}

这样焦点就会按照 Grid 的行顺序移动,不会乱跳了。

Flexbox 用 flex-flow,其他布局也有对应的值。

这个功能对无障碍访问很重要,不过目前只有 Chrome 支持。

等其他浏览器跟进之前,最好不要大量重排布局。

值得期待的功能

还有一些功能在开发中,但还没正式发布:

Masonry 布局虽然各浏览器实现不同,但在稳步推进。

Safari 的 random() 函数可以生成随机数,玩起来很有意思。

margin-trim 可以自动去掉容器边缘元素的外边距,Safari 独占中。

sibling-index() 和 sibling-count() 函数在 Chrome 能用,做交错动画很方便。

View Transitions 的 match-element 不用给每个元素起名字了,而且 Firefox 也在开发中。

还有很多其他功能在路上。

别忘了这些已经能用的

Container Queries 和 :has() 这些去年的新功能,现在已经全浏览器支持。

View Transitions、Anchor Positioning、Scroll-Driven Animations 也都在 Safari 上线了。

dvh 这些视口单位也成为标准了。

CSS 现在能做的事情越来越多,写起来也越来越顺手。

参考:frontendmasters.com/blog/what-y…

其他好文推荐

2025 最新!独立开发者穷鬼套餐

Windows 安装 Claude Code 的新姿势,保姆级教程

Claude Code 从入门到精通:最全配置指南和工具推荐

Claude Code 终极配置指南:一行命令搞定各种配置

一个配置文件搞定!Claude Code 多模型智能切换

这个 361k Star 的项目,一定要收藏!

搞定 XLSX 预览?别瞎找了,这几个库(尤其最后一个)真香!

【完整汇总】近 5 年 JavaScript 新特性完整总览

关于 Node,一定要学这个 10+万 Star 项目!

【翻译】使用 React 19 操作构建可复用组件

2025年11月29日 16:40

使用 React 19 Actions 构建可复用的 React 组件,通过 useTransition()useOptimistic() 实现功能。通过实际案例学习如何追踪待处理状态、实现乐观更新,并在 Next.js 应用路由器中暴露动作属性以实现自定义逻辑。

作者:Aurora Scharff

首发于 aurorascharff.no

React 19 Actions 简化了待处理状态、错误、乐观更新和顺序请求的处理。本文将探讨如何在 Next.js App Router 中使用 React 19 Actions 构建可复用组件。我们将利用 useTransition() 追踪过渡状态,使用 useOptimistic() 向用户提供即时反馈,并暴露 action 属性以支持父组件中的自定义逻辑。

React 19 Actions

根据更新后的 React 文档,动作(Actions)是在过渡(Transitions)内部调用的函数。过渡可以更新状态并执行副作用,相关操作将在后台执行,不会阻塞页面上的用户交互。过渡内部的所有动作都会被批量处理,组件仅在过渡完成时重新渲染一次。

Action 可用于自动处理待定状态、错误、乐观更新及顺序请求。在 React 19 表单中使用 <form action={} 属性时,以及向 useActionState() 传递函数时,也会自动创建动作。有关这些 API 的概述,请参阅我的 React 19 速查表或官方文档。

使用 useTransition() 钩子时,您还将获得一个 pending 状态,这是一个布尔值,用于指示过渡是否正在进行。这有助于在过渡过程中显示加载指示器或禁用按钮。

const [isPending, startTransition] = useTransition(); 
const updateNameAction = () => { 
  startTransition(async () => { 
    await updateName(); 
  }) 
})

此外,在钩子版本的 startTransition() 中调用的函数抛出的错误将被捕获,并可通过错误边界进行处理。

Action函数是常规事件处理的替代方案,因此应相应地命名。否则,该函数的使用者将无法明确预期其行为类型。

用例:路由器选择组件

假设我们要构建一个可复用的下拉菜单组件,该组件会将下拉菜单选中的值设置为URL中的参数。其实现方式可能如下所示:

export interface RouterSelectProps { 
  name: string; 
  label?: string; 
  value?: string; 
  options: Array<{ value: string; label: string }>; 
} 

export const RouterSelect = React.forwardRef<HTMLSelectElement, RouterSelectProps>(   
  function Select({ name, label, value, options, ...props }, 
    ref 
) { 
... 
return ( 
  <div> 
    {label && <label htmlFor={name}>{label}</label>} 
      <select 
        ref={ref} 
        id={name} 
        name={name} 
        value={value} 
        onChange={handleChange} 
        {...props} 
      > 
        {options.map((option) => ( 
           <option key={option.value} value={option.value}> 
             {option.label} 
           </option> 
         ))} 
      </select> 
  </div> 
  ) 
}

它可能会这样处理变化:

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  
  // Update URL 
  const url = new URL(window.location.href); 
  url.searchParams.set(name, newValue); 
  
  // Simulate a delay that would occur if the route destination is doing async work 
  await new Promise((resolve) => setTimeout(resolve, 500)); 
  
  // Navigate 
  router.push(url.href, { scroll: false }); 
};

可通过路由器传递 searchParams 来使用:

<RouterSelect
  name="lang" 
  options={Object.entries(languages).map(([value, label]) => { 
    return { value, label, }; 
  })} 
  label="Language" 
  value={searchParams.lang} 
/>

由于我们使用 Next.js 应用路由器,当延迟推送到路由时,下拉框的值不会立即更新,而是等到 router.push() 完成且搜索参数更新后才会刷新。

这会导致糟糕的用户体验:用户必须等待路由推送完成才能看到下拉框的新值,可能因此产生困惑,误以为下拉框功能失效。

使用Action追踪待处理状态

让我们创建一个使用 useTransition() 钩子的 Action 来追踪推送至路由器的状态。

我们将向路由器的推送封装在返回的 startNavTransition() 函数中,该函数将追踪该转场的待处理状态。这将使我们能够知道转场的进展以及何时完成。

  const [isNavPending, startNavTransition] = useTransition(); 
  const handleChange = async ( 
    event: React.ChangeEvent<HTMLSelectElement> 
  ) => { 
    const newValue = event.target.value; 
    startNavTransition(async () => { 
      const url = new URL(window.location.href); 
      url.searchParams.set(name, newValue); 
      await new Promise((resolve) => setTimeout(resolve, 500)); 
      router.push(url.href, { scroll: false }); 
    }); 
  };

现在,我们可以利用 isNavPending 状态在过渡过程中显示加载指示器,并添加 aria-busy 等辅助功能属性。

<div> 
  {label && <label htmlFor={name}>{label}</label>} 
  <select 
    ref={ref} 
    id={name} 
    name={name} 
    aria-busy={isNavPending} 
    value={value} 
    onChange={handleChange} 
    {...props} 
  > 
    {options.map((option) => ( 
      <option key={option.value} value={option.value}> 
        {option.label} 
      </option> 
    ))}
  </select> 
  {isNavPending && 'Pending nav...'} 
</div>

现在,用户将收到关于其与下拉菜单交互的反馈,不会认为它无法正常工作。

然而,下拉菜单仍然无法立即更新。

使用 useOptimistic() 添加乐观更新

此时就需要用到 useOptimistic() 函数。它允许我们立即更新状态,同时仍能追踪过渡的待处理状态。我们可以在过渡内部调用它:

const [optimisticValue, setOptimisticValue] = useOptimistic(value); 

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  startNavTransition(async () => { 
    setOptimisticValue(newValue); 
    const url = new URL(window.location.href); 
    url.searchParams.set(name, newValue); 
    await new Promise((resolve) => setTimeout(resolve, 500)); 
    router.push(url.href, { scroll: false }); 
  }); 
};

在过渡期间,optimisticValue 将作为临时客户端状态,用于立即更新下拉菜单。过渡完成后,optimisticValue 将最终更新为路由器返回的新值。

现在,我们的下拉菜单实现了即时更新,用户在过渡过程中即可看到菜单中的新值。

暴露Action属性

假设作为 RouterSelect 的用户,我们希望在选项变更时执行额外逻辑。例如,可能需要更新父组件中的其他状态或触发副作用。此时可暴露一个在选项变更时执行的函数。

参照 React 文档,我们可以向父组件暴露一个 action 属性。由于暴露的是 Action,命名时应符合规范,以便组件使用者明确预期行为。

具体实现如下:

export interface RouterSelectProps { 
  name: string; 
  label?: string; 
  value?: string; 
  options: Array<{ value: string; label: string }>; 
  setValueAction?: (value: string) => void; 
}

我们可以在handleChange过渡中调用此属性:

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  startNavTransition(async () => { 
    setOptimisticValue(newValue); 
    setValueAction?.(newValue); '
    const url = new URL(window.location.href); 
    url.searchParams.set(name, newValue); 
    await new Promise((resolve) => setTimeout(resolve, 500)); 
    router.push(url.href, { scroll: false }); 
  });
};

我们还应支持 async 函数。这使得操作回调既可以是同步的,也可以是异步的,而无需额外使用 startTransition 来包裹操作中的 await 语句。

export interface RouterSelectProps { 
  ...// other props 
  setValueAction?: (value: string) => void | Promise<void>; 
}

然后只需 await 操作完成,再推送到路由器:

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  startNavTransition(async () => { 
    setOptimisticValue(newValue); 
    await setValueAction?.(newValue); 
    ... // Push to router 
  }); 
};

在父组件中使用 Action 属性

现在,我们可以通过 setValueAction 属性执行状态更新,并且由于命名规范,我们清楚会行为的结果。

例如,如果我们使用 useState() 设置一条消息:

const [message, setMessage] = useState(''); 
return ( 
  <> 
  <div> 
    Message: {message} <br /> 
  </div> 
  <RouterSelect 
    setValueAction={(value) => { 
      setMessage(`You selected ${value}`);
    }}

我们知道,此状态更新将在向路由器推送完成后发生。

此外,若现在需要乐观更新,可调用 useOptimistic()

const [message, setMessage] = useState(''); 
const [optimisticMessage, setOptimisticMessage] = useOptimistic(message); 

return ( 
  <> 
  <div> 
    Message: {message} <br /> 
    Optimistic message: {optimisticMessage} 
  </div> 
  <RouterSelect 
    setValueAction={(value) => { 
      setOptimisticMessage(`You selected ${value}`); 
      setMessage(`You selected ${value}`); 
    }}

我们知道此状态更新将立即发生。

最终的select实现如下所示:

'use client'; 

... 
export interface RouterSelectProps { 
  name: string; 
  label?: string; 
  value?: string | string[]; 
  options: Array<{ value: string; label: string }>; 
  setValueAction?: (value: string) => void | Promise<void>; 
} 

export const RouterSelect = React.forwardRef<HTMLSelectElement, RouterSelectProps>( 
  function Select( 
    { name, label, value, options, setValueAction, ...props }, 
    ref 
  ) { 
    const router = useRouter(); 
    const [isNavPending, startNavTransition] = React.useTransition(); 
    const [optimisticValue, setOptimisticValue] = React.useOptimistic(value); 
    
    const handleChange = async ( 
      event: React.ChangeEvent<HTMLSelectElement> 
    ) => { 
      const newValue = event.target.value; 
      startNavTransition(async () => { 
        setOptimisticValue(newValue); 
        await setValueAction?.(newValue); 
        const url = new URL(window.location.href); 
        url.searchParams.set(name, newValue); 
        await new Promise((resolve) => setTimeout(resolve, 500)); 
        router.push(url.href, { scroll: false }); 
      }); 
    }; 
    
    return ( 
      <div> 
        {label && <label htmlFor={name}>{label}</label>} 
        <select 
          ref={ref} 
          id={name} 
          name={name} 
          value={optimisticValue} 
          onChange={handleChange} 
          {...props} 
        > 
          {options.map((option) => ( 
            <option key={option.value} value={option.value}> 
              {option.label} 
            </option> 
          ))} 
          </select> 
          {isNavPending && 'Pending nav...'} 
      </div> 
    ); 
  } 
);

查看这个StackBlitz,获取一个可运行的示例。

若需查看本文所述模式的更实用、更贴近实际的应用示例,请参阅我Next.js 15 Conferences项目中的Filters.tsx组件。

构建复杂、可重用的组件

在构建更复杂的可复用组件时,我们可能会遇到限制,迫使我们将乐观更新等逻辑移至父组件。

以我尝试的Ariakit示例为例,显示值的生成必须在可复用选择组件外部完成。这意味着我们无法在可复用选择组件内部调用 useOptimistic 。为解决此问题,可暴露 setValueAction 属性,然后在父组件中调用 useOptimistic() 立即更新状态。

通过这种方式,既能保持组件复用性,又允许父组件实现自定义Action逻辑。

关键要点

  • 动作是在过渡中调用的函数,可更新状态并执行副作用。
  • useTransition() 提供待处理状态以追踪过渡进度。
  • useOptimistic() 允许在过渡中立即更新状态。
  • 向可复用组件暴露动作属性,可在父组件中实现自定义逻辑。
  • 在父组件中使用 useOptimistic() 可立即更新状态,同时保持组件的复用性。
  • 动作的命名对向组件使用者传达预期行为至关重要。

结论

在本篇博文中,我们探讨了如何利用 React 19 动作构建可复用组件,追踪过渡状态,采用乐观更新策略,并暴露动作属性以实现自定义逻辑。我们演示了 useTransition() 如何提供待处理状态以优化用户反馈,useOptimistic() 如何实现即时 UI 更新,以及暴露动作属性如何在保持组件复用性的同时允许父组件执行自定义逻辑。

通过遵循动作命名规范并运用 React 的并发特性,我们能够构建出复杂度极低却能提供流畅用户体验的组件。

源码

当我把 proto 打印出来那一刻,我懂了JS的原型链

作者 栀秋666
2025年11月29日 16:19

💬 前言:我本以为我会面向对象,结果我连“对象”都没搞懂

刚开始学 JavaScript 的时候,我以为:

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

const p1 = new Person('小明');
console.log(p1.name); // 小明

这不就是面向对象吗?简单!

直到有一天,我在控制台敲下:

console.log(p1.__proto__);

然后——我的世界崩塌了。

满屏的 [[Prototype]]constructor__proto__……我仿佛掉进了一个无限嵌套的俄罗斯套娃里。

“我是谁?”
“我从哪里来?”
“我要到哪里去?”
——来自一个被原型链逼疯的学生的灵魂三问。

今天,就用一个真实学习者的视角,带你从困惑到理解,一步步揭开 prototype 的神秘面纱。没有高深术语,只有大白话 + 可运行代码 + 我踩过的坑。


🚪 一、为什么需要原型?—— 因为我不想每个对象都背一份方法

假设我们要创建多个学生对象:

❌ 错误写法:每个学生都自带“技能包”

function Student(name) {
  this.name = name;
  // 每个学生都独立拥有一个 sayHello 方法
  this.sayHello = function() {
    console.log(`大家好,我是${this.name}`);
  };
}

const s1 = new Student('张三');
const s2 = new Student('李四');

console.log(s1.sayHello === s2.sayHello); // false → 完全不同的两个函数!

问题来了:如果创建 1000 个学生,就会有 1000 个 sayHello 函数,内存直接爆炸 💥。

这就像学校给每个学生发一本《礼仪手册》,其实大家看的都是同一本书,但每人一本——太浪费了!

✅ 正确姿势:把公共方法放进“共享书架”(prototype)

function Student(name) {
  this.name = name; // 每个学生独有的属性
}

// 所有学生共享的方法,统一挂载到 prototype 上
Student.prototype.sayHello = function() {
  console.log(`大家好,我是${this.name}`);
};

const s1 = new Student('张三');
const s2 = new Student('李四');

console.log(s1.sayHello === s2.sayHello); // true → 同一个函数,只存一份!
s1.sayHello(); // 大家好,我是张三
s2.sayHello(); // 大大家好,我是李四

📌 我的理解

  • prototype 就是构造函数的“共享书架”
  • 实例自己没有的方法,会自动去书架上找
  • 既节省内存,又方便统一管理

这就是原型存在的意义:让对象学会“蹭”!


🔗 二、四大核心概念:别再混淆 prototype 和 proto 了!

刚开始我总分不清 prototype__proto__,后来我画了张图,终于懂了。


1️⃣ 构造函数:创建实例的“模板”

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

它就是一个普通函数,但通常:

  • 首字母大写
  • new 调用

new 的过程可以简化为:

  1. 创建空对象 {};
  2. this 指向它;
  3. 执行函数体;
  4. 返回这个对象。

2️⃣ prototype:构造函数的“共享仓库”

每个函数都有一个 prototype 属性,它是一个对象,用来存放所有实例共享的内容

Student.prototype.species = '人类';
Student.prototype.study = function() {
  console.log(`${this.name}在努力学习`);
};

⚠️ 注意:prototype函数才有的属性


3️⃣ __proto__:实例通往原型的“梯子”

每个对象(包括实例)都有一个 __proto__ 属性(非标准但广泛支持),它指向其构造函数的 prototype

const s1 = new Student('张三');

console.log(s1.__proto__ === Student.prototype); // true

👉 这就是实例能访问到 sayHello 的原因:
s1.sayHello() → 自己没有 → 顺着 __proto__ 找 → 找到 Student.prototype.sayHello

🎯 记住一句话:实例的 __proto__ 指向构造函数的 prototype


4️⃣ constructor:原型的“回老家按钮”

原型对象上有一个 constructor 属性,指向构造函数本身。

console.log(Student.prototype.constructor === Student); // true
console.log(s1.constructor === Student); // true

⚠️ 重要提醒:手动重写 prototype 要修复 constructor!

Student.prototype = {
  sayHello() { console.log('hi') }
};

const s1 = new Student('张三');
console.log(s1.constructor === Student); // false ❌
console.log(s1.constructor === Object); // true → 错了!

// ✅ 修复:
Student.prototype = {
  constructor: Student,
  sayHello() { console.log('hi') }
};

否则后续 instanceof 判断可能出错。


📊 核心关系图(建议收藏)

lQLPJwvG0tJ1vuPNASLNAkSwAikK2ZgDD2YJAF6EAku5AA_580_290.png

📌 再说一遍:实例的 __proto__ 指向构造函数的 prototype,原型的 constructor 指向构造函数

🔍 三、原型查找机制:JS是怎么找到方法的?

当你调用 s1.sayHello() 时,JS 引擎是这样找的:

  1. 先看 s1 自己有没有 sayHello
  2. 没有?那就通过 __proto__Student.prototype 找;
  3. 还没有?继续通过 Student.prototype.__proto__ 找上一级;
  4. 直到找到,或者查到 null

这个链条,就是原型链

🖼️JavaScript 原型链完整关系图

66b94b61f939741c0ca1db2e69984697.png

1. 查找示例

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

Student.prototype.species = '人类';
Student.prototype.study = function() {
  console.log(`${this.name}在学习`);
};

const s1 = new Student('张三');

console.log(s1.name);        // 张三 → 自身属性
console.log(s1.species);     // 人类 → 来自 prototype
console.log(s1.toString());  // [object Object] → 来自 Object.prototype
console.log(s1.abc);         // undefined → 找不到

2. 原型链终点:null

console.log(Object.prototype.__proto__); // null → 终点!

// 验证整个链:
console.log(s1.__proto__);                 // Student.prototype
console.log(s1.__proto__.__proto__);       // Object.prototype
console.log(s1.__proto__.__proto__.__proto__); // null

3. 实例属性可以“屏蔽”原型属性

function Student(name) {
  this.name = name;
  this.species = '外星人'; // 覆盖原型属性
}

Student.prototype.species = '人类';

const s1 = new Student('张三');
console.log(s1.species); // 外星人

delete s1.species;
console.log(s1.species); // 人类 → 删除后重新查找原型

✅ 应用:为个别实例定制行为,不影响全局。


🧬 四、原型式继承:JS的“继承”到底是什么?

传统语言是“类继承”(血缘关系),而 JS 是“委托继承”——你不会,就去问你爸,你爸不会,就去问爷爷。

1. 经典继承实现

// 父类
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  console.log(`你好,我是${this.name}`);
};

// 子类
function Student(name, grade) {
  Person.call(this, name); // 继承父类实例属性
  this.grade = grade;
}

// 继承父类原型方法
Student.prototype = new Person();
Student.prototype.constructor = Student;

// 扩展子类方法
Student.prototype.study = function() {
  console.log(`${this.name}在读${this.grade}年级`);
};

const s1 = new Student('张三', 3);
s1.greet(); // 你好,我是张三(继承)
s1.study(); // 张三在读3年级(自有)

2. ES6 class 只是语法糖

class Person {
  constructor(name) { this.name = name; }
  greet() { console.log(`你好,我是${this.name}`); }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  study() { console.log(`${this.name}在读${this.grade}年级`); }
}

底层依然是原型链驱动。class 不是新东西,只是让你写得更爽。


💡 五、原型的实际应用

1. 工具类共享方法

function Utils() {}
Utils.prototype.formatDate = function(date) { /* ... */ };

2. 扩展原生对象(谨慎!)

Array.prototype.unique = function() {
  return [...new Set(this)];
};
[1,2,2,3].unique(); // [1,2,3]

⚠️ 注意:生产环境慎用,避免污染全局。

3. 单例模式

function Singleton() {
  if (Singleton.prototype.instance) {
    return Singleton.prototype.instance;
  }
  this.data = '唯一实例';
  Singleton.prototype.instance = this;
}

4. 框架中的应用(如 Vue)

Vue.prototype.$http = axios; // 所有组件都能用 this.$http

⚠️ 六、常见误区

❌ 误区1:混淆 prototype 和 proto

  • prototype:函数才有,是“仓库”
  • __proto__:对象都有,是“梯子”

❌ 误区2:覆盖 prototype 不修 constructor

会导致 instanceof 失效。

✅ 正确做法:永远记得修 constructor!


🏁 七、总结:原型是JS的灵魂

核心要点 说明
🔹 核心价值 共享方法,节省内存
🔹 核心关系 实例.__proto__ === 构造函数.prototype
🔹 查找机制 自身 → 原型链 → null
🔹 继承本质 委托查找,非类继承
🔹 class 本质 原型的语法糖

🌟 最后感悟
学原型的过程,就像在迷宫中找出口。
一开始觉得混乱,但当你画出那张关系图,执行第一段可运行代码,听到“啊哈!”的那一声——
你就真正理解了 JavaScript 的灵魂。

Taro 小程序页面返回传参完整示例

2025年11月29日 16:00

前言

  • 我们在开发的时候,有时候会遇到,A页面跳转到B页面,B页面改一些数据(例如:收藏状态),回到A页面的时候不想刷新A页面,并看到最新的数据状态;
  • 对于以上场景,有以下几种解决方案;

方法一:EventChannel(推荐)

PageA.jsx - 跳转页面

import React, { useState } from 'react'
import { View, Button, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'

const PageA = () => {
  const [receivedData, setReceivedData] = useState(null)

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index',
      events: {
        // 监听返回数据
        // ⚠️ 这里监听的事件名 必须和 子页面绑定的事件名称相同
        onReturnData: (data) => {
          console.log('接收到返回数据:', data)
          setReceivedData(data)
        },
        // 可以监听多个事件
        onSelectItem: (item) => {
          console.log('选中的项目:', item)
        }
      }
    })
  }

  return (
    <View className="page-a">
      <Button onClick={goToPageB}>跳转到页面B</Button>
      
      {receivedData && (
        <View className="received-data">
          <Text>接收到的数据:</Text>
          <Text>{JSON.stringify(receivedData)}</Text>
        </View>
      )}
    </View>
  )
}

export default PageA

PageB.jsx - 返回页面

  • 这是
import React, { useState, useEffect } from 'react'
import { View, Button, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'

const PageB = () => {
  // 可以使用 useState 或 useRef 存储 EventChannel
  const [eventChannel, setEventChannel] = useState(null)
  const [inputValue, setInputValue] = useState('')
  const [count, setCount] = useState(0)

  useEffect(() => {
    // 获取 EventChannel
    const channel = Taro.getCurrentInstance().page?.getOpenerEventChannel?.()
    if (channel) {
      setEventChannel(channel)
    }
  }, [])

  const handleReturn = () => {
    if (eventChannel) {
      // 发送数据给上个页面
      eventChannel.emit('onReturnData', {
        inputValue,
        timestamp: Date.now(),
        source: 'pageB'
      })
    }
    
    // 返回上个页面
    Taro.navigateBack()
  }

  const handleSelectItem = (item) => {
    if (eventChannel) {
      eventChannel.emit('onSelectItem', item)
    }
  }
  
  // ---- 页面销毁传递参数 Start ----
  // 若是使用小程序的导航栏的返回按钮,可以在页面销毁的时候,向父页面传递参数
  // 需要注意的是,useUnload 的参数若是依赖于一些数据,需要使用 useCallback 对函数进行缓存
  const handleBack = useCallback(() => {
    if (eventChannel) {
      eventChannel?.emit('onReturnPageA', { name: 'PageA', count })
      console.log('数据发送成功')
    }
  }, [eventChannel, count])

  useUnload(handleBack)
  // ---- 页面销毁传递参数 End ----

  return (
    <View className="page-b">
      <Input
        value={inputValue}
        onInput={(e) => setInputValue(e.detail.value)}
        placeholder="输入要传递的数据"
      />
      
      <Button onClick={handleReturn}>返回并传递数据</Button>
      
      <Button onClick={() => handleSelectItem({ id: 1, name: '选项1' })}>
        选择选项1
      </Button>
      
      <Button onClick={() => setCount(v => v++)}>
        改变count
      </Button>
      
      <Button onClick={() => handleSelectItem({ id: 2, name: '选项2' })}>
        选择选项2
      </Button>
    </View>
  )
}

export default PageB

方法二:使用 Zustand 状态管理

store/index.js

import { create } from 'zustand'

const useAppStore = create((set, get) => ({
  // 页面返回数据
  pageReturnData: null,
  
  // 设置返回数据
  setPageReturnData: (data) => set({ pageReturnData: data }),
  
  // 清除返回数据
  clearPageReturnData: () => set({ pageReturnData: null }),
  
  // 获取并清除返回数据
  getAndClearReturnData: () => {
    const data = get().pageReturnData
    set({ pageReturnData: null })
    return data
  }
}))

export default useAppStore

PageA.jsx - 使用状态管理

import React, { useEffect } from 'react'
import { View, Button, Text } from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro'
import useAppStore from '../store'

const PageA = () => {
  const { pageReturnData, clearPageReturnData } = useAppStore()

  // 页面显示时检查返回数据
  useDidShow(() => {
    if (pageReturnData) {
      console.log('接收到返回数据:', pageReturnData)
      // 处理数据后清除
      handleReturnData(pageReturnData)
      clearPageReturnData()
    }
  })

  const handleReturnData = (data) => {
    // 处理返回的数据
    Taro.showToast({
      title: `接收到: ${data.message}`,
      icon: 'success'
    })
  }

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index'
    })
  }

  return (
    <View className="page-a">
      <Button onClick={goToPageB}>跳转到页面B</Button>
    </View>
  )
}

export default PageA

PageB.jsx - 设置状态并返回

import React, { useState } from 'react'
import { View, Button, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'
import useAppStore from '../store'

const PageB = () => {
  const [message, setMessage] = useState('')
  const setPageReturnData = useAppStore(state => state.setPageReturnData)

  const handleReturn = () => {
    // 设置要传递的数据
    setPageReturnData({
      message,
      timestamp: Date.now(),
      type: 'user_input'
    })
    
    // 返回上个页面
    Taro.navigateBack()
  }

  return (
    <View className="page-b">
      <Input
        value={message}
        onInput={(e) => setMessage(e.detail.value)}
        placeholder="输入消息"
      />
      
      <Button onClick={handleReturn}>返回并传递消息</Button>
    </View>
  )
}

export default PageB

方法三:自定义 Hook 封装

hooks/usePageReturn.js

import { useState, useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'

// 全局存储返回数据
let globalReturnData = new Map()

export const usePageReturn = (pageKey) => {
  const [returnData, setReturnData] = useState(null)

  useDidShow(() => {
    const data = globalReturnData.get(pageKey)
    if (data) {
      setReturnData(data)
      globalReturnData.delete(pageKey)
    }
  })

  const setReturnDataForPage = (targetPageKey, data) => {
    globalReturnData.set(targetPageKey, data)
  }

  const clearReturnData = () => {
    setReturnData(null)
  }

  return {
    returnData,
    setReturnDataForPage,
    clearReturnData
  }
}

// 导航并设置返回监听
export const navigateToWithReturn = (url, pageKey, onReturn) => {
  return Taro.navigateTo({
    url,
    events: {
      returnData: (data) => {
        if (onReturn) {
          onReturn(data)
        }
      }
    }
  })
}

使用自定义 Hook

// PageA.jsx
import React from 'react'
import { View, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { usePageReturn } from '../hooks/usePageReturn'

const PageA = () => {
  const { returnData, clearReturnData } = usePageReturn('pageA')

  useEffect(() => {
    if (returnData) {
      console.log('接收到返回数据:', returnData)
      // 处理数据
      clearReturnData()
    }
  }, [returnData])

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index?fromPage=pageA'
    })
  }

  return (
    <View>
      <Button onClick={goToPageB}>跳转到页面B</Button>
    </View>
  )
}

export default PageA

最佳实践建议

  1. 简单场景:使用 EventChannel(方法一)
  2. 复杂应用:使用状态管理(方法二)
  3. 多页面复用:封装自定义 Hook(方法三)
  4. 数据量大:避免使用 URL 参数,选择状态管理
  5. 临时数据:使用 EventChannel,自动清理
  6. 持久数据:结合本地存储使用

注意事项

  • EventChannel 只在 navigateTo 时可用,redirectTo 不支持
  • 状态管理要注意及时清理数据,避免内存泄漏
  • 复杂对象传递时注意序列化问题
  • 考虑页面栈的层级关系,避免数据传递错乱

TypeScript的泛型工具集合

作者 ErMao
2025年11月29日 15:52

TypeScript的泛型工具集合

TypeScript中集合了很多泛型工具,在日常开发中,我们经常会看到这类工具的使用,所以属性这类工具也是必备的。

工具集大概能够分为几类:

  • 对象与属性工具
  • 联合类型工具
  • 函数工具
  • 类工具
  • 字符串工具
  • this工具
  • promise工具

对象与属性工具
Partial<T>

将类型的所有属性设置为可选属性

interface User {
  id: number;
  name: string;
  age: number;
}
const user2: Partial<User> = { id: 2, name: "ErMao" };
Required<T>

将类型的所有属性设置为必填

interface Config {
  port?: number;
  host?: string;
}
const c1: Config = { port: 8080 };
const c2: Required<Config> = { port: 8080, host: "localhost" };
Readonly<T>

将类型的所有属性设置为只读

interface Todo {
  title: string;
  done: boolean;
}
const t: Readonly<Todo> = { title: "Clean", done: false };
// t.done = true // 错误:不能分配到 "done" ,因为它是只读属性
Record<K,T>

用联合类型键映射到统一的值类型。这个工具很特别,可以把类型作为对象的键。

type Status = "success" | "error" | "loading";

const statusMap: Record<Status, string> = {
  success: "成功",
  error: "错误",
  loading: "加载中",
};
Pick<T, K>

从类型 T 中选择一组属性 K

interface User {
  id: number;
  name: string;
  age: number;
}

const u3: Pick<User, "id" | "name"> = { id: 2, name: "ErMao" };
Omit<T, K>

从类型 T 中排除一组属性 K

interface TodoList {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}
type TodoWithoutMeta = Omit<TodoList, "createdAt" | "completed">;
const x: TodoWithoutMeta = { title: "Clean", description: "Room" };

从这些方法中,可以从对象的键数量和属性进行记忆:

多 >>> 少:Partial、Pice 、Omit

少 >>> 多:Required 、Record

属性:Readonly

联合类型工具
Exclude<T, U>

从类型 T 中排除 U 类型的成员

// Exclude<T, U> : 从类型 T 中排除 U 类型的成员
type Status2 = "pending" | "success" | "error";
type NonError = Exclude<Status2, "error">;
NonNullable<T>

从类型 T 中排除 null 和 undefined 类型的成员,和 Exclude 类似。

type MaybeString = string | null | undefined;
type StrictString = NonNullable<MaybeString>;
Extract<T, U>

从类型 T 中提取 U 类型的成员。类似于交集,但是与&交叉类型又有不同。

type S1 = "a" | "b" | "c";
type S2 = "b" | "d";
type O1 = {name: string}
type O2 = {age: number}
type In2 = O1 & O2
const in2: In2 = {name: "ErMao", age: 18}
type In = S1 & S2;
type Intersection = Extract<S1, S2>;
type In3 = Extract<O1 , O2> // never

排除 : Exclude、NonNullable
交集 : Extract

函数工具
Parameters<T>

从函数类型 T 中提取参数类型的元组

function fn(a: number, b: string) {}
type Args = Parameters<typeof fn>;
const valid: Args = [123, "hi"];
ReturnType<T>

获取函数返回类型

function makePoint() {
  return { x: 0, y: 0 };
}
type Point = ReturnType<typeof makePoint>;
const p: Point = { x: 1, y: 2 };
this 工具
ThisParameterType<T>

提取函数显式 this 参数的类型

interface Person {
  name: string;
}
function say(this: Person, msg: string) {
  return `${this.name}: ${msg}`;
}
type ThisT = ThisParameterType<typeof say>;

OmitThisParameter<T>

移除函数显式 this 参数

interface Person {
  name: string;
}
function say2(this: Person, msg: string) {
  return `${this.name}: ${msg}`;
}
const boundSay = say.bind({ name: "Ann" });
type FnNoThis = OmitThisParameter<typeof say2>;
const f: FnNoThis = boundSay;
ThisType<T>

为对象字面量中的 this 指定类型

type ObjectDescriptor<D, M> = {
  data: D;
  methods: M & ThisType<D & M>
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  return Object.assign({}, desc.data, desc.methods);
}
const obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
    },
  },
});
obj.moveBy(2, 3);
类工具
ConstructorParameters<T>

获取构造函数参数的元组类型

class Box {
  constructor(width: number, height?: number) {}
}
type CtorArgs = ConstructorParameters<typeof Box>;
const args: CtorArgs = [100, 50];
InstanceType<T>

获取构造函数实例的类型

class UserService {
  constructor(public name: string) {}
  greet() {
    return `Hi ${this.name}`;
  }
}
type ServiceInstance = InstanceType<typeof UserService>;
const svc: ServiceInstance = new UserService("Leo");

字符串工具
Uppercase<S>

转成全大写

type U = Uppercase<"hello world">
Lowercase<S>

转成全小写

type L = Lowercase<"HELLO WORLD">
Capitalize<S>

将字符串的第一个字符转换为大写

type Capitalized = Capitalize<"hello world">
Uncapitalize<S>

将字符串的第一个字符转换为小写

type Uncapitalized = Uncapitalize<"Hello World">

Promise 工具
Awaited<T>
type A = Awaited<Promise<string>>
type B = Awaited<Promise<Promise<number>>>
type C = Awaited<boolean | Promise<number>>
async function fetchNum() { return 42 }
type R = Awaited<ReturnType<typeof fetchNum>>

element-plus源码解读1——useNamespace

作者 Joie
2025年11月29日 15:50

useNamespace

useNamespace位于packages/hooks/use-namespace, 旨在帮所有组件统一生成类名/变量名,遵循BEM规范

什么是BEM规范?可阅读下面这篇文章blog.csdn.net/fageaaa/art…

element-plus的BEM类名生成函数_bem

const _bem = (
  namespace: string, // 命名空间,通常是el
  block: string, // 块名,例如button
  blockSuffix: string, // 块后缀(可选),用于块的变体
  element: string, // 元素(可选),用__连接
  modifier: string // 修饰符(可选),用--连接
) => {
  let cls = `${namespace}-${block}`
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  if (element) {
    cls += `__${element}`
  }
  if (modifier) {
    cls += `--${modifier}`
  }
  return cls
}

### 1. 参数说明

-   namespace:命名空间,通常是 'el'
-   block:块名,如 'button'
-   blockSuffix:块后缀(可选),用于块的变体
-   element:元素(可选),用 __ 连接
-   modifier:修饰符(可选),用 -- 连接

### 2. 生成规则(按顺序拼接)

-   基础:namespace-block → 'el-button'
-   如果有 blockSuffix:追加 -${blockSuffix} → 'el-button-suffix'
-   如果有 element:追加 __${element} → 'el-button__icon'
-   如果有 modifier:追加 --${modifier} → 'el-button--primary'

el-button组件为例子

const ns = useNamespace('button')

ns.namespace.value  // → 'el'
  • b-Block(块)
const b = (blockSuffix = '') => _bem(namespace.value, block, blockSuffix, '', '')

ns.b()  // el-button
ns.b('group')  // el-button-group
  • e-Element(元素)
const e = (element?: string) => element ? _bem(namespace.value, block, '', element, '') : ''

ns.e('icon')  // el-button__icon
ns.e('text')  // el-button__text
ns.e()  // 返回一个空字符串'', 因为传入的element:string参数是空
  • e-Modifier(修饰符)
const m = (modifier?: string) => modifier ? _bem(namespace.value, block, '', '', modifier) : ''

ns.m('primary')  // el-button--primary
ns.m('small')  // el-button--small
ns.m('disabled')  // el-button--disabled
ns.m()  // '' (空字符串)
  • be-Block+Element (块后缀+元素)
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element
      ? _bem(namespace.value, block, blockSuffix, element, '')
      : ''

ns.be('group', 'item') // el-button-group__item
ns.be('group', '') // ''
ns.be('', 'group')  // ''
  • em-Element+Modifier (元素+修饰符)
  const em = (element?: string, modifier?: string) =>
    element && modifier
      ? _bem(namespace.value, block, '', element, modifier)
      : ''
      
ns.em('icon', 'loading') // el-button__icon--loading
ns.em('text', 'expand') // el-button__text--expand
ns.em('icon', '') // ''
ns.em('', 'loading') // ''
  • bm-Block+Modifier (块后缀+修饰符)
  const bm = (blockSuffix?: string, modifier?: string) =>
    blockSuffix && modifier
      ? _bem(namespace.value, block, blockSuffix, '', modifier)
      : ''
      
ns.bm('group', 'vertical') // el-button-group--vertical
ns.bm('group', '') // ''
ns.bm('', 'primary') // ''
  • bem-Block+Element+Modifier (块后缀+元素+修饰符)
  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
    blockSuffix && element && modifier
      ? _bem(namespace.value, block, blockSuffix, element, modifier)
      : ''
      
ns.bem('group', 'item', 'active') // el-button-group__item--active
ns.bem('group', 'item', '') // '' 必须三个参数都有值
  • is-State 状态类
  const statePrefix = 'is-'

  const is: {
    (name: string, state: boolean | undefined): string
    (name: string): string
  } = (name: string, ...args: [boolean | undefined] | []) => {
    const state = args.length >= 1 ? args[0]! : true // args[0]! ts的非空断言
    return name && state ? `${statePrefix}${name}` : ''
  }
  
ns.is('loading')  // is-loading
ns.is('loading', true)  // is-loading
ns.is('loading', false)  // ''
ns.is('disabled', true)  // is-disabled
ns.is('disabled', undefined) // ''
  • cssVar-CSS变量(全局命名空间)
  const cssVar = (object: Record<string, string>) => {
    const styles: Record<string, string> = {}
    for (const key in object) {
      if (object[key]) {
        styles[`--${namespace.value}-${key}`] = object[key]
      }
    }
    return styles
  }
  
  ns.cssVar({ color: 'red', size: '10px'}) // {'--el-color': 'red', '--el-size': '10px'}
  • cssVarName-CSS 变量名(全局)
const cssVarName = (name: string) => `--${namespace.value}-${name}`

ns.cssVarName('color')  // → '--el-color'
ns.cssVarName('size')   // → '--el-size'

补充:命名空间与变量名的区别 命名空间:用{}包裹起来的批量的CSS变量+赋值,可以直接绑定到元素的style属性上 变量名:仅仅是一个单独的没有被赋值的变量,需要自己使用

cssVar 的使用场景(批量设置变量值)

<template>
  <div :style="customStyles">
    <!-- 这个 div 会应用这些 CSS 变量 -->
  </div>
</template>

<script setup>
const ns = useNamespace('button')
const customStyles = ns.cssVar({
  color: 'blue',
  fontSize: '16px'
})
// customStyles = { '--el-color': 'blue', '--el-fontSize': '16px' }
</script>

cssVarName 的使用场景(引用已存在的变量)

<template>
  <div :style="{ color: `var(${colorVarName})` }">
    <!-- 使用 cssVarName 获取变量名,然后用 var() 引用 -->
  </div>
</template>

<script setup>
const ns = useNamespace('button')
const colorVarName = ns.cssVarName('color')
// colorVarName = '--el-color'

// 然后在 CSS 或 style 中使用:
// color: var(--el-color)
</script>
  • cssVarBlock-CSS变量(带block)
  const cssVarBlock = (object: Record<string, string>) => {
    const styles: Record<string, string> = {}
    for (const key in object) {
      if (object[key]) {
        styles[`--${namespace.value}-${block}-${key}`] = object[key]
      }
    }
    return styles
  }
  
============
// 步骤 1: 创建命名空间实例,传入 'button' 作为 block
const ns = useNamespace('button')
// 此时 ns 内部保存了 block = 'button'

// 步骤 2: 调用 cssVarBlock
ns.cssVarBlock({ color: 'blue', fontSize: '14px' })

// 步骤 3: cssVarBlock 内部使用闭包中的 block
// 生成:'--el-button-color': 'blue'
// 生成:'--el-button-fontSize': '14px'
===========
ns.cssVarBlock({ color: 'blue', fontSize: '14px' })
// → { '--el-button-color': 'blue', '--el-button-fontSize': '14px' }
  • cssVarBlockName-CSS变量名(带block)
  const cssVarBlockName = (name: string) =>
    `--${namespace.value}-${block}-${name}`

ns.cssVarBlockName('color') // --el-button-color
ns.cssVarBlockName('bgColor') // --el-button-bgColor

深入理解 Async/Await:现代 JavaScript 异步编程的优雅解决方案

2025年11月29日 15:22

在现代 JavaScript 开发中,异步编程是一个无法回避的话题。从早期的回调函数到 Promise,再到 Generator 函数,JavaScript 一直在探索更优雅的异步编程解决方案。而 async/await 的出现,可以说是 JavaScript 异步编程领域的一次重大突破,它让异步代码的书写和阅读变得更加直观和简洁。

什么是 Async 函数?

Async 函数实际上是 Generator 函数的语法糖,但它在多个方面进行了重要优化,使得异步编程变得更加简单和直观。

内置执行器

与 Generator 函数需要额外的执行器(如 co 模块)不同,async 函数内置了执行器,可以像普通函数一样直接调用:

javascript

复制下载

async function fn() {
  return '张三';
}

const result = fn(); // 直接调用,无需额外执行器
console.log(result); // Promise {<fulfilled>: '张三'}

更好的语义

从字面上看,async 和 await 关键字直接表达了异步操作的语义。async 表示函数内部有异步操作,await 表示需要等待一个异步操作的完成。这种直观的表达方式大大提高了代码的可读性。

更广的适用性

await 命令后面不仅可以跟 Promise 对象,还可以跟原始类型的值(数值、字符串、布尔值等),这时这些值会被自动转成立即 resolve 的 Promise 对象:

javascript

复制下载

async function f() {
  const a = await 'hello'; // 等同于 await Promise.resolve('hello')
  const b = await 123;     // 等同于 await Promise.resolve(123)
  return a + b;
}

f().then(console.log); // 'hello123'

返回值是 Promise

async 函数总是返回一个 Promise 对象,这意味着我们可以使用 then 方法链式处理异步操作的结果:

javascript

复制下载

async function fn() {
  return '张三';
}

fn().then(value => {
  console.log(value); // '张三'
});

Async 函数的返回值详解

async 函数的返回值行为有几种不同的情况,理解这些细节对于正确使用 async 函数至关重要。

返回非 Promise 类型的对象

当 async 函数返回一个非 Promise 类型的对象时,返回值会被包装成一个成功状态的 Promise 对象:

javascript

复制下载

async function fn() {
  return '张三';
}

const result = fn();
console.log(result); // Promise {<fulfilled>: '张三'}

fn().then(value => {
  console.log(value); // '张三'
});

抛出错误

当 async 函数内部抛出错误时,返回值是一个失败状态的 Promise:

javascript

复制下载

async function fn() {
  throw new Error('出错了');
}

const result = fn();
console.log(result); // Promise {<rejected>: Error: 出错了}

fn().then(
  value => console.log(value),
  reason => console.log(reason) // Error: 出错了
);

返回 Promise 对象

当 async 函数返回一个 Promise 对象时,该 Promise 对象的状态决定了 async 函数返回的 Promise 状态:

javascript

复制下载

async function fn() {
  return new Promise((resolve, reject) => {
    // resolve('成功了');
    reject('失败了');
  });
}

const result = fn();
console.log(result); // Promise {<rejected>: '失败了'}

fn().then(
  value => console.log(value),
  reason => console.log(reason) // '失败了'
);

Await 表达式的深入理解

await 表达式是 async/await 的核心,它只能在 async 函数内部使用,具有以下几个重要特性。

等待 Promise 完成

await 后面通常跟一个 Promise 对象,它会暂停 async 函数的执行,等待 Promise 完成,然后返回 Promise 的成功值

javascript

复制下载

const p = new Promise((resolve, reject) => {
  resolve('成功了');
  // reject('失败了');
});

async function f1() {
  const result = await p;
  console.log(result); // '成功了'
}
f1();

错误处理

当 await 后面的 Promise 变为拒绝状态时,await 表达式会抛出异常,需要通过 try...catch 结构来捕获:

javascript

复制下载

const p = new Promise((resolve, reject) => {
  reject('失败了');
});

async function f2() {
  try {
    const result = await p;
    console.log(result);
  } catch(err) {
    console.log(err); // '失败了'
  }
}
f2();

等待 Thenable 对象

await 后面不仅可以跟 Promise 对象,还可以跟任何定义了 then 方法的对象(thenable 对象),await 会将其视为 Promise 对象来处理:

javascript

复制下载

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(() => {
      resolve(Date.now() - startTime);
    }, this.timeout);
  }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime); // 大约 1000
})();

这种特性使得我们可以创建自定义的异步操作,只要对象实现了 then 方法,就可以与 await 一起使用。

错误处理策略

在 async 函数中,错误处理是一个需要特别注意的方面。

中断执行的问题

默认情况下,任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行:

javascript

复制下载

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

防止中断执行的策略

有时我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将 await 放在 try...catch 结构里面:

javascript

复制下载

async function f() {
  try {
    await Promise.reject('出错了');
  } catch (e) {
    // 捕获错误,但不中断执行
  }
  return await Promise.resolve('hello world');
}

f().then(v => console.log(v)); // 'hello world'

实际应用场景

文件读取

async/await 在处理多个顺序执行的异步操作时特别有用,比如文件读取:

// 模拟文件读取函数
function read1() {
  return new Promise((resolve, reject) => {
    // 模拟异步文件读取
    setTimeout(() => {
      resolve('文件1的内容');
    }, 1000);
  })
}

function read2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('文件2的内容');
    }, 1000);
  })
}

function read3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('文件3的内容');
    }, 1000);
  })
}

async function main() {
  try {
    const result1 = await read1();
    console.log(result1);
    const result2 = await read2();
    console.log(result2);
    const result3 = await read3();
    console.log(result3);
  } catch(err) {
    console.log(err);
  }
}

main();

并发执行优化

虽然上面的例子展示了顺序执行异步操作,但在实际开发中,如果多个异步操作之间没有依赖关系,我们可以使用 Promise.all 来并发执行,提高效率:

javascript

复制下载

async function main() {
  try {
    const [result1, result2, result3] = await Promise.all([
      read1(),
      read2(),
      read3()
    ]);
    console.log(result1, result2, result3);
  } catch(err) {
    console.log(err);
  }
}

Async/Await 与传统异步方案的对比

与 Promise 链的对比

使用传统的 Promise 链:

javascript

复制下载

function fetchData() {
  return fetch('/api/data1')
    .then(response => response.json())
    .then(data1 => {
      return fetch('/api/data2')
        .then(response => response.json())
        .then(data2 => {
          return { data1, data2 };
        });
    });
}

使用 async/await:

javascript

复制下载

async function fetchData() {
  const response1 = await fetch('/api/data1');
  const data1 = await response1.json();
  
  const response2 = await fetch('/api/data2');
  const data2 = await response2.json();
  
  return { data1, data2 };
}

可以看到,async/await 版本的代码更加直观,逻辑更加清晰。

与 Generator 函数的对比

使用 Generator 函数处理异步:

javascript

复制下载

function* fetchData() {
  const response1 = yield fetch('/api/data1');
  const data1 = yield response1.json();
  
  const response2 = yield fetch('/api/data2');
  const data2 = yield response2.json();
  
  return { data1, data2 };
}

// 需要执行器
function run(generator) {
  const iterator = generator();
  
  function iterate(iteration) {
    if (iteration.done) return iteration.value;
    const promise = iteration.value;
    return promise.then(result => iterate(iterator.next(result)));
  }
  
  return iterate(iterator.next());
}

run(fetchData);

使用 async/await:

javascript

复制下载

async function fetchData() {
  const response1 = await fetch('/api/data1');
  const data1 = await response1.json();
  
  const response2 = await fetch('/api/data2');
  const data2 = await response2.json();
  
  return { data1, data2 };
}

// 直接调用
fetchData();

明显可以看出,async/await 方案更加简洁,无需额外的执行器。

最佳实践和注意事项

1. 始终处理错误

在使用 async/await 时,不要忘记错误处理。可以使用 try...catch 结构,或者使用 .catch() 方法:

javascript

复制下载

// 方式一:使用 try...catch
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('获取数据失败:', error);
    throw error; // 或者返回默认值
  }
}

// 方式二:使用 .catch()
fetchData().catch(error => {
  console.error('获取数据失败:', error);
});

2. 避免不必要的 await

不要滥用 await,只有在需要等待异步操作完成时才使用它:

javascript

复制下载

// 不推荐
async function example() {
  const a = await 1; // 不必要的 await
  const b = await 2; // 不必要的 await
  return a + b;
}

// 推荐
async function example() {
  const a = 1;
  const b = 2;
  return a + b;
}

3. 合理使用并发

当多个异步操作之间没有依赖关系时,应该并发执行它们,而不是顺序执行:

// 不推荐 - 顺序执行
async function fetchSequential() {
  const user = await fetchUser();
  const posts = await fetchPosts();
  const comments = await fetchComments();
  return { user, posts, comments };
}

// 推荐 - 并发执行
async function fetchConcurrent() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  return { user, posts, comments };
}

总结

Async/await 是 JavaScript 异步编程的重大进步,它通过更加直观和简洁的语法,让我们能够以近乎同步的方式编写异步代码,同时保持了异步操作的非阻塞特性。

从本质上讲,async 函数是 Generator 函数的语法糖,但它通过内置执行器、更好的语义、更广的适用性和 Promise 返回值等优化,大大提升了开发体验。await 表达式则让我们能够以同步的方式编写异步逻辑,使代码更加清晰易读。

深入理解 JavaScript 原型链:从 Promise.all 到动态原型的实战探索

2025年11月29日 15:22

本文将带你穿越 ES6 异步编程与 JavaScript 面向对象的核心机制,通过一段看似“诡异”的代码,揭示原型链的本质、动态性及其在实际开发中的意义。


引子:一段“奇怪”的代码

先来看这段来自 2.js 的代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.speci = '人类';

let zhen = new Person('郑总', 18);
console.log(zhen.speci); // 输出:人类

const kong = {
  name: '孔子',
  hobbies: ['读书', '喝酒']
};

zhen.__proto__ = kong;
console.log(zhen.hobbies, zhen.speci); // 输出:['读书', '喝酒'] undefined

你可能会疑惑:

  • 为什么修改 zhen.__proto__ 后,speci 属性就消失了?
  • 这和我们常说的“原型链”有什么关系?
  • 这种操作在真实项目中有用吗?

别急,让我们从 JavaScript 的面向对象本质说起。


一、JavaScript 的面向对象:不是血缘,而是委托

与 Java、C++ 等基于“类继承”的语言不同,JavaScript 的面向对象是基于原型(Prototype)的。它没有“父子类”的血缘概念,只有对象之间的委托关系

核心三要素:

  1. 构造函数(Constructor)
    Person,用于创建实例。
  2. 原型对象(Prototype)
    每个函数都有一个 prototype 属性,指向一个对象,该对象会被实例的 __proto__ 所引用。
  3. 原型链(Prototype Chain)
    当访问一个对象的属性时,若自身没有,则沿着 __proto__ 向上查找,直到 null

✅ 记住:obj.__proto__ === ObjConstructor.prototype

因此,当我们执行:

let zhen = new Person('郑总', 18);

实际上建立了这样的关系:

zhen.__proto__Person.prototypeObject.prototypenull

所以 zhen.speci 能找到 '人类',因为它委托给了 Person.prototype


二、动态原型:运行时改变对象的“行为模板”

关键来了!JavaScript 的原型链是动态可变的

当你写下:

zhen.__proto__ = kong;

你就强行切断了 zhenPerson.prototype 的联系,转而让它委托给 kong 对象。

于是新的原型链变成:

zhen.__proto__ → kong → Object.prototypenull
  • zhen.hobbies → 在 kong 上找到 → ['读书', '喝酒']
  • zhen.specikong 上没有 → 继续找 Object.prototype → 没有 → 返回 undefined

⚠️ 注意:这种操作虽然合法,但性能差且不推荐(现代引擎会优化固定原型链,动态修改会破坏优化)。但在某些特殊场景(如 mock、调试、元编程)中仍有价值。


三、从原型链到异步:Promise.all 与 ES6 的设计哲学

你可能注意到 readme.md 中提到了:

Promise.all Promise es6提供的异步解决方案 实例 proto 指向原型对象

这其实暗示了一个更深层的联系:ES6 的 Promise 也是基于原型链构建的

例如:

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);

const all = Promise.all([p1, p2]);
console.log(all.__proto__ === Promise.prototype); // true

Promise.all 返回的仍然是一个 Promise 实例,它继承了 Promise.prototype 上的方法(如 .then, .catch)。这体现了 JavaScript 一切皆对象、统一委托模型的设计哲学。

🌟 无论是同步的对象方法,还是异步的 Promise 链,底层都依赖同一个原型机制。


四、思考:原型链 vs 类继承 —— 哪种更灵活?

特性 原型链(JS) 类继承(Java/Python)
运行时修改 ✅ 支持(如 __proto__ ❌ 编译时固定
多继承模拟 ✅ 通过混入(Mixin) ❌ 通常单继承
性能 ⚠️ 动态修改影响优化 ✅ 静态结构易优化
可读性 ❌ 初学者易混淆 ✅ 直观

JavaScript 的原型系统牺牲了一点“直观性”,换来了极致的灵活性。这也是为什么像 Vue、React 等框架能通过原型扩展实现强大的响应式或组件系统。


五、最佳实践建议

  1. 避免直接操作 __proto__
    使用 Object.setPrototypeOf(obj, proto)(仍不推荐),或更好的方式:在创建对象时就确定原型(如 Object.create(proto))。

  2. 理解 constructor 的作用
    Person.prototype.constructor === Person,但如果你重写整个 prototype,记得手动修复:

    Person.prototype = { /* ... */ };
    Person.prototype.constructor = Person; // 修复
    
  3. 善用原型进行方法共享
    将公共方法放在 prototype 上,节省内存:

    Person.prototype.sayHi = function() { console.log(`Hi, I'm ${this.name}`); };
    

结语:原型链,JavaScript 的灵魂

new Person()Promise.all(),从静态属性到动态委托,原型链是贯穿 JavaScript 语言的核心脉络。它不仅是面试题,更是理解这门语言“为何如此设计”的钥匙。

下次当你看到 __proto__,不要只想到“黑魔法”,而应看到:这是一个对象在运行时寻找答案的旅程

正如孔子曰:“学而不思则罔。”
在 JS 的世界里,用而不悟原型,则码如浮云


pnpm 凭啥吊打 npm/Yarn?前端包管理的 “硬链接魔法”,破解三大痛点

2025年11月29日 15:13

从npm到pnpm——JavaScript包管理工具的演进之路:一场前端工程的“减肥”与“提速”革命

🚀 1. 引言:为什么包管理工具如此重要?

想象一下,你的前端项目是一艘准备远航的巨轮,而那些动辄几百上千的依赖包,就是船上所需的各种物资和零件。如果没有一个高效、可靠的“港口管理员”,这艘船会怎样?

轻则物资堆放混乱,找个零件得翻箱倒柜;重则零件版本不兼容,船还没出海就抛锚了。

JavaScript 这个日新月异的生态中,包管理工具正是扮演着这个至关重要的“港口管理员”角色。它负责依赖的下载、安装、版本控制,确保你的项目能够稳定、高效地运行。

今天,我们不只是回顾历史,而是要进行一场关于 npmYarnpnpm 的“三代同堂”演进分析。我们将看到,每一次工具的迭代,都是前端工程师们对 “更快、更省、更稳” 的不懈追求。

❓ 提出问题:为什么我们需要不断迭代包管理工具?

答案很简单:因为老工具在面对日益庞大和复杂的项目时,已经力不从心了。接下来的故事,就是关于我们如何从“绿皮火车”一路升级到“磁悬浮列车”的历程。

👴 2. npm:包管理的起点与“甜蜜的烦恼”

✨ 2.1. npm的起源和基本功能

npm(Node Package Manager)诞生于 2010 年,是随着 Node.js 一起出现的官方包管理器,可以说是 JavaScript 模块化的奠基人。

它的核心功能简单而强大:

  • 连接庞大的生态: 拥有全球最大的软件包注册表 npmjs.com
  • 核心命令: npm installnpm update,简单粗暴,一键搞定依赖。
  • 版本锁定: 通过 package.json 和后来的 package-lock.json 来管理依赖版本。

⚠️ 2.2. npm的早期问题:“又大又慢的node_modules

npm 早期(尤其是 v2 时代)的依赖管理是 嵌套结构,导致了著名的 “依赖地狱”(Dependency Hell) 。为了解决这个问题,npm v3 引入了 扁平化(Hoisting) 机制。

然而,扁平化虽然解决了“地狱”,却带来了新的“烦恼”:

痛点 描述 形象比喻
安装速度慢 早期 npm 采用串行下载和安装,I/O 操作频繁。 一个人排队去超市买 100 样东西。
磁盘空间浪费 即使 10 个项目都依赖 lodash,每个项目的 node_modules 里都会有一份完整的 lodash 副本。 10 个邻居各自买了一模一样的 10 台电视机。
幽灵依赖 依赖包被提升到根目录,导致项目可以访问未在 package.json 中声明的依赖。 你没买票,却坐上了头等舱,一旦“查票”(依赖升级),你就得露馅。

案例: 想象一个大型 Monorepo 项目,安装一次依赖可能需要 5-10 分钟,而最终生成的 node_modules 文件夹体积轻松突破 5GB。这不仅耗费时间,对 CI/CD 流程也是巨大的负担。

过渡: 面对这些问题,社区开始寻找更快的“跑车”,于是 Yarn 登场了。

🏃 3. Yarn:速度与确定性的改进

✨ 3.1. Yarn的出现背景

2016 年,Facebook(现 Meta)推出了 Yarn,它直接对标 npm 的痛点,喊出了 “更快、更可靠、更安全” 的口号。

🔄 3.2. Yarn如何解决问题

Yarn 的核心改进在于 速度确定性

  1. 速度优化:

    • 并行下载: 告别串行,多个依赖可以同时下载。
    • 离线缓存: 引入全局缓存,如果本地有包,下次安装直接从缓存读取,无需联网。
  2. 确定性保证:

    • yarn.lock 强制引入锁文件,精确记录了依赖树的结构和版本,确保了“在我电脑上能跑,在你电脑上也能跑”的团队协作一致性。

案例: 在一个中型项目中,npm install 可能需要 2 分钟,而 yarn install 往往能缩短到 30 秒 左右,提速效果立竿见影。

⚠️ 3.3. Yarn的局限性:未解决的“肥胖”问题

Yarn 成功解决了速度和确定性问题,但它在 磁盘空间利用率 上,依然沿用了 npm 的扁平化结构,这意味着:

  • node_modules 依然庞大: 尽管安装快了,但每个项目依然要存储一份依赖副本,磁盘空间浪费问题没有根本解决。
  • 幽灵依赖仍在: 扁平化结构是幽灵依赖的温床,项目仍然可能意外地使用到未声明的依赖。

过渡: 既然速度已经够快,下一个目标自然是 “如何让我们的项目更瘦、更安全” 。于是,一个专注于“极致效率”的工具——pnpm 出现了。

🚀 4. pnpm:高效与存储优化的新时代

✨ 4.1. pnpm的起源与核心思想

pnpm(Performant npm)由 Zoltan Kochan 在 2017 年开发,它的核心思想是: “只存一份,多处使用” 。它通过一种巧妙的文件系统操作,彻底解决了困扰前端多年的 磁盘空间浪费幽灵依赖 问题。

🔧 4.2. pnpm如何实现“瘦身”与“提速”

pnpm 的魔法在于它对 node_modules 结构的颠覆:

1. 终极“瘦身”秘诀:内容可寻址存储 + 硬链接

pnpm 在你的电脑上创建了一个 全局内容可寻址存储区(Content-addressable Store) ,所有依赖包的实际文件内容都只在这个地方 存储一份

当你在项目 A 和项目 B 中安装 lodash 时,pnpm 不会复制文件,而是通过 硬链接(Hard Link) 的方式,将全局仓库中的 lodash 文件链接到项目 A 和 B 的 node_modules 中。

  • 硬链接 几乎不占用额外的磁盘空间。
  • 这意味着,你有 100 个项目依赖 lodash,它在你的硬盘上也只占用 一份 空间。

storage_comparison.png

2. 告别“幽灵依赖”:严格的符号链接结构

pnpm 采用了一种 非扁平化node_modules 结构,它通过 符号链接(Symbolic Link) 来严格控制依赖的访问权限。

在 pnpm 的 node_modules 根目录下,你只会看到你 显式声明 的依赖包。这些包实际上是指向一个特殊目录(.pnpm)的符号链接。

// 你的项目根目录下的 node_modules 结构
node_modules/
  .pnpm/  // 实际的依赖文件都在这里,通过硬链接指向全局仓库
  └── my-project -> .pnpm/my-project@1.0.0/node_modules/my-project
  └── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash

这种结构保证了:

  • 安全性: 你只能访问你声明的依赖,彻底杜绝了“幽灵依赖”的隐患。
  • 速度: 由于大量使用了链接而非复制,安装速度比 Yarn 更快,尤其是在 CI/CD 环境中。

📊 4.3. pnpm相对于npm和Yarn的改进总结

特性 npm (v3+) Yarn (v1) pnpm
模块结构 扁平化 (Hoisting) 扁平化 (Hoisting) 嵌套 + 符号链接
磁盘占用 高(多份副本) 中(多份副本) 最低(全局硬链接)
安装速度 慢/中 极快(链接操作)
依赖隔离 不严格(有幽灵依赖) 不严格(有幽灵依赖) 严格(默认杜绝幽灵依赖)
Monorepo支持 Workspaces Workspaces 最佳(内置支持,高效复用)

案例: 在一个拥有 10 个子包的 Monorepo 项目中,pnpm 可以节省 70% 以上 的磁盘空间,并且在 CI/CD 流程中,安装时间可以从 3 分钟缩短到 30 秒以内

过渡: pnpm 几乎是当前包管理工具的“最优解”,但它并非完美无缺。

🚧 5. 当前包管理工具的挑战与问题

❌ 5.1. 通用问题:生态的“隐形炸弹”

  • 生态碎片化: package-lock.jsonyarn.lockpnpm-lock.yaml 互不兼容,团队协作中切换工具容易引发混乱。
  • 安全漏洞: 供应链攻击(Supply Chain Attacks)日益猖獗,依赖链越长,风险越高。

❓ 5.2. pnpm特有问题:学习曲线与兼容性

pnpm 虽好,但也有其“个性”:

  • 学习曲线稍陡: 严格的依赖结构(非扁平化)可能让习惯了 npm/Yarn 扁平结构的开发者感到不适应,尤其是在处理一些老旧的、依赖于“幽灵依赖”特性的库时。
  • 迁移难度: 对于大型老项目,从 npm/Yarn 迁移到 pnpm 可能需要调整部分代码,以修复因幽灵依赖被消除而引发的错误。
  • 文件系统兼容性: 硬链接和符号链接在某些非主流文件系统或 Windows 的 WSL 环境下,可能会遇到一些权限或兼容性问题(不过目前已基本解决)。

未来展望:Bun 这样内置了包管理器的工具正在出现,它们试图将包管理、运行时和构建工具三合一,或许能从根本上解决这些痛点。

💡 6. 总结对比:npm vs Yarn vs pnpm

维度 npm (v3+) Yarn (v1) pnpm
演进定位 奠基者 速度优化者 效率与存储优化者
核心机制 扁平化复制 扁平化复制 + 缓存 硬链接 + 符号链接
磁盘空间 浪费严重 浪费严重 极致节省
安装速度 极快
依赖安全 差(幽灵依赖) 差(幽灵依赖) 优秀(严格隔离)
Monorepo 支持(一般) 支持(较好) 最佳(高效复用)
适用场景 小型、个人项目 中型项目、追求速度 大型/企业级项目、Monorepo

演进路径回顾:

  1. npm: 解决了“有没有”的问题,但带来了“大”和“慢”的问题。
  2. Yarn: 解决了“慢”的问题,但没有解决“大”的问题。
  3. pnpm: 彻底解决了“大”和“幽灵依赖”的问题,同时将“快”推向了极致。

🏆 7. 公司推荐:为什么选择pnpm?

基于以上对比,我的建议非常明确:

对于任何追求工程化、拥有多个项目或正在使用 Monorepo 架构的团队,pnpm 都是当前最值得推荐的包管理工具。

选择 pnpm,你选择的不仅仅是更快的安装速度,更是:

  1. 巨大的成本节约: 节省 CI/CD 运行时间,就是节省金钱。
  2. 提升开发体验: 告别漫长的 npm install 等待,将更多时间投入到业务开发中。
  3. 项目稳定性: 严格的依赖隔离机制,从根本上杜绝了因“幽灵依赖”引发的潜在 Bug。

实施建议:

  • 新项目: 直接使用 pnpm init 启动。
  • 老项目迁移: 建议先在非核心项目尝试,通过 pnpm import 导入 package-lock.jsonyarn.lock,然后运行 pnpm install,并根据报错信息修复因幽灵依赖导致的错误。

结语:

前端工程化的发展,就是不断地在追求极致的效率和稳定性。包管理工具的演进,清晰地展现了这一点。拥抱 pnpm,就是拥抱更高效、更稳定的前端未来。

你还在用 npm 吗?是时候换个“跑鞋”了!

Vue3 + Less 实现动态圆角 TabBar:从代码到优化实践

作者 颜渊呐
2025年11月29日 14:53

在前端开发中,TabBar 是一个非常常见的 UI 组件,而在一些设计需求中,我们希望激活的 Tab 有动态变化的圆角效果,并且宽度可以根据选中状态自适应。本文将分享一个 Vue3 + Less 实现的 TabBar 组件案例,完整分析代码结构、实现思路,并展示优化技巧。

效果预览:

image.png

image.png

image.png

🧩 需求分析

我们希望 TabBar 拥有以下特性:

  1. 动态激活状态:根据 modelValue 高亮当前 Tab。
  2. 圆角过渡效果:激活 Tab 的上下圆角动态显示。
  3. 自适应宽度:选中 Tab 宽度放大,未选中 Tab 等分。
  4. 复用性强:多种激活形态(左圆角、右圆角、左右圆角)可复用样式。
  5. 响应用户点击:选中状态更新并触发事件。

🛠 组件实现

1️⃣ 模板部分

<div class="tabbar" :style="{
      'grid-template-columns': getGridFr
    }">
  <div
    :class="{
    'tabbar-item': true,
    'active': modelValue === it.value,
    'active3': index > 0 && index < tabs.length - 1,
    'active1': index === 0,
    'active2': index === tabs.length - 1,
    }"
    v-for="(it, index) in tabs"
    @click="emit('update:modelValue', it.value)"
  >
    <div class="top"></div>
    <div style="display: flex;align-items: center">
      <img style="width: 20px;margin-right: 5px;" :src="it.icon" alt="">
      {{ it.label }}
    </div>

  </div>
</div>
  • 动态类绑定:根据不同 Tab 值,使用 active1/active2/active3 控制圆角样式。
  • 网格布局grid-template-columns 根据选中状态动态调整宽度。

样式部分(Less )

.tabbar {
  display: grid;
  transition: all 0.3s;
  --th: 8px;

  &-item {
    text-align: center;
    height: 40px;
    background-color: var(--el-color-primary);
    color: #fff;
    font-size: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    user-select: none;
  }

  // ----------- 公共激活样式 -----------
  .active-base() {
    color: var(--el-color-primary);
    background-color: #fff;
    position: relative;
    --bh: 40px;

    .top {
      position: absolute;
      height: var(--th);
      background-color: #fff;
      top: calc(var(--th) * -1);
    }
  }

  // 圆角片段(左右通用)
  .circle(@pos, @clip) {
    display: block;
    content: '';
    position: absolute;
    width: var(--bh);
    height: var(--bh);
    background-color: var(--el-color-primary);
    @{pos}: 0;
    bottom: 0;
    z-index: 1;
    clip-path: @clip;
  }

  // ----------- 各种激活样式 -----------

  // 左右都有圆角
  .active {
    .active-base();

    .top {
      border-radius: 100px 100px 20px 20px;
      width: calc(100% - 80px);
    }

    &::before { .circle(left, ellipse(100% 100% at 0% 0%)); }
    &::after  { .circle(right, ellipse(100% 100% at 100% 0%)); }
  }

  // 右侧圆角
  .active1 {
    .active-base();

    .top {
      border-radius: 0 100px 20px 20px;
      width: calc(100% - 40px);
      left: 0;
    }

    &::after { .circle(right, ellipse(100% 100% at 100% 0%)); }
  }

  // 左侧圆角
  .active2 {
    .active-base();

    .top {
      border-radius: 100px 0 20px 20px;
      width: calc(100% - 40px);
      right: 0;
    }

    &::before { .circle(left, ellipse(100% 100% at 0% 0%)); }
  }
}

前端跨界破壁:用Web技术打造智能报工系统——扫码、称重与多协议打印实战

作者 一川_
2025年11月29日 14:53

在现代制造业数字化转型中,前端开发者的角色正在发生深刻变化。我们不再只是构建用户界面,更要成为连接物理世界与数字世界的桥梁。本文将分享我如何用纯Web技术栈,攻克扫码报工、智能称重、蓝牙/USB双模打印等硬件集成难题,打造出一套高效的车间智能报工系统。

一、 需求场景:当浏览器走进车间

我们的目标是在工业安卓PDA的浏览器环境中,实现完整的报工作业流程:

  1. 扫码绑定 - 扫描设备二维码,自动载入对应工单
  2. 智能称重 - 连接电子秤,重量实时显示并自动计算数量
  3. 灵活打印 - 支持蓝牙和USB两种方式的标签打印

技术亮点:  纯Web方案,无需原生App,跨平台部署,维护成本极低。

二、 技术架构:面向硬件的现代Web技术栈

bash

# 核心依赖
"@vue/composition-api"  # 状态管理
"qrcode.vue"            # 二维码生成(测试用)
"@capacitor/browser"    # 增强的浏览器能力(可选)

三、 核心难点攻克与实践

1. 二维码扫描:原生Barcode Detection API

export function useBarcodeScanner() {
  const startScan = async () => {
    // 1. 获取摄像头权限
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: "environment" }
    });
    
    // 2. 创建检测器
    const barcodeDetector = new BarcodeDetector({ formats: ['qr_code'] });
    
    // 3. 实时检测循环
    const detectFrame = async () => {
      const barcodes = await barcodeDetector.detect(videoElement);
      if (barcodes.length > 0) {
        const result = barcodes[0].rawValue;
        await handleScanResult(JSON.parse(result));
        return;
      }
      requestAnimationFrame(detectFrame);
    };
  };
}

技术深度:

  • 性能优化:使用 requestAnimationFrame 避免过度检测
  • 降级方案:不支持原生API时 fallback 到 jsqr 库

2. 电子秤对接:Web Serial API 实现实时数据流

export function useScaleReader() {
  const connectScale = async () => {
    // 用户选择电子秤设备
    const port = await navigator.serial.requestPort();
    await port.open({ baudRate: 9600, dataBits: 8 });
    
    // 创建数据流处理管道
    const decoder = new TextDecoderStream();
    const readableStreamClosed = port.readable.pipeTo(decoder.writable);
    const reader = decoder.readable.getReader();
    
    // 持续读取数据
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      
      const weight = parseWeightData(value);
      if (weight !== null) {
        // 应用数字滤波算法
        filteredWeight.value = applyKalmanFilter(weight);
      }
    }
  };
  
  // 移动平均滤波实现
  const applyMovingAverage = (newValue) => {
    weightWindow.push(newValue);
    if (weightWindow.length > 5) weightWindow.shift();
    return weightWindow.reduce((a, b) => a + b) / weightWindow.length;
  };
}

3. 双模打印:蓝牙 & USB 灵活切换

方案A:WebUSB 打印(高性能推荐)

export function useUsbPrinter() {
  const printViaUSB = async (zplCode) => {
    // 1. 选择USB设备
    const device = await navigator.usb.requestDevice({ 
      filters: [{ vendorId: 0x0C2E }] // 斑马打印机厂商ID
    });
    
    await device.open();
    await device.claimInterface(2); // 打印机接口
    
    // 2. 发送ZPL指令
    const encoder = new TextEncoder();
    const data = encoder.encode(zplCode);
    await device.transferOut(5, data); // 输出端点
  };
}

方案B:Web Bluetooth 打印(无线便捷)

export function useBluetoothPrinter() {
  const printViaBluetooth = async (zplCode) => {
    // 1. 搜索并连接蓝牙设备
    const device = await navigator.bluetooth.requestDevice({
      filters: [{ services: ['generic_access'] }],
      optionalServices: ['serial_port'] // 串口服务
    });
    
    const server = await device.gatt.connect();
    const service = await server.getPrimaryService('serial_port');
    const characteristic = await service.getCharacteristic('tx_characteristic');
    
    // 2. 发送打印数据
    const encoder = new TextEncoder();
    const data = encoder.encode(zplCode);
    await characteristic.writeValue(data);
  };
}

ZPL标签动态生成:

function generateZPLLabel(labelData) {
  const { productName, workOrder, weight, quantity } = labelData;
  return `
    ^XA
    ^FO50,30^A0N,36,36^FD${productName}^FS
    ^FO50,80^A0N,28,28^FD工单:${workOrder}^FS
    ^FO50,120^A0N,28,28^FD重量:${weight}kg^FS
    ^FO50,160^A0N,28,28^FD数量:${quantity}^FS
    ^FO50,200^A0N,24,24^FD${new Date().toLocaleString()}^FS
    ^XZ
  `.trim();
}

四、 高级特性实现

1. 连接策略管理

export function usePrintManager() {
  const printStrategies = {
    usb: useUsbPrinter(),
    bluetooth: useBluetoothPrinter()
  };
  
  const autoDetectAndPrint = async (zplCode) => {
    // 尝试USB优先,失败后降级到蓝牙
    try {
      await printStrategies.usb.print(zplCode);
    } catch (usbError) {
      console.warn('USB打印失败,尝试蓝牙:', usbError);
      await printStrategies.bluetooth.print(zplCode);
    }
  };
}

2. 设备状态监控

// 监控打印机连接状态
const monitorPrinterStatus = async () => {
  const characteristics = await service.getCharacteristics();
  const statusChar = characteristics.find(c => c.uuid === 'status_characteristic');
  
  statusChar.addEventListener('characteristicvaluechanged', event => {
    const status = event.target.value.getUint8(0);
    updatePrinterStatusUI(status);
  });
  await statusChar.startNotifications();
};

3. 打印队列管理

class PrintQueue {
  constructor() {
    this.queue = [];
    this.isPrinting = false;
  }
  
  async addJob(zplData) {
    this.queue.push(zplData);
    if (!this.isPrinting) {
      await this.processQueue();
    }
  }
  
  async processQueue() {
    this.isPrinting = true;
    while (this.queue.length > 0) {
      const job = this.queue.shift();
      try {
        await autoDetectAndPrint(job);
      } catch (error) {
        console.error('打印任务失败:', error);
        // 重试逻辑
      }
    }
    this.isPrinting = false;
  }
}

五、 实战经验与性能优化

  1. 内存管理

    // 及时释放硬件资源
    const cleanup = () => {
      stream?.getTracks().forEach(track => track.stop());
      serialReader?.cancel();
      bluetoothDevice?.gatt?.disconnect();
    };
    
  2. 错误恢复机制

    const withRetry = async (fn, maxRetries = 3) => {
      for (let i = 0; i < maxRetries; i++) {
        try {
          return await fn();
        } catch (error) {
          if (i === maxRetries - 1) throw error;
          await new Promise(resolve => setTimeout(resolve, 1000 * i));
        }
      }
    };
    
  3. PWA离线支持

    // service-worker.js
    self.addEventListener('fetch', (event) => {
      if (event.request.url.includes('/api/print-templates')) {
        event.respondWith(caches.match(event.request));
      }
    });
    

六、 总结

通过这个项目,我们证明了现代Web技术完全有能力处理复杂的工业场景:

  • Web Serial API 让浏览器能够直接与串口设备通信
  • WebUSB API 提供了高性能的设备访问能力
  • Web Bluetooth API 实现了无线设备连接
  • Barcode Detection API 让扫码变得简单高效

技术价值:

  1. 降低成本 - 无需开发维护多个原生App
  2. 部署灵活 - 更新即时生效,无需应用商店审核
  3. 硬件兼容 - 统一API处理多种连接方式
  4. 用户体验 - 响应迅速,操作直观

这套方案不仅在报工场景中表现出色,更为前端在物联网、工业4.0领域开辟了新的可能性。当我们突破浏览器的传统边界,前端工程师就能在数字化转型中扮演更加关键的角色。


希望这篇从前端深度视角展开的技术文章对你有帮助!如果还需要调整某些技术细节,我可以继续完善。

C#彻底搞懂属性(Property)与字段(Field)

作者 烛阴
2025年11月28日 09:50

一、字段(Field)

字段是类或结构中直接声明的变量。它的唯一使命,就是作为对象状态的一部分,存储数据。

1. 字段的声明

public class Student
{
    // 这就是两个字段
    public string Name; // 学生姓名
    public int Age;     // 学生年龄
}

2. 访问修饰符 -- Public

Student stu = new Student();
stu.Name = "张三";
stu.Age = -10; // 灾难!年龄怎么可能是负数?

Console.WriteLine($"{stu.Name}的年龄是 {stu.Age}岁。");
// 输出: 张三的年龄是 -10岁。
  • 使用Public可以在任何时候,任何地方直接访问,并且修改,这会存在某些隐患

3. 访问修饰符 -- Private

  • Getter/Setter方法模式
public class Student
{
    // 将字段设为private,外界无法直接访问
    private string _name;
    private int _age;

    // 为了让外界能设置和获取值,我们提供一对公开的方法
    public string GetName()
    {
        return _name;
    }

    public void SetName(string name)
    {
        // 可以在这里加入验证逻辑
        if (string.IsNullOrWhiteSpace(name))
        {
            Console.WriteLine("姓名不能为空!");
            return;
        }
        _name = name;
    }

    public int GetAge()
    {
        return _age;
    }

    public void SetAge(int age)
    {
        // 验证逻辑!
        if (age < 0 || age > 150)
        {
            Console.WriteLine("年龄不合法!");
            return;
        }
        _age = age;
    }
}

// === 使用 ===
Student stu = new Student();
stu.SetName("李四");
stu.SetAge(-10); // 输出: 年龄不合法!
stu.SetAge(25);

Console.WriteLine($"{stu.GetName()}的年龄是 {stu.GetAge()}岁。");

二、属性(Property)

属性是字段的自然演进。它为私有字段提供了一个公开的、受控的访问层,但在语法上,它看起来就像一个公有字段。它巧妙地结合了方法的灵活性和字段的简洁性。

1. 属性的完整声明

一个完整的属性包含一个私有的字段和一对get/set访问器(Accessor)

public class Student
{
    // 1. 私有字段,用于实际存储数据
    private int _age;

    // 2. 公开属性,作为外界访问的入口
    public int Age
    {
        // 3. get访问器:当读取属性时执行
        get
        {
            // 可以加入逻辑,比如权限检查
            return _age;
        }

        // 4. set访问器:当给属性赋值时执行
        set
        {
            // 'value'是一个上下文关键字,代表赋过来的值
            if (value < 0 || value > 150)
            {
                // 可以抛出异常,这是更推荐的做法
                throw new ArgumentOutOfRangeException(nameof(Age), "年龄必须在0到150之间。");
            }
            _age = value;
        }
    }
}

// === 使用属性 ===
Student stu = new Student();
try
{
    stu.Age = 25; // 调用set访问器,value为25
    Console.WriteLine(stu.Age); // 调用get访问器

    stu.Age = -10; // 调用set访问器,value为-10,抛出异常
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

三、属性的进阶 - 语法糖

C#语言在不断发展,属性的写法也越来越简洁。

1. 自动实现属性(Auto-Implemented Properties)

在很多情况下,我们的属性不需要任何特殊的验证逻辑,只是简单地存取一个值。为这种情况手写字段和get/set代码块显得很冗余。于是,C# 引入了自动属性。

public class Product
{
    // 这就是自动属性
    // 编译器会自动在幕后创建一个私有的、匿名的字段
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// === 使用 ===
Product phone = new Product();
phone.Name = "Phone X"; // 背后调用了自动生成的set
phone.Price = 999.99m;

这是我们日常开发中最常用的形式。简洁、高效。只有当你需要添加自定义逻辑时,才需要退回到手动实现字段的方式。

2. 控制可访问性

我们可以为getset访问器设置不同的访问级别。

public class Order
{
    // Id只能在类的内部被设置(比如在构造函数中)
    // 但可以在任何地方被读取
    public int Id { get; private set; }

    public DateTime OrderDate { get; } // 只有get,这是一个只读属性

    public Order(int id)
    {
        this.Id = id; // 在内部设置是合法的
        this.OrderDate = DateTime.UtcNow; // 只读属性只能在构造函数或声明时赋值
    }
}

3. 表达式主体属性

对于一些只读的、由其他数据计算得出的属性,C# 提供了更简洁的写法。

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    // 这个只读属性的值是计算出来的
    public string FullName => $"{FirstName} {LastName}";
}

四、C#中的特殊属性

1. init访问器

init 访问器让属性拥有了特殊的“只写一次”能力。它允许属性在对象初始化器中被赋值,但在那之后就变成只读。

public class Book
{
    public string Isbn { get; init; } // 注意是init
    public string Title { get; init; }
}

// === 使用 ===
var book = new Book
{
    Isbn = "978-0321765723", // 合法!在对象初始化器中赋值
    Title = "The C# Programming Language"
};

// book.Title = "New Title"; // 编译错误!初始化完成后,不能再修改

2. required修饰符

创建对象时必须为某个属性赋值。

public class User
{
    public int Id { get; set; }
    public required string Username { get; set; } // 必须在创建时提供值
    public string? Email { get; set; } // 可选
}

// === 使用 ===
// var user1 = new User(); // 编译错误!Username是required的,但没有被赋值。
var user2 = new User { Username = "alice" }; // 正确

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文!

【鸿蒙开发实战篇】鸿蒙6 AI智能体集成实战

2025年11月29日 09:52

大家好,我是 V 哥。 鸿蒙6的 Agent Framework Kit 是连接应用与小艺智能体生态的核心工具,允许开发者在应用中嵌入智能体入口,实现“应用+智能体”协同服务。以下结合电商、工具类等典型场景,详解集成步骤、代码实战及避坑指南。

联系V哥获取 鸿蒙学习资料


一、核心概念与使用前提

关键概念 说明
FunctionComponent 智能体入口UI组件,根据是否设置title自动切换为图标按钮形态。
AgentController 控制器,用于检查智能体可用性、监听对话框状态(如打开/关闭)。
agentId 智能体唯一标识,需从小艺开放平台获取,长度限制1~64字符。

环境要求

  • 设备:鸿蒙6.0.0(20)及以上版本的手机/平板(模拟器可能不支持)。
  • 依赖:在module.json5中声明权限 "reqPermissions": [{ "name": "ohos.permission.INTERNET" }]

二、基础集成:快速拉起智能体

场景示例:电商App在商品详情页添加“智能客服”入口,用户点击直接唤起智能体咨询商品信息。

1. 基础代码实现
import { FunctionComponent, FunctionController } from '@kit.AgentFrameworkKit';
import { common, BusinessError } from '@kit.AbilityKit';

@Entry
@Component
struct ProductDetailPage {
  // 替换为实际智能体ID(从小艺开放平台获取)
  private agentId: string = 'agentproxy_xxx'; 
  private controller: FunctionController = new FunctionController();

  build() {
    Column() {
      // 商品信息展示...
      Text("华为Mate 60 Pro").fontSize(20)

      // 智能客服入口(按钮形态)
      FunctionComponent({
        agentId: this.agentId,
        onError: (err: BusinessError) => {
          console.error("智能体拉起失败:", err.code, err.message); // 错误处理必填
        },
        options: {
          title: '智能客服',     // 设置标题后显示为按钮
          queryText: '咨询华为Mate 60 Pro的续航和拍照功能' // 预设用户意图
        },
        controller: this.controller
      })
      .margin(20)
    }
  }
}
2. 形态适配策略
  • 图标形态(不设title):适合首页导航栏等综合入口。
  FunctionComponent({
    agentId: this.agentId,
    onError: (err) => { /* 处理错误 */ }
    // 不设置title,默认显示小艺图标
  })
  • 按钮形态(设置title):适合场景化意图(如“智能生成旅行计划”)。

三、进阶实战:状态监听与可用性检查

场景示例:工具类App在智能体对话框关闭后刷新页面数据(如智能生成报表后更新UI)。

1. 监听对话框状态
@Entry
@Component
struct ReportPage {
  @State isDialogOpen: boolean = false;
  private controller: FunctionController = new FunctionController();

  aboutToAppear() {
    // 监听对话框打开
    this.controller.on('agentDialogOpened', () => {
      this.isDialogOpen = true;
      console.info("智能体对话框已打开");
    });
    
    // 监听对话框关闭(关键:对话框关闭后刷新数据)
    this.controller.on('agentDialogClosed', () => {
      this.isDialogOpen = false;
      this.refreshReportData(); // 自定义数据刷新逻辑
    });
  }

  // 销毁时移除监听
  aboutToDisappear() {
    this.controller.off('agentDialogOpened');
    this.controller.off('agentDialogClosed');
  }

  build() {
    Column() {
      if (this.isAgentSupported) { // 需先检查可用性
        FunctionComponent({
          agentId: this.agentId,
          onError: (err) => { /* 错误处理 */ },
          controller: this.controller
        })
      } else {
        Text("当前设备不支持智能体功能").fontColor(Color.Red)
      }
    }
  }
}
2. 预检查智能体可用性(避免无效加载)
import { common } from "@kit.AbilityKit";

@Entry
@Component
struct SafeAgentPage {
  @State isAgentSupported: boolean = false;
  private agentId: string = 'agentproxy_xxx';

  async aboutToAppear() {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      // 异步检查智能体是否可用
      this.isAgentSupported = await FunctionController.isAgentSupport(context, this.agentId);
    } catch (err) {
      console.error("检查支持状态失败:", err);
    }
  }

  build() {
    Column() {
      if (this.isAgentSupported) {
        FunctionComponent({
          agentId: this.agentId,
          onError: (err) => { /* 处理错误 */ }
        })
      } else {
        Button("下载智能体支持模块")
          .onClick(() => { /* 引导用户升级系统 */ })
      }
    }
  }
}

四、常见问题与优化策略

问题场景 解决方案
agentId无效或设备不支持 使用isAgentSupport()预检查,降级显示提示或引导用户升级。
智能体拉起无响应 检查网络权限、确认小艺版本更新,错误回调中输出具体code。
界面卡顿 避免在主线程执行智能体相关操作,耗时逻辑放入TaskPool

五、业务场景扩展建议

  1. 电商场景

    • 商品页设置“智能推荐”按钮,预设查询文本如“推荐适合老年人的手机”。
    • 订单页嵌入“物流查询”智能体,自动读取订单号生成查询意图。
  2. 工具场景

    • 笔记App用图标形态智能体作为全局入口,支持语音速记。
    • 旅行App通过按钮形态智能体生成行程规划(queryText: "规划北京3日游")。

通过以上步骤,可快速在鸿蒙6应用中集成智能体功能,提升用户交互体验。

【鸿蒙开发实战篇】鸿蒙开发中如何利用代码检查工具(codelinter)的技巧和经验

2025年11月29日 09:51

大家好,我是 V 哥。 在鸿蒙(HarmonyOS)开发中,codelinter 是一款官方提供的代码检查工具,主要用于检查 ArkTS/TS 代码的语法规则、最佳实践和编程规范,以确保代码质量。

联系V哥获取 鸿蒙学习资料

以下是 codelinter 工具的详细使用方法和步骤:

一、 使用场景

codelinter 工具支持两种主流的使用方式,适用于不同的业务场景:

  1. 在 IDE 中快速检查与修复

    • 适用场景 :在日常开发过程中,快速对单个或多个文件进行代码质量检查,并能立即查看问题和进行修复。
    • 操作方法 :在 DevEco Studio 编辑器窗口中,右键点击想要检查的文件或目录,然后选择 Code Linter 即可开始检查。
  2. 通过命令行进行自动化检查

    • 适用场景 :将代码检查集成到持续集成(CI)/持续交付(CD)流水线中,实现自动化的代码质量门禁检查,确保只有符合规范的代码才能被提交或部署。
    • 操作方法 :通过命令行工具调用 codelinter 对整个工程进行检查,并可以生成报告。

二、 详细使用步骤

我们将重点介绍更具复用性和自动化价值的 命令行工具 的使用方法。

第一步:获取并配置命令行工具

  1. 下载工具 :从华为开发者官网的 CommandLine 工具包中获取 codelinter 命令行工具。
  2. 解压与环境变量配置 :将下载的工具包解压,并将其 bin 目录添加到系统的环境变量中,以便在任意位置使用 codelinter 命令。

第二步:配置检查规则(可选但推荐)

为了让代码检查更贴合团队的编码规范,您可以在工程根目录下创建一个名为 code-linter.json5 的配置文件。

  • 核心配置项说明
    • filesignore:用来指定需要检查的文件范围。
        {
          "files": [" **/*.ets", "** /*.ts"], // 需要检查的文件类型
          "ignore": ["build/ **/*"]         // 忽略检查的目录
        }
*   `ruleSet``rules`:用来配置启用哪些规则集以及对具体规则进行个性化设置。
        {
          "ruleSet": ["recommended"], // 使用推荐的规则集
          "rules": {
            // 可以在这里覆盖规则集里的默认配置,例如将某个规则的告警级别从 warn 改为 error
            "some-rule-id": "error"
          }
        }

** 第三步:执行代码检查 **

配置完成后,您就可以在工程目录下运行 codelinter 命令了。

基础语法:

    codelinter [options] [dir]
*   `dir`:指定要检查的工程根目录,不指定则默认为当前目录。
*   `options`:一系列可选参数。

常用命令组合:

1.  检查指定工程,并使用特定配置文件:
        codelinter -c ./path/to/code-linter.json5 /your/project/dir
2.  检查当前目录,并自动修复可快速修复的问题:
        codelinter --fix
3.  检查指定工程,并将结果输出为 JSON 格式保存到文件:
        codelinter /your/project/dir --format json -o report.json

三、 实际案例演示

假设我们有一个简单的鸿蒙项目,其目录结构如下:

my-harmony-project/
├── src/
│   └── main/
│       ├── pages/
│       │   └── index.ets
│       └── utils/
│           └── helper.ts
└── build/
    └── ... (编译产物)

我们想对 src/main 目录下的所有 .ets.ts 文件进行代码检查,但排除 build 目录。

操作步骤如下:

  1. 创建配置文件: 在 my-harmony-project 根目录下创建 code-linter.json5 文件,并写入以下内容:
    {
      "files": ["src/main/** /*.ets", "src/main/ **/*.ts"],
      "ignore": ["build/** /*"],
      "ruleSet": ["recommended"]
    }
  1. 执行检查命令 : 在 my-harmony-project 根目录打开终端,执行以下命令:
    codelinter -c code-linter.json5 .
  1. 查看检查结果 : 命令执行后,终端会输出详细的检查报告,列出所有发现的问题及其位置和描述。您可以根据报告中的指引手动修改代码,或者再次运行命令加上 --fix 参数来自动修复部分问题。

通过以上步骤,您就可以系统化地在鸿蒙6开发中使用 codelinter 工具来保证代码质量了。

❌
❌