阅读视图

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

学习 ReactJS – 完整路线图

学习 ReactJS – 完整路线图

如译文有误,还请在评论区提出

image.png

Web 开发是一个值得探索、学习和理解的迷人领域。

作为初学者,你有很多libraries资源frameworks可以帮助你开发 Web 项目。有时,选择一个资源并开始使用可能很棘手。

但当谈到学习时ReactJS,这不应该是一个难以做出的决定。

ReactJS(又名 React)是一个基于 JavaScript 的开源用户界面库。它在当今的 Web 开发社区中非常流行,React Native在移动应用开发者中也同样受欢迎。

在本文中,我们将介绍学习 ReactJS 的完整路线图。如果您正在考虑开始学习 ReactJS,并希望获得循序渐进的方法,那么本文非常适合您。此外,如果您是一位 ReactJS 开发者,并且正在考虑接下来要学习哪些高级主题,也请阅读本文。

为什么需要路线图来学习 React?

好问题。想想看。你身处一座陌生的城市,需要从一个地方前往另一个地方。你最需要的是一张地图或指南来帮助你的旅行。

学习新事物并不一定非要经历巨大的变化。如果你有一张地图或指南,告诉你“如果你先学A,接下来尝试学习B。然后你会发现C更容易”,那么学习过程就会顺利得多。

它还可以帮助您决定替代路线、一次走多远以及何时暂停。

在本指南中,我们将分阶段构建 ReactJS 学习路线图。在我们进一步探索路线图的过程中,你还可以找到一些资源(可选)。

👀 请记住,第一次看到这份路线图时,你可能会感到有点不知所措。不过别担心,它并没有看起来那么难。我还提供了一些资源,帮助你以惊人的速度学习。

这份路线图是基于我六年多的 ReactJS 工作经验而制定的。所以,如果您有不同意见,我完全接受。

由于 ReactJS 非常流行,您可能会发现这里与其他许多建议的路线图有些相似。但它们并不完全相同。

将路线图分为几个阶段

我们将整个路线图分为四个阶段。它们分别是:

  1. 在学习 React 之前要学什么
  2. React 初学者该学什么
  3. 如何从初学者晋升为中级 React 开发人员
  4. 如何从中级 React 开发人员晋升为高级 React 开发人员

学习 React 之前要学习什么

ReactJS是基于 JavaScript 的。为了快速学习 React,你需要熟悉一些知识(除了 JavaScript 基础知识之外)。

🔵 Git 版本控制

Git是一款版本控制工具,可帮助您更好地管理源代码。它与 ReactJS 没有直接关系。但学习它的基本用法会很有帮助,这样您就可以充分利用 React 的开发生态系统。

当你专注于一些基本的事情时,Git 很容易学习,例如,

  • 如何启动存储库
  • 如何暂存/取消暂存你的更改
  • 如何将你的更改提交到仓库
  • 如何推送到远程仓库
  • 如何解决合并冲突?

除此之外Git,您还需要了解如何使用基于 Git 的存储库管理服务,例如GitHub。您可以在此 YouTube 播放列表中了解有关 Git 和 GitHub 的所有信息:Git 和 GitHub 播放列表

🔵 HTML

HTML 提供了网页的结构。在 ReactJS 中编写代码时,你将使用一种名为 JSX 的新语法来使用 HTML 结构。

您无需精通 HTML,但您应该对最常用的标签和语义有基本的了解。您可以查看这个HTML 初学者速成课程这个 HTML 基础指南来开始学习。

您还可以查看 freeCodeCamp 最新更新的响应式网页设计认证,以开始使用 HTML。

🔵 CSS

说到 CSS,范围非常广。不过,请关注我在这条Tweet中提到的主题:

twitter.com/tapasadhika…

您可以在CSS-Tricks网站上学习很多有关 CSS 的知识,这里有一个基于项目的教程,可以帮助您将基本的 CSS 概念付诸实践。

您还可以查看 freeCodeCamp 的响应式网页设计认证来学习 CSS。

🔵 JavaScript

你必须了解 JavaScript 中的以下概念,

如果您想了解的话,freeCodeCamp 还提供JavaScript 认证。

🔵 NPM 生态系统

您应该知道如何使用npmyarn和节点版本管理器 (nvm) 来帮助您在本地运行和测试 ReactJs 应用程序。

了解它们的高阶工作原理总是有助于在遇到问题时调试你的环境。这里有一些资源可以帮助你快速上手:Node.js 安装、npm、yarn、nvm

🔵 如何部署、托管和公开您的应用:

如果你用 ReactJS 创建了一个很酷的东西,却无法向世界展示,那可就没那么有趣了。所以,你应该知道如何部署应用并使其可供公众访问。

Vercel像或 这样的工具Netlify让你只需点击几下即可轻松部署你的 React 应用。例如,这里有一个关于将 React 应用部署到 Netlify 的教程。

React 初学者该学什么

现在,我们来关注一下在实践中享受 React 乐趣至少需要学习的内容。这些都是基础知识,所以一定要投入足够的时间和实践来真正理解这些概念。

有句名言叫做“用 React 思考”。这些基本概念应该能帮助你培养这种“用 React 思考”的思维模式。

🟡 理解 React 是什么

你应该明白 ReactJs 有什么特别之处了。它是一个声明式的、基于组件的用户界面库。

这是什么意思?观看此视频,清晰地解释所有这些概念。

🟡 学习如何设置你的开发环境

有多种方法可以为 ReactJs 设置开发环境。至少,你可以从脚本文件中指向CDN 分发。

这种方法入门还行,但不可持续。作为初学者,你可能不想在项目的 Babel 或 Webpack 相关配置上花费太多时间。

在我看来,开始使用 ReactJS 开发环境最智能、最快捷的方法是使用Create React App。您可以按照其主页上的简单步骤在几分钟内完成操作。

🟡 了解 JSX

ReactJS 允许用户界面逻辑与渲染逻辑、事件、处理状态变化等逻辑耦合。这种耦合是为了鼓励构建独立组件的实践。

JSX是一种看起来像 的语法HTML,但它的功能却与 相同JavaScript。此语法可帮助开发人员编写包含所有必要元素(例如数据获取、条件、循环、表达式等)的 UI 逻辑。

请注意,您可以在不使用 JSX 语法的情况下编写 ReactJS 应用程序 - 但开发体验不会那么好。

这里有一个很好的资源可以帮助您了解 React 中的 JSX

🟡 了解 React 组件

组件是 ReactJs 应用的核心。我们创建可复用、自包含且隔离的独立组件。每个组件应该正确地执行一项任务。多个组件共同构建整个应用。

在 ReactJS 中,你可以使用 JavaScript 类或简单的函数来创建组件。我建议使用函数式组件,因为它更直观,并且所需的代码更少。

这里有一篇关于如何编写更好的 React 组件的文章,还有一篇关于使用 React 组件代替 HTML 的文章。

🟡 React 中的状态

状态是组件私有的数据。我们不会跨组件共享状态。组件的“状态”用于渲染和修改信息。

您可以查看有关React 中的状态的深入指南,以更好地了解其工作原理。

🟡 React 中的 Props

在实际编程中,你需要组件之间相互交互。状态是组件私有的,但你需要在组件之间传递数据。这就是Propsprops 的作用所在。注意,props 是只读的。

这里有一个关于如何在 React 中使用 props 的快速入门教程。这里还有一个实用的速查表,分享了你应该知道的 10 种 props 模式

最后,这里有一篇关于React 中的 props 和 state的适合初学者的文章,以防你需要了解任何基础知识。

🟡 React 中的列表和键

我们使用 list 在 React 组件中渲染项目列表。列出用户、待办事项和其他内容是一项非常常见的任务。我们使用 listmap()函数迭代列表并渲染结果。

keys帮助识别列表中哪些项目已更改,以通知 React 重新渲染。如果您忘记提及列表的键,ReactJS 会发出警告。

🟡 React 中的生命周期方法

我们讨论过,“状态”是组件的私有属性。状态可以是动态的,可能需要修改。我们还需要在组件销毁时执行资源清理。ReactJs 提供了各种生命周期方法来检测各个阶段并采取行动。

如果你刚开始使用 ReactJS,你应该了解函数式组件的生命周期管理。你可以使用内置的钩子来实现,例如useStateuseEffect等等。

这里有一段视频可以帮助你了解React 组件的生命周期。还有一篇文章可以教你 React Hooks 的基础知识

如果您需要一份全面的初学者指南来巩固这些概念,这里有一本深入的React 手册可以帮助您入门。

如何从初学者晋升为中级 React 开发人员

现在让我们了解如何从 React 初学者晋升到中级水平。

在这个阶段,你将开始关注应用程序的完整性。在这个阶段结束时,你将能够应对大多数 ReactJs 挑战,并享受完成它们的乐趣。

🟣 React 中的样式

我们都希望自己的应用看起来清新美观。您可以使用普通的 CSS 来设计您的 ReactJS 应用。或者,您也可以使用 Sass 或其他 CSS 驱动的组件库,例如TailwindCSSChakraUIreact-bootstrapMUI。选择权完全在您手中。

例如,这里有一个关于使用 TailwindCSS 设置 React 应用程序样式的教程。

🟣 React 中的表单处理

处理表单是 Web 应用程序中的一项基本需求。你需要了解如何以 ReactJS 的方式处理表单元素。

例如,您可以使用 react-hook-form 库轻松构建表单。以下是一些关于react-hook-form入门的教程。

🟣 React 中的数据处理

这是应用程序开发的关键部分。你需要学习如何使用fetch API或类似 的库node-fetch,以及axios如何与 API 交互以及在组件中处理数据。

这里有一个帮助您开始使用 Fetch API 的速查表,这里是如何将 Axios 与 React 结合使用的深入指南。

🟣 React 中的协调过程

ReactJS 使用虚拟 DOM 和 diffing 算法来决定何时以及在实际渲染时更新哪些 DOM。了解其底层工作原理将有助于你的调试。

这里有一个关于 DOM(即文档对象模型)的入门指南,这里还有一个关于如何操作 DOM 的指南。然后,你可以查看这个关于虚拟 DOM 的概述以及它在 React 中的工作原理。

🟣 React Hooks

希望你已经了解了一些内置钩子,比如useState useEffect你学习生命周期时学到的。你还需要学习其他一些有用的内置钩子,并结合用例来学习。千万不要忽略它们。

这是一个有趣的基于项目的指南,介绍如何通过构建 Paint 应用程序来学习 React Hooks

🟣 自定义 React Hooks

自定义钩子有助于提高可复用性。你必须寻找机会将组件逻辑提取到可复用的钩子中。使用自定义钩子可以使代码变得简洁且模块化。

以下是有关如何构建您自己的自定义 React Hooks 的分步指南。

🟣 React 中的 Context

在 React 应用中,我们将数据从父组件传递到子组件。这种传递是单向且自上而下的。如果父组件层级过深,数据(props)就必须经过多个组件。

另外,如果您需要在不属于层次结构的组件之间共享某些值,则需要一种机制。这时,您可以使用Context

您可以通过本初学者指南了解有关 React 上下文的所有信息

如何从中级 React 开发人员晋升到高级

在此阶段,你将学习一些专家级主题。只有当你使用 ReactJS 进行更广泛的应用程序开发时,才需要了解这些概念。

请注意,您可以在准备好时逐一学习这些概念。此外,您不必全部学习。

🟢 React 中的延迟加载

ReactJS 支持代码拆分。它可以让当前用户延迟加载所需内容,从而避免生成庞大的构建包。该dynamic import功能是在 React 应用中包含代码拆分的最佳方式。

这是有关React 中的延迟加载的基本教程,可帮助您入门。

🟢 React 中的 Portal

在处理需要更佳事件处理能力的模态框、对话框或工具提示时,你可能需要使用 Portal。ReactJS 已提供开箱即用的支持。

🟢 React 中的状态管理

在大型应用程序中,必须在组件之间共享信息。有时,Props和 的默认支持Context可能不够用。

Redux在这些情况下,你可能需要类似或的状态管理解决方案MobX。但同样,你可以自行决定是否需要它们。

这里是一份便捷的Redux 初学者指南,可以帮助你快速上手。如果你想深入了解 Redux,这里有一本完整的书籍可以阅读

🟢 React 中的路由

多页应用需要路由功能。将特定页面添加到书签或使用浏览器的后退按钮在应用中来回切换也很有帮助。

React Router是最流行的路由解决方案,有助于声明式路由。

这是学习 React Router 的深入指南,可帮助您了解所有基础知识。

🟢 React 中的主题

主题是 Web 应用的一项现代功能。我们应该让用户自主选择他们想要的主题(例如浅色或深色),以便他们在使用您的网站或应用时感到舒适。

您甚至可以在某些应用程序中创建自定义主题并应用它们。您可以通过多种方式为 React 应用设置主题。请选择最适合您应用程序 CSS 堆栈的方案。

🟢 React 中的模式

React 中有很多常见的问题解决方案,例如各种模式。随着时间的推移,ReactJS 开发者们已经找到了一些可以帮助他们避免重复造轮子的模式。

学习这些模式对你大有裨益。访问此网站,查找最常用的 ReactJS 模式及其示例。

🟢 React 中的反模式

反模式是你在 ReactJS 应用程序中应该避免使用的做法。你应该学习它们,并学习一些有用的模式。

请记住,学习 React 的高级概念并不止于此。您可以根据需要继续学习可访问性、测试框架以及更多高级概念。

那么,如何开始使用 React?

这是一个价值数十亿美元的问题!有很多优秀的资源(书籍、文章、视频)可以帮助你了解上述主题,其中许多资源在每个部分都有链接。所以,你可以找到最适合你的资源。

您还可以查看以下资源:

我还应该学习 React 吗?

另一个常见问题——答案是肯定的。React 正在不断发展,社区也在快速壮大。没有理由退缩。

此外,React 是许多其他流行框架的基础,例如Next.jsGatsbyJS和最近的Remix

我并不是在将 React 与 Angular、Vue 或 Svelte 框架进行比较。它们都有各自的优秀之处,例如 ReactJS 就是一个优秀的用户界面开发库。

在我们结束之前...

希望这份路线图对您有所帮助。请在开始学习之前做好充足的练习。Twitter如果您想进一步讨论,欢迎随时联系我的私信。

让我们联系吧。我也会在这些平台上分享我在 JavaScript、Web 开发和博客方面的学习成果:

期待下篇文章与大家见面。在此之前,请保重,保持快乐。


作者:Tapas Adhikary

原文地址:Learn ReactJS – Complete Roadmap(英)

计算机图形学中的四元数详解

一、四元数基础概念

在计算机图形学中,四元数是一种用于表示旋转和方向的强大数学工具。四元数由一个实部和三个虚部组成,形式为q = w + xi + yj + zk ,其中w是实部,x、y、z分别是虚部i、j、k的系数。虚部i、j、k满足特定的乘法规则:i * i = j * j = k * k = -1,i * j = k,j * k = i,k * i = j ,并且乘法不满足交换律,例如j * i = -k。

四元数与复数类似,但扩展到了四维空间,相比传统的欧拉角和旋转矩阵,四元数在表示旋转时具有诸多优势,如避免万向节死锁、更高效的插值运算等。

二、四元数的运算

1. 四元数的加法

两个四元数q1 = w1 + x1i + y1j + z1k和q2 = w2 + x2i + y2j + z2k相加,结果是一个新的四元数q = (w1 + w2) + (x1 + x2)i + (y1 + y2)j + (z1 + z2)k。在 JavaScript 中可以这样实现:

function quaternionAdd(q1, q2) {
    return {
        w: q1.w + q2.w,
        x: q1.x + q2.x,
        y: q1.y + q2.y,
        z: q1.z + q2.z
    };
}

2. 四元数的乘法

四元数乘法遵循上述虚部的乘法规则,通过展开各项并合并同类项得到结果。两个四元数q1和q2相乘的 JavaScript 实现如下:

function quaternionMultiply(q1, q2) {
    return {
        w: q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
        x: q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
        y: q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
        z: q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w
    };
}

3. 四元数的共轭

四元数q = w + xi + yj + zk的共轭q* = w - xi - yj - zk,即虚部取反。在 JavaScript 中:

function quaternionConjugate(q) {
    return {
        w: q.w,
        x: -q.x,
        y: -q.y,
        z: -q.z
    };
}

4. 四元数的模

四元数q的模||q|| = √(w² + x² + y² + z²),表示四元数的大小。计算模的 JavaScript 代码如下:

function quaternionMagnitude(q) {
    return Math.sqrt(q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z);
}

5. 四元数的归一化

归一化四元数是将其四元数的模变为 1,通过将四元数的每个分量除以它的模来实现。归一化的 JavaScript 函数:

function quaternionNormalize(q) {
    const magnitude = quaternionMagnitude(q);
    return {
        w: q.w / magnitude,
        x: q.x / magnitude,
        y: q.y / magnitude,
        z: q.z / magnitude
    };
}

三、四元数表示旋转

在计算机图形学中,四元数常用于表示三维空间中的旋转。绕单位向量(x, y, z)旋转角度θ的四元数可以表示为:

q = cos(θ/2) + x * sin(θ/2) * i + y * sin(θ/2) * j + z * sin(θ/2) * k

将一个三维向量v用四元数q进行旋转,需要先将向量v转换为纯四元数p = 0 + vx * i + vy * j + vz * k ,然后通过计算q * p * q*得到旋转后的向量对应的四元数,其虚部即为旋转后的三维向量。以下是用 JavaScript 实现向量旋转的代码:

function rotateVectorByQuaternion(v, q) {
    const p = {w: 0, x: v.x, y: v.y, z: v.z};
    const qp = quaternionMultiply(q, p);
    const qpq = quaternionMultiply(qp, quaternionConjugate(q));
    return {x: qpq.x, y: qpq.y, z: qpq.z};
}

四、四元数插值

在动画和计算机图形应用中,常常需要在两个旋转之间进行平滑过渡,这就需要用到四元数插值。常见的插值方法是球面线性插值(Slerp)。

Slerp 在两个四元数q1和q2之间,根据插值因子t(0 到 1 之间)计算插值结果。其基本思想是在四维空间的超球面上沿着最短路径进行插值。JavaScript 实现 Slerp 的代码如下:

function slerp(q1, q2, t) {
    let dot = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z;
    if (dot < 0) {
        q2 = {w: -q2.w, x: -q2.x, y: -q2.y, z: -q2.z};
        dot = -dot;
    }
    const k0 = 1 - t;
    const k1 = t;
    if (dot > 0.9995) {
        return {
            w: k0 * q1.w + k1 * q2.w,
            x: k0 * q1.x + k1 * q2.x,
            y: k0 * q1.y + k1 * q2.y,
            z: k0 * q1.z + k1 * q2.z
        };
    }
    const theta = Math.acos(dot);
    const sinTheta = Math.sin(theta);
    return {
        w: Math.sin((1 - t) * theta) / sinTheta * q1.w + Math.sin(t * theta) / sinTheta * q2.w,
        x: Math.sin((1 - t) * theta) / sinTheta * q1.x + Math.sin(t * theta) / sinTheta * q2.x,
        y: Math.sin((1 - t) * theta) / sinTheta * q1.y + Math.sin(t * theta) / sinTheta * q2.y,
        z: Math.sin((1 - t) * theta) / sinTheta * q1.z + Math.sin(t * theta) / sinTheta * q2.z
    };
}

通过以上内容,你已经对四元数在计算机图形学中的概念、运算、旋转表示和插值有了深入了解。在实际应用中,四元数可以用于游戏开发中的角色动画、3D 建模软件中的物体旋转等场景,发挥其高效和稳定的优势 。

Three.js 模型优化全流程教学文章

在使用 Three.js 开发 3D 应用时,模型优化是提升性能的关键。未经优化的模型可能导致加载缓慢、帧率下降,影响用户体验。本文将详细介绍 Three.js 中模型优化的完整过程,帮助你打造高效流畅的 3D 应用。

一、选择合适的模型格式

模型格式对性能影响很大,Three.js 支持多种格式,推荐优先使用 GLTF/GLB 格式。它是一种高效的二进制格式,支持动画、材质、纹理等多种特性,并且文件体积较小,加载速度快。

使用 GLTFLoader 加载模型的代码如下:

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
    'path/to/your/model.gltf',
    (gltf) => {
        const model = gltf.scene;
        scene.add(model);
    },
    (xhr) => {
        console.log((xhr.loaded / xhr.total * 100) + '% loaded');
    },
    (error) => {
        console.log('An error happened');
    }
);

二、模型几何优化

1. 减少多边形数量

复杂的模型包含大量多边形,会消耗更多计算资源。可以使用 3D 建模软件(如 Blender)对模型进行简化,减少多边形数量。在 Blender 中,通过 “Decimate” 修改器,设置合适的比例参数,能在保持模型大致形状的前提下,有效降低多边形数量 。

2. 合并几何体

场景中有多个小几何体时,将它们合并成一个大几何体,可以减少绘制调用次数,提升渲染效率。利用BufferGeometryUtils.mergeBufferGeometries方法实现合并:

import { BufferGeometryUtils } from 'three/addons/utils/BufferGeometryUtils.js';
const geometry1 = new THREE.BoxGeometry(1, 1, 1);
const geometry2 = new THREE.SphereGeometry(0.5);
const geometries = [geometry1, geometry2];
const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);

3. 使用 LOD(Level of Detail)技术

根据相机与模型的距离,展示不同精度的模型。近距离展示高精度模型,远距离展示低精度模型,在不影响视觉效果的同时节省性能。示例代码如下:

const highDetailMesh = new THREE.Mesh(highDetailGeometry, material);
const mediumDetailMesh = new THREE.Mesh(mediumDetailGeometry, material);
const lowDetailMesh = new THREE.Mesh(lowDetailGeometry, material);
const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0);
lod.addLevel(mediumDetailMesh, 100);
lod.addLevel(lowDetailMesh, 200);
scene.add(lod);

三、纹理优化

1. 纹理压缩

未经压缩的纹理文件体积大,加载慢。使用纹理压缩工具,将纹理压缩成 ETC、ASTC 等格式。例如,对于安卓设备,ETC 格式兼容性和性能表现较好;对于 iOS 设备,ASTC 格式更为合适。

在 Three.js 中加载压缩纹理:

import { CompressedTextureLoader } from 'three/addons/loaders/CompressedTextureLoader.js';
const loader = new CompressedTextureLoader();
loader.setPath('textures/');
loader.load('yourTexture.etc', (texture) => {
    material.map = texture;
    material.needsUpdate = true;
});

2. 控制纹理尺寸

纹理尺寸应尽量为 2 的幂次方(如 256×256、512×512),并且避免使用过大尺寸的纹理,一般不超过 2048×2048,防止占用过多内存。

四、材质与着色器优化

1. 选择合适的材质

Three.js 提供多种材质,不同材质性能不同。MeshBasicMaterial不考虑光照,性能最好;MeshStandardMaterial和MeshPhysicalMaterial支持物理正确的渲染,但计算复杂,性能消耗大。根据场景需求选择合适的材质,如不需要光照效果的背景模型,可使用MeshBasicMaterial。

const basicMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, basicMaterial);
scene.add(mesh);

2. 优化自定义着色器

如果使用自定义着色器,要尽量简化计算逻辑。避免使用过多的循环和复杂的数学运算,减少不必要的变量声明,缓存计算结果以避免重复计算,从而提升着色器执行效率。

五、渲染优化

1. 实例化(Instancing)

当场景中有大量相同模型时,使用实例化技术,只需绘制一次模型,通过不同的实例矩阵设置每个模型的位置、旋转和缩放,大幅减少绘制调用次数。

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const instanceCount = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, instanceCount);
for (let i = 0; i < instanceCount; i++) {
    const matrix = new THREE.Matrix4();
    matrix.setPosition(
        Math.random() * 100 - 50,
        Math.random() * 100 - 50,
        Math.random() * 100 - 50
    );
    instancedMesh.setMatrixAt(i, matrix);
}
scene.add(instancedMesh);

2. 视锥体剔除(Frustum Culling)

Three.js 默认开启视锥体剔除,只渲染相机视锥体内的对象。也可手动控制,对于一些复杂的对象组合,可以在不需要显示时手动关闭其渲染,提升性能。

mesh.frustumCulled = true; // 开启视锥体剔除

3. 层级剔除(Hierarchical Culling)

对于大型复杂场景,使用 BVH(Bounding Volume Hierarchy,包围体层次结构)或八叉树(Octree)结构,快速判断对象是否在视锥体内,加速剔除过程。

六、其他优化技巧

1. 延迟加载

将非关键的模型或资源进行延迟加载,比如在场景初始化完成后,或者用户触发特定操作时再加载,减少初始加载时间,提升应用启动速度。

2. 内存管理

及时释放不再使用的资源,如通过dispose()方法释放纹理、几何体和材质,避免内存泄漏,防止应用运行过程中内存占用过高导致卡顿甚至崩溃。

mesh.geometry.dispose();
mesh.material.dispose();
if (mesh.material.map) {
    mesh.material.map.dispose();
}

3. 性能监控

使用Stats.js等工具实时监控帧率、内存使用情况等性能指标,定位性能瓶颈,有针对性地进行优化。

<script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r17/Stats.min.js"></script>
<script>
    const stats = new Stats();
    document.body.appendChild(stats.dom);
    function animate() {
        requestAnimationFrame(animate);
        stats.begin();
        renderer.render(scene, camera);
        stats.end();
    }
    animate();
</script>

通过以上全面的模型优化过程,从模型格式选择、几何处理、纹理优化,到材质着色器、渲染优化以及其他实用技巧,能够有效提升 Three.js 应用的性能,打造出流畅、高效的 3D 应用。在实际开发中,根据具体场景灵活运用这些优化方法,不断调试和改进,以达到最佳效果。

HarmonyOS NEXT 使用 relationalStore 实现数据库操作

大家好,我是V哥。在 HarmonyOS NEXT 开发中,如何操作数据库,V 哥在测试中总结了以下学习代码,分享给你,如何想要系统学习鸿蒙开发,可以了解一下 V 哥最近刚刚上架出版的 《HarmonyOS 鸿蒙开发之路 卷2 从入门到应用篇》,V 哥在这本书里系统的介绍纯血鸿蒙的细枝末节,可以让零基础的朋友快速上手鸿蒙应用开发。

在鸿蒙开发中,系统 API 提供了基于SQLite组件的一套完整的对本地数据库进行管理的机制,对外提供了一系列的增、删、改、查等接口,也可以直接运行用户输入的SQL语句来满足复杂的场景需要。支持通过ResultSet.getSendableRow方法获取Sendable数据,进行跨线程传递。

这里要注意一下,为保证插入并读取数据成功,建议一条数据不超过2MB。如果数据超过2MB,插入操作将成功,读取操作将失败。

大数据量场景下查询数据可能会导致耗时长甚至应用卡死,如有相关操作可参考文档批量数据写数据库场景,且有建议如下:

  • 单次查询数据量不超过5000条。
  • 在TaskPool中查询。
  • 拼接SQL语句尽量简洁。
  • 合理地分批次查询。

该模块提供以下关系型数据库相关的常用功能:

  • RdbPredicates:数据库中用来代表数据实体的性质、特征或者数据实体之间关系的词项,主要用来定义数据库的操作条件。
  • RdbStore:提供管理关系数据库(RDB)方法的接口。
  • ResultSet:提供用户调用关系型数据库查询接口之后返回的结果集合。
  • Transaction:提供管理事务对象的接口。

案例代码

接下来,V 哥通一个完整案例来介绍如何使用。


import relationalStore from '@ohos.data.relationalStore';
import promptAction from '@ohos.promptAction';

@Entry
@Component
struct RdbStoreExample {
  @State userList: Array<{ id: number; name: string; age: number }> = [];
  @State nameInput: string = '';
  @State ageInput: string = '';
  private rdbStore: relationalStore.RdbStore | null = null;
  private rdbConfig: relationalStore.RdbStoreConfig = {
    name: 'UserData.db',
    securityLevel: relationalStore.SecurityLevel.S1
  };
  private CREATE_TABLE_USER = 
    'CREATE TABLE IF NOT EXISTS User (' +
    'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
    'name TEXT NOT NULL, ' +
    'age INTEGER NOT NULL)';

  aboutToAppear() {
    this.openRdbStore();
  }

  aboutToDisappear() {
    this.closeRdbStore();
  }

  // 打开数据库
  async openRdbStore() {
    try {
      this.rdbStore = await relationalStore.getRdbStore(this.rdbConfig, 1, 
        (version: number, rdbStore: relationalStore.RdbStore) => {
          if (version === 1) {
            rdbStore.executeSql(this.CREATE_TABLE_USER, []);
          }
        });
      await this.queryUsers();
    } catch (error) {
      console.error(`Failed to open RDB store: ${error}`);
      promptAction.showToast({ message: '数据库打开失败' });
    }
  }

  // 关闭数据库
  async closeRdbStore() {
    if (this.rdbStore) {
      try {
        await this.rdbStore.close();
        this.rdbStore = null;
      } catch (error) {
        console.error(`Failed to close RDB store: ${error}`);
      }
    }
  }

  // 插入数据
  async insertUser() {
    if (!this.nameInput || !this.ageInput) {
      promptAction.showToast({ message: '请输入姓名和年龄' });
      return;
    }
    
    try {
      const valuesBucket = {
        name: this.nameInput,
        age: parseInt(this.ageInput)
      };
      
      const id = await this.rdbStore?.insert('User', valuesBucket);
      if (id && id > 0) {
        promptAction.showToast({ message: '插入成功' });
        await this.queryUsers();
        this.nameInput = '';
        this.ageInput = '';
      }
    } catch (error) {
      console.error(`Failed to insert data: ${error}`);
      promptAction.showToast({ message: '插入失败' });
    }
  }

  // 查询数据
  async queryUsers() {
    try {
      const resultSet = await this.rdbStore?.querySql(
        'SELECT * FROM User', []);
      
      if (resultSet) {
        const users = [];
        resultSet.goToFirstRow();
        
        while (!resultSet.isAtEnd()) {
          users.push({
            id: resultSet.getLong(resultSet.getColumnIndex('id')),
            name: resultSet.getString(resultSet.getColumnIndex('name')),
            age: resultSet.getInt(resultSet.getColumnIndex('age'))
          });
          resultSet.goToNextRow();
        }
        
        this.userList = users;
        resultSet.close();
      }
    } catch (error) {
      console.error(`Failed to query data: ${error}`);
      promptAction.showToast({ message: '查询失败' });
    }
  }

  // 更新数据
  async updateUser(id: number, name: string, age: number) {
    try {
      const valuesBucket = {
        name: name,
        age: age
      };
      
      const conditions = new relationalStore.RdbStorePredicates('User');
      conditions.equalTo('id', id.toString());
      
      const rowsAffected = await this.rdbStore?.update(
        valuesBucket, conditions);
      
      if (rowsAffected && rowsAffected > 0) {
        promptAction.showToast({ message: '更新成功' });
        await this.queryUsers();
      }
    } catch (error) {
      console.error(`Failed to update data: ${error}`);
      promptAction.showToast({ message: '更新失败' });
    }
  }

  // 删除数据
  async deleteUser(id: number) {
    try {
      const conditions = new relationalStore.RdbStorePredicates('User');
      conditions.equalTo('id', id.toString());
      
      const rowsAffected = await this.rdbStore?.delete(conditions);
      
      if (rowsAffected && rowsAffected > 0) {
        promptAction.showToast({ message: '删除成功' });
        await this.queryUsers();
      }
    } catch (error) {
      console.error(`Failed to delete data: ${error}`);
      promptAction.showToast({ message: '删除失败' });
    }
  }

  build() {
    Column() {
      // 输入表单
      Column() {
        Input({
          placeholder: '请输入姓名',
          type: InputType.Text
        })
        .width('100%')
        .margin({ top: 10, bottom: 10 })
        .onChange((value: string) => {
          this.nameInput = value;
        })
        
        Input({
          placeholder: '请输入年龄',
          type: InputType.Number
        })
        .width('100%')
        .margin({ top: 10, bottom: 10 })
        .onChange((value: string) => {
          this.ageInput = value;
        })
        
        Button('添加用户')
        .width('100%')
        .onClick(() => {
          this.insertUser();
        })
      }
      .width('90%')
      .margin({ top: 20, bottom: 20 })
      
      // 用户列表
      List() {
        ForEach(this.userList, (user) => {
          ListItem() {
            Row() {
              Column() {
                Text(`姓名: ${user.name}`)
                Text(`年龄: ${user.age}`)
              }
              .width('70%')
              
              Column() {
                Button('修改')
                .onClick(() => {
                  const newName = promptAction.showPromptDialog({
                    message: '请输入新姓名',
                    defaultValue: user.name
                  });
                  const newAge = promptAction.showPromptDialog({
                    message: '请输入新年龄',
                    defaultValue: user.age.toString()
                  });
                  if (newName && newAge) {
                    this.updateUser(user.id, newName, parseInt(newAge));
                  }
                })
                
                Button('删除')
                .onClick(() => {
                  this.deleteUser(user.id);
                })
              }
              .width('30%')
            }
            .width('100%')
          }
        })
      }
      .width('90%')
      .margin({ top: 10, bottom: 10 })
    }
    .width('100%')
    .height('100%')
    .padding(15)
  }
}

下面来解释下这个示例:

  1. 首先要创建数据库配置并打开数据库,在openRdbStore方法里完成这些操作,创建了一个名为UserData.db的数据库,还定义了建表语句。

  2. 插入数据时,使用insert方法,把姓名和年龄封装成valuesBucket对象传进去,插入成功后会更新用户列表。

  3. 查询数据是通过querySql方法执行SQL语句,然后遍历结果集把数据存到userList里。

  4. 更新数据用update方法,要先创建RdbStorePredicates对象设置更新条件,根据ID来更新对应的记录。

  5. 删除数据也是先创建条件对象,然后调用delete方法,根据ID删除记录。

  6. 在界面上,提供了输入框让用户输入姓名和年龄,还有添加按钮,下面展示用户列表,每条记录都有修改和删除按钮。

操作技巧总结如下

  1. 打开和关闭数据库操作要在页面生命周期的合适时机进行,比如在aboutToAppear时打开,aboutToDisappear时关闭。
  2. 执行数据库操作时一定要进行异常处理,避免程序崩溃。
  3. 使用预编译语句可以提高性能,特别是在批量操作的时候。
  4. 操作完成后要及时关闭ResultSet,释放资源。
  5. 合理设计表结构,设置合适的主键和索引能提升查询效率。
  6. 更新和删除操作记得设置好条件,防止误操作。
  7. 对于复杂查询,可以使用原生SQL语句,但要注意SQL助入问题。

好了,以上内容供你参考,学习鸿蒙开发,抢占市场风口,国产化之路,V 哥与你搀扶前行。

YesPlayMusic美化你的音乐体验,带你畅享音乐的极致简约! cpolar内网穿透实验室第587个成功挑战

NO.587 YesPlayMusic-1.jpg

软件介绍

在这个音频visual盛行的时代,音乐客户端似乎都走上了“越功能越好的”老路。而YesPlayMusic则像一缕清风,让我们看到了另一种可能。它是第三方音乐客户端,但从第一眼看到它的UI开始,我就被惊艳到了——简约得让人心旷神怡,甚至有种“白月光”的错觉。

功能亮点

  • 简约设计

YesPlayMusic 的界面清爽得像是一个精心设计的艺术品。没有复杂的功能堆砌,没有让人眼花缭乱的按钮,只有你需要的最基本功能,简直是为追求极致简约的人群量身定制。

  • 跨平台支持

Windows、Mac、Linux,无论你是什么操作系统的忠实粉丝,YesPlayMusic都能完美适配。它就像一个通用的万能钥匙,让你的音乐之旅随心切换,无需担心平台限制。

  • 歌词同步显示

这款软件最让人惊喜的功能之一就是歌词同步显示。它不仅支持多种格式的歌词展示,还能自动调整节奏,做到精准同步,让你随时随地都能享受到“听歌看歌词”的双重乐趣。

  • 黑暗模式

深夜追剧、刷视频时,屏幕的强光总是让人难以入眠。YesPlayMusic贴心地提供了黑暗模式,不仅保护眼睛,还能营造出一种独特的沉浸式音乐体验。

NO.587 YesPlayMusic-2.jpg

实用场景

  • 工作学习时的良伴

YesPlayMusic 的简洁界面不会干扰你的注意力,让你在工作和学习时保持专注。轻松切换歌曲,随时调整音量,帮助你在高效办公的同时享受音乐带来的愉悦感。

  • 休息放松时的好选择

无论是午后小憩还是周末悠闲,这款软件都能为你的休息时间增添一份乐趣。轻点播放,看歌词随音乐流动,仿佛进入了一个全新的音乐世界。

  • 视频娱乐时的得力助手

在追剧、刷短视频时,YesPlayMusic 也能发挥它的独特作用。你可以一边听音乐,一边观看视频,享受多感官的刺激和乐趣。

NO.587 YesPlayMusic-3.jpg

cpolar内网穿透技术带来的便利

是的,你没看错!cpolar内网穿透技术 的加入,让YesPlayMusic不再局限于本地播放。你可以突破地域限制,随时随地访问各种音乐资源,享受更广阔的音乐世界。

总结

如果你也厌倦了功能复杂、界面杂乱的传统音乐客户端,不妨给YesPlayMusic 一次机会。它简约的设计和强大的功能,会让你的音乐体验更上一层楼。

NO.587 YesPlayMusic-4.jpg

YesPlayMusic+cpolar内网穿透打包如下,拿走不谢😊

1. 安装Docker

本文演示环境:CentOS7,Xshell7远程ssh

没有安装Docker的小伙伴需安装Docker,已有Docker可跳过以下步骤。

如没有安装Docker,需先安装Docker:

  • 安装软件包(提供实用程序)并设置存储库

    $ sudo yum install -y yum-utils
    $ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    

    安装Docker引擎

    sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    

    启动Docker

    sudo systemctl start docker
    

    通过运行映像来验证 Docker 引擎安装是否成功

    sudo docker run hello-world
    

2. 本地安装部署YesPlayMusic

检查Docker服务状态

检查Docker服务是否正常运行,确保Docker正常运行。

systemctl status docker

image-20240206162936342

检查Docker版本

docker -v

检查docker compose版本,确保2.0以上版本

docker compose version

下载YesPlayMusic镜像,从docker hub拉取YesPlayMusic镜像

docker pull  fogforest/yesplaymusic

image-20240206163602086

使用docker-cli创建YesPlayMusic容器,执行以下命令,映射端口可自行修改

docker run -d --name yesplaymusic --restart always -p 7900:80 fogforest/yesplaymusic

查看下是否安装成功

docker ps

访问YesPlayMusic

访问地址:http://192.168.149.143:7900,将IP替换为自己服务器IP地址,进入到YesPlayMusic登录页。如果无法访问,则检查服务器防火墙是否设置,云服务器的安全组端口是否放行等。

image-20240207143527001

在线播放音乐

可以登录自己的网易云账号进行绑定,也可以直接在线播放歌曲。

eaad4868f55eb895799f3ff9a04fd52

我们就成功在本地搭建了YesPlayMusic,但如果想实现出门在外,也能随时随地听到自己储存在本地的歌曲,就需要借助cpolar内网穿透工具来实现公网访问了!接下来介绍一下如何安装cpolar内网穿透并实现YesPlayMusic公网访问!

3. 部署公有云YesPlayMusic播放器

3.1 安装cpolar内网穿透

下面是安装cpolar步骤:

cpolar官网地址: www.cpolar.com

  • 使用一键脚本安装命令
curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bash
  • 向系统添加服务
sudo systemctl enable cpolar
  • 启动cpolar服务
sudo systemctl start cpolar

cpolar安装成功后,在外部浏览器上访问Linux 的9200端口即:【http://服务器的局域网ip:9200】,使用cpolar账号登录,登录后即可看到cpolar web 配置界面,结下来在web 管理界面配置即可。

接下来配置一下YesPlayMusic的公网地址,

登录后,点击左侧仪表盘的隧道管理——创建隧道,

创建一个YesPlayMusic的公网http地址隧道

  • 隧道名称:可自定义命名,注意不要与已有的隧道名称重复
  • 协议:选择http
  • 本地地址:7900 (本地访问的地址)
  • 域名类型:免费选择随机域名
  • 地区:选择China Top

点击创建

2661ecb733394201a153fb96e39d51e

隧道创建成功后,点击左侧的状态——在线隧道列表,查看所生成的公网访问地址,有两种访问方式,一种是http 和https

image-20240207150010486

使用上面的Cpolar https公网地址,在任意设备的浏览器进行访问,即可成功看到YesPlayMusic界面,这样一个公网地址且可以远程访问就创建好了,使用了cpolar的公网域名,无需自己购买云服务器,即可到公网进行远程访问音乐播放器了!

image-20240207151705738

小结

如果我们需要长期异地远程访问YesPlayMusic音乐播放器,由于刚才创建的是随机的地址,24小时会发生变化。另外它的网址是由随机字符生成,不容易记忆。如果想把域名变成固定的二级子域名,并且不想每次都重新创建隧道来听歌,我们可以选择创建一个固定的http地址来解决这个问题。

3.2 固定YesPlayMusic公网地址

我们接下来为其配置固定的HTTP端口地址,该地址不会变化,方便分享给别人长期查看你的博客,而无需每天重复修改服务器地址。

配置固定http端口地址需要将cpolar升级到专业版套餐或以上。

登录cpolar官网,点击左侧的预留,选择保留二级子域名,设置一个二级子域名名称,点击保留,保留成功后复制保留的二级子域名名称

image-20240207152153246

保留成功后复制保留成功的二级子域名的名称

image-20240207152223703

返回登录Cpolar web UI管理界面,点击左侧仪表盘的隧道管理——隧道列表,找到所要配置的隧道,点击右侧的编辑

image-20240207152322636

修改隧道信息,将保留成功的二级子域名配置到隧道中

  • 域名类型:选择二级子域名
  • Sub Domain:填写保留成功的二级子域名

点击更新(注意,点击一次更新即可,不需要重复提交)

image-20240207152521988

更新完成后,打开在线隧道列表,此时可以看到公网地址已经发生变化,地址名称也变成了固定的二级子域名名称的域名

image-20240207152606441

最后,我们使用固定的公网https地址访问,可以看到访问成功,这样一个固定且永久不变的公网地址就设置好了,可以随时随地听到自己储存在本地的歌曲了!

image-20240207152840805

CSS五大核心布局方式

一、块级布局(Block Layout)

基础特性 作为最传统的布局方式,块级元素以垂直堆叠形式构建页面骨架。其显著特征包括:

  • 独占整行:每个元素默认占据父容器100%宽度(如<div>会自动换行)
  • 尺寸可控:支持直接设置width/height属性(示例:section { width: 80%; }
  • 间距精准:通过margin/padding实现精确的盒模型控制

典型元素

<header>
  <h1>页面标题</h1>
  <nav>导航菜单</nav>
</header>
<main>
  <article>
    <p>正文段落内容...</p>
  </article>
</main>

应用场景

  • 构建页面基础结构框架
  • 创建垂直排列的内容区块
  • 实现传统文档流布局

二、行内布局(Inline Layout)

流动特性
行内元素如同文本般自然流动,适用于精细的内容修饰:

  • 无缝衔接:多个元素可在同一行显示(如<span><a>并排)
  • 尺寸自适应:宽度由内容决定(图片标签<img>默认保持原始尺寸)
  • 垂直对齐:通过vertical-align微调位置

注意事项

  • 避免对行内元素设置上下margin
  • 内边距可能导致相邻元素位置偏移

三、浮动布局(Float Layout)

革命性突破
浮动布局开启了多栏布局的新纪元:

  • 脱离文档流:元素可左右浮动(float: left实现图文混排)
  • 内容环绕:文字自然环绕浮动元素
  • 清除浮动:需配合clear: bothoverflow:hidden

适用场景

  • 杂志式图文排版
  • 传统两栏/三栏布局
  • 浮动导航菜单

四、弹性布局(Flexbox)

Flexbox彻底改变了元素排列方式:

  • 智能分配空间:自动调整子元素尺寸(flex:1实现等分容器)
  • 轴向控制:通过flex-direction切换行列方向
  • 精准对齐justify-contentalign-items双剑合璧

实战演示

.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

优势领域

  • 响应式导航栏
  • 卡片等宽排列
  • 垂直水平居中

五、网格布局(Grid Layout)

二维布局王者
Grid开启了真正的平面布局时代:

  • 行列矩阵grid-template-columns定义复杂列结构
  • 自由定位grid-area实现跨行跨列布局
  • 间隙控制gap属性统一行列间距

典型架构

.dashboard {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-gap: 20px;
}

核心应用

  • 后台管理系统布局
  • 图片瀑布流展示
  • 复杂表单结构

布局方案选择指南

布局类型 维度 核心优势 适用场景
块级布局 一维 结构清晰 文档流布局
行内布局 一维 内容融合 文本修饰
浮动布局 一维 内容环绕 传统多栏
Flexbox 一维 弹性分配 组件排列
Grid 二维 精准控制 复杂架构

如何用FRB(flutter_rust_bridge)快速搭建Flutter+Rust混编项目

01. Rust和Flutter开发环境

Rust开发环境安装,网络顺畅的可以直接官网找链接安装,也可以用rsproxy镜像。

RsProxy 入门 - Rust 程序设计语言

Flutter开发环境安装,也可以按网络条件选择安装方式

安装和环境配置 | Flutter 中文文档 - Flutter 中文开发者网站 - Flutter 在中国网络环境下使用 Flutter | Flutter 中文文档 - Flutter 中文开发者网站 - Flutter

02. FRB安装使用

Introduction | flutter_rust_bridge

flutter_rust_bridge v2版本已经很稳定。 首先安装FRB命令行工具,FRB命令行是以Rust的crate库形式发布的,安装方式:

cargo install flutter_rust_bridge_codegen

然后用flutter_rust_bridge_codegen命令创建新项目(llfile是项目名):

flutter_rust_bridge_codegen create llfile

没有错误输出既表示创建成功。

用Android Studio打开llfile项目,比纯Flutter项目多了不少内容。

Pasted image 20241219152924.png

主要关注rust目录和lib/src.rust/api目录,其他的大都是胶水相关代码和配置文件。

  • rust目录:

这是个rust标准crate库结构,编译出的动态链接库会通过FFI给Flutter代码提供API

simple.rs是个简单示例,新创建的项目默认会在主界面展示此模块产生的内容。

  • lib/src.rust目录:

rust目录里通过FFI导出的API会在此目录生成对应的dart可直接调用的API代码

Pasted image 20241219154038.png

Pasted image 20241219154111.png 可以看到rust代码里的API(通过frb属性标注的函数),和导出到dart代码里API的函数签名都是一致的。

实际FFI转换过程比较复杂,也相当繁琐,FRB做了比较完善的处理,比如同步异步,Struct和Stream都有完善支持。

Pasted image 20241219154643.png

Pasted image 20241219155127.png

最后在界面代码里像引用普通dart代码一样调用到Rust实现的API,整体比较丝滑,默认示例运行展示正常。

03. Tip

开发过程中如果变动的是Rust侧代码,需要通过flutter_rust_bridge_codegen generate --watch来实时监控代码变动自动生成Dart侧API代码

Pasted image 20241219155519.png

simple.rs是默认示例,新增rust模块时,init_app函数原样复制过去,就可以增加自己的实现逻辑了。

Pasted image 20241219155818.png

深入理解 TypeScript 声明文件 (.d.ts)

一、引言:声明文件的核心价值

在 TypeScript 的生态系统中,.d.ts文件扮演着至关重要的角色。它如同一份精确的 "类型说明书",为 JavaScript 代码提供类型注解,让 TypeScript 编译器能够理解非 TypeScript 代码的类型结构。随着 Node.js 生态的繁荣和前端框架的复杂化,大量遗留 JavaScript 库和第三方工具需要与 TypeScript 项目集成,声明文件成为解决类型兼容问题的关键桥梁。

1.1 声明文件的本质作用

  • 类型描述层:分离类型定义与具体实现,允许在不修改原代码的前提下添加类型信息
  • 跨语言桥梁:让 TypeScript 能够识别 JavaScript 库的类型结构,实现类型安全的互操作
  • 文档化工具:类型定义本身构成可执行的 API 文档,提升代码可读性

1.2 发展历程与生态现状

  • 早期阶段:手动编写声明文件(如 DefinitelyTyped 初始阶段)
  • 工具化阶段:dts-gen、typescript@types自动生成工具普及
  • 当前生态:超过 80% 的 NPM 包提供官方声明文件,DefinitelyTyped 托管超 20 万份声明

二、基础概念:声明文件的核心要素

2.1 声明文件的基本结构

// my-library.d.ts
declare namespace MyLibrary {
  interface Config {
    timeout: number;
    debug: boolean;
  }
  function create(config: Config): Instance;
  class Instance {
    constructor(config: Config);
    start(): void;
    stop(): void;
  }
}

2.2 核心语法要素

2.2.1 声明空间

  • 全局声明:直接声明在文件顶层(污染全局命名空间)
  • 模块声明:包含在declare module块中(推荐 ES 模块风格)
  • 命名空间声明:使用declare namespace封装逻辑分组

2.2.2 类型实体

实体类型 声明语法 作用场景
接口 interface 对象类型建模
类型别名 type 复杂类型别名定义
枚举 enum 固定值集合定义
函数 function 函数类型声明
class 类的类型结构声明

2.2.3 模块系统

// ES模块风格声明
export interface Config { /* ... */ }
export function create(config: Config): Instance;
// CommonJS模块声明
declare module 'my-library' {
  export const version: string;
  export function init(): void;
}

2.3 与.ts 文件的本质区别

特性 .d.ts 文件 .ts 文件
执行代码 不允许 允许
类型声明 必须declare修饰 可选
文件用途 类型描述 逻辑实现
编译输出 生成.js 文件

三、声明文件的层次结构

3.1 全局声明文件

// global.d.ts
declare function alert(message: string): void; // 扩展全局函数
declare namespace NodeJS { // 扩展Node.js内置类型
  interface ProcessEnv {
    API_KEY: string;
  }
}

3.2 模块声明文件

3.2.1 外部模块声明

// declare module 'lodash'
declare module 'lodash' {
  function debounce<F extends Function>(func: F, wait: number): F;
  export = debounce;
}

3.2.2 UMD 模块声明

// declare module 'my-umd-library'
declare var MyLibrary: {
  new (config: Config): Instance;
  version: string;
} & {
  create(config: Config): Instance;
};
export = MyLibrary;

3.3 项目级声明结构

src/
├── types/           # 项目自定义声明
│   ├── index.d.ts   # 公共类型声明
│   └── utils.d.ts   # 工具类声明
├── @types/          # 第三方库声明(非@types包)
│   └── legacy-lib.d.ts
└── tsconfig.json    # 配置"typeRoots"指向声明目录

四、高级语法:声明文件的深度应用

4.1 声明合并

4.1.1 接口合并

declare interface EventEmitter {
  on(event: 'click', listener: () => void): this;
}
declare interface EventEmitter {
  on(event: 'submit', listener: (data: any) => void): this;
}
// 合并后形成过载签名

4.1.2 命名空间与类合并

declare class Vue {
  $data: object;
}
declare namespace Vue {
  interface ComponentOptions { /* ... */ }
}
// 通过命名空间扩展类的静态成员

4.2 泛型声明

declare function reverse<T>(array: T[]): T[];
declare class Collection<T> {
  constructor(items: T[]);
  get(index: number): T;
}
declare namespace Algorithms {
  function sort<T extends Comparable>(list: T[]): T[];
}

4.3 条件类型应用

declare type OptionalProps<T, K extends keyof T> = T & {
  [P in K]?: T[P]
};
declare function withDefaults<T extends object, K extends keyof T>(
  obj: T,
  defaults: Pick<T, K>
): OptionalProps<T, K>;

4.4 类型断言增强

declare global {
  interface Window {
    __REDUX_DEVTOOLS_EXTENSION__: any;
  }
  const __REDUX_DEVTOOLS_EXTENSION__: any;
}
// 允许直接访问window上的非标准属性

五、实战指南:声明文件编写全流程

5.1 为第三方库编写声明文件

5.1.1 分析库的使用方式

  • CommonJS/ES 模块 / UMD 模块类型判断
  • 全局变量暴露方式(如通过 window 对象)
  • 函数式 API vs 类式 API

5.1.2 基础声明模板

// declare module 'legacy-js-library'
declare const library: {
  version: string;
  createElement: (tag: string) => HTMLElement;
  addListener: (element: HTMLElement, event: string, handler: Function) => void;
};
export = library;

5.1.3 处理重载场景

declare module 'math-utils' {
  function clamp(value: number, min: number, max: number): number;
  function clamp(value: number[], min: number, max: number): number[];
  // 支持数组和数值的过载处理
}

5.2 项目内部声明管理

5.2.1 公共类型定义

// src/types/index.d.ts
declare namespace App {
  type UserID = string & { __brand: 'UserID' };
  interface User {
    id: UserID;
    name: string;
    roles: Role[];
  }
  enum Role { Admin, Editor, Viewer }
}

5.2.2 模块扩展

// src/types/react.d.ts
declare module 'react' {
  interface HTMLAttributes<T> {
    'data-testid': string; // 扩展React的HTML属性
  }
}

5.3 跨语言交互声明

5.3.1 JavaScript 文件类型声明

// my-library.js
// @ts-check
/** @type {import('./types').Config} */
const defaultConfig = { /* ... */ };
// types.d.ts
declare const defaultConfig: Config;
export = defaultConfig;

5.3.2 Node.js 内置模块扩展

// src/types/node.d.ts
declare module 'node:fs' {
  interface ReadFileOptions {
    encoding?: 'utf8' | 'base64'; // 扩展Node.js核心模块类型
  }
}

六、最佳实践:高质量声明文件的特征

6.1 命名规范

场景 命名规则 示例
全局类型 PascalCase UserConfig
类型别名 PascalCase StringArray
命名空间 PascalCase MyApp.Utils
枚举成员 UPPER_CASE ENUM_VALUE

6.2 模块化设计

types/
├── vendors/       # 第三方库声明
│   ├── react.d.ts
│   └── lodash.d.ts
├── utilities.d.ts # 工具类型声明
├── components.d.ts# 组件类型声明
└── index.d.ts     # 公共导出声明

6.3 类型简化原则

// 推荐:使用具体类型
interface ButtonProps {
  onClick: (event: MouseEvent) => void;
}
// 避免:过度使用泛型
interface ButtonProps<T extends MouseEvent = MouseEvent> {
  onClick: (event: T) => void;
}

6.4 版本兼容性

// 使用@ts-ignore处理低版本不兼容
// @ts-ignore
declare module 'old-library' {
  // 针对旧版本的特殊处理
}
// 使用条件编译
// @ts-nocheck
// @if (ts.version >= 4.5)
declare const feature: NewFeature;
// @endif

七、工具链与生态:提升声明文件开发效率

7.1 TypeScript 编译器选项

{
  "compilerOptions": {
    "typeRoots": ["node_modules/@types", "src/types"],
    "types": ["node", "jest"],
    "declaration": true,        // 自动生成声明文件
    "declarationDir": "dist/types"
  }
}

7.2 类型检查工具

7.2.1 dtslint

npx dtslint src/types --fix # 执行类型声明lint

7.2.2 typescript@types

npx typescript@types my-library.js # 自动生成声明文件

7.3 IDE 支持

7.3.1 VSCode 智能提示

  • 类型跳转(F12)
  • 错误诊断(红色波浪线)
  • 自动完成(基于声明文件)

7.3.2 WebStorm 集成

  • 类型重构支持
  • 声明文件导航
  • 实时类型验证

7.4 持续集成

# GitHub Actions配置
- name: Check Type Declarations
  run: npx tsc --noEmit --strict

八、常见问题与解决方案

8.1 类型冲突问题

// 当第三方库声明与项目类型冲突时
declare module 'conflicting-library' {
  // 扩展或重写冲突类型
  interface ConflictingType {
    newProperty: string;
  }
}

8.2 声明文件缺失

# 安装 DefinitelyTyped 声明
npm install @types/legacy-library --save-dev
# 手动创建声明文件
touch src/@types/legacy-library.d.ts

8.3 类型过度严格

// 使用类型断言放宽检查
const element = document.getElementById('input') as HTMLInputElement;
// 使用类型兼容标记
declare let value: string & { __internal: true };
const publicValue: string = value; // 允许兼容赋值

九、未来趋势:声明文件的进化方向

9.1 ES 模块优先

// 未来推荐的模块声明方式
export interface User { /* ... */ }
export function fetchUser(id: number): Promise<User>;

9.2 类型声明即文档

  • JSDoc 与类型声明的深度融合
  • 生成交互式 API 文档工具(如 TypeDoc)

9.3 自动化程度提升

  • AI 辅助生成声明文件
  • 智能类型推断算法优化
  • 跨框架声明文件自动转换

9.4 TypeScript 5.0 新特性支持

// 支持装饰器元数据
declare function Component(options: ComponentOptions): ClassDecorator;
// 模板字面量类型扩展
declare type EventName = `on${Capitalize<string>}`;

十、结论:声明文件的战略价值

TypeScript 声明文件不仅仅是类型系统的基础设施,更是现代软件开发中类型驱动开发(TDD, Type-Driven Development)的核心载体。随着软件系统复杂度的提升,高质量的声明文件能够:

  1. 降低认知成本:通过可执行的类型文档快速理解 API 契约
  1. 提升协作效率:在大型团队中建立统一的类型规范
  1. 保障代码质量:通过编译时类型检查提前发现潜在问题
  1. 促进生态融合:让不同语言、不同时代的代码库实现类型安全的互操作

对于企业级项目而言,建立完善的声明文件管理体系(包括第三方库声明审核、项目内部类型规范、自动化声明生成流程)已经成为 TypeScript 项目成功的关键因素。随着 TypeScript 在后端、嵌入式等领域的拓展,.d.ts 文件的重要性将进一步提升,成为构建类型安全软件生态的基石。

在实践中,开发者应遵循 "类型声明与实现分离" 的原则,充分利用 TypeScript 的声明合并、泛型编程等高级特性,结合现代工具链提升开发效率。同时,积极参与 DefinitelyTyped 社区贡献,推动整个 TypeScript 生态的健康发展。

总之,深入理解和掌握.d.ts 文件的编写与管理,是每个 TypeScript 开发者从初级迈向高级的必经之路,也是应对复杂软件系统类型挑战的核心竞争力。

vue学习路线(5.el与data的两种写法)

一、el的两种写法

  1. el: "#root",new Vue的时候配置el属性, el用于指定当前vue实例为哪个容器服务,值通常为css选择器字符串,也可以写成 el:document.getElementById('root')
      new Vue({
        el: "#root", //第一种写法
        data: {
          year: 2025,
        },
      });

2.vm.$mount('#root'),先创建vue实例,随后再指定el的值。mount意思为挂载,vm.$mount的作用是,手动地把未挂载的vue实例挂载到指定的DOM元素上,这种方式更加灵活。

      const vm = new Vue({
        data: {
          year: 2025,
        },
      });
      console.log(vm); //输出vue实例
      vm.$mount("#root"); //第二种写法

源码解读:

在创建vue实例的时候,vue的构造函数自动运行 this._init(options)(启动函数)

image.png

init中调用vm.$mount(vm.$options.el),将实例挂载到dom上,至此启动函数完成。

image.png

二、data的两种写法

1.对象式

 const vm = new Vue({
     data: { // data写法一:对象式
       year: 2025,
     },
 });

2.函数式

由vue管理的函数,一定不要写箭头函数,一旦写了箭头函数,this就不再是vue实例了, 因为箭头函数没有自己的this,只能往外找,this指的是全局的window。

 const vm = new Vue({
      data(){
         console.log(this)//此处的this是vue实例对象
         return {
           year: 2025,
         }
      }
 });

如何选择:目前哪种写法都可以,以后学习到组件时,data必须使用函数式,否则会报错

谈谈你对nextTick的理解

什么是nextTick为什么要使用nextTick呢?

  • nextTick是一个异步方法,接收一个回调函数作为参数 因为vue中的dom更新是异步的,那么在一些场景下为了确保获取到数据更新后最新的dom,可以使用vue提供的nextTick方法,它的核心是利用了事件循环机制来实现的。

  • 看看这个例子

<template>
  <div id="box">{{ txt }}</div>
</template>
 
<script>
export default {
  data() {
    return {
      txt: "Hello World"
    };
  },
  mounted() {
    this.txt = "cc";
    console.log(document.getElementById("box").innerText); // 这里打印的还是Hello World
    this.$nextTick(() => {
      console.log(document.getElementById("box").innerText); // 这里打印的是更新之后的cc
    });
  }
};
</script>

主要是利用了事件循环机制来实现nextTick,那事件循环是什么呢?

简单的画了个图,将就看看

image.pngimage.png

js代码分为了同步代码和异步代码,他们的执行顺序是先执行同步再执行异步,代码开始执行会将同步推到执行栈里面执行,当执行栈清空之后,会执行异步,异步分为了宏任务和微任务,优先到微任务队列中查看是否有任务,如果有会遵循先进先出的原则推到执行栈里面执行,不断重复这样的操作直到微任务队列中没有任务,就会再到宏任务队列中也是跟微任务重复一样的操作,直到整个执行栈执行完毕

image.png

那么nextTick内部就是利用了这样的机制通过创建一个宏任务或者微任务的方式来实现,因为微任务的执行时机是在当前执行栈清空之后立即执行的,此时dom已经更新好了,所以考虑到执行时机的问题,所以vue中优先使用微任务来实现,至于使用微任务还是宏任务来实现取决于当前浏览器的支持,vue内部有一个timeFunc,这个函数内部会对当前环境进行不断的降级处理,尝试使用Promise.then、MutationObserver和setImmediate,如果这些都不支持最终才会使用settimeout

简单的理解为在下一次 事件循环结束时更新 DOMnextTick 的回调可以等到 DOM 更新完成后再执行操作。

vue学习路线(4.vue数据绑定:单向绑定&双向绑定)

vue中有2种数据绑定的方式:

1.单向绑定(v-bind:数据只能从data流向页面。

2.双向绑定(v-model:数据不仅能从data流向页面,还可以从页面流向data。

备注:

1.双向绑定,一般都应用在表单类元素上(元素需要有value属性,如:input、select等)。

2.v-model:value 可以简写为v-model,因为v-model默认收集的就是value值。

完整代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>数据绑定</title>
    <!-- 引入开发版vue -->
    <script src="../js/vue.js"></script>
  </head>
  <body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <h1>普通写法</h1>
      单向数据绑定:<input type="text" v-bind:value="name1" /> v-bind:value
      <hr />
      双向数据绑定:<input type="text" v-model:value="name2" /> v-model:value
      <br />
      <h1>简写</h1>
      单向数据绑定:<input type="text" :value="name1" /> :value
      <hr />
      双向数据绑定:<input type="text" v-model="name2" /> v-model

      <!-- 如下代码是错误的,因为v-model只能应用在表单类元素(元素需要有value属性)上 -->
      <h2 v-model="name">测试</h2>
    </div>
    <script type="text/javascript">
      //创建vue实例
      const vm = new Vue({
        el: "#root",
        data: {
          name1: "jack",
          name2: "jack",
        },
      });
    </script>
  </body>
</html>

image.png

Modal使用的疑惑与讨论

最近有一个疑惑,就是不知道大家是怎么使用弹窗的,下面是测试代码react + antd

版本1

一开始我是这样使用弹窗的,所有的参数单独传入,如下:

import { Button } from 'antd'
import ModalTest, { Item } from './ModalTest';
import { useState } from 'react';

const App = () => {
  const [visible, setVisible] = useState(false)
  const [record, setRecord] = useState<Item>()

  return (
    <>
      <Button onClick={() => {
        setRecord({
          id: 1,
          name: '2'
        })
        setVisible(true)
      }}>编辑</Button>
      <ModalTest visible={visible} record={record} onClose={() => setVisible(false)} />
    </>
  );
};

export default App;

弹窗内容是这样定义的:

// ModalTest.tsx
import { Modal } from 'antd'
import { FC } from 'react'

export type Item = {
  id: number
  name: string
}

type IProps = {
  visible: boolean
  record?: Item
  onClose: (update: boolean) => void
}

const ModalTest: FC<IProps> = ({ visible, record, onClose }) => {
  const onSubmit = () => {
    return Promise.resolve().then(() => {
      onClose(true)
    })
  }
  return <Modal title="测试" open={visible} onCancel={() => onClose(false)} onOk={onSubmit}>
    {record?.name}
  </Modal>
}

export default ModalTest

这种方式得定义多个state,相对比较繁琐,因此后面我更多使用了版本2

版本2

这个是我目前比较常用的方式,就是在弹窗层面定义好数据类型和默认数据,然后提供给父组件使用,如果需要覆盖默认数据,那么只在一个state处理就可以了

import { Button } from 'antd'
import ModalTest, { getModalTestData } from './ModalTest';
import { useState } from 'react';

const App = () => {
  const [modalData, setModalData] = useState(getModalTestData)

  return (
    <>
      <Button onClick={() => {
        setModalData({
          open: true,
          record: {
            id: 1,
            name: '2'
          },
          onClose: () => {
            setModalData(getModalTestData)
          }
        })
      }}>编辑</Button>
      <ModalTest {...modalData} />
    </>
  );
};

export default App;
import { Modal, ModalProps } from 'antd'
import { FC, memo } from 'react'

type Item = {
  id: number
  name: string
}

type IProps = {
  record?: Item
  onClose: (update: boolean) => void
} & ModalProps

export const getModalTestData = (): IProps => {
  return {
    open: false,
    onClose: () => {}
  }
}

const ModalTest: FC<IProps> = ({  record, onClose, ...rest }) => {
  const onSubmit = () => {
    return Promise.resolve().then(() => {
      onClose(true)
    })
  }
  return <Modal title="测试" onCancel={() => onClose(false)} onOk={onSubmit} {...rest}>
    {record?.name}
  </Modal>
}

export default memo(ModalTest)

版本3

当然我也见过把弹窗行为封装起来的方式,如下:

import { Button } from 'antd'
import QuickModal from './QuickModal';

const App = () => {
  return (
    <>
      <QuickModal type='custom' title="测试" content={<div>12312</div>}>
        <Button>编辑</Button>
      </QuickModal>
    </>
  );
};

export default App;
import { Modal, ModalFuncProps } from 'antd'
import { cloneElement } from 'react'

type RequiredType = 'type' | 'children'
export type QuickModalProps = Omit<ModalFuncProps, RequiredType> &
  Required<Pick<ModalFuncProps, RequiredType>>

export default ({ type, children, ...rest }: QuickModalProps) => {
  const showModal = () => {
    Modal[type](rest)
  }

  return cloneElement(children, {
    onClick: showModal
  })
}

但是这种只适合内容相对较少的弹窗,对于相对复杂的弹窗来说在同一个文件内写会导致内容相对较多,不利于维护

版本4

还有一种是封装好了,然后传入相关参数,可以看链接,这种方式我没有使用过,下面是摘抄的使用方式

import NiceModal from '@ebay/nice-modal-react';
import MyAntdModal from './my-antd-modal'; // created by above code

function App() {
  const showAntdModal = () => {
    // Show a modal with arguments passed to the component as props
    NiceModal.show(MyAntdModal, { name: 'Nate' })
  };
  return (
    <div className="app">
      <h1>Nice Modal Examples</h1>
      <div className="demo-buttons">
        <button onClick={showAntdModal}>Antd Modal</button>
      </div>
    </div>
  );
}

总结

这是我见过的 4 种使用方式,不知道其他方式是怎样的

Vue3 Hooks初探:手把手实现筛选面板组件

Vue3 Hooks初探:手把手实现筛选面板组件

前言

Vue3 的组合式 API(Composition API)带来了更灵活的代码组织方式,其中 Hooks 机制是核心特性之一。本文通过构建一个功能完整的筛选面板组件,从基础概念到实际应用,逐步深入讲解 Vue3 Hooks 的使用技巧,帮助开发者掌握响应式数据、计算属性、生命周期钩子等核心功能。


一、Vue3 Hooks 基础概念

1. 什么是 Hooks?

在 Vue3 中,Hooks 是指通过组合式 API 定义的函数,用于在组件中复用逻辑。Hooks 函数通常以 use 开头命名(如 useStateuseFetch),其本质是封装了响应式数据(ref/reactive)、计算属性(computed)、生命周期钩子(onMounted)等功能的函数。

2. Hooks 的核心特性

  • 模块化:将组件逻辑拆分为独立函数,提升复用性。
  • 响应式:基于 refreactive 实现数据驱动视图更新。
  • 生命周期集成:通过 onMountedonUnmounted 等钩子处理副作用。
  • 组合能力:多个 Hooks 可自由组合,形成复杂逻辑。

二、筛选面板需求分析

我们需要实现一个支持以下功能的筛选面板:

  1. 分类筛选:多选类别(如电子产品、服装)。
  2. 价格区间:滑动条选择最低和最高价格。
  3. 评分筛选:单选按钮选择最低评分。
  4. 实时搜索:输入关键词过滤商品名称。
  5. 结果展示:根据筛选条件动态渲染商品列表。

三、代码实现步骤

1. 环境准备

使用 Vite 创建 Vue3 项目:

npm init vite@latest vue3-filter-panel --template vue
cd vue3-filter-panel
npm install

2. 基础组件结构

创建 FilterPanel.vue 组件,定义基础状态:

<template>
  <div class="filter-panel">
    <!-- 分类筛选 -->
    <div>
      <h3>Categories</h3>
      <div v-for="category in categories" :key="category">
        <input type="checkbox" :id="'cat-' + category" :value="category" v-model="filterState.categories" />
        <label :for="'cat-' + category">{{ category }}</label>
      </div>
    </div>

    <!-- 价格区间 -->
    <div>
      <h3>Price Range</h3>
      <input type="number" v-model.number="filterState.priceRange[0]" placeholder="Min" />
      <input type="number" v-model.number="filterState.priceRange[1]" placeholder="Max" />
    </div>

    <!-- 评分筛选 -->
    <div>
      <h3>Rating</h3>
      <select v-model.number="filterState.minRating">
        <option :value="0">All</option>
        <option value="1">1+ Stars</option>
        <option value="2">2+ Stars</option>
        <option value="3">3+ Stars</option>
        <option value="4">4+ Stars</option>
        <option value="5">5 Stars</option>
      </select>
    </div>

    <!-- 搜索框 -->
    <div>
      <h3>Search</h3>
      <input v-model="filterState.searchQuery" placeholder="Search products..." />
    </div>

    <!-- 结果展示 -->
    <div class="results">
      <h2>Results ({{ filteredProducts.length }})</h2>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Category</th>
            <th>Price</th>
            <th>Rating</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="product in filteredProducts" :key="product.id">
            <td>{{ product.name }}</td>
            <td>{{ product.category }}</td>
            <td>${{ product.price }}</td>
            <td>{{ product.rating }}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'

// 原始数据
const products = ref([
  { id: 1, name: 'Smartphone', category: 'Electronics', price: 599, rating: 4.5 },
  { id: 2, name: 'Laptop', category: 'Computers', price: 999, rating: 4.7 },
  { id: 3, name: 'Headphones', category: 'Electronics', price: 199, rating: 4.3 },
  { id: 4, name: 'Coffee Maker', category: 'Home Appliances', price: 89, rating: 4.1 },
  { id: 5, name: 'Sneakers', category: 'Fashion', price: 129, rating: 4.6 }
])

// 筛选条件状态
const filterState = reactive({
  categories: [], // 选中的分类
  priceRange: [0, 1000], // 价格区间
  minRating: 0, // 最低评分
  searchQuery: '' // 搜索关键词
})

// 计算筛选后的商品列表
const filteredProducts = computed(() => {
  return products.value.filter(product => {
    // 分类筛选
    if (filterState.categories.length && !filterState.categories.includes(product.category)) {
      return false
    }
    // 价格区间筛选
    if (product.price < filterState.priceRange[0] || product.price > filterState.priceRange[1]) {
      return false
    }
    // 评分筛选
    if (filterState.minRating > 0 && product.rating < filterState.minRating) {
      return false
    }
    // 搜索关键词匹配(不区分大小写)
    if (filterState.searchQuery && !product.name.toLowerCase().includes(filterState.searchQuery.toLowerCase())) {
      return false
    }
    return true
  })
})
</script>

<style scoped>
.filter-panel { display: flex; flex-wrap: wrap; gap: 20px; }
.filter-panel > div { flex: 1 1 200px; background: #f5f5f5; padding: 16px; border-radius: 8px; }
input[type="number"], select { width: 100%; margin-top: 8px; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
</style>

四、核心功能解析

1. 响应式数据管理

  • ref vs reactive
    • ref 用于单一数据(如 searchQuery)。
    • reactive 用于对象或数组(如 filterState),便于整体响应式追踪。
  • 示例filterState 使用 reactive 定义,所有筛选条件变化均会自动触发计算属性更新。

2. 计算属性(computed

  • 作用:综合所有筛选条件,返回过滤后的数据。
  • 优势:自动缓存计算结果,仅在依赖项变化时重新计算。
  • 示例filteredProducts 通过链式过滤条件生成最终结果。

3. 生命周期钩子

  • onMounted:模拟异步数据加载。
    onMounted(() => {
      // 模拟 API 请求
      setTimeout(() => {
        products.value = [...products.value, { id: 6, name: 'Smartwatch', category: 'Electronics', price: 249, rating: 4.2 }]
      }, 1000)
    })
    
  • onBeforeUnmount:清理定时器,防止内存泄漏。

4. 实时搜索防抖(watch

  • 问题:搜索输入频繁触发计算属性,造成性能浪费。
  • 解决方案:使用 watch 监听 searchQuery,添加防抖处理。
    let searchTimeout = null
    
    watch(
      () => filterState.searchQuery,
      (query) => {
        clearTimeout(searchTimeout)
        searchTimeout = setTimeout(() => {
          console.log('Search query:', query)
          // 可在此调用后端 API
        }, 300)
      },
      { immediate: true }
    )
    

五、优化与扩展

1. 封装自定义 Hooks

将筛选逻辑抽象为可复用的函数:

// useFilter.js
import { reactive, toRefs } from 'vue'

export function useFilter() {
  const state = reactive({
    categories: [],
    priceRange: [0, 1000],
    minRating: 0,
    searchQuery: ''
  })
  return toRefs(state)
}

在组件中使用:

const { categories, priceRange, minRating, searchQuery } = useFilter()

2. 持久化筛选条件

利用 localStorage 保存用户选择:

watch(
  () => filterState,
  (newVal) => { localStorage.setItem('filterState', JSON.stringify(newVal)) },
  { deep: true }
)

onMounted 中恢复状态:

onMounted(() => {
  const saved = JSON.parse(localStorage.getItem('filterState'))
  if (saved) Object.assign(filterState, saved)
})

3. 异步数据加载

products 改为从 API 获取:

const products = ref([])
onMounted(async () => {
  products.value = await fetchDataFromAPI()
})

六、完整代码示例

完整代码已整合至前述 FilterPanel.vue,关键逻辑总结如下:

  1. 响应式状态filterState 管理所有筛选条件。
  2. 计算属性filteredProducts 实现多条件联合过滤。
  3. 生命周期钩子onMounted 模拟异步数据加载。
  4. 防抖处理watch 监听搜索输入,避免频繁计算。
  5. 持久化存储localStorage 保存用户筛选状态。

七、最佳实践总结

  1. 模块化逻辑:将复杂逻辑拆分为独立 Hooks,提升复用性。
  2. 合理使用 reactive:对对象或数组使用 reactive,单一数据用 ref
  3. 生命周期管理:在 onMounted 处理副作用,onBeforeUnmount 清理资源。
  4. 性能优化:对高频操作(如搜索)使用防抖或节流。
  5. 命名规范:Hooks 函数以 use 开头,明确功能意图。

通过本文的筛选面板案例,你可以掌握 Vue3 Hooks 的核心用法,并将其扩展到更复杂的场景中。建议尝试以下练习:

  • 添加排序功能(如按价格升序/降序)
  • 集成分页功能
  • 将筛选逻辑封装为独立 Hooks 并在其他组件中复用

python实现的电脑网速测试工具

电脑网速测试工具

功能介绍

这是一个简单的电脑网速测试工具,使用Python编写,基于speedtest库实现。用户可以点击"测试网速"按钮,程序会自动测试当前网络的下载速度和上传速度,并以动态变化的方式显示结果。

安装依赖

在运行程序之前,请确保已安装以下依赖库:

  1. 安装speedtest-cli库:

    pip install speedtest-cli
    
  2. 安装tkinter库(通常Python自带,无需额外安装):

    sudo apt-get install python3-tk  # Linux系统
    
  3. 安装pyinstaller(如果需要打包为可执行文件):

    pip install pyinstaller
    

使用说明

  1. 运行程序后,点击"测试网速"按钮。
  2. 程序会动态显示当前的下载速度和上传速度,并显示测试进度。
  3. 测试完成后,界面会显示最终的下载速度和上传速度。

打包说明

如果需要将程序打包为可执行文件(.exe),请按照以下步骤操作:

  1. 确保已安装pyinstaller

    pip install pyinstaller
    
  2. 打包程序:

    pyinstaller --onefile --windowed speed_test_tool.py
    
  3. 打包完成后,生成的.exe文件会出现在dist文件夹中。

创作过程

  1. 使用tkinter库创建图形用户界面。
  2. 使用speedtest库获取网络速度数据。
  3. 通过多线程避免界面卡死,并动态显示网速变化。

1.png

SwiftUI 视图如何“乖巧地”自动刷新不可观察(Unobservable)属性?

在这里插入图片描述

概述

在 SwiftUI 早期版本中,我们可以通过观察遵守 ObservableObject 协议的对象来适时的刷新视图界面。从 SwiftUI 5.0(iOS 17)开始,苹果通过 Swift 5.9 引入了全新的 @Observable 宏让我们在监听状态的变化上更加大有可为。

在这里插入图片描述

不过,出于某些原因我们需要禁止可观察对象中的某些属性被观察,这是为什么?又该如何监听这些属性的改变呢?

在本篇博文中,您将学到如下内容:

  1. 何为不可观察属性?它存在的目的是什么?
  2. 刷新不可观察属性的妙招
  3. 题外话:如何避免属性刷新“污染”? 源代码

相信学完本课后,小伙伴们对不可观察属性的应对会更加游刃有余、得心应手。

那还等什么呢?让我们马上开始“观察”大冒险吧! Let's go!!!;)


1. 何为不可观察属性?它存在的目的是什么?

在 SwiftUI 5.0 之前,对于自定义类来说我们必须让其遵守 ObservableObject 协议才能将它们融入到视图可观察的世界里:

class OldModel: ObservableObject {
    @Published var laserIntensity = 0.0
    var darkEnergy = 0
}

如上代码所示,在 ObservableObject 对象中只有被 @Published 显式修饰的属性才是可观察的。所谓可观察是指:该属性内容的改变会引起 SwiftUI 视图的刷新,即重新渲染(Rerender)。相反的,未用 @Published 修饰的属性则不是可观察的,视图 UI 对它们的改变将“一无所知”。

从 SwiftUI 5.0 开始,苹果新引入的可观察框架(Observation)中的 Observable 对象也有异曲同工之妙。

在下面的代码中,我们用 @Observable 宏同样创建了一个可观察对象的类,只不过和上面 ObservableObject 类所不同的是,默认 @Observable 宏修饰的可观察对象里所有属性都是可观察的,如果不希望它们被观察我们则需要显式用 @ObservationIgnored 来修饰:

@Observable
class Model {
    var laserIntensity = 0.0

    @ObservationIgnored
    var darkEnergy = 0
}

那么为什么我们会将某些属性设置为不可观察呢?原因很简单:

  • 这些对象会频繁改变,可能造成渲染引擎“压力山大”;
  • 这些对象无需参与到视图的刷新渲染中,它们只是表示模型的逻辑判定;
  • 这些属性可能在系统框架或第三方库的某些类里面,它们只是没有被设置为可观察,仅此而已;

那么,如果当这些不可观察属性发生改变时,我们想在 SwiftUI 视图的界面里对其做出相应反馈,又该如何是好呢?

2. 刷新不可观察属性的妙招

为了更好的向大家表明我们的意图,让我们先将之前的 Model 类做一番扩展:

import Combine

@Observable
class Model {
    var laserIntensity = 0.0
    
    @ObservationIgnored
    var darkEnergy = 0
    
    private var cancel: AnyCancellable?
    
    init(){
        cancel = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect().sink { _ in
            self.darkEnergy += Int.random(in: 0...10)
        }
    }
    
    deinit {
        cancel?.cancel()
    }
}

如您所见,我们在 Model 对象的创建后自动累增了不可观察属性 darkEnergy 的值,但是如果我们在 SwiftUI 视图中“嵌入” Model 对象的 darkEnergy 属性,则并不会造成界面的刷新:

struct ContentView: View {
    
    @Environment(Model.self) var model    
    
    var body: some View {
        NavigationStack {
            Form {
                Section("可观察属性") {
                    LabeledContent("激光强度") {
                        VStack {
                            Text("\(model.laserIntensity, specifier: "%0.1f")")
                            
                                .font(.system(size: 99, weight: .black, design: .rounded))
                                .foregroundStyle(.red)
                            
                            Button("增加激光强度") {
                                withAnimation {
                                    model.laserIntensity += 0.1
                                }
                            }
                            .font(.largeTitle)
                            .buttonStyle(.borderedProminent)
                            .tint(.pink)
                            .containerRelativeFrame(.horizontal, alignment: .center)
                        }
                    }
                }
                
                Section("不可观察属性") {
                    LabeledContent("暗能量威能") {
                        Text("\(model.darkEnergyWrap)")
                            .font(.system(size: 99, weight: .black, design: .rounded))
                            .foregroundStyle(.primary)
                            .animation(.bouncy, value: model.darkEnergyWrap)
                        
                    }
                }
            }
        }
    }
}

从运行的结果可以看到:即使 darkEnergy 属性的值在不断改变,但视图 UI 仍会置若罔闻,这就是不可观察属性原有的样子啊!只有当我们改变可观察属性 laserIntensity 时,由于视图刷新所产生的副作用(刷新污染),darkEnergy 属性的改变才会“顺带”展现出来。

在这里插入图片描述

那么,我们如何让 darkEnergy 属性自己在界面里保持自动刷新呢?

我们有很多种解决方案,但不外乎都需要增加另一个可观察属性作为触发器来驱动 darkEnergy 属性的刷新。

其实,SwiftUI 本身就为我们提供了解决之道,那就是内置的 TimelineView 原生视图:

在这里插入图片描述

TimelineView 视图可以作为容器,按照我们秃头码农要求的时间间隔渲染内部的子视图。比如,如果我们希望 TimelineView 中的内容每隔 1 秒刷新一次,则可以这么撸码:

TimelineView(.periodic(from: startDate, by: 1)) { context in
    AnalogTimerView(date: context.date)
}

其实,只要遵守 TimelineSchedule 协议,我们就能传入任何类型的实例作为 TimelineView 的进度(schedule)实参以达到按需刷新的目的。

比如,如果我们希望让 SwiftUI 运行时(Runtime)来决定以“最优”的间隔来刷新视图,则 AnimationTimelineSchedule 类型的值可能会更加恰如其分一些:

在这里插入图片描述

通过上面的讨论,现在利用 TimelineView 视图我们可以非常 Nice 的让原本不可观察的属性自动刷新啦:

Section("不可观察属性") {
// 让 SwiftUI 自动决定最优的刷新间隔
    TimelineView(.animation()) { _ in
        LabeledContent("暗能量威能") {
            Text("\(model.darkEnergy)")
                .font(.system(size: 99, weight: .black, design: .rounded))
                .foregroundStyle(.primary)
                .animation(.bouncy, value: model.darkEnergy)
            
        }
    }
}

运行看一下美美哒的效果吧:

在这里插入图片描述

3. 题外话:如何避免属性刷新“污染”?

通过上面的讨论,我们注意到这样一个细节:当 Model 中的可观察属性 laserIntensity 发生改变时,会间接导致其不可观察属性 darkEnergy 在界面上的刷新,这称之为“刷新污染”,在某些情况下这是不可接受的!

在大多数理想情况下,我们希望各个(可观察)属性的改变在 SwiftUI 视图界面中不会影响其它无关属性的显示。

一种简单的方法是:将这些属性限制在特定的自定义子视图中。

struct LaserIntensityView: View {
    
    @Environment(Model.self) var model
    
    var body: some View {
        VStack {
            Text("\(model.laserIntensity, specifier: "%0.1f")")
            
                .font(.system(size: 99, weight: .black, design: .rounded))
                .foregroundStyle(.red)
            
            Button("增加激光强度") {
                withAnimation {
                    model.laserIntensity += 0.1
                }
            }
            .font(.largeTitle)
            .buttonStyle(.borderedProminent)
            .tint(.pink)
            .containerRelativeFrame(.horizontal, alignment: .center)
        }
    }
}

在上面的代码里,我们将原先放在主视图 ContentView 内 model.laserIntensity 属性对应的 UI 描述代码,单独“拎出来”构成一个独立的自定义视图 LaserIntensityView,然后再将其作为子视图嵌入原来的位置:

Section("可观察属性") {
    LaserIntensityView()
}

这样一来,可观察属性 model.laserIntensity 的改变只会局限在 LaserIntensityView 内部,而不会导致父视图 ContentView 其它部分的刷新:

在这里插入图片描述

就像大家看到的那样:现在当我们增加 laserIntensity 属性的值时,其它无关属性的改变并不会对界面有任何影响,棒棒哒!💯

源代码

全部源代码在此:

import SwiftUI
import Combine

class OldModel: ObservableObject {
    @Published var laserIntensity = 0.0
    var darkEnergy = 0
    
    private var cancel: AnyCancellable?
    
    init() {
        cancel = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect().sink { _ in
            self.darkEnergy += Int.random(in: 0...10)
            self.laserIntensity += 0.1
        }
    }
    
    deinit {
        cancel?.cancel()
    }
}

@Observable
class Model {
    var laserIntensity = 0.0
   
    @ObservationIgnored
    var darkEnergy = 0
    
    @ObservationIgnored
    private var cancel: AnyCancellable?
    
    init(){
        cancel = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect().sink { _ in
            self.darkEnergy += Int.random(in: 0...10)
        }
    }
    
    deinit {
        cancel?.cancel()
    }
}

struct LaserIntensityView: View {
    
    @Environment(Model.self) var model
    //@EnvironmentObject var model: OldModel
    
    var body: some View {
        VStack {
            Text("\(model.laserIntensity, specifier: "%0.1f")")
            
                .font(.system(size: 99, weight: .black, design: .rounded))
                .foregroundStyle(.red)
            
            Button("增加激光强度") {
                withAnimation {
                    model.laserIntensity += 0.1
                }
            }
            .font(.largeTitle)
            .buttonStyle(.borderedProminent)
            .tint(.pink)
            .containerRelativeFrame(.horizontal, alignment: .center)
        }
    }
}

struct ContentView: View {
    
    @Environment(Model.self) var model
    //@EnvironmentObject var model: OldModel
    
    var body: some View {
        NavigationStack {
            Form {
                Section("可观察属性") {
                    LaserIntensityView()
                }
                
                Section("不可观察属性") {
                    TimelineView(.animation()) { _ in
                        LabeledContent("暗能量威能") {
                            Text("\(model.darkEnergy)")
                                .font(.system(size: 99, weight: .black, design: .rounded))
                                .foregroundStyle(.primary)
                                .animation(.bouncy, value: model.darkEnergy)
                            
                        }
                    }
                }
            }
            .scrollContentBackground(.hidden)
            .background(Color.teal.gradient)
            .contentTransition(.numericText(countsDown: false))
            .navigationTitle("自动刷新不可观察属性")
            .toolbar {
                Text("大熊猫侯佩 @ \(Text("CSDN").foregroundStyle(.red))")
                    .font(.headline.bold())
                    .foregroundStyle(.gray)
            }
        }
    }
}

#Preview {
    ContentView()
        .environment(Model())
        //.environmentObject(OldModel())
}

总结

在本篇博文中,我们介绍了何为“不可观察属性”以及它的应用场景,并随后讨论了如何“怡然自得”的自动刷新原本不可观察属性的改变。

感谢观赏,再会啦!8-)

❌