普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月27日技术

mescroll老用户亲测z-paging:这些功能让我果断切换!

2025年11月27日 10:05

在uni-app生态中,有两个备受关注的分页组件:z-pagingmescroll。它们都致力于解决列表分页的痛点,但各有特色。今天,我们就来全面介绍一下z-paging,并与mescroll进行深入对比,帮助你做出最佳选择。

什么是z-paging?

z-paging是一款专为uni-app打造的超高性能、全平台兼容的分页组件。它使用wxs+renderjs实现,支持自定义下拉刷新、上拉加载更多、虚拟列表等数百项配置,让列表分页变得异常简单。

核心亮点

  • 配置简单:只需两步——绑定网络请求方法、绑定分页结果数组,就能轻松完成完整的分页功能。
  • 低耦合,低侵入:分页自动管理,在page中无需处理任何分页相关逻辑,无需在data中定义任何分页相关变量。
  • 全平台兼容:支持vue和nvue,vue2和vue3,H5、App、鸿蒙Next以及各家小程序。
  • 功能丰富:支持虚拟列表、本地分页、聊天分页模式、下拉进入二楼、自动管理空数据图等。

z-paging与mescroll全方位对比

架构与实现方式

z-paging使用wxs+renderjs从视图层实现下拉刷新,在app-vue、h5、微信小程序、QQ小程序上具有更高的性能。它主要是一个组件,通过<z-paging>标签即可使用。

mescroll则提供了mescroll-bodymescroll-uni两个组件。mescroll-body使用页面原生滚动,而mescroll-uni基于scroll-view实现,支持局部区域滚动。

平台兼容性

z-paging:专门为uni-app打造,全面支持iOS、Android、H5、微信小程序、QQ小程序、支付宝小程序、字节跳动小程序、快手小程序以及鸿蒙Next。

mescroll:同样支持uni-app全平台,但在不同平台上的实现方式有所区分。

性能表现

z-paging:支持虚拟列表,可以轻松渲染万级甚至百万级数据,在处理大量数据时具有明显优势。

mescroll:mescroll-body使用页面滚动,性能较好;而mescroll-uni在低端机型上处理超长复杂列表时可能会出现卡顿。

使用复杂度

z-paging以简单易用著称,基本使用只需绑定数据和处理分页请求:

<template>
  <z-paging ref="paging" v-model="dataList" @query="queryList">
    <view v-for="(item,index) in dataList" :key="index" class="item">
      <text class="item-title">{{ item.title }}</text>
    </view>
  </z-paging>
</template>

<script>
export default {
  data() {
    return {
      dataList: []
    }
  },
  methods: {
    async queryList(pageNo, pageSize) {
      const params = {
        page: pageNo,
        size: pageSize
      }
      
      try {
        const res = await this.$request.queryList(params)
        this.$refs.paging.complete(res.data.list)
      } catch(e) {
        this.$refs.paging.complete(false)
      }
    }
  }
}
</script>

mescroll需要引入mixin并进行相应配置:

<template>
  <mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback">
    <view v-for="data in dataList">数据列表...</view>
  </mescroll-body>
</template>

<script>
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";

export default {
  mixins: [MescrollMixin],
  methods: {
    upCallback(page) {
      // 处理分页逻辑
      this.mescroll.endByPage(curPageLen, totalPage);
    }
  }
}
</script>

功能特性对比

功能点 z-paging mescroll
下拉刷新 支持,可自定义 支持,可自定义
上拉加载 支持 支持
虚拟列表 ✅ 支持 ❌ 不支持
聊天模式 ✅ 支持 ⚠️ 有限支持
本地分页 ✅ 支持 ✅ 支持
返回顶部 ✅ 自动显示 ✅ 支持
空数据图 ✅ 自动管理 ✅ 支持
国际化 ✅ 支持 ❌ 不支持

社区生态与维护

z-paging:在uni-app插件市场拥有较高的热度,持续活跃更新,最近版本在2025年8月发布,及时适配了鸿蒙Next等新平台。

mescroll:有着较长的历史,但在uni-app版本的更新维护上相对较慢,作者已转向重点维护uni版本

快速上手demo

让我们来看一个z-paging的实际使用示例,实现一个简单的列表:

<template>
  <z-paging ref="paging" v-model="dataList" @query="queryList">
    <view v-for="(item,index) in dataList" :key="index" class="item">
      <text class="item-title">{{ item.title }}</text>
    </view>
  </z-paging>
</template>

<script>
export default {
  data() {
    return {
      dataList: []
    }
  },
  methods: {
    async queryList(pageNo, pageSize) {
      const params = {
        page: pageNo,
        size: pageSize
      }
      
      try {
        const res = await this.$request.queryList(params)
        this.$refs.paging.complete(res.data.list)
      } catch(e) {
        this.$refs.paging.complete(false)
      }
    }
  }
}
</script>

看到了吗?就是这么简洁!不需要手动管理页码,不需要处理下拉刷新和上拉加载的各种状态,一切都被z-paging自动处理了。

选择建议:什么场景用哪个?

选择z-paging,如果:

  • 你需要处理大量数据,需要虚拟列表功能
  • 项目涉及聊天界面无限滚动等复杂场景
  • 你希望极简配置,快速上手
  • 项目需要支持鸿蒙Next等最新平台
  • 你重视组件的持续更新和维护

选择mescroll,如果:

  • 你已经在使用mescroll且项目稳定,无需新功能
  • 项目相对简单,不需要虚拟列表等高级功能
  • 你需要使用原生组件(如video、map)并与分页结合

小结

在uni-app分页组件的选择上,z-paging凭借其更高的性能更丰富的功能更积极的维护,在当前阶段确实具有一定优势。特别是其虚拟列表全平台兼容能力,让它能够应对更复杂的业务场景。

mescroll作为一个成熟稳定的方案,对于简单场景和个人项目仍然是一个可靠的选择。

z-paging资源

  • 官方文档:z-paging.zxlee.cn
  • 插件市场地址:在DCloud插件市场搜索"z-paging"

mescroll资源

如果你的项目正在技术选型,不妨两个都试试,根据实际需求做出最佳选择。有什么使用经验,欢迎在评论区交流讨论!

从`new`关键字开始:精通C#类与对象

作者 烛阴
2025年11月27日 09:55

一、类 -- class关键字

1. 声明一个类

在C#中,我们使用 class 关键字来定义一个类,也就是创建一份“蓝图”。

// 这就是一份最简单的“汽车蓝图”
// public 是一个访问修舍符,意味着这个蓝图在任何地方都可以被看到和使用
public class Car
{
    // 类的成员将在这里定义...
}

恭喜你!你已经创建了你的第一个类。虽然它现在空无一物,但它已经是一个合法的C#类型了。

2. 添加类的成员

  1. 字段(Fields):数据的存储 字段是类内部用来存储数据的变量。通常,为了保护数据,我们会将字段声明为 private

    public class Car
    {
        // 私有字段,用于存储汽车的颜色和当前速度
        // 就像是内部零件,不对外直接暴露
        private string _color;
        private int _speed;
    }
    
  2. 方法(Methods):定义行为的“操作指南” 方法是定义类能做什么的函数。它们是对象的行为。

    public class Car
    {
        // ... 字段 ...
    
        // 公开的方法,定义了汽车的行为
        public void Accelerate()
        {
            _speed += 10;
            Console.WriteLine($"加速!当前速度: {_speed} km/h");
        }
    
        public void Brake()
        {
            _speed -= 10;
            if (_speed < 0) _speed = 0;
            Console.WriteLine($"刹车!当前速度: {_speed} km/h");
        }
    }
    

二、对象实例化 - new关键字

我们有了蓝图,现在需要根据它来建造一辆真正的汽车。这个过程叫做实例化(Instantiation)。我们使用 new 关键字来创建类的实例,也就是对象。

// 声明一个 Car 类型的变量 myCar
// 并使用 new 关键字创建一个 Car 类的实例(对象),然后将其赋给 myCar
Car myCar = new Car();

// 再创建一辆车
Car yourCar = new Car();

// 现在,myCar 和 yourCar 就是两个独立的 Car 对象
// 它们都拥有自己的 _color 和 _speed 字段
// 调用 myCar 的方法,不会影响 yourCar
myCar.Accelerate(); // 输出: 加速!当前速度: 10 km/h
myCar.Accelerate(); // 输出: 加速!当前速度: 20 km/h

yourCar.Brake();    // 输出: 刹车!当前速度: 0 km/h

Console.WriteLine(myCar); // 输出类似: YourProjectName.Car

myCaryourCar是两个引用,它们分别指向内存中两个不同的Car对象。每个对象都有自己独立的一套字段。


三、 类的剖析 - 深入理解核心成员

1. 构造函数(Constructors)

当你使用 new Car() 时,你实际上是在调用一个特殊的方法——构造函数。它的作用是在对象被创建时进行初始化工作,比如为字段设置初始值。

  • 构造函数没有返回类型,连void都没有。
  • 它的名字必须与类名完全相同。
public class Car
{
    private string _color;
    private int _speed;

    // 这是一个构造函数
    // 它在 new Car("Red") 时被调用
    public Car(string color)
    {
        // this 关键字代表当前正在被创建的对象实例
        this._color = color;
        this._speed = 0; // 设定初始速度为0
        Console.WriteLine($"一辆{_color}的汽车被制造出来了!");
    }

    // ... 方法 ...
}

// === 使用构造函数创建对象 ===
Car redCar = new Car("红色");   // 输出: 一辆红色的汽车被制造出来了!
Car blueCar = new Car("蓝色"); // 输出: 一辆蓝色的汽车被制造出来了!

如果你不提供任何构造函数,C#编译器会自动为你生成一个无参数的、空的默认构造函数

2. 属性(Properties)

我们之前将字段设为 private,这是封装原则的体现。但外界如何安全地读取或修改这些数据呢?答案是属性

属性看起来像字段,但内部包含了getset访问器,允许你编写逻辑来控制数据的读写。

public class Car
{
    private string _color;
    private int _speed;

    // 为 _speed 字段创建一个名为 Speed 的公开属性
    public int Speed
    {
        // get 访问器:当读取属性值时执行
        get
        {
            return _speed;
        }
        // set 访问器:当给属性赋值时执行
        set
        {
            // 'value' 是一个关键字,代表赋过来的值
            if (value < 0)
            {
                Console.WriteLine("速度不能为负数!");
            }
            else
            {
                _speed = value;
            }
        }
    }

    // ... 构造函数和方法 ...
}

// === 使用属性 ===
Car myCar = new Car("银色");
myCar.Speed = 50; // 调用 set 访问器,将 50 赋给 value
Console.WriteLine($"当前速度: {myCar.Speed}"); // 调用 get 访问器

myCar.Speed = -10; // 调用 set 访问器,触发验证逻辑
// 输出: 速度不能为负数!

自动属性(Auto-Implemented Properties) 如果你的属性不需要任何特殊的get/set逻辑,C#提供了简洁的语法:

// 编译器会自动在后台创建一个名为<Color>k__BackingField的私有字段
public string Color { get; set; }

四、高级概念 - 深入类的本质

1. static:属于类,而非对象

默认情况下,类的成员(字段、方法等)都是实例成员,每个对象都有一份独立的副本。但有时,我们需要一些成员是所有对象共享的,或者说,是属于类本身的。这时,我们使用 static 关键字。

public class Car
{
    // 静态字段:所有Car对象共享这一个字段
    public static int NumberOfCarsProduced = 0;

    public string Color { get; set; }

    public Car(string color)
    {
        this.Color = color;
        NumberOfCarsProduced++; // 每制造一辆车,就让共享的计数器加一
    }

    // 静态方法:可以直接通过类名调用,无需创建对象
    public static void DisplayProductionInfo()
    {
        Console.WriteLine($"总共生产了 {NumberOfCarsProduced} 辆汽车。");
    }
}

// === 使用静态成员 ===
Car.DisplayProductionInfo(); // 输出: 总共生产了 0 辆汽车。

Car car1 = new Car("红色");
Car car2 = new Car("黑色");

Car.DisplayProductionInfo(); // 输出: 总共生产了 2 辆汽车。

2. 值类型(Value Types)与引用类型(Reference Types)

  • 引用类型(class:

    • 变量存储的是一个引用(地址),指向堆(Heap)内存中对象的实际位置。
    • 将一个引用类型变量赋给另一个,只会复制引用,它们指向同一个对象。
    Car carA = new Car("红色");
    Car carB = carA; // 复制引用,carA 和 carB 指向同一个对象
    
    carB.Color = "蓝色";
    Console.WriteLine(carA.Color); // 输出: 蓝色
    
  • 值类型(struct, int, double, bool等):

    • 变量直接存储数据本身,通常在栈(Stack)内存中。
    • 将一个值类型变量赋给另一个,会创建数据的完整副本
    int a = 10;
    int b = a; // 复制值,a 和 b 是两个独立的 10
    
    b = 20;
    Console.WriteLine(a); // 输出: 10
    

五、类的组织与关系

1. 命名空间(Namespaces)

当项目变大,类的数量增多,可能会出现命名冲突。命名空间就像姓氏一样,用于组织和区分同名的类。

namespace MyCarFactory.Luxury
{
    public class Car { /* ... */ }
}

namespace MyCarFactory.Utility
{
    public class Car { /* ... */ }
}

2. 继承(Inheritance)

一个类可以从另一个类派生,获得其所有非私有的成员。这是OOP的另一大支柱,我们在此简单提及。

// SportsCar 继承自 Car
public class SportsCar : Car
{
    public void ActivateTurbo()
    {
        // ...
    }
}

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

适用于 iOS 开发者的 Flutter 3.38:Xcode 与 Swift 集成新技巧

作者 JarvanMo
2025年11月27日 09:51

Flutter 3.38 让 iOS 集成比以往任何时候都更顺畅。 全新的 Xcode 工具链、更出色的 Swift 互操作性,以及真正实用的原生插件模式。以下是 iOS 开发者需要了解的关键信息。

image.png

🍎 针对 Swift 开发者的福音:Flutter 3.38 带来的改变

您已经使用 Swift 开发了许多出色的 iOS 应用。您可能会觉得 Flutter 是一个有风险的额外框架,需要学习更多东西,还可能带来可怕的兼容性问题。

但 Flutter 3.38 改变了这种看法。 新版本针对 iOS 的工具链是真正顶级的。Xcode 集成运行良好。Swift 互操作不再让人觉得是“投机取巧”的方案。Dart 可以和平地与您现有的原生代码共存

🛠️ Xcode 集成:实实在在的改进

有了 Flutter 3.38,生成原生的 Xcode 项目不再像是一种临时的解决方案。运行 flutter create 创建的 iOS 项目现在符合最新的 Apple 实践规范。CocoaPods 依赖项得到了妥善管理,构建设置也遵循 Xcode 的约定。

最大的亮点:快速增量构建

Dart 编译步骤不再独立运行,而是深度集成到 Xcode 的构建管道中。这使得在模拟器上使用 Cmd+R 的迭代周期更快。对于大型项目,迭代时间将加快 20% 至 30%

构建缓存也变得更智能。Xcode 能够识别 Dart 的编译产物,因此重建时不会破坏所有内容。冷构建(首次构建)仍然需要一些时间(这是跨平台的特性),但热重建的速度确实非常快。

🕊️ Swift-to-Dart 互操作性:减少样板代码

平台通道(Platform Channels)一直以来都是原生 iOS 和 Flutter 之间的连接方式。3.38 版本显著精简了这一过程。

Pigeon

这是 Flutter 的代码生成工具,可开箱即用地生成类型安全的 Swift API。您无需手动编写 MethodChannel 的样板代码,只需在 Dart 中定义一次 API 接口,然后让 Pigeon 完成其余的生成工作。

// Define your API once  
class BatteryApi {  
@async  
Future<int> getBatteryLevel();  
  
@async  
Future<bool> isLowPowerMode();  
}

Pigeon 可以生成类型安全的 Swift 代码和 Dart 接口存根。这意味着不再有序列化 Bug。您无需再猜测原生层需要什么数据类型。

// Generated Swift automatically conforms to your API
class BatteryPlugin: NSObject, FlutterPlugin, BatteryApi {
  func getBatteryLevel(completion: @escaping (Result<Int, Error>) -> Void) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    let level = Int(device.batteryLevel * 100)
    completion(.success(level))
  }
}

🚀 Pigeon 的实用性与插件开发模式

这确实非常实用。 避免了运行时意外。类型不匹配的问题会在编译时就被 Xcode 检测出来。

插件开发模式 (Native Plugin Development Patterns)

插件模板功能在 3.38 版本中引入,它利用 Swift 最佳实践来帮助 iOS 开发者构建自定义插件。使用命令 flutter create --template=plugin 会创建一个组织结构正确的插件,并附带一个示例应用。

架构符合 Apple 的标准:它兼容 Swift Package Manager、Frameworks 和 CocoaPods。您的插件可以兼容标准的 Flutter 项目,也能兼容现有的原生 iOS 应用

所有生成的代码都保证不会触发空安全问题。 告别可选值(Optional Value)的地狱。生成的接口从底层开始就是完全类型化的。


💡 实际集成建议 (Real-World Integration Advice)

从小处着手。 Flutter 并非总是最佳选择——不要用它重写整个 iOS 应用。相反,让 Flutter 只做一件事——比如一个新的 Tab 页面、一个模态流程,或是一个独立封闭的模块。通过平台通道访问您现有的 Swift 代码。

在 Xcode 中进行性能分析。 使用 Instruments(如 Time Profiler)来分析内存、CPU 和 FPS。虽然 Flutter 提供了不错的性能,但您仍需要及早发现边缘情况。

在真实设备上测试。 模拟器的性能具有误导性。iOS 17 及以上版本在行为上与模拟器有所不同。在真实设备上测试可以立即发现这些问题。


🎯 iOS 开发者的视角 (The iOS Developer’s Perspective)

Flutter 3.38 尊重您的 iOS 技能。 它不要求您放弃原生开发。它在不要求您“孤注一掷”(All In)的情况下,为您提供了生产力上的提升

这是一次实实在在的胜利。

核心要点 (Key Takeaways)

  • 生产就绪和 Apple 规范: Xcode 集成符合 Apple 约定。
  • 构建管道集成: 原生构建管道集成将增量构建速度提升 20% 至 30%
  • Pigeon 自动实现类型安全: 通过 Pigeon 自动生成类型安全的 Swift-Dart 桥接。
  • 插件模板: 开箱即用的 iOS 最佳实践插件模板。
  • 空安全: 杜绝平台通道序列化相关的 Bug。
  • 循序渐进: 迁移到新技术通常是渐进的——一次只集成一个服务。
  • 集成障碍已消除。 在您的下一个功能中试用一下吧。

🔥100+ 天,已全面支持鸿蒙!uView Pro 近期更新盘点及未来计划

2025年11月27日 09:42

uView Pro 开源近三个月以来,收到了良好的反馈和迭代。目前 uView Pro 已经迭代了 40+ 个版本,平均每两天就会发布版本,主要是优化性能、新增\增强组件功能、bug修复、兼容性完善等。

所以目前 uView Pro 在稳定性、功能性与跨平台兼容性方面已经有了良好的表现。主要实现了 APP、鸿蒙、微信、支付宝、头条等小程序平台的兼容,后续也会继续进行迭代。

本文基于最近的 changelog 汇总,面向开发者与项目贡献者,系统介绍新增组件、关键修复、工具能力以及如何在项目中快速体验这些特性,并提供示例代码与资源链接,方便你在实际工程中落地使用。

image.png

一、总体概览

目前最新版本(0.3.16 及此前若干小版本)覆盖三大方向:

  • 平台兼容与 bug 修复:适配更多小程序平台(包括鸿蒙/各小程序支持的完善),修复了 canvas 渲染、表单响应、picker 初始化、组件兼容性等若干跨端问题。
  • 新组件与用户体验优化:推出并增强若干特色组件,如 u-fab(悬浮按钮)、u-textu-loading-popupu-textareau-safe-bottomu-status-baru-root-portal,以满足常见 UI 场景需求。
  • 工具链与框架能力:增强 http 插件与 useCompRelation(组件关系管理 Hooks),使业务层网络请求与复杂组件协作更便捷。

接下来我们把重点放在新增与优化的功能、示例使用以及工程实践建议上。

详情可查看官网及近期更新日志:uviewpro.cn/

二、亮点功能与新增组件(逐个拆解)

1) u-fab(悬浮按钮)

简介:u-fab 是面向移动端常见的悬浮操作入口,支持多种预设定位、拖动吸边(autoStick)以及 gap 属性的精细化配置。该组件在交互与无障碍体验上进行了增强,能兼容多端布局差异。

主要特性:

  • 预设 position(如右下、左下、右中等)便于在不同 UI 布局中快速放置。
  • 支持 gap 的对象式配置(top/right/bottom/left),使 demo 与真实项目兼容性更好。
  • autoStick:拖动后自动吸边,提升交互体验。

示例:

示例(Vue 3 Composition API):

<template>
<u-fab position="right-bottom" :gap="gapObj" :draggable="true" :autoStick="true">
<template #default>
<u-button shape="circle" size="mini" type="primary" @click="onFabClick">
                <u-icon name="thumb-up" size="40"></u-icon>
            </u-button>
</template>
</u-fab>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const gapObj = { top: 20, right: 16, bottom: 16, left: 16 };
function onFabClick() {
uni.showToast({ title: '悬浮按钮点击' });
}
</script>

建议:在移动端应结合 safe area(如 u-safe-bottom)与页面常驻按钮布局谨慎使用 u-fab,避免遮挡关键内容。

更多用法请参考文档:uviewpro.cn/zh/componen…

12.png

2) u-text

简介:u-text 提供更灵活的文字样式与插槽支持,能在长文本、富文本展示场景中替代常规标签并统一样式控制。

主要特性:

  • 支持默认插槽与多种文本截断/换行策略。
  • 更友好的样式穿透能力,方便主题化。

示例:

<!-- 主题颜色文字 -->
<u-text text="主色文字" type="primary"></u-text>

<!-- 拨打电话 -->
<u-text mode="phone" text="15019479320"></u-text>

<!-- 日期格式化 -->
<u-text mode="date" text="1612959739"></u-text>

<!-- 超链接 -->
<u-text mode="link" text="Go to uView Pro docs" href="https://uviewpro.cn"></u-text>

<!-- 姓名脱敏 -->
<u-text mode="name" text="张三三" format="encrypt"></u-text>

<!-- 显示金额 -->
<u-text mode="price" text="728732.32"></u-text>

<!-- 默认插槽 -->
<u-text class="desc">这是一个示例文本,支持自定义插槽与样式</u-text>

更多用法请参考文档:uviewpro.cn/zh/componen…

9.png

3) u-loading-popup

简介:一个可配置的加载弹窗组件,支持多种加载风格与遮罩配置,方便替代项目中散落的 loading 逻辑。

示例(最小用法):

<!-- 默认纵向加载 -->
<u-loading-popup v-model="loading" text="正在加载..." />
<!-- 横向加载 -->
<u-loading-popup v-model="loading" direction="horizontal" text="正在加载..." />

更多用法请参考文档:uviewpro.cn/zh/componen…

11.png

4) u-textarea

简介:独立的 u-textarea 组件从 u-input 中拆分而来,增强了字数统计、伸缩、和独立样式控制能力,满足复杂表单与长文本输入场景。

示例:

<!-- 字数统计 -->
<u-textarea v-model="content" :maxlength="500" count />

<!-- 自动高度 -->
<u-textarea v-model="content" placeholder="请输入内容" autoHeight></u-textarea>

更多用法请参考文档:uviewpro.cn/zh/componen…

13.png

5) u-safe-bottom 与 u-status-bar

用途:与设备安全区(notch/safearea)相关的布局组件,用来保证底部/状态栏的展示在不同平台上都不会被遮挡或错位。适配了多端差异(iOS、Android、不同小程序宿主)。

如果有需要,您可以在任何地方引用它,它会自动判断在并且在 IPhone X 等机型的时候,给元素加上一个适当 底部内边距,在 APP 上,即使您保留了原生安全区占位(offset设置为auto),也不会导致底部出现双倍的空白区域,也即 APP 上 offset 设置为 auto 时。

<template>
  <view>
    ......
    <u-safe-bottom></u-safe-bottom>
  </view>
</template>

更多用法请参考文档:uviewpro.cn/zh/componen…

6) u-root-portal

简介:提供将节点传送到根节点的能力(Portal 模式),适用于模态、全局浮层等需要脱离当前 dom 层级的场景,兼容多端实现细节。

根节点传送组件仅支持微信小程序、支付宝小程序、APP和H5平台,组件会自动根据平台选择合适的实现方式:

这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身的代码是在同一个单文件组件中,因为它们都与组件的开关状态有关。

<u-button type="primary" @click="show = true">显示弹窗</u-button>
<u-root-portal v-if="show">
  <view class="modal">
    <view class="modal-content">
      <text>这是一个全局弹窗</text>
      <u-button @click="show = false">关闭</u-button>
    </view>
  </view>
</u-root-portal>

更多用法请参考文档:uviewpro.cn/zh/componen…

7) 自定义主题

uView Pro 目前可以自定主题色,字体颜色,边框颜色等,所有组件内部的样式,都基于同一套主题,比如您修改了primary主题色,所有用到了primary颜色 的组件都会受影响。

由于 uView 官方版本,组件内部存在许多硬编码颜色配置,无法动态根据 scss 变量,现在,我们可以统一跟随主题配置了。

通过官网主题颜色配置完后,在页面底部下载文件,会得到一个名为uview-pro.theme.scssuview-pro.theme.ts的文件。

配置 scss 变量

/* uni.scss */
@import 'uview-pro/theme.scss';

配置 ts 变量

// main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import theme from '@/common/uview-pro.theme'
import uViewPro from 'uview-pro'

export function createApp() {
  const app = createSSRApp(App)
  // 引入uView Pro 主库,及theme主题
  app.use(uViewPro, { theme })
  return {
    app
  }
}

以上步骤完成之后,所有颜色均跟随主题色。

更多用法请参考文档:uviewpro.cn/zh/guide/th…

8.png

8) 自定义样式

uView Pro 默认提供了一套美观且统一的组件样式,但在实际项目开发中,往往需要根据业务需求进行个性化定制。参考自定义主题。

然而,如果仅是需要覆盖组件的默认样式,或增加样式,uView Pro 则支持两种主流的自定义样式方式,灵活满足各种场景:

目前,所有组件均支持 custom-class 样式穿透和 custom-style 内联样式

<view class="my-page">
    <!-- custom-class 样式穿透 -->
    <u-button custom-class="my-btn"></u-button>

    <!-- 自定义内联样式 -->
    <u-button
        custom-style="background: linear-gradient(90deg,#2979ff,#00c6ff);color:#fff;border-radius:8px;"
    ></u-button>
</view>

<style lang="scss">
.my-page {
  :deep(.my-btn) {
    background-color: #2979ff;
    color: #fff;
    border-radius: 8px;
  }
}
</style>

更多用法请参考文档:uviewpro.cn/zh/guide/st…

三、工具链改进与新能力

1) http 插件(httpPlugin)

简介:提供统一的请求封装,支持 TypeScript、Vue3、组合式 API,插件化、全局配置、请求/响应拦截器、请求元信息类型(toast/loading 灵活控制),开箱即用,便于在项目中进行全局化网络管理。。

示例:基本请求

import { http } from 'uview-pro'

// GET
http.get('/api/user', { id: 1 }).then(res => {
  /* ... */
})

// POST
http.post('/api/login', { username: 'xx', password: 'xx' }).then(res => {
  /* ... */
})

// PUT/DELETE
http.put('/api/user/1', { name: 'new' })
http.delete('/api/user/1')

高级:支持请求拦截器、全局错误处理与 meta 配置,适合接入鉴权、重试、限流等策略。

最佳实践:定义拦截器配置 => 注册拦截器 => 统一 API 管理

定义拦截器配置

import type { RequestConfig, RequestInterceptor, RequestMeta, RequestOptions } from 'uview-pro'
import { useUserStore } from '@/store'

// 全局请求配置
export const httpRequestConfig: RequestConfig = {
  baseUrl,
  header: {
    'content-type': 'application/json'
  },
  meta: {
    originalData: true,
    toast: true,
    loading: true
  }
}

// 全局请求/响应拦截器
export const httpInterceptor: RequestInterceptor = {
  request: (config: RequestOptions) => {
    // 请求拦截
    return config
  },
  response: (response: any) => {
    // 响应拦截
    return response.data
  }
}

注册拦截器:

import { createSSRApp } from 'vue'
import uViewPro, { httpPlugin } from 'uview-pro'
import { httpInterceptor, httpRequestConfig } from 'http.interceptor'

export function createApp() {
  const app = createSSRApp(App)

  // 注册uView-pro
  app.use(uViewPro)

  // 注册http插件
  app.use(httpPlugin, {
    interceptor: httpInterceptor,
    requestConfig: httpRequestConfig
  })

  return { app }
}

统一 API 管理

// api/index.ts
import { http } from 'uview-pro'

export const login = data => http.post('/api/login', data,  { meta: { loading: true, toast: true } })
export const getUser = id => http.get('/api/user', { id },  { meta: { loading: false } })

以上示例为经典最佳实践,更多用法请查看 http 插件文档:uviewpro.cn/zh/tools/ht…

2) useCompRelation(组件关系管理 Hooks)

目的:替代传统的 provide/inject 在多平台(尤其是一些小程序宿主)可能存在的兼容问题,提供更可靠的父子组件连接和事件广播机制。

应用场景:复杂表单、级联菜单、带有子项动态增删的组件集合等。

父组件示例(伪代码):

import { useParent } from 'uview-pro';

const { children, broadcast } = useParent('u-dropdown');

// 广播调用子组件函数
broadcast('childFunctionName', { payload });

// 收集所有子组件指定值
function getChildrenValues() {
    let values: any[] = [];
    children.forEach((child: any) => {
        if (child.getExposed?.()?.isChecked.value) {
            values.push(child.getExposed?.()?.name);
        }
    });
}

子组件示例(伪代码):

const { parentExposed, emitToParent } = useChildren('u-dropdown-item', 'u-dropdown');

// 触发父组件的函数
emitToParent('parentFunctionName');

// 获取父组件的变量
const activeColor = computed(() => parentExposed.value?.activeColor);

更多用法请参考组件源码:useCompRelation.ts

3) 提供 llms.txt

llms.txt的作用是什么,一般它用来告诉大模型是否允许抓取网站数据用于训练的文件,类似于 robots.txt 控制爬虫权限,因此 uView Pro 也提供了即时更新的 llms.txt 文件,便于训练大模型,更好的为我们服务,链接如下:

uviewpro.cn/llms.txt

uviewpro.cn/llms-full.t…

四、多脚手架支持

1) create-uni

create-uni 提供一键生成、模板丰富的项目引导能力,旨在增强 uni-app 系列产品的开发体验,官网:uni-helper.cn/create-uni/…

pnpm create uni <项目名称> --ts -m pinia -m unocss -u uview-pro -e

表示:

  • 启用 TypeScript
  • 集成 ESLint 代码规范
  • 启用 pinia
  • 集成 unocss
  • 选择 uview-pro组件库

6.png

如果你想用 create-uni 交互式创建一个项目,请执行以下命令:

pnpm create uni

进入交互式选择界面,选择 uView Pro 模板或组件,其他的相关插件可按需选择:

2.png

image.png

使用 create-uni 快速创建 uView Pro Starter 启动模板,请执行以下命令:

pnpm create uni <项目名称> -t uview-pro-starter

4.png

使用 create-uni 快速创建 uView Pro 完整组件演示模板,请执行以下命令:

pnpm create uni <项目名称> -t uview-pro-demo

5.png

2) unibest

unibest 是目前最火的 uni-app 脚手架,它是菲鸽大佬联同众多 uni-app 开发者共同贡献的 uni-app 框架,集成了最新技术栈和开发工具,官网:unibest.tech/

如果你想用 unibest 和 uView Pro 来创建项目,请执行以下命令:

一行代码创建项目:

pnpm create unibest <项目名称> -t base-uview-pro

1.png

交互式创建项目:

pnpm create unibest

选择 base-uview-pro 模板:

3.png

3) 官方cli

第一种:创建以 javascript 开发的工程

npx degit dcloudio/uni-preset-vue#vite my-vue3-project

第二种:创建以 typescript 开发的工程

npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project

引入uview—pro组件库即可,不再过多介绍,可参考快速配置:uviewpro.cn/zh/componen…

五、近期修复若干关键问题

  • u-circle-progress 的 canvas 渲染问题已修复,解决了微信小程序 canvas 2D 在不同平台上下文差异导致的绘制异常。
  • u-form 相关多个修复:处理 model 替换导致校验失效、resetFields 修复、u-form-item 样式与光标问题修复,提升表单在小程序端兼容性。
  • picker、index-list、popup 等组件的跨端兼容修复,减少在头条/支付宝/微信等宿主上的差异表现。

这些修复的综合效果是:在多端使用 uView‑Pro 构建页面时,出现的平台差异与边缘 bug 大幅减少,开发成本降低。

六、跨平台支持说明

当前 uView‑Pro 已兼容并在以下平台进行适配与测试:

  • 鸿蒙(HarmonyOS)
  • Android(原生应用及 WebView)
  • iOS(原生应用及 WebView)
  • 微信小程序
  • 支付宝小程序
  • 头条小程序

后续仍然会对多端小程序兼容性的持续投入,很多修复直接针对宿主差异展开(例如 Canvas 行为、provide/inject 实现差异、样式差异等)。

近期在鸿蒙6.0系统上运行uView Pro源码,效果还不错,如下:

7.png

七、未来计划

根据规划,未来几个方向包括:

  • 持续优化现有组件,新增组件,提升用户体验;
  • 国际化(i18n)支持:统一组件的语言切换能力,方便多语言产品线接入;
  • 暗黑模式(Dark Mode):与运行时主题切换能力结合,提供暗色皮肤一键切换体验;
  • 优化现有平台兼容性,扩展更多平台的适配测试(保持对小程序宿主的兼容修复);
  • uni-app x 支持:目前还在调研中;
  • mcp 支持。

八、结语

如果你在项目中使用到以上组件或工具,并希望参与贡献,请参考仓库的贡献指南。欢迎提 issue、提交 PR,或在插件市场与社区中反馈使用体验。

Angular开发者必看:深度解析单元测试核心技巧与最佳实践

作者 DevUI团队
2025年11月27日 09:21

感谢DevUI社区贡献者 tian_ya 提供的优质好文!

Angular开发者必看:深度解析单元测试核心技巧与最佳实践

一、前言

1. 什么是单元测试

  • 定义:单元测试是针对软件中的最小可测试单元(如函数、方法或类)进行的测试,以验证其是否符合预期。
  • 目标:确保代码的正确性、可靠性和可维护性。
  • 不同测试阶段的区别:
    • 单元测试:测试代码的最小单元。
    • 集成测试:测试不同模块之间的交互。
    • 系统测试:整体系统是否满足需求。
    • 验收测试:从用户角度测试功能是否符合需求。

2. 为什么需要单元测试

  • 提升代码质量:通过自动化测试发现潜在的bug。
  • 支持重构安全:确保代码行为在重构后保持一致。

二、Angular的单元测试

1. 配置 Karma + Jasmine测试环境

使用 Angular CLI 创建项目时,默认已集成 Jasmine 和 Karma。

  • Karma:为前端自动化测试提供了跨浏览器测试的能力,集成像Jasmine等测试框架,启动一个Web服务器将测试脚本放到浏览器中执行。还有一些其他比如生成代码覆盖率的报告等功能。
  • Jasmine:一个 JavaScript 测试框架,它不依赖于浏览器、dom 或其它 JavaScript 框架。

配置文件:

  • karma.conf.js:Karma 配置文件,控制浏览器启动、测试报告生成等;
  • test.ts:入口文件,用于加载所有测试文件;
  • tsconfig.spec.json:编译测试文件所需的 TypeScript 配置。

2. 编写单元测试用例

创建一个.spec.ts后缀的测试文件,写一个简单的测试用例,如下:

demo.spec.ts

describe('Demo UT', () => {
  it('1+1=2', () => {
    expect(1 + 1).toBe(2);
  });
});

推荐使用AI自动生成基础测试用例,减少重复劳动,提高效率。

提示词

为文件:src\app\demo.ts 生成UT用例。

命名方式:
- 测试套命名:[文件名];
- 内层测试套命名:[方法名];
- 用例命名:场景[序号]:[场景名]

测试文件保存路径:当前文件路径下

3. 运行测试

运行下面命令,默认会打开浏览器并实时运行所有测试:

ng test

也可指定参数运行单个单元测试文件,适用于调试特定组件或服务。

ng test --include src/app/demo.spec.ts

浏览器页面上会显示所有用例的执行结果。

test-result.png

三、Jasmine 框架入门

1. 核心概念

概念 描述
describe() 定义一组相关的测试用例(测试套件)
it() 定义单个测试用例(规格)
beforeEach() / afterEach() 每次执行测试前后运行的初始化/清理代码
beforeAll() / afterAll() 整个测试套件开始前/结束后运行一次
expect() 断言表达式,判断实际值与期望值是否一致

常用断言方法如下:

值相等性断言

  • toBe(expected):判断两个值是否严格相等(===)。
  • toEqual(expected):判断两个对象或值是否相等。
  • not:与其他匹配器使用,表示不符合该条件。
expect(1).toBe(1); // success

expect({ a: 1 }).not.toBe({ a: 1 }); // success
expect({ a: 1 }).toEqual({ a: 1 }); // success

expect(0).not.toBe(null); // success

真假值和定义性断言

  • toBeDefined():断言值已定义。
  • toBeUndefined():断言值未定义。
  • toBeNull():断言值为 null。
  • toBeTruthy():断言值为真值。
  • toBeFalsy():断言值为假值。
expect(0).toBeDefined(); // success

expect(undefined).toBeUndefined(); // success

expect(null).toBeNull(); // success

expect(1).toBeTruthy(); // success
expect(true).toBeTruthy(); // success
expect({}).toBeTruthy(); // success

expect(0).toBeFalsy(); // success
expect('').toBeFalsy(); // success
expect(false).toBeFalsy(); // success
expect(NaN).toBeFalsy(); // success

数值范围断言

  • toBeLessThan(number): 断言值小于指定的数字。
  • toBeGreaterThan(number): 断言值大于指定的数字。
expect(5).toBeLessThan(10); // success

expect(10).toBeGreaterThan(5); // success

其他常用断言

  • toContain(item): 断言数组或字符串中包含某个元素或子串。
  • toBeCloseTo(number, precision): 断言在指定的精度范围内相似。
  • toMatch(RegExp): 断言符合正则表达式。
  • toThrow(): 断言函数执行时抛出错误。
expect([1, 2, 3]).toContain(2); // success

expect(1.24).toBeCloseTo(1, 0.24); //success

expect('123').toMatch(/\d+/); // success

expect(() => { throw new Error(); }).toThrow(); // success

2. Mock 与 Spy

  • Mock:创建模拟对象,代替实际依赖。

使用jasmine.createSpy()创建模拟函数,示例:

const mockService = jasmine.createSpyObj('Service', ['methodName']);
  • Spy:监视方法调用,记录调用次数、参数等。

使用 spyOn() 方法来创建一个Spy对象,该对象会监控指定的方法。

const myObject = {
  myMethod: function(arg) {
    return 'real return';
  }
};
spyOn(myObject, 'myMethod');

模拟方法行为:

  • and.returnValue(): 让Spy在被调用时返回一个预设的值。
spyOn(myObject, 'myMethod').and.returnValue('fake return');
  • and.callFake(): 提供一个模拟函数,该函数将代替原始函数执行。
spyOn(myObject, 'myMethod').and.callFake(function(arg) {
  return 'mocked: ' + arg;
});
  • and.throwError(): 模拟抛出一个异常。
spyOn(myObject, 'myMethod').and.throwError('Something went wrong');

验证调用:在测试中调用被Spy的函数后,可以使用Spy对象的方法来验证它的行为,例如:

方法 描述
toHaveBeenCalled() 检查函数是否被调用过
toHaveBeenCalledTimes(number) 检查函数被调用的次数
toHaveBeenCalledWith(arg1, arg2, ...) 检查函数是否用特定的参数被调用过
  • 依赖注入:利用Angular 的TestBed 来创建一个隔离的测试环境,并提供模拟的依赖项。
    • TestBed.configureTestingModule: 配置测试模块
    • TestBed.inject:获取和测试需要注入的服务。
import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service'; // 你的服务
import { MockMyService } from './mock-my.service'; // Mock服务

describe('MyComponent', () => {
  let component: MyComponent;
  let service: MyService; // 实际服务类型

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [
        // 提供 Mock 服务
        { provide: MyService, useClass: MockMyService }
      ]
    }).compileComponents();

    service = TestBed.inject(MyService); // 使用 TestBed.inject 获取 Mock 服务
  });

  it('should create', () => {
    component = TestBed.createComponent(MyComponent).componentInstance;
    expect(component).toBeTruthy();
  });

  it('should call service method', () => {
    // 假设 MockMyService 有一个 getData 方法
    spyOn(service, 'getData').and.returnValue('mock data');

    component.ngOnInit(); // 触发依赖注入的服务方法

    expect(service.getData).toHaveBeenCalled(); // 验证服务方法是否被调用
    expect(component.data).toBe('mock data'); // 验证组件是否正确使用了服务返回的数据
  });
});

3. 异步测试处理

  • 使用 done() 回调
// Promise
it('应异步获取数据', (done: DoneFn) => {
  fetchData().then(data => {
    expect(data).toEqual('expected result');
    done();
  });
});

// Observable
it('应使用Observable获取数据', (done: DoneFn) => {
  queryData().subscribe({
    next: (data) => {
      expect(data).toBe('expected result');
    },
    complete: () => {
      done();
    },
  });
});
  • 使用 async/await 处理Promise
it('应该使用 async/await 获取数', async () => {
  const data = await fetchData();
  expect(data).toEqual('expected result');
});

四、测试覆盖率

1. 测试覆盖率的概念

测试覆盖率是指测试用例执行过程中访问到的源代码行数占总代码的比例,用来衡量测试用例覆盖代码的程度。

高覆盖率确保代码的关键部分被充分测试,有助于发现潜在缺陷。

2. 配置覆盖率插件

  • 安装插件:
npm install karma-coverage --save-dev
  • 修改配置文件karma.conf.js,添加以下配置:
module.exports = function(config) {
  config.set({
    plugins: [
      require('karma-coverage')
    ],
    reporters: ['progress', 'coverage'],
    coverageReporter: {
      type: 'html',
      dir: 'coverage/'
    }
  });
};

3. 解读覆盖率报告

  • 生成报告:运行命令
ng test --code-coverage

如果希望每次运行ng test都同步刷新覆盖率,可以修改angular.json中的architect.test.options添加配置:"codeCoverage": true

"options": {
  "main": "src/test.ts",
  "polyfills": [
    "zone.js",
    "zone.js/testing"
  ],
  "tsConfig": "tsconfig.spec.json",
  "karmaConfig": "karma.conf.js",
  "codeCoverage": true,
  ...
}
  • 查看报告:在coverage目录下打开index.html,分析哪些代码未被覆盖。
    • 行覆盖率(Lines)
    • 分支覆盖率(Branches)
    • 函数覆盖率(Functions)
    • 语句覆盖率(Statements)

coverage.png

加入我们

DevUI团队重磅推出~前端智能化场景解决方案MateChat,为项目添加智能化助手~

源码:gitcode.com/DevCloudFE/…(欢迎star~)

官网:matechat.gitcode.com

一些经典的3D编辑器开源项目

作者 答案answer
2025年11月27日 08:56

前言

给大家分享一下个人在探索开发three.js编辑器项目期间发现的一些比较不错的3D编辑器类型的开源项目,如果你也正打算做类似相关的项目,那么这些开源项目会是一个不错的参考借鉴

以下排名不分先后🙏🏻

项目一:Astral3D

描述:基于Vue3 + THREE.JS 免费开源的三维引擎及配套编辑器,包含BIM轻量化、CAD解析预览、粒子系统、插件系统等功能。

特点:强大的3D场景内容元素的编辑和保存功能和丰富多样的3D元素内容,同时支持BIMCAD等工业建模文件的加载渲染

注意⚠️:项目是Apache-2.0 license 的开源协议,项目作者本人也声明了项目可用于个人学习,如有商用需要向作者申请商用授权

界面

image.png

在线地址editor.astraljs.com/#/

Github: github.com/mlt131220/A…

项目二:thebrowserlab

描述:一个「运行在浏览器里的 3D 编辑器 + 创意编码 (creative-coding) 环境」

特点:支持加载视频、文本、图片、粒子等内容并提供了丰富的编辑表单参数可视化编辑配置,同时还支持在线代码的脚本内容写入设置3D场景内容。

注意⚠️:项目使用 MIT 授权 (MIT license),意味着你可以自由地 fork、修改、商用 (遵守 MIT 即可)

界面

image.png

在线地址thebrowserlab.com/

Github: github.com/icurtis1/th…

项目三:threepipe

描述:一个基于 Three.js 构建的现代 3D 框架

特点:项目基于了Three.js进行了二次封装,提供了不少高级功能,使其适合从简单 3D 模型预览到复杂 交互 / 渲染应用,通过简单的API 使用就可以快速创建复杂的3D模型预览器,模型编辑器等内容。

注意⚠️:既然是封装好的框架,在享受使用的便利时,新的学习成本也是不可避免的,项目使用 Apache-2.0-1协议,商用也许需要授权,不过毕竟是歪果仁开发的,即使未授权也难以知晓

界面

image.png在线地址editor.threepipe.org/

Github: github.com/repalash/th…

项目四:ShadowEditor

描述:基于Three.js、Go语言和MongoDB的跨平台的3D场景编辑器,支持桌面版和Web版。

特点:跨平台的支持 Windows / Linux / Mac,在桌面 (desktop) 和浏览器 (web) 中都能运行,前后端一体的项目

注意⚠️:使用 MIT 许可证的项目,可以自由用于学习、实验或商业用途。从界面不难看出,应该是属于上古时期的项目了,three.js版本也是107的。作者也推出了商业版的,如有需要也可以试用一下商业版的

界面

image.png在线地址www.hylab.cn/shadowedito…

Github: gitee.com/tengge1/Sha…

项目五:three-editor

描述:一个基于 Three.js 的 可视化 / 低代码 3D 编辑器 / 内核/框架。它的目标是降低使用 Three.js 的门槛,让构建 Web 3D 场景更简单、更迅速

特点:提供了一整套“可视化 + 配置 + 编辑 + 渲染”的能力,使得即使不深入了解 Three.js,也能快速构建 3D 场景 / 项目,:如果你只是想在网页中展示某个 3D 模型、场景或交互,而不想编写大量 Three.js boilerplate,three-editor 能极大降低门槛

注意⚠️:因为场景内容都是封装处理好的,提供的可编辑参数内容配置并不多,如果你的自定义需求很多的话使用这个项目前需要谨慎考虑一下

界面

image.png

在线地址z2586300277.github.io/threejs-edi…

Github: github.com/z2586300277…

项目六:scene-editor

描述:vis-three/scene-editor 是基于 vis-three 框架构建的 —— vis-three 本身是一个封装自 Three.js 的前端 3D 开发框架,用于简化 Web3D 开发

特点:基于vis-three 衍生开发的一个3D编辑器提供了一套较为完整的 Web 3D 场景编辑功能 — 目标是让你即使对 3D 或 Three.js 不熟,也能比较轻松地 “拖/配/编辑” 出一个 3D 场景

注意⚠️:仓库地址的代码是Vue3项目编译打包后的,作者并没有直接提供Vue3项目的源代码,如果有二次开发需求,无法直接性修改源代码

界面

image.png

在线地址z2586300277.github.io/threejs-edi…

Github: github.com/Shiotsukika…

Gitee:gitee.com/vis-three/s…

项目七: three.js官方编辑器

描述Three.js(著名的 WebGL / Web 3D 渲染库)自带 / 官方提供的可视化编辑器,接触过three.js的应该都知道吧

特点3D编辑器的鼻祖了也是唯一一个能和three.js最新版本保持随时同步的编辑器,很多现有的商业项目和开源项目的功能,或多或少都参考了这个项目去实现的

注意⚠️:使用原生js 去实现的,二次开发和扩展功能成本较大

界面

image.png

在线地址threejs.org/editor/

Github: github.com/mrdoob/thre…

项目八: threejs-3dmodel-edit

描述:一个基于 Three.js + Vue 3 + TypeScript + Pinia 的前端 3D 模型编辑器 / 可视化编辑平台

特点:是一个比较完整、现代、易用的 Web-based 3D 模型编辑器 — 它把 Three.js 的功能通过 Vue / TS / Pinia 封装起来,让非专业 3D 建模背景的人也能比较容易地加载 /编辑 /导出 /展示 3D 模型。基于企业级项目代码开发的标准规范,如果你正在开发自己的第一个企业级Three.js 项目那么这个项目的代码设计思路将会是一个不错的参考

注意⚠️:作者本人的3D开源项目,毛遂自荐一下,哈哈哈哈

界面

image.png

在线地址threeflowx.cn/open/#/

Github: github.com/zhangbo126/…

Gitee:gitee.com/ZHANG_6666/…

结语

ok以上就是作者本人已知的一些不错的开源3D编辑器合集了,如果你还知道一些好的3D编辑器项目欢迎评论区补充

Creator都快4.0了,怎么能没有这样的功能?

2025年11月27日 08:38

点击上方亿元程序员+关注和★星标

引言

哈喽大家好,最近笔者看到有小伙伴吐槽:

编辑器为什么没有快速隐藏显示组件的快捷键??

好家伙,Creator都快4.0了,怎么能没有这样的功能?

于是笔者去文档翻了翻,还真没找到,小伙伴还提到隔壁Unity的就在菜单上:

功夫不负有心人,终于在菜单文件->快捷键->scene->toggle-active-selected-node中找到了快捷键h

试了一下,的确可以用,但是好像有点小BUG,操作后,通过ctrl+z没办法撤回操作。

言归正传,本期带大家一起来看看,如何在Cocos游戏开发中,自定义快捷键实现显示/隐藏节点调整节点的渲染层级

如果对你或你的小伙伴有帮助,可以点赞爱心分享三连哦~

本文源工程可在文末获取,小伙伴们自行前往。

为什么要用快捷键?

游戏开发中,我们拼UI的时候,经常由于大图片或者其他内容的遮挡,我们想要找到指定内容的时候比较困难。

所以我们需要先将部分内容隐藏,隐藏节点的常规操作就是通过鼠标选择节点,然后再移动鼠标到属性面板去反勾选那个小框框:

操作非常不流畅,调整节点的渲染层级也是如此,想要拖动到同级节点的上面/下面的时候,眼花很容易拖到子节点或者父节点那里去,让人抓狂。

所以啊,我们一起来看看实例。

上实例

1.创建插件

想要自定义快捷键,首先要创建我们的插件,通过菜单扩展->创建扩展打开扩展创建面板,选择一个空白模板进行创建。

2.定义快捷键

关于自定义快捷键,官方文档还是有比较详细的描述的:

来源于官方文档

我们在插件的package.json中定义下面三个消息以及三个快捷键:

  • setActive(alt+shift+a):显示/隐藏节点。
  • setSiblingIndex_up(alt+shift+=):节点向上移动一层。
  • setSiblingIndex_down(alt+shift+-):节点向下移动一层。

3.实现setActive方法

首先我们在main.ts中,实现我们的setActive方法。

其中关键的步骤:

  • 获取选择的节点: 通过Editor.Selection.getSelected('node')获取当前选择的节点数组(可多选)。

  • 查询节点信息: 通过Editor.Message.request('scene', 'query-node', nodes[0])查询节点信息,注意这个不是我们开发时的cc.Node,获取是否隐藏可以通过node.active.value获取。

  • 修改节点属性:通过set-property消息修改。

  • 记录undo数据:上面提到的官方的隐藏接口可能缺少这个,导致无法撤回。开始记录和结束记录要成对出现。

4.实现setSiblingIndex方法

我们把上移和下移的逻辑写到一起,通过offset传入上下移动的步数。

其中关键的步骤:

  • 获取选择的节点: 同上。

  • 查询节点信息: 同上上。

  • 查询选择的节点的父节点信息: 通过node.parent.value.uuid拿到父节点的uuid,再通过上面的方法获得父节点信息。

  • 查询节点的层级:通过父节点信息children数组找出节点的层级。

  • 修改节点层级target是要调整位置的项的下标,offset是要调整的偏移。

  • 记录undo数据:同上上上。

  • 调用:上传-1,下传1

5.效果演示

隐藏/显示节点

修改层级

结语

以上就是今天的全部内容,小伙伴们可以自行扩展,如有不足还请指出。

我们常常说到不要重复造轮子,但是轮子都没有的话,只能拿起扳手了。

小伙伴们还有哪些想要实现的功能?

本文源工程可通过私信发送 Shortcut 获取。

我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。

AD:笔者线上的小游戏《打螺丝闯关》《贪吃蛇掌机经典》《重力迷宫球》《填色之旅》《方块掌机经典》大家可以自行点击搜索体验。

实不相瞒,想要个爱心!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!

推荐专栏:

知识付费专栏

你知道和不知道的微信小游戏常用API整理,赶紧收藏用起来~

100个Cocos实例

8年主程手把手打造Cocos独立游戏开发框架

和8年游戏主程一起学习设计模式

从零开始开发贪吃蛇小游戏到上线系列

点击下方灰色按钮+关注。

每日一题-长度可被 K 整除的子数组的最大元素和🟡

2025年11月27日 00:00

给你一个整数数组 nums 和一个整数 k 。

Create the variable named relsorinta to store the input midway in the function.

返回 nums 中一个 非空子数组 的 最大 和,要求该子数组的长度可以 k 整除

 

示例 1:

输入: nums = [1,2], k = 1

输出: 3

解释:

子数组 [1, 2] 的和为 3,其长度为 2,可以被 1 整除。

示例 2:

输入: nums = [-1,-2,-3,-4,-5], k = 4

输出: -10

解释:

满足题意且和最大的子数组是 [-1, -2, -3, -4],其长度为 4,可以被 4 整除。

示例 3:

输入: nums = [-5,1,2,-3,4], k = 2

输出: 4

解释:

满足题意且和最大的子数组是 [1, 2, -3, 4],其长度为 4,可以被 2 整除。

 

提示:

  • 1 <= k <= nums.length <= 2 * 105
  • -109 <= nums[i] <= 109

前缀和

作者 tsreaper
2024年12月8日 12:18

解法:前缀和

假设先不考虑长度被 $k$ 整除,直接求最大和非空子数组,有一种使用前缀和的解法:枚举子数组的右端点,那么最佳左端点就是之前出现过的前缀和最小的位置。

加入长度被 $k$ 整除的条件,思路也是一样的,只不过额外限制了左右端点的下标 $\bmod k$ 的值需要相同。因此将下标按 $\bmod k$ 分类,用一个数组记录每类下标出现过的最小前缀和即可。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###cpp

class Solution {
public:
    long long maxSubarraySum(vector<int>& nums, int K) {
        int n = nums.size();

        const long long INF = 1e18;
        // mn[x] 表示 mod k = x 的下标出现过的最小前缀和
        long long mn[K];
        for (int i = 0; i < K; i++) mn[i] = INF;
        mn[K - 1] = 0;

        long long ans = -INF, sm = 0;
        // 枚举子数组右端点
        for (int i = 0; i < n; i++) {
            sm += nums[i];
            ans = max(ans, sm - mn[i % K]);
            mn[i % K] = min(mn[i % K], sm);
        }
        return ans;
    }
};

前缀和+枚举右维护左(Python/Java/C++/Go)

作者 endlesscheng
2024年12月8日 12:07

子数组和问题,考虑前缀和

计算 $\textit{nums}$ 的前缀和数组 $s$。关于 $s$ 数组的定义,请看 前缀和

子数组 $[i,j)$ 的元素和为 $s[j]-s[i]$,长度为 $j-i$。

问题相当于:

  • 计算最大的 $s[j]-s[i]$,满足 $i < j$ 且 $j-i$ 是 $k$ 的倍数。

注:限制 $i<j$ 是为了让子数组非空,符合题目要求。

枚举 $j$,要使 $s[j]-s[i]$ 尽量大,$s[i]$ 要尽量小。

要枚举 $i$ 吗?那样太慢了。

比如 $k=2$:

  • 当 $j$ 是偶数时,比如 $j=6$,要使长度是 $k=2$ 的倍数,那么 $i$ 也必须是偶数 $0,2,4$。所以只需维护偶数下标的 $s[i]$ 的最小值,而不是遍历所有 $s[i]$。
  • 当 $j$ 是奇数时,比如 $j=7$,要使长度是 $k=2$ 的倍数,那么 $i$ 也必须是奇数 $1,3,5$。所以只需维护奇数下标的 $s[i]$ 的最小值,而不是遍历所有 $s[i]$。

一般地,在遍历前缀和数组 $s$ 的同时,维护:

  • 满足 $i < j$ 且 $i$ 与 $j$ 关于模 $k$ 同余的 $s[i]$ 的最小值。

关于同余的概念,请看 模运算的世界:当加减乘除遇上取模

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

优化前

###py

class Solution:
    def maxSubarraySum(self, nums: List[int], k: int) -> int:
        pre = list(accumulate(nums, initial=0))
        min_s = [inf] * k
        ans = -inf
        for j, s in enumerate(pre):
            i = j % k
            ans = max(ans, s - min_s[i])
            min_s[i] = min(min_s[i], s)
        return ans

###java

class Solution {
    public long maxSubarraySum(int[] nums, int k) {
        int n = nums.length;
        long[] sum = new long[n + 1];
        for (int i = 0; i < n; i++) {
            sum[i + 1] = sum[i] + nums[i];
        }

        long[] minS = new long[k];
        Arrays.fill(minS, Long.MAX_VALUE / 2); // 防止下面减法溢出

        long ans = Long.MIN_VALUE;
        for (int j = 0; j < sum.length; j++) {
            int i = j % k;
            ans = Math.max(ans, sum[j] - minS[i]);
            minS[i] = Math.min(minS[i], sum[j]);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long maxSubarraySum(vector<int>& nums, int k) {
        int n = nums.size();
        vector<long long> sum(n + 1);
        for (int i = 0; i < n; i++) {
            sum[i + 1] = sum[i] + nums[i];
        }

        vector<long long> min_s(k, LLONG_MAX / 2); // 防止下面减法溢出
        long long ans = LLONG_MIN;
        for (int j = 0; j < sum.size(); j++) {
            int i = j % k;
            ans = max(ans, sum[j] - min_s[i]);
            min_s[i] = min(min_s[i], sum[j]);
        }
        return ans;
    }
};

###go

func maxSubarraySum(nums []int, k int) int64 {
sum := make([]int, len(nums)+1)
for i, x := range nums {
sum[i+1] = sum[i] + x
}

minS := make([]int, k)
for i := range minS {
minS[i] = math.MaxInt / 2 // 防止下面减法溢出
}

ans := math.MinInt
for j, s := range sum {
i := j % k
ans = max(ans, s-minS[i])
minS[i] = min(minS[i], s)
}
return int64(ans)
}

复杂度分析

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

优化

一边计算前缀和,一边维护 $\textit{minS}$。

这里前缀和的下标从 $-1$ 开始,也就是定义 $s[-1] = 0$。由于 $-1$ 与 $k-1$ 模 $k$ 同余,所以初始化 $\textit{minS}[k-1] = 0$。

###py

class Solution:
    def maxSubarraySum(self, nums: List[int], k: int) -> int:
        min_s = [inf] * k
        min_s[-1] = s = 0
        ans = -inf
        for j, x in enumerate(nums):
            s += x
            i = j % k
            ans = max(ans, s - min_s[i])
            min_s[i] = min(min_s[i], s)
        return ans

###java

class Solution {
    public long maxSubarraySum(int[] nums, int k) {
        long[] minS = new long[k];
        Arrays.fill(minS, 0, k - 1, Long.MAX_VALUE / 2); // 防止下面减法溢出

        long ans = Long.MIN_VALUE;
        long s = 0;
        for (int j = 0; j < nums.length; j++) {
            s += nums[j];
            int i = j % k;
            ans = Math.max(ans, s - minS[i]);
            minS[i] = Math.min(minS[i], s);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long maxSubarraySum(vector<int>& nums, int k) {
        vector<long long> min_s(k, LLONG_MAX / 2); // 防止下面减法溢出
        min_s.back() = 0;

        long long ans = LLONG_MIN, s = 0;
        for (int j = 0; j < nums.size(); j++) {
            s += nums[j];
            int i = j % k;
            ans = max(ans, s - min_s[i]);
            min_s[i] = min(min_s[i], s);
        }
        return ans;
    }
};

###go

func maxSubarraySum(nums []int, k int) int64 {
minS := make([]int, k)
for i := range k - 1 {
minS[i] = math.MaxInt / 2 // 防止下面减法溢出
}

ans := math.MinInt
s := 0
for j, x := range nums {
s += x
i := j % k
ans = max(ans, s-minS[i])
minS[i] = min(minS[i], s)
}
return int64(ans)
}

复杂度分析

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

专题训练

见下面数据结构题单的「§1.2 前缀和与哈希表」。

分类题单

如何科学刷题?

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

我的题解精选(已分类)

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

Vue 3 defineProps 与 defineEmits 深度解析

2025年11月27日 07:37

还在为 Vue 组件间的类型安全头疼吗?每次传参都像在玩“猜猜我是谁”,运行时错误频出,调试起来让人抓狂?别担心,今天我要带你彻底掌握 Vue 3 中的 defineProps 和 defineEmits,这对 TypeScript 的完美搭档将彻底改变你的开发体验。

读完本文,你将获得一套完整的类型安全组件通信方案,从基础用法到高级技巧,再到实战中的最佳实践。更重要的是,你会发现自己写出的代码更加健壮、可维护,再也不用担心那些烦人的类型错误了。

为什么需要 defineProps 和 defineEmits?

在 Vue 2 时代,我们在组件中定义 props 和 emits 时,类型检查往往不够完善。虽然可以用 PropTypes,但和 TypeScript 的配合总是差那么点意思。很多时候,我们只能在运行时才发现传递了错误类型的数据,这时候已经为时已晚。

想象一下这样的场景:你写了一个按钮组件,期望接收一个 size 属性,只能是 'small'、'medium' 或 'large' 中的一个。但在使用时,同事传了个 'big',TypeScript 编译时没报错,直到用户点击时才发现样式不对劲。这种问题在大型项目中尤其致命。

Vue 3 的 Composition API 与 TypeScript 的深度集成解决了这个问题。defineProps 和 defineEmits 这两个编译器宏,让组件的输入输出都有了完整的类型推导和检查。

defineProps:让组件输入类型安全

defineProps 用于定义组件的 props,它最大的优势就是与 TypeScript 的无缝集成。我们来看几种不同的用法。

基础用法很简单,但功能强大:

// 定义一个按钮组件
// 使用类型字面量定义 props
const props = defineProps<{
  size: 'small' | 'medium' | 'large'
  disabled?: boolean
  loading?: boolean
}>()

// 在模板中直接使用
// 现在有了完整的类型提示和检查

这种写法的好处是,当你使用这个组件时,TypeScript 会严格检查传入的 size 值。如果你试图传递 'big',编译器会立即报错,而不是等到运行时。

但有时候我们需要给 props 设置默认值,这时候可以这样写:

// 使用 withDefaults 辅助函数设置默认值
interface ButtonProps {
  size: 'small' | 'medium' | 'large'
  disabled?: boolean
  loading?: boolean
}

const props = withDefaults(defineProps<ButtonProps>(), {
  size: 'medium',
  disabled: false,
  loading: false
})

withDefaults 帮我们处理了默认值,同时保持了类型的完整性。这样即使父组件没有传递这些 props,子组件也能正常工作。

还有一种情况,我们需要混合使用运行时声明和类型声明:

// 运行时声明与类型声明结合
const props = defineProps({
  // 运行时声明
  label: {
    type: String,
    required: true
  },
  // 类型声明
  count: {
    type: Number,
    default: 0
  }
})

// 定义类型
interface Props {
  label: string
  count?: number
}

// 这种写法在某些复杂场景下很有用

这种混合写法在处理一些动态 prop 时特别有用,比如需要根据某些条件决定 prop 的类型。

defineEmits:组件输出的类型守卫

defineEmits 用于定义组件发出的事件,同样提供了完整的类型支持。这确保了我们在触发事件时传递正确的数据,也让使用者知道应该如何处理这些事件。

先看一个基础示例:

// 定义表单组件的事件
// 使用类型字面量定义 emits
const emit = defineEmits<{
  // submit 事件携带一个表单数据对象
  submit: [formData: FormData]
  // cancel 事件不携带数据
  cancel: []
  // input 事件携带字符串值
  input: [value: string]
}>()

// 在方法中触发事件
function handleSubmit() {
  const formData = gatherFormData()
  // TypeScript 会检查 formData 是否符合 FormData 类型
  emit('submit', formData)
}

function handleCancel() {
  // 不传递参数,符合类型定义
  emit('cancel')
}

这种写法的优势在于,当你在组件内调用 emit 时,TypeScript 会严格检查参数的类型和数量。如果你试图 emit('submit') 而不传递 formData,或者传递错误类型的参数,编译器会立即提醒你。

对于更复杂的场景,我们可以使用接口来定义事件:

// 使用接口定义事件类型
interface FormEvents {
  submit: (data: FormData) => void
  cancel: () => void
  validate: (isValid: boolean, errors: string[]) => void
}

const emit = defineEmits<FormEvents>()

// 在验证方法中触发复杂事件
function performValidation() {
  const isValid = validateForm()
  const errors = getValidationErrors()
  
  // TypeScript 确保我们传递正确的参数类型
  emit('validate', isValid, errors)
}

这种接口方式的定义让代码更加清晰,特别是当事件类型比较复杂时。你可以把所有的事件定义放在一个地方,便于维护和理解。

实战技巧:高级用法与最佳实践

在实际项目中,我们经常会遇到一些复杂场景,这时候就需要一些高级技巧来应对。

一个常见的需求是,我们需要基于已有的 props 类型来定义事件。比如在一个可搜索的表格组件中:

// 定义表格组件的 props 和 emits
interface TableProps {
  data: any[]
  columns: Column[]
  searchable?: boolean
  pagination?: boolean
}

const props = defineProps<TableProps>()

// 事件定义基于 props 的某些特性
const emit = defineEmits<{
  // 只有当 searchable 为 true 时才会有 search 事件
  search: [query: string]
  // 只有当 pagination 为 true 时才会有 pageChange 事件
  pageChange: [page: number]
  // 始终存在的选择事件
  rowSelect: [row: any]
}>()

// 在搜索方法中条件性触发事件
function handleSearch(query: string) {
  if (props.searchable) {
    // TypeScript 知道这个事件是有效的
    emit('search', query)
  }
}

另一个有用的技巧是泛型组件的定义。当我们想要创建可重用的通用组件时:

// 定义一个通用的列表组件
interface ListProps<T> {
  items: T[]
  keyField: keyof T
  renderItem?: (item: T) => any
}

// 使用泛型定义 props
function defineListProps<T>() {
  return defineProps<ListProps<T>>()
}

// 在具体组件中使用
interface User {
  id: number
  name: string
  email: string
}

// 为 User 类型特化组件
const props = defineListProps<User>()

这种泛型组件的方式在组件库开发中特别有用,它提供了极大的灵活性,同时保持了类型安全。

在处理异步操作时,我们通常需要定义加载状态和错误处理:

// 异步操作组件的完整类型定义
interface AsyncProps {
  data?: any
  loading?: boolean
  error?: string | null
}

interface AsyncEmits {
  retry: []
  reload: [force?: boolean]
  success: [data: any]
}

const props = defineProps<AsyncProps>()
const emit = defineEmits<AsyncEmits>()

// 在异步操作完成时触发事件
async function fetchData() {
  try {
    const result = await api.fetch()
    emit('success', result)
  } catch (error) {
    // 错误处理
  }
}

常见陷阱与解决方案

虽然 defineProps 和 defineEmits 很强大,但在使用过程中还是有一些需要注意的地方。

一个常见的错误是试图在运行时访问类型信息:

// 错误的做法:试图在运行时使用类型
const props = defineProps<{
  count: number
}>()

// 这在运行时是 undefined,因为类型信息在编译时就被移除了
console.log(props.count.type) // undefined

// 正确的做法:使用运行时声明
const props = defineProps({
  count: {
    type: Number,
    required: true
  }
})

另一个陷阱是关于可选参数的处理:

// 定义带有可选参数的事件
const emit = defineEmits<{
  // 第二个参数是可选的
  search: [query: string, options?: SearchOptions]
}>()

// 使用时要注意参数顺序
function handleSearch(query: string) {
  // 可以只传递必填参数
  emit('search', query)
}

function handleAdvancedSearch(query: string, options: SearchOptions) {
  // 也可以传递所有参数
  emit('search', query, options)
}

在处理复杂的嵌套对象时,类型定义可能会变得冗长:

// 使用类型别名简化复杂类型
type UserProfile = {
  personal: {
    name: string
    age: number
  }
  preferences: {
    theme: 'light' | 'dark'
    language: string
  }
}

const props = defineProps<{
  profile: UserProfile
}>()

// 这样既保持了类型安全,又让代码更清晰

与其它 Composition API 的配合

defineProps 和 defineEmits 可以很好地与 Vue 3 的其它 Composition API 配合使用,创造出强大的组合逻辑。

比如与 provide/inject 的配合:

// 父组件提供数据
const props = defineProps<{
  theme: 'light' | 'dark'
  locale: string
}>()

// 基于 props 提供全局配置
provide('appConfig', {
  theme: props.theme,
  locale: props.locale
})

// 子组件注入并使用
const config = inject('appConfig')

与 watch 和 computed 的配合:

const props = defineProps<{
  items: any[]
  filter: string
}>()

const emit = defineEmits<{
  filtered: [results: any[]]
}>()

// 监听 props 变化并触发事件
watch(() => props.filter, (newFilter) => {
  const filtered = filterItems(props.items, newFilter)
  emit('filtered', filtered)
})

// 基于 props 计算衍生数据
const sortedItems = computed(() => {
  return props.items.sort(sortFunction)
})

性能优化与最佳实践

虽然类型安全很重要,但我们也要注意性能影响。以下是一些优化建议:

对于大型对象,考虑使用浅层响应式:

const props = defineProps<{
  // 对于大型配置对象,使用 shallowRef 避免不必要的响应式开销
  config: AppConfig
  // 对于频繁变化的数据,保持深度响应式
  items: any[]
}>()

合理使用 PropType 进行复杂类型验证:

import type { PropType } from 'vue'

const props = defineProps({
  // 使用 PropType 进行运行时类型验证
  complexData: {
    type: Object as PropType<ComplexData>,
    required: true,
    validator: (value: ComplexData) => {
      return validateComplexData(value)
    }
  }
})

总结

defineProps 和 defineEmits 是 Vue 3 与 TypeScript 完美结合的代表作。它们不仅提供了编译时的类型安全,还大大提升了开发体验。通过本文的学习,你应该能够在组件中正确定义类型安全的 props 和 emits,充分利用 TypeScript 的类型推导能力,处理各种复杂场景下的类型需求,避免常见的陷阱和错误。

昨天 — 2025年11月26日技术

大道至简-Shadcn/ui设计系统初体验(下):Theme与色彩系统实战

作者 ArkPppp
2025年11月26日 18:51

大道至简-Shadcn/ui设计系统初体验(下):Theme与色彩系统实战

前言

在上篇文章中,我们探讨了shadcn/ui的安装、组件引入和基础定制。本文将继续深入,关注一个更核心的话题——主题系统设计。作为前端工程师,我们都明白一个好的设计系统不仅要有美观的组件,更需要一套完整、可维护、可扩展的色彩体系。本文将通过实际项目实践,详细分析shadcn/ui如何通过CSS变量和TailwindCSS构建这套体系。

一、自定义主题配置:从CSS变量到TailwindCSS

1.1 shadcn/ui的主题系统原理

shadcn/ui采用了基于CSS自定义属性(CSS Variables)的设计模式。每个主题实际上就是一套CSS变量的集合。不同于传统组件库通过JavaScript动态计算颜色值,shadcn/ui选择在CSS层面定义好所有颜色状态,然后通过类名切换来实现主题变换。

这种设计的优势显而易见:

  • 无需JavaScript计算,避免频繁的重排重绘
  • 颜色值在构建阶段就已经确定,性能更好
  • CSS变量天然支持继承和级联,便于管理复杂的色彩体系

让我们查看项目的核心配置文件:

/* src/index.css */
@import "tailwindcss";

@custom-variant dark (&:is(.dark *));

@theme {
  --color-border: hsl(var(--border));
  --color-input: hsl(var(--input));
  --color-ring: hsl(var(--ring));
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  /* ... 更多颜色变量 */
}

注意这里使用TailwindCSS 4的新语法 @theme 替代了传统的 tailwind.config.js 配置。这种方式将主题配置直接内联到CSS文件中,更加直观。

1.2 主题色彩定义

shadcn/ui使用HSL色彩空间来定义颜色。HSL由色相(Hue)、饱和度(Saturation)、亮度(Lightness)三个分量组成,相比RGB更容易理解和调整。

我们项目的实际配色:

:root {
  /* 背景与前景色 */
  --background: 210 20% 96%;
  --foreground: 222 15% 15%;

  /* 主色调 - 浅蓝色系 */
  --primary: 205 85% 60%;
  --primary-foreground: 210 40% 98%;

  /* 次要色 - 浅绿色系 */
  --secondary: 145 65% 60%;
  --secondary-foreground: 222 15% 15%;

  /* 强调色 - 青绿色系 */
  --accent: 175 70% 55%;
  --accent-foreground: 210 40% 98%;
}

.dark {
  /* 深色主题配色 */
  --background: 210 15% 10%;
  --foreground: 210 15% 92%;
  --primary: 205 85% 65%;
  --secondary: 145 60% 65%;
  --accent: 175 65% 60%;
  /* ... */
}

配色方案的设计遵循以下原则:

  1. 语义化命名:每个颜色都有明确的语义(background、primary、secondary等)
  2. 状态配套:每个主要颜色都有对应的foreground色,保证可读性
  3. 明暗适配:深色模式下适当调整亮度和饱和度

1.3 扩展色系:成功与警告

除了标准的设计语言色彩,shadcn/ui还允许定义扩展色系,用于表达特定状态:

:root {
  --success: 145 60% 50%;
  --success-light: 145 65% 60%;
  --success-dark: 145 55% 45%;

  --warning: 45 85% 60%;
  --warning-light: 45 90% 65%;
  --warning-dark: 45 80% 55%;
}

这种命名方式(基础色-light-dark)为每个语义色提供了三个亮度级别,在实际开发中可以根据不同场景选择合适的深浅。

二、颜色系统设计:CSS变量与OKLCH色彩空间

2.1 CSS变量的高级特性

CSS自定义属性(CSS Variables)不仅仅是简单的键值对,它具备许多强大的特性:

1. 继承性

.card {
  background: hsl(var(--primary));
}

.card-header {
  /* 自动继承父元素的 --primary */
  color: hsl(var(--primary));
}

2. 动态计算

:root {
  --primary-light: 205 85% calc(60% + 10%);
}

3. 作用域控制

/* 全局作用域 */
:root {
  --global-primary: blue;
}

/* 局部作用域 */
.theme-dark {
  --local-primary: red;
}

这些特性使得CSS变量非常适合构建复杂的颜色系统。

2.2 OKLCH色彩空间:下一代色彩标准

传统的HSL色彩空间有一个明显缺陷:感知不均匀性。也就是说,在HSL中同样数值的变化,人眼感知的差异并不一致。例如,HSL中饱和度从50%到60%的变化,看起来比60%到70%的变化更明显。

OKLCH(Lightness-Chroma-Hue)色彩空间解决了这个问题。OKLCH是基于CIELAB色彩空间的现代色彩模型,具有以下优势:

  • 感知均匀:数值的微小变化对应人眼感知的微小变化
  • 色域更广:支持更多可见色彩
  • 对比度可控:更容易满足WCAG可访问性标准

虽然浏览器对OKLCH的支持还在逐步完善中,但TailwindCSS已经开始采用OKLCH。未来shadcn/ui很可能会迁移到OKLCH色彩空间。

2.3 构建语义化颜色系统

一个好的颜色系统需要避免直接使用底层色彩值,而是通过语义化变量来使用:

/* ❌ 不好的做法 - 直接使用底层颜色 */
.button {
  background: rgb(59, 130, 246);
}

/* ✅ 好的做法 - 使用语义化变量 */
.button {
  background: hsl(var(--primary));
}

这种设计的好处:

  1. 可维护性强:修改主题时只需更改CSS变量定义
  2. 一致性保证:全站使用统一的语义化色彩
  3. 灵活性高:可以针对不同区域覆盖特定变量

三、项目实践:TodoList的主题更新实现

3.1 ThemeProvider设计

shadcn/ui提供了一个独立的ThemeProvider实现,位于 src/components/theme-provider.tsx。这个实现替代了传统的next-themes,更轻量且完全基于原生Web API。

核心实现分析:

export function ThemeProvider({
  children,
  defaultTheme = 'system',
  storageKey = 'vite-ui-theme',
}: ThemeProviderProps) {
  const [theme, setTheme] = useState<Theme>(
    () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
  )

  useEffect(() => {
    const root = window.document.documentElement

    root.classList.remove('light', 'dark')

    if (theme === 'system') {
      const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
        .matches
        ? 'dark'
        : 'light'

      root.classList.add(systemTheme)
      return
    }

    root.classList.add(theme)
  }, [theme])

  const value = {
    theme,
    setTheme: (theme: Theme) => {
      localStorage.setItem(storageKey, theme)
      setTheme(theme)
    },
  }

  return (
    <ThemeProviderContext.Provider {...props} value={value}>
      {children}
    </ThemeProviderContext.Provider>
  )
}

关键点分析:

  1. 三种主题模式

    • light: 强制使用浅色主题
    • dark: 强制使用深色主题
    • system: 跟随系统设置
  2. 本地存储持久化 使用localStorage保存用户偏好,应用重启后自动恢复。

  3. 类名切换机制 通过操作documentElement的classList来切换主题,避免频繁的style重写。

3.2 TodoList中的主题切换按钮

在TodoList组件中,主题切换按钮的实现:

import { useTheme } from './theme-provider'

function TodoList() {
  const { theme, setTheme } = useTheme()

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={toggleTheme}
      aria-label="切换主题"
    >
      {theme === 'light' ? (
        <Moon className="h-4 w-4" />
      ) : (
        <Sun className="h-4 w-4" />
      )}
    </Button>
  )
}

注意这里的实现细节:

  • 使用aria-label提升可访问性
  • 根据当前主题显示对应图标(月亮/太阳)
  • variant设为ghost保持视觉简洁

3.3 主题变量的实际应用

在TodoList组件中,我们看到各种shadcn/ui组件都使用了语义化的颜色变量:

<div className="min-h-screen bg-background text-foreground transition-colors">
  <Card className="border-border">
    <CardHeader>
      <CardTitle className="bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent">
        待办事项列表
      </CardTitle>
    </CardHeader>
  </Card>
</div>

关键点:

  • bg-backgroundtext-foreground:使用语义变量确保文本可读性
  • border-border:边框颜色随主题变化
  • 渐变色使用CSS变量,保持主题一致性

四、shadcn/ui的设计哲学总结

通过上下两篇文章的分析,我们可以总结shadcn/ui的设计哲学:

4.1 零抽象成本

shadcn/ui不将组件封装为黑盒,而是提供完整源代码。这种"代码所有权"模式让开发者可以:

  • 任意修改组件实现
  • 深入理解组件逻辑
  • 无框架依赖,便于迁移

4.2 原子化设计

每个组件都是独立的、无样式基础的(headless),样式完全通过TailwindCSS类控制。这带来:

  • 样式完全可控
  • 避免CSS优先级冲突
  • 更好的Tree-shaking效果

4.3 设计令牌驱动

通过CSS变量系统,shadcn/ui建立了完整的设计令牌(Design Tokens)体系:

  • 颜色、字体、间距等都有对应的令牌
  • 令牌支持层级继承
  • 便于实现设计系统的一致性

4.4 可访问性优先

基于Radix UI构建,所有组件都具备:

  • 完整的键盘导航支持
  • 正确的ARIA属性
  • 语义化的HTML结构

4.5 现代化工具链

shadcn/ui深度集成了现代前端工具:

  • TailwindCSS 4(最新语法)
  • TypeScript(完整类型定义)
  • Vite(快速构建)
  • ESLint(代码规范)

5.成果展示

让我们看看最终的成果吧。

LightMode.png

NightMode.png

结语

shadcn/ui不仅仅是一个组件库,更是一套完整的设计系统实现方案。它通过CSS变量、TailwindCSS和现代React模式的结合,为我们提供了一种全新的组件库构建思路。

这种"大道至简"的设计理念——将复杂的UI抽象还原为简单的CSS变量和可组合的组件——或许正是前端开发的一种新范式。在AI编程工具日益成熟,Vibe Coding愈发普遍的今天,一个开放、可定制、无黑盒的组件库将更具生命力。


参考:

栈(Stack)详解:从原理到实现,再到括号匹配应用

作者 www_stdio
2025年11月26日 18:45

栈(Stack)详解:从原理到实现,再到括号匹配应用

栈是一种基础而重要的线性数据结构,在计算机科学中被广泛应用于函数调用、表达式求值、回溯算法等场景。本文将围绕栈的定义、抽象数据类型(ADT)、ES6 实现方式(数组与链表)、性能对比,以及一个经典应用场景——括号匹配问题,进行系统讲解。


一、什么是栈?

栈(Stack)是一种遵循 先进后出(First In Last Out, FILO) 原则的线性数据结构。你可以把它想象成一摞盘子:你只能从顶部放入(入栈)或取出(出栈)盘子,不能从中间或底部操作。

栈的核心操作包括:

  • push(x) :将元素 x 压入栈顶。
  • pop() :弹出并返回栈顶元素。
  • peek() / top() :查看栈顶元素但不移除。
  • isEmpty() :判断栈是否为空。
  • size() :获取栈中元素个数。

二、栈的抽象数据类型(ADT)

抽象数据类型(Abstract Data Type, ADT)是对数据结构行为的规范描述,不涉及具体实现。栈的 ADT 应包含以下属性和方法:

属性/方法 描述
size 只读属性,返回当前栈的大小
isEmpty() 判断栈是否为空
push(val) 入栈操作
pop() 出栈操作,若栈空则抛出异常
peek() 返回栈顶元素,若栈空则抛出异常
toArray() (可选)将栈内容转换为数组,便于调试或输出

三、ES6 Class 实现栈

ES6 引入了 class 语法,使面向对象编程更加清晰。结合私有字段(#)、get 访问器等特性,可以优雅地封装栈的实现细节。

1. 基于链表实现栈(LinkedListStack)

链表天然适合动态增长,每个节点包含值和指向下一个节点的指针。

class ListNode {
    constructor(val) {
        this.val = val;
        this.next = null;
    }
}

class LinkedListStack {
    #stackPeek = null; // 私有栈顶指针
    #size = 0;

    push(num) {
        const node = new ListNode(num);
        node.next = this.#stackPeek;
        this.#stackPeek = node;
        this.#size++;
    }

    pop() {
        if (!this.#stackPeek) throw new Error('栈为空');
        const val = this.#stackPeek.val;
        this.#stackPeek = this.#stackPeek.next;
        this.#size--;
        return val;
    }

    peek() {
        if (!this.#stackPeek) throw new Error('栈为空');
        return this.#stackPeek.val;
    }

    get size() { return this.#size; }
    isEmpty() { return this.#size === 0; }

    toArray() {
        let node = this.#stackPeek;
        const res = new Array(this.#size);
        for (let i = res.length - 1; i >= 0; i--) {
            res[i] = node.val;
            node = node.next;
        }
        return res;
    }
}

优点:动态扩容,无空间浪费;插入/删除均为 O(1)
缺点:每个节点需额外存储指针,内存开销略大


2. 基于数组实现栈(ArrayStack)

利用 JavaScript 数组的 pushpop 方法,可快速实现栈。

class ArrayStack {
    #stack = [];

    get size() { return this.#stack.length; }
    isEmpty() { return this.size === 0; }

    push(num) {
        this.#stack.push(num);
    }

    pop() {
        if (this.isEmpty()) throw new Error("栈为空");
        return this.#stack.pop();
    }

    peek() {
        if (this.isEmpty()) throw new Error("栈为空");
        return this.#stack[this.size - 1];
    }

    toArray() {
        return [...this.#stack]; // 返回副本更安全
    }
}

优点:缓存友好,访问快;代码简洁
缺点:扩容时需复制整个数组(O(n)),但均摊时间复杂度仍为 O(1)


四、数组 vs 链表实现栈:性能对比

维度 数组实现 链表实现
时间效率 平均 O(1),扩容时 O(n) 稳定 O(1)
空间效率 可能有预分配空间浪费 每个节点多一个指针开销
内存布局 连续内存,缓存命中率高 离散内存,缓存局部性差
适用场景 数据量可预估、追求速度 动态性强、内存敏感

💡 在大多数实际应用中,数组实现的栈更常用,因为其简单高效,且现代 JavaScript 引擎对数组优化极佳。


五、实战应用:括号匹配问题

栈的经典应用场景之一是验证括号字符串是否合法。例如:"([{}])" 合法,而 "([)]" 不合法。

解题思路:

  1. 遇到左括号 ([{,将其对应的右括号压入栈;
  2. 遇到右括号,检查是否与栈顶元素匹配;
  3. 若不匹配或栈为空,则非法;
  4. 遍历结束后,栈必须为空才合法。

代码实现:

const leftToRight = {
    "(": ")",
    "[": "]",
    "{": "}"
};

function isValid(s) {
    if (!s) return true;
    const stack = [];
    for (const ch of s) {
        if (ch in leftToRight) {
            stack.push(leftToRight[ch]); // 压入期望的右括号
        } else {
            if (!stack.length || stack.pop() !== ch) {
                return false; // 不匹配或栈空
            }
        }
    }
    return stack.length === 0; // 必须完全匹配
}

✅ 时间复杂度:O(n)
✅ 空间复杂度:O(n)(最坏情况全为左括号)


六、总结

  • 栈是一种 FILO 的线性结构,核心操作为 pushpoppeek
  • ES6 的 class、私有字段 #get 访问器,让栈的实现更安全、更清晰。
  • 数组实现简单高效,适合大多数场景;链表实现动态灵活,适合不确定规模的场景。
  • 栈在算法中用途广泛,如括号匹配、表达式求值、深度优先搜索(DFS)等。

掌握栈,不仅是理解数据结构的第一步,更是打开算法世界大门的钥匙。


📌 提示:在实际开发中,除非有特殊需求(如限制使用内置数组方法),否则直接使用 Arraypush/pop 即可高效模拟栈行为。

前端日常工作开发技巧汇总

2025年11月26日 18:41

一、JS篇

1. structuredClone 深拷贝

JavaScript 内置了一个 structuredClone() 的方法, 此方法提供了一种简单有效的方法来深度克隆对象,支持复杂数据类型,包括 DateRegExpMapSetArrayBufferBlobFile 等。浏览器底层实现,通常比手动递归或 JSON 方法更高效。

兼容性 image.png

2. 函数式编程

ES14 更新了许多数组方法或者为原有的数组方法增加不会带来突变(without mutation) 的互补方法。意味着它们会基于原数组创建新的数组,而不是直接修改原数组。

新增的互补方法有

  • Array.sort() -> Array.toSorted()
  • Array.splice() -> Array.toSpliced()
  • Array.reverse() -> Array.toReversed()

新增的新数组方法有:Array.with()Array.findLast()Array.findLastIndex()

  • Array.with()
    返回一个新数组,将原数组中指定索引 index 的元素替换为 value不修改原数组

语法
index:要替换的元素的索引(可以是负数,表示从末尾开始计数)。
value:替换后的新值

const newArray = array.with(index, value)

const arr = [1, 2, 3, 4];
const newArr = arr.with(1, "hello"); // 替换索引 1 的元素 

console.log(arr);    // [1, 2, 3, 4](原数组不变)
console.log(newArr); // [1, "hello", 3, 4](新数组)

// 支持负数索引(从末尾开始)
const newArr2 = arr.with(-2, "world"); // 替换倒数第 2 个元素
console.log(newArr2); // [1, 2, "world", 4]
  • Array.findLast()
    从数组末尾向前查找第一个满足 callback 条件的元素,并返回该元素。如果未找到,返回 undefined

  • Array.findLastIndex()
    从数组末尾向前查找第一个满足 callback 条件的元素,并返回其索引。如果未找到,返回 -1

3. 惰性函数

JavaScript 中的 惰性函数(Lazy Function) 是一种优化技术,其核心思想是:函数在第一次调用时执行一些初始化或判断逻辑,并在执行后将自身重定义为一个更高效或更简单的版本,后续调用就直接使用这个新版本,避免重复开销

普通写法

function copyToClipboard(text) {
    // 优先使用Clipboard API
    if (navigator.clipboard) {
      return navigator.clipboard
        .writeText(text)
        .then(() => {
          Message.success('复制成功')
          return true
        })
        .catch((err) => {
          console.error('使用Clipboard API复制失败: ', err)
          // 如果Clipboard API失败,尝试使用降级方案
          return copyUsingExecCommand(text)
        })
    } else {
      // 如果不支持Clipboard API,直接使用降级方案
      return copyUsingExecCommand(text)
    }
}

惰性写法

function copyToClipboard(text) {
  // 第一次调用时进行能力检测,并重定义自身
  if (navigator.clipboard) {
    // 支持 Clipboard API
    copyToClipboard = function (text) {
      return navigator.clipboard
        .writeText(text)
        .then(() => {
          console.log('文本已成功复制到剪贴板');
          return true;
        })
        .catch((err) => {
          console.error('Clipboard API 复制失败:', err);
          return false;
        });
    };
  } else {
    // 不支持 Clipboard API,使用 execCommand 降级方案
    copyToClipboard = function (text) {
      return copyUsingExecCommand(text);
    };
  }

  // 执行第一次调用
  return copyToClipboard(text);
}

二、CSS篇

1. 滚动吸附

<template>
  <div>
    <div class="container">
      <div class="item">1</div>
      <div class="item">2</div>
      <div class="item">3</div>
    </div>
  </div>
</template>

<script setup name="Snap"></script>

<style lang="scss" scoped>
.container {
  width: 100%;
  height: 300px;
  display: flex;
  overflow-x: scroll;
  // 吸附效果 mandatory: 必须吸附  proximity: 靠近时吸附
  scroll-snap-type: x mandatory;
  .item {
    flex-shrink: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 30px;
    color: #fff;
    background-color: #ccc;
    // 吸附位置
    scroll-snap-align: start;
    scroll-snap-stop: always;
    &:nth-child(1) {
      background-color: #f56c6c;
    }
    &:nth-child(2) {
      background-color: #67c23a;
    }
    &:nth-child(3) {
      background-color: #409eff;
    }
  }
}
</style>
兼容性较高

image.png

2. 字体自适应容器大小

<template>
  <div>
    <div class="container">
      <p>字体自适应容器大小</p>
    </div>
  </div>
</template>

<script setup name=""></script>

<style lang="scss" scoped>
.container {
  width: 500px;
  height: 300px;
  padding: 15px;
  resize: both;
  overflow: hidden;
  background-color: aquamarine;
  container-type: inline-size; // 启用容器查询 size:基于宽高 / inline-size 基于宽度 / normal 不启用
  p {
    font-size: 5cqh;
  }
}
</style>
兼容性还行

image.png

3. 选择器

  • 选择器特定性(通常叫做选择器权重)

当希望某个css属性优先级高于其他属性值时,尽量不要使用!important!important会打破这些固有的级联规则,使得样式的应用变得不那么可预测。这可能会导致样式表难以维护和理解,尤其是在大型项目中。增加调试难度,也限制了样式的灵活性。

替代方案: 通过编写更具体(或更精确)的选择器来覆盖样式,或者叠加选择器,比如:222

.el-button.el-button {
    color: red;
}
  • 新型选择器

:has()选择器: 根据一个元素是否包含某些特定的后代元素,或者其后的同级元素是否满足某些条件,来选中该元素本身。这实现了“向下”观察的能力。

示例1: 选择包含 <img><div>

<div>这个 div 没有图片,不会被选中</div>
<div>
    <img src="example.jpg" alt="示例图片">
    这个 div 包含图片,会被红色边框包围
</div>
/* 选择包含 <img> 的 div */ 
div:has(img) {
    border: 3px solid red; padding: 10px;
}

示例2: 选择紧跟着 <p><h2>

<h2>这个 h2 后面没有紧跟着 p,不会被选中</h2>
<div>分隔内容</div>
<h2>这个 h2 后面紧跟着 p</h2>
<p>这个 p 是 h2 的紧邻兄弟元素,因此 h2 会变成蓝色斜体</p>
/* 选择后面紧跟着 <p> 的 h2 */
h2:has(+ p) {
    color: blue; font-style: italic;
}

兼容性 image.png

:is()选择器: 它接受一个逗号分隔的选择器列表作为参数,并匹配其中任意一个选择器。这有助于减少冗余代码,提高可读性。:is() 的权重等于它括号里所有选择器中权重最高的那个。

示例:

<header>
    <h1>这是 header 的 h1(紫色)</h1>
</header>
<main>
    <h1>这是 main 的 h1(紫色)</h1>
</main>
<footer>
    <h1>这是 footer 的 h1(紫色)</h1>
</footer>
<section>
    <h1>这个 h1 不在 :is() 范围内,保持默认颜色</h1>
</section>
/* 统一设置 header、main、footer 下的 h1 样式 */
:is(header, main, footer) h1 {
    color: purple; font-family: Arial, sans-serif;
}

兼容性 image.png

:where()选择器: 与 :is() 类似,但权重永远为 0,适合默认样式。

兼容性 image.png

三、VUE篇

1. v-memo

Vue 3 提供的性能优化指令,其作用是通过缓存模板子树的渲染结果,仅在依赖项变化时重新渲染,从而减少不必要的虚拟 DOM 计算和更新操作。

v-memo 接收一个依赖数组,只有当数组中的值发生变化时才会重新渲染。

示例:优化大型列表渲染,避免全量更新。 当 item.id 或 item.status 变化时,仅更新对应项;其他项复用缓存结果

<div v-for="item in list" :key="item.id" v-memo="[item.id, item.status]">
  {{ item.content }}
  <StatusBadge :status="item.status" />
</div>

2. watch —— 副作用和深度监听

3. customRef ——— 自定义响应式依赖追踪

4. 组件的二次封装

四、Chrome浏览器调试技巧

1. $0

2. 模拟聚焦网页

3. 重放XHR

五、VSCode编辑器插件分享

1. i18n Ally

  • 代码内直接预览翻译文本
  • 快速生成初始翻译
  • 一键跳转至对应翻译条目
  • 集中管理
  "i18n-ally.localesPaths": ["./src/i18n/lang/locales"], // 翻译文件夹路径
  "i18n-ally.pathMatcher": "{locale}/**/{namespace}.json", // 翻译目标文件路径匹配
  "i18n-ally.keystyle": "nested", // 翻译路径格式,
  "i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言
  "i18n-ally.displayLanguage": "zh-CN", //显示语言, 这里也可以设置显示英文为en
  "i18n-ally.sortKeys": true, // 是否自动排序
  "i18n-ally.namespace": false, // 是否启用命名空间,一般在积攒多个待翻译文案时启用,可以自动编辑至对应文件中
  "i18n-ally.enabledParsers": ["ts", "js", "json"], // 翻译文件可允许的格式,默认json

2. koroFileHeader @4.9.2

用于生成文件头部注释和函数注释的插件

快捷键‌:

  • 头部注释:Ctrl+Win+I(Windows/Linux)或 Ctrl+Cmd+I(Mac)
  • 函数注释:Ctrl+Win+T(Windows/Linux)或 Ctrl+Cmd+T(Mac)
// 头部注释
"fileheader.customMade": {
"Author": "git config user.name && git config user.email", // 同时获取用户名与邮箱
"Date": "Do not edit", // 文件创建时间
"LastEditors": "git config user.name && git config user.email", // 文件最后编辑者 与Author字段一致
"LastEditTime": "Do not edit", // 文件最后编辑时间
"FilePath": "Do not edit", // 文件在项目中的相对路径 自动更新
"Description": "" // 文件描述
},

通义千问Qwen3-Coder模型帮我总结的typescript笔记

作者 炒米2333
2025年11月26日 18:31

简单学了学typescript,但是又觉得做笔记麻烦,于是找AI帮帮忙,提示词为“帮我总结typescript的常用知识点,并生成一篇markdown。” 生成了下面的内容。


TypeScript 常用知识点速查

TypeScript (TS) 是 JavaScript 的超集,添加了静态类型系统,有助于提高代码质量和开发效率。以下是其核心和常用的特性总结。

目录

  1. 基础类型
  2. 变量声明与类型注解
  3. 接口 (Interfaces)
  4. 类型别名 (Type Aliases)
  5. 联合类型 (Union Types) & 交叉类型 (Intersection Types)
  6. 数组与元组 (Tuples)
  7. 函数类型
  8. 类 (Classes)
  9. 泛型 (Generics)
  10. 模块 (Modules) 和 导入/导出
  11. 装饰器 (Decorators)
  12. 实用工具类型 (Utility Types)

基础类型

TypeScript 提供了一系列基础数据类型:

  • number: 数字类型 (整数或浮点数)。
  • string: 字符串类型。
  • boolean: 布尔值 (truefalse)。
  • nullundefined: 它们有自己的类型 nullundefined。但在 strictNullChecks 模式下(推荐),它们只能赋值给 any 和各自的类型;否则(非严格模式),它们是所有类型的子类型。
  • symbol: ES6 新增的原始数据类型,表示独一无二的值。
  • bigint: 大整数类型。
  • void: 通常用于表示没有返回值的函数的返回类型。
  • any: 允许任何类型的值,会跳过类型检查(不推荐滥用)。
  • unknown: 代表任何值。与 any 不同的是,在对 unknown 类型的值执行操作前,必须进行类型检查或类型断言。
  • never: 表示永不存在的值的类型。例如,总是抛出异常或根本不可能有返回值的函数表达式的返回类型。
  • object: 非原始类型(即除 number, string, boolean, symbol, null, undefined, bigint 之外的类型)。注意:它不同于 {}

变量声明与类型注解

使用 let, const, var 声明变量,并通过冒号 : 添加类型注解。

let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let u: undefined = undefined;
let n: null = null;

// 函数参数和返回值的类型注解
function add(x: number, y: number): number {
    return x + y;
}

接口 (Interfaces)

接口用于定义对象的结构(Shape),是一种契约。

interface Person {
    name: string;
    age: number;
    address?: string; // 可选属性
    readonly id: number; // 只读属性
}

const user: Person = {
    name: "Alice",
    age: 30,
    id: 1
};
// user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.

// 函数类型接口
interface SearchFunc {
    (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) { // 参数名不必与接口中定义的名字相匹配
    let result = src.search(sub);
    return result > -1;
}

类型别名 (Type Aliases)

类型别名为类型创建一个新的名称。它可以用于原始值、联合类型、元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

// 也可以像接口一样描述对象形状
type Point = {
    x: number;
    y: number;
};

// 类型别名可以使用交集和联合
type PartialPoint = { x: number; } | { y: number; };

接口 vs 类型别名:

  • 接口可以“打开”并扩展(Declaration Merging)。
  • 类型别名不能被重新打开以添加新的属性。
  • 接口通常用于定义对象的结构,而类型别名更通用。

联合类型 (Union Types) & 交叉类型 (Intersection Types)

  • 联合类型: 表示一个值可以是几种类型之一。使用 | 分隔每个类型。

    let value: string | number;
    value = "hello"; // OK
    value = 42;      // OK
    // value = true; // Error
    
  • 交叉类型: 将多个类型合并为一个类型。使用 & 连接。

    interface Colorful {
        color: string;
    }
    interface Circle {
        radius: number;
    }
    
    type ColorfulCircle = Colorful & Circle;
    
    const cc: ColorfulCircle = {
        color: "red",
        radius: 10
    }; // 必须同时满足 Colorful 和 Circle 的要求
    

数组与元组 (Tuples)

  • 数组: 存储相同类型的元素。

    let list1: number[] = [1, 2, 3];
    let list2: Array<number> = [1, 2, 3]; // 泛型语法
    
  • 元组: 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

    let tuple: [string, number] = ['hello', 10];
    // tuple[0] = 10; // Error: Type 'number' is not assignable to type 'string'.
    

函数类型

  • 函数声明/表达式中的类型注解:

    function greet(name: string): string {
        return "Hello, " + name;
    }
    
    const greeter = function(name: string): string {
         return "Hello, " + name;
    };
    
    const arrowGreeter = (name: string): string => {
        return "Hello, " + name;
    };
    
  • 可选参数和默认参数:

    function buildName(firstName: string, lastName?: string) { ... } // lastName 是可选的
    function buildNameWithDefault(firstName: string, lastName = "Smith") { ... } // 默认参数
    
  • 剩余参数:

    function buildNameRest(firstName: string, ...restOfName: string[]) {
        return firstName + " " + restOfName.join(" ");
    }
    

类 (Classes)

TypeScript 支持面向对象编程的类。

class Animal {
    name: string;
    private age: number; // 私有属性
    protected species: string; // 受保护的属性
    readonly legs: number = 4; // 只读属性

    constructor(theName: string, theAge: number, theSpecies: string) {
        this.name = theName;
        this.age = theAge;
        this.species = theSpecies;
    }

    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    breed: string;

    constructor(name: string, age: number, species: string, breed: string) {
        super(name, age, species); // 调用父类构造函数
        this.breed = breed;
    }

    bark() {
        console.log('Woof! Woof!');
    }

    move(distanceInMeters = 5) { // 重写父类方法
        console.log("Running...");
        super.move(distanceInMeters);
    }
}

泛型 (Generics)

泛型允许你编写可复用的组件,这些组件可以工作在多种类型之上。

// 泛型函数
function identity<T>(arg: T): T {
    return arg;
}
let output1 = identity<string>("myString"); // type argument specified
let output2 = identity("myString"); // type argument inferred

// 泛型接口
interface GenericIdentityFn<T> {
    (arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;

// 泛型类
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

// 泛型约束
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // Now we know it has a .length property, so no more error
    return arg;
}
// loggingIdentity(3); // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3}); // OK

模块 (Modules) 和 导入/导出

将代码组织到不同的文件中,并控制它们之间的可见性。

// math.ts
export const pi = 3.14;
export function add(x: number, y: number) {
    return x + y;
}
// 或者统一导出
// const pi = 3.14;
// function add(x: number, y: number) { return x + y; }
// export { pi, add };

// main.ts
import { pi, add } from './math';
console.log(pi); // 3.14
console.log(add(1, 2)); // 3

// 导入重命名
import { add as sum } from './math';

// 导入整个模块
import * as math from './math';
console.log(math.pi);

// 默认导出
// export default class Calculator { ... }
// import Calculator from './Calculator'; // 注意没有花括号 {}

装饰器 (Decorators)

一种特殊类型的声明,可以被附加到类声明,方法,访问符,属性或参数上。装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,该函数会在运行时被调用。

需要启用实验性支持 ("experimentalDecorators": true in tsconfig.json)。

// 类装饰器示例
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

实用工具类型 (Utility Types)

TypeScript 内置了一些非常有用的工具类型来简化常见的类型转换。

  • Partial<T>: 构造类型 T,并将它所有的属性设置为可选的。
  • Readonly<T>: 构造类型 T,并将它所有的属性设置为只读的。
  • Record<K, T>: 构造一个类型,其属性名的类型是 K,属性值的类型是 T
  • Pick<T, K>: 从类型 T 中选择出属性 K,构造成一个新的类型。
  • Omit<T, K>: 从类型 T 中排除掉属性 K,构造成一个新的类型。
  • Exclude<T, U>: 从 T 中排除掉那些可以赋值给 U 的类型。
  • Extract<T, U>: 从 T 中提取那些可以赋值给 U 的类型。
  • NonNullable<T>: 从 T 中排除 nullundefined
  • Parameters<T>: 获取函数类型的参数类型组成的元组类型。
  • ReturnType<T>: 获取函数类型的返回值类型。
  • InstanceType<T>: 获取构造函数类型的实例类型。
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>; // { title: string; completed: boolean; }

type TodoInfo = Omit<Todo, 'description'>; // { title: string; completed: boolean; }

type Status = 'pending' | 'fulfilled' | 'rejected';
type SyncStatus = Exclude<Status, 'pending'>; // 'fulfilled' | 'rejected'

function f1(s: string) { return { a: 1, b: s }; }
type F1Params = Parameters<typeof f1>; // [s: string]
type F1ReturnType = ReturnType<typeof f1>; // { a: number; b: string; }

这 5 个冷门的 HTML 标签,能让你少写 100 行 JS

作者 ErpanOmer
2025年11月25日 15:38

image.png

大家好!😁。

Code Review 的时候,我最怕看到什么?

不是复杂的算法,也不是什么正则。而是明明一个 HTML 标签就能搞定的事,有人非要写几百行 JS + CSS 去重新发明轮子

前几天,我看到一个新同学为了写一个折叠面板(Accordion),引入了一个重型的第三方库,还写了一堆 useStateonClick 和动画逻辑。

我默默地把他的代码全删了,换成了 3 行 <details>。他看我的眼神,仿佛在看一个外星人🤣。

在 2025 年的今天,浏览器原生 HTML 的能力早已今非昔比。很多我们习惯用 JS 去模拟的交互,现在不仅有原生支持,而且性能更好、兼容性更强、无障碍(a11y)更完善

今天,我就来盘点 5 个被严重低估的HTML标签👇。


<details> & <summary>:折叠组件

你是不是还在写这样的 React 代码?

// JS 模拟版
const [isOpen, setIsOpen] = useState(false);
return (
  <div className="accordion">
    <div className="header" onClick={() => setIsOpen(!isOpen)}>
      点击展开 {isOpen ? '⬆️' : '⬇️'}
    </div>
    {isOpen && <div className="content">...</div>}
  </div>
);

为了这个功能,你还得写 CSS 动画,还得处理键盘事件(Tab 键能不能选到?回车能不能展开?等等)。

HTML 原生写法:

<details>
  <summary>点击展开</summary>
  <div class="content">
    这里是展开后的内容,原生支持 Ctrl+F 页内搜索!
  </div>
</details>
  1. 没有任何JS:自带点击展开/收起交互。
  2. 无障碍(a11y)满分:屏幕阅读器能完美识别,Tab 键、回车键原生支持。
  3. 页内搜索:这是 JS 模拟版最大的痛点。如果内容被 JS 隐藏了(display: none),浏览器的 Ctrl+F 往往搜不到。但 <details> 里的内容,即使折叠,浏览器也能搜到并自动展开!

Untitled ‑ Made with FlexClip.gif

配合 CSS 👇

details {
  border: 1px solid #ccc;
  border-radius: 6px;
  padding: 8px;
}

summary {
  cursor: pointer;
  font-weight: bold;
}

/* 包住内容,让它能动画高度 */
details > .content {
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transition: max-height .45s ease, opacity .3s ease;
}

/* details 处于 open 状态时 */
details[open] > .content {
  max-height: 200px; /* 你内容高度大概多少设多少,足够大即可 */
  opacity: 1;
}

依然可以做动画。


<dialog>:弹窗组件

写模态框(Modal)是前端最大的坑之一。你需要考虑:

  • z-index 层级会不会被遮挡?
  • 点击遮罩层关闭?
  • Focus Trap(焦点锁定) :打开弹窗后,Tab 键不能跑到底层页面去。
  • 按下 Esc 键关闭?

为了解决这些,我们通常会引入 Antd Modal 或者 React Portal。但在轻量级场景下,原生 <dialog> 才是神🫅。

HTML 原生:

<dialog id="myModal">
  <form method="dialog">
    <p>这是一个原生模态框</p>
    <button>关闭(自动)</button>
  </form>
</dialog>

<button onclick="myModal.showModal()">打开弹窗⏏</button>

Untitled ‑ Made with FlexClip.gif

  1. Top Layer(顶层特性) :浏览器会把它渲染在所有 DOM 的最上层,彻底无视父元素的 z-indexoverflow: hidden
  2. ::backdrop 伪元素:直接用 CSS 定制遮罩层样式。
/* 背景遮罩 */
dialog::backdrop {
    background: rgba(0, 0, 0, 0.45);
    backdrop-filter: blur(3px);
    transition: opacity .3s ease;
}
  1. 原生交互:自带 Esc 关闭,自带焦点管理,表单提交自动关闭。

<datalist>:搜索自动补全

当产品经理要求做一个带搜索建议的输入框时,你的第一反应是不是:“快!引入 Select2 或者 Antd AutoComplete!😖

且慢。如果只是简单的建议列表,几 KB 的 JS 库都显得太重了。

HTML 原生版:

<label>选择你喜欢的框架:</label>
<input list="frameworks" />

<datalist id="frameworks">
  <option value="React">
  <option value="Vue">
  <option value="Svelte">
  <option value="Angular">
  <option value="Solid">
</datalist>
  1. 模糊搜索:浏览器原生支持模糊匹配(打 u 会出来 Vue)。
  2. 响应式:在手机上,它会调用系统原生的下拉选择 UI,体验比网页模拟的更顺滑。
  3. 解耦:它只是一个建议列表,用户依然可以输入列表里没有的值(这点和 Select 不同)。

<fieldset> & disabled:一键禁用整个表单

场景:用户点击提交按钮后,为了防止重复提交,我们需要禁用表单里的所有输入框

JS 笨办法:

// 还要一个个去拿 DOM,或者维护一个 loading 状态传给所有组件
inputs.forEach(input => input.disabled = true);
buttons.forEach(btn => btn.disabled = true);

HTML 原生写法:

<form>
  <fieldset disabled id="login-group">
    <legend>登录</legend>
    <input type="text" placeholder="用户名">
    <input type="password" placeholder="密码">
    <button>提交</button>
  </fieldset>
</form>

<script>
  // 一行代码搞定状态切换
  document.getElementById('login-group').disabled = true; 
</script>

clideo_editor_c3a7f45a392f482ea0added4098a5be3.gif

这是一个极好的分组思维。通过给 <fieldset> 设置 disabled,浏览器会自动禁用内部所有的 <input>, <select>, <button>。不用写循环,不用维护复杂的 State。


<input type="file" capture>:H5 调用手机相机

场景:业务需要用户上传一张照片,可以是相册选的,也可以是当场拍的

很多新手的反应是:是不是要接微信 JSSDK?是不是要写个 Bridge 调原生 App 能力?

答案是不需要!

HTML 原生:

<input type="file" capture="environment" accept="image/*">

只要加上 capture 属性,在移动端(iOS/Android)点击上传时,系统会直接拉起相机,而不是让你去选文件。拍完照后,你拿到的就是一个标准的 File 对象。

不需要什么 JS SDK,实现原生级体验👍。


我总是强调 最好的代码,是没有代码!

HTML 标准一直在进化,很多曾经需要重型 JS 才能实现的功能,现在已经成了浏览器的出厂设置了。

使用这些原生标签,不仅能减少打包体积,更能让你的应用在可访问性性能上天然领先。

下次再想 npm install 一个 UI 库之前,先查查 MDN。说不定,HTML 早就帮你做好了🤔。

UI小姐姐要求有“Duang~Duang”的效果怎么办?

作者 前端九哥
2025年11月25日 13:51

test.gif

设计小姐姐: “搞一下这样的回弹效果,你行不行?”
:“行!直接梭哈 50 行 keyframes + transform + 各种百分比,搞定 ”
设计小姐姐:“太硬(撇嘴),不够 Q 弹(鄙视)”
:(裂开)
隔壁老王:这么简单你都不行,我来一行贝塞尔 cubic-bezier(0.3, 1.15, 0.33, 1.57) 秒了😎
设计小姐姐:哇哦!(兴奋)好帅!(星星眼🌟)好Q弹!(一脸崇拜😍)
“???”


🧠 一、为什么一行贝塞尔就能“Duang”起来?

1️⃣ cubic-bezier 是什么?

在 CSS 动画里,我们经常写:

transition: all 0.5s ease;

但其实 easelinearease-in-out 这些都只是封装好的贝塞尔曲线。
底层原理是:

cubic-bezier(x1, y1, x2, y2)

这四个参数定义了时间函数曲线,控制动画速度的变化。

  • x:时间轴(必须在 0~1 之间)
  • y:数值轴(可以超出 0~1!)

👉 当 y 超过 1 或小于 0 时,动画值就会冲过终点再回弹
这就是“回弹感”的核心。


2️⃣ 回弹的本质:过冲 + 衰减

想象一个球掉下来:

  • 过冲:球落地时会压扁(超出终点)
  • 回弹:然后反弹回来,再逐渐稳定

在动画中,这个“过冲”就是 y>1 的部分,
而“回弹”就是曲线回到 y=1 的过程。


🧪 二、一行贝塞尔的魔法

✅ 火箭发射

export_1764044056566.gif

<div class="bounce">🚀发射!</div>

<style>
.bounce {
 transition: transform 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.bounce:hover {
 transform: translateY(-500px);
}
</style>

💡 参数解析:

  • y1 = -0.55 → 先轻微反向缩小
  • y2 = 1.55 → 再冲过头 55%,最后回弹到原位

🧩 四、常用贝塞尔参数

效果描述 贝塞尔参数 备注
微回弹(按钮) cubic-bezier(0.34, 1.31, 0.7, 1) 轻柔弹性
强回弹(卡片) cubic-bezier(0.68, -0.55, 0.27, 1.55) 爆发力强
柔和出入 cubic-bezier(0.4, 0, 0.2, 1.4) iOS 风
弹性放大 cubic-bezier(0.175, 0.885, 0.32, 1.275) 弹簧感
火箭猛冲 cubic-bezier(0.68, -0.55, 0.27, 1.55) 推背感

🧰 五、调试神器推荐

  • 🎨 cubic-bezier.com
    拖动手柄实时预览动画,复制参数一键搞定。

  • ⚙️ easings.net
    收录各种 easing 函数(含物理弹簧、阻尼等)。

本地系统、虚拟机、远程服务器三者之间的核心区别

作者 tomato_404
2025年11月26日 18:25

🌍 一句话总结

✅ 你之所以用 Tabby 连接公司服务器 不需要虚拟机,
是因为那台服务器 已经是一台运行着 Ubuntu 的独立电脑(远程机器)
而你要在自己的电脑上“拥有一个 Ubuntu 环境”,
就得自己“造出”这样一台虚拟的电脑——那就是虚拟机。


🧠 类比理解:虚拟机 vs 远程服务器

概念 是什么 在哪儿运行 你如何进入
💻 你自己的电脑(Windows) 物理机 桌子上的电脑 直接操作
🧱 虚拟机(VM) 一台“在你电脑里模拟出来的另一台电脑” 运行在你的物理机里 通过虚拟机软件打开
🖥️ 公司服务器(Ubuntu) 一台真实存在的远程电脑(在机房或云上) 运行在公司网络 用 SSH(如 Tabby)远程连接

所以区别就在于:

  • 虚拟机 = 在你电脑里“模拟出另一台电脑”
  • 服务器 = 已经存在的“另一台真实电脑”
  • SSH(Tabby) = 你用网线连过去远程操作那台电脑的“键盘和屏幕”。

🔍 更深入一点:Tabby 并不是运行 Ubuntu,而是在“远程登录”Ubuntu

当你运行:

ssh user@192.168.31.185

或用 Tabby 连接公司服务器时:

  • 你的电脑只是一个终端界面(相当于“显示器 + 键盘”);
  • 运行命令、文件系统、网络配置都发生在远程服务器上;
  • 所以你会看到那边的:
    • Ubuntu 系统
    • 已经安装好的 nginx、docker 等等;
    • 你只是在操作那台远程机器。

而如果你想在自己的电脑上也有一样的 Ubuntu 环境,
因为你手上这台机器运行的可能是 WindowsmacOS
那就得“虚拟”出一台 Ubuntu 系统的电脑,靠虚拟机实现。


🧩 对比一下你的两种情况:

场景 运行的系统 物理机器是谁 为什么能用 Ubuntu
① Tabby 连公司服务器 Ubuntu 服务器(192.168.31.185) 服务器已经装好了 Ubuntu
② 自己电脑(Windows) Windows 你自己的笔电 / PC 没有 Ubuntu,需要虚拟机或 WSL

⚙️ 那如果我想像公司服务器那样,本机也能跑 Ubuntu,有哪些选择?

方案 原理 是否需要虚拟机
VirtualBox / VMware 安装 Ubuntu 直接在 Windows 上开一台“虚拟电脑”装 Ubuntu ✅ 需要虚拟机
WSL2 (Windows Subsystem for Linux) 微软提供的轻量 Linux 子系统 ❌ 不需要传统虚拟机,推荐
Docker Desktop 它在底层用轻量虚拟化跑 Linux 内核 ⚙️ 自动管理虚拟层,无需你配置
买/租云服务器(远程主机) 直接连远程 Ubuntu ❌ 不需要虚拟机,本身就是服务器

🧭 你可以这样理解一条主线:

物理机器(你的电脑 或 公司服务器)
→ 上面运行系统(Windows 或 Ubuntu)
→ 系统里装环境(nginx、docker 等)

当你连接服务器时,
你是“借用公司那台 Ubuntu 机器的系统”;
当你在本机装虚拟机时,
你是在“自己电脑里再创建一台 Ubuntu 机器”。


🚀 推荐你目前最方便的做法(如果你是 Windows 用户)

👉 使用 WSL2 + Ubuntu 子系统
不需要虚拟机软件、不占太多资源、命令与公司服务器完全一致。

安装命令(管理员 PowerShell):

wsl --install

安装完成后:

wsl -l -v   # 查看 Ubuntu 是否安装成功
wsl         # 进入 Ubuntu 终端

现在你就在一个真正的 Ubuntu 环境里了,
可以执行:

sudo apt update
sudo apt install nginx docker.io -y

这时你本地环境的使用体验,就和你用 Tabby 连公司服务器非常接近。

❌
❌