Avalonia与WPF的差异及避坑指南 (持续更新)

文章目录

  • 核心差异
  • [Avalonia 避坑指南 (Avalonia for WPF Developers)](#Avalonia 避坑指南 (Avalonia for WPF Developers))
    • [1. 布局与测量系统](#1. 布局与测量系统)
      • [1.1.Margin 和 Padding](#1.1.Margin 和 Padding)
      • [1.2.Width/Height = "Auto"](#1.2.Width/Height = "Auto")
      • 1.3.StackPanel
      • 1.4.Bounds
    • [2. 数据绑定 (Data Binding)](#2. 数据绑定 (Data Binding))
    • [3. 样式与控件模板 (Styles & Control Templates)](#3. 样式与控件模板 (Styles & Control Templates))
      • 3.1.样式
      • [1. 类型选择器 (Type Selector)](#1. 类型选择器 (Type Selector))
      • [2. 类选择器 (Class Selector)](#2. 类选择器 (Class Selector))
      • [3. 子选择器 (Child Selector)](#3. 子选择器 (Child Selector))
      • [4. 后代选择器 (Descendant Selector)](#4. 后代选择器 (Descendant Selector))
      • [5. 属性选择器 (Property Selector)](#5. 属性选择器 (Property Selector))
      • [6. 伪类选择器 (Pseudo-class Selector)](#6. 伪类选择器 (Pseudo-class Selector))
      • [7. ID 选择器 (ID Selector)](#7. ID 选择器 (ID Selector))
      • [8. 复合选择器 (Compound Selector)](#8. 复合选择器 (Compound Selector))
      • [9. 通配符选择器 (Universal Selector)](#9. 通配符选择器 (Universal Selector))
      • [10. 否定选择器 (Not Selector)](#10. 否定选择器 (Not Selector))
      • [11. 相邻兄弟选择器 (Adjacent Sibling Selector)](#11. 相邻兄弟选择器 (Adjacent Sibling Selector))
      • [12. 通用兄弟选择器 (General Sibling Selector)](#12. 通用兄弟选择器 (General Sibling Selector))
      • [13. 属性包含选择器 (Attribute Contains Selector)](#13. 属性包含选择器 (Attribute Contains Selector))
      • [14. 属性开头选择器 (Attribute Starts With Selector)](#14. 属性开头选择器 (Attribute Starts With Selector))
      • [15. 属性结尾选择器 (Attribute Ends With Selector)](#15. 属性结尾选择器 (Attribute Ends With Selector))
      • [16. 嵌套样式选择器](#16. 嵌套样式选择器)
      • [17. 多样式选择器](#17. 多样式选择器)
      • [18. 模板选择器](#18. 模板选择器)
      • [19. 条件样式选择器](#19. 条件样式选择器)
      • [20. 派生类选择器](#20. 派生类选择器)
      • [21. 子元素位置 ControlType:nth-child(An+B)](#21. 子元素位置 ControlType:nth-child(An+B))
      • 功能符号
      • 其他
      • 3.2.控件模板
    • [4. 线程上的差异(Async)](#4. 线程上的差异(Async))
    • [5. 平台交互与依赖服务](#5. 平台交互与依赖服务)
    • [6. 内存泄漏 (Memory Leaks)](#6. 内存泄漏 (Memory Leaks))
    • [7. UI 虚拟化 (UI Virtualization)](#7. UI 虚拟化 (UI Virtualization))
    • [8. 命令 (Commands) 的使用](#8. 命令 (Commands) 的使用)
    • [9. 事件](#9. 事件)
    • 10.其他

核心差异

Avalonia 受 WPF 启发,API 设计非常相似,但它是为了跨平台而从头构建的。因此:

  • WPF: 深度集成于 Windows,依赖 DirectX、Windows 主题和 Win32。
  • Avalonia: 使用自绘引擎,抽象了平台细节,以实现对 Windows, macOS, Linux, 甚至 Web (WASM) 和移动端的支持。

类结构上的区别

  • Control -> TemplatedControl
    • WPF中的UIElement和FrameworkElement是非模板控件的基类,大致对应于Avalonia中的Control类。
    • WPF中的Control类则是模板控件,Avalonia中相应的类是TemplatedControl。
  • FrameworkElement -> Control
    • 在WPF/UWP中,从FrameworkElement类继承来创建新的自定义绘制控件,
    • 而在Avalonia中,该从Control继承。

类结构图上的区别


Avalonia架构


Avalonia 避坑指南 (Avalonia for WPF Developers)

从 WPF 转向 Avalonia,需要特别注意以下差异:

1. 布局与测量系统

1.1.Margin 和 Padding

MarginPadding的行为在复杂布局中可能与 WPF 有细微差别,可能导致控件"消失"或布局错乱。

1.2.Width/Height = "Auto"

axaml里面控件Width/Height默认值为Auto,如果强制设置为Auto,会报错

复制代码
Width="Auto"
Height="Auto"
Inner Exception 1:
Exception: Auto is not a valid value for Double.

Inner Exception 2:
FormatException: Input string was not in a correct format.

1.3.StackPanel

StackPanel并不会为每一个控件设置宽度或高度,而是优先控件 (WPF中不是这样):

Eg.

例如在StackPanel中放置若干个TextBlock,你会看到TextBlock延伸到了StackPanel外部,而并没有按照stackpanel的宽度来适配,会超过控件,可以

1.设置绑定: Width="{Binding Width , ElementName=MyStackPanel(是控件x:Name)}"

2.在后台遍历设置宽度:

1.4.Bounds

原WPF属性都可以在这里面Bounds找到:

如元素的真实高宽:Bounds.Height/Width

元素相对于父控件的位置:Bounds.Position.X/Y

2. 数据绑定 (Data Binding)

  • 坑: 绑定表达式和转换器的语法高度相似,但背后的机制和部分特性不同。
  • 避坑:
    • 绑定模式: Avalonia 的默认绑定模式是 Default,其行为取决于属性。对于可写属性,Default 通常意味着 TwoWay。显式指定绑定模式(如 {Binding Name, Mode=OneWay})是好习惯,可以避免意外双向更新。
    • 更新触发器: WPF 的 UpdateSourceTrigger=PropertyChanged 很常用。在 Avalonia 中,你需要为 TextBox 等控件显式设置 Text="{Binding Name, Mode=TwoWay}" 才能实现输入时实时更新。或者使用 TextBox.TextInput 事件。
    • ElementName 绑定: Avalonia 11 之前不支持直接的 ElementName 绑定。常用替代方案是使用 {Binding #elementName} 语法或通过 RelativeSource 查找。

3. 样式与控件模板 (Styles & Control Templates)

3.1.样式

WPF中的触发器TriggerDataTriggerEventTrigger 在 Avalonia 中被 Selectors 取代。

Avalonia没有像WPF中定义Style的Key值,然后引用Style这种方式,替换为Selectors

选择器,其语法更接近 CSS,与 WPF 的 Style.Triggers 差异很大。

复制代码
<!-- WPF -->
<Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
        <Setter Property="Background" Value="Red"/>
    </Trigger>
</Style.Triggers>

<!-- Avalonia -->
<Style Selector="Button:pointerover">
    <Setter Property="Background" Value="Red"/>
</Style>

在 Avalonia 中,样式选择器用于定义如何将样式应用于特定的控件。Avalonia 提供了多种样式选择器,类似于 WPF 和 CSS 中的选择器。以下是 Avalonia 中常用的样式选择器类型:

1. 类型选择器 (Type Selector)

根据控件的类型来选择应用样式。

复制代码
<Style Selector="Button">
    <Setter Property="Background" Value="Red"/>
</Style>

这将应用于所有的 Button 控件。

2. 类选择器 (Class Selector)

根据控件的 Classes 属性来选择应用样式。

复制代码
<Style Selector="Button.myClass">
    <Setter Property="Background" Value="Blue"/>
</Style>

<Button Classes="myClasses"/>

这将应用于所有具有 myClass 类的 Button 控件。

3. 子选择器 (Child Selector)

选择某个控件的子控件。

复制代码
<Style Selector="StackPanel > Button">
    <Setter Property="Background" Value="Green"/>
</Style>

这将应用于所有作为 StackPanel 直接子元素的 Button 控件。

4. 后代选择器 (Descendant Selector)

选择某个控件的后代控件。

复制代码
<Style Selector="StackPanel Button">
    <Setter Property="Background" Value="Yellow"/>
</Style>

这将应用于所有在 StackPanel 内的 Button 控件,无论它们嵌套多深。

5. 属性选择器 (Property Selector)

根据控件的属性值来选择应用样式。

复制代码
<Style Selector="Button[IsDefault=true]">
    <Setter Property="Background" Value="Orange"/>
</Style>

这将应用于所有 IsDefault 属性为 true 的 Button 控件。

6. 伪类选择器 (Pseudo-class Selector)

根据控件的状态来选择应用样式。

复制代码
<Style Selector="Button:pointerover">
    <Setter Property="Background" Value="Purple"/>
</Style>

这将应用于所有鼠标悬停时的 Button 控件。

7. ID 选择器 (ID Selector)

根据控件的 Name 属性来选择应用样式。

复制代码
<Style Selector="#myButton">
    <Setter Property="Background" Value="Pink"/>
</Style>

这将应用于 Name 为 myButton 的控件。

8. 复合选择器 (Compound Selector)

组合多个选择器来选择应用样式。

复制代码
<Style Selector="Button.myClass:pointerover">
    <Setter Property="Background" Value="Brown"/>
</Style>

这将应用于所有具有 myClass 类并且鼠标悬停时的 Button 控件。

9. 通配符选择器 (Universal Selector)

选择所有控件。

复制代码
<Style Selector="*">
    <Setter Property="Margin" Value="5"/>
</Style>

这将应用于所有控件。

10. 否定选择器 (Not Selector)

排除某些控件。

复制代码
<Style Selector="Button:not(.myClass)">
    <Setter Property="Background" Value="Gray"/>
</Style>

<Button Classes="myClass"/>
<Button Width="120" Height="40"/>

这将应用于所有不具有 myClass 类的 Button 控件。

11. 相邻兄弟选择器 (Adjacent Sibling Selector)

选择紧跟在某个控件后面的兄弟控件。

复制代码
<Style Selector="Button + TextBlock">
    <Setter Property="Foreground" Value="Red"/>
</Style>

这将应用于紧跟在 Button 后面的 TextBlock 控件。

12. 通用兄弟选择器 (General Sibling Selector)

选择某个控件后面的所有兄弟控件。

复制代码
<Style Selector="Button ~ TextBlock">
    <Setter Property="Foreground" Value="Blue"/>
</Style>

这将应用于所有在 Button 后面的 TextBlock 控件。

13. 属性包含选择器 (Attribute Contains Selector)

根据属性值是否包含某个字符串来选择应用样式。

复制代码
<Style Selector="Button[Content*= 'Save']">
    <Setter Property="Background" Value="Green"/>
</Style>

这将应用于 Content 属性包含 "Save" 的 Button 控件。

14. 属性开头选择器 (Attribute Starts With Selector)

根据属性值是否以某个字符串开头来选择应用样式。

复制代码
<Style Selector="Button[Content^= 'Save']">
    <Setter Property="Background" Value="Yellow"/>
</Style>

这将应用于 Content 属性以 "Save" 开头的 Button 控件。

15. 属性结尾选择器 (Attribute Ends With Selector)

根据属性值是否以某个字符串结尾来选择应用样式。

复制代码
<Style Selector="Button[Content$= 'Cancel']">
    <Setter Property="Background" Value="Red"/>
</Style>

16. 嵌套样式选择器

样式可以嵌套在其他样式中。要嵌套样式,只需将子样式作为父 <Style> 元素的子元素包含,并在子选择器的开头加上嵌套选择器 ^ (可以理解为外部样式的缩写)

复制代码
<Style Selector="TextBlock.h1">
    <Setter Property="FontSize" Value="24"/>
    <Setter Property="FontWeight" Value="Bold"/>

    <Style Selector="^:pointerover">
        <Setter Property="Foreground" Value="Red"/>
    </Style>
</Style>

等价于:

复制代码
<Style Selector="TextBlock.h1">
    <Setter Property="FontSize" Value="24"/>
    <Setter Property="FontWeight" Value="Bold"/>
</Style>
<Style Selector="TextBlock.h1:pointerover">
    <Setter Property="FontSize" Value="24"/>
    <Setter Property="FontWeight" Value="Bold"/>
    <Setter Property="Foreground" Value="Red"/>
</Style>

17. 多样式选择器

如果你想样式应用于多个不同控件,可以用逗号分隔:ControlType1,ControlType2

复制代码
<Style Selector="TextBlock, Button">
  <Setter Property="Background" Value="Yellow"/>
</Style>

18. 模板选择器

复制代码
<StackPanel>
  <StackPanel.Styles>
    <Style Selector="Button:pressed /template/ ContentPresenter">
        <Setter Property="TextBlock.Foreground" Value="Red"/>
    </Style>
  </StackPanel.Styles>
  <Button>I will have red text when pressed.</Button>
</StackPanel>

19. 条件样式选择器

复制代码
<Style Selector="Button">
  <Setter Property="Foreground" Value="Blue"/>
</Style>
<Style Selector="Button.accent">
  <Setter Property="Foreground" Value="Red"/>
</Style>

<Button Classes.accent="{Binding IsSpecial}" />
// 如果我们想要取反,直接加!即可 !IsSpecial

20. 派生类选择器

is(ControlBase)

复制代码
<Style Selector=":is(Button)">
<Style Selector=":is(local|Button)">

// 派生自Control,且Classes = "margin2"
<Style Selector=":is(Control).margin2">
<Style Selector=":is(local|Control.margin2)">

21. 子元素位置 ControlType:nth-child(An+B)

从前往后,第B个开始,步长为A,子元素位置与An+B结果相等时应用样式,计算结果小于1时不起作用,n=0,1,2,3...

复制代码
<Style Selector="TextBlock:nth-child(2n+3)">

与此相反,从末尾开始往前计算的也有:ControlType:nth-last-child(An+B)

复制代码
<Style Selector="TextBlock:nth-last-child(2n+3)">

直接指定位置:

复制代码
<Style Selector="TextBlock:nth-child(3)">

奇偶数:odd / even

复制代码
<Style Selector="TextBlock:nth-child(odd)">
<Style Selector="TextBlock:nth-child(even)">

功能符号

An+B 表示列表中的元素,其索引有An+B定义的自定义数字模式中的索引匹配,其中:

A 是整数步长

B 是整数偏移量

n 是所有非负整数,从0开始。

可以理解为从B开始的每个A th元素。

这里有一个在线测试网站,可以方便的观察CSS样式

在线测试::nth Tester

其他

如果控件不是Avalonia自带的,需要带上程序集名(xmlns名称) ,通过竖线 | 分开

复制代码
<Style Selector="controls|ImageRadioButton">
	<Setter Property="Foreground" Value="{DynamicResource B700}" />
	<Setter Property="Background" Value="{DynamicResource N950}" />
	<Setter Property="FontSize" Value="{DynamicResource BaseFontSize60}" />
	<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>

3.2.控件模板

  • 模板绑定: 在控件模板中,始终使用 {TemplateBinding} 而不是普通绑定来连接模板化父控件的属性。这是性能和安全性的最佳实践。
  • 主题与资源: 资源字典(ResourceDictionary)的概念类似。

4. 线程上的差异(Async)

在 UI 线程外操作 UI 控件会导致跨线程访问异常。

  • 使用 Dispatcher.UIThread.InvokeAsync: 在任何后台线程或异步方法中更新 UI 时,必须通过 Dispatcher 派回 UI 线程。

    await Task.Run(async () =>
    {
    var result = await HeavyWorkAsync();
    await Dispatcher.UIThread.InvokeAsync(() =>
    {
    MyTextBox.Text = result;
    });
    });

访问UI线程与WPF类似也是使用Dispatcher:

4.1.异步执行不等待Post

复制代码
Dispatcher.UIThread.Post(()=>LongRunningTask(), DispatcherPriority.Background);

Post 方法是 Avalonia.Threading.Dispatcher.UIThread.Post 的一个方法。它将一个操作(通常是一个 Action)添加到 UI 线程的任务队列中,然后立即返回,不返回任何 Task 或等待机制。

特点:

  • "即发即忘": Post 不会返回 Task,因此你无法用 await 来等待它完成。它只是把任务扔给了 UI 线程,然后就"不管了"。
  • 非阻塞: 它永远不会阻塞调用线程。
  • 返回值: 它不返回任何东西。

使用场景:

当你只想在 UI 线程上执行一个操作,并且 不需要知道它什么时候完成,也不需要获取它的结果 时,Post 非常适合。例如,一个后台任务运行完毕后,只想更新一下 UI 上的状态文本,但并不关心这个更新动作本身是否成功或何时完成。

4.2.异步执行可等待InvokeAsync

复制代码
await Dispatcher.UIThread.InvokeAsync(()=>LongRunningTask(), DispatcherPriority.Background);

InvokeAsync 方法是 Avalonia.Threading.Dispatcher.UIThread.InvokeAsync 的一个方法。它将一个操作添加到 UI 线程的任务队列中,并返回一个 Task。

特点:

  • 可等待: 因为它返回一个 Task,你可以使用 await 来等待 UI 线程上的操作完成,这使得它非常适合于异步编程模型。
  • 非阻塞 : await 使得调用线程在等待时不会被阻塞。
  • 可返回值: 如果你调用的操作返回一个值,InvokeAsync 可以返回一个 Task,让你获取到 UI 线程上的操作结果。

使用场景:

当你需要 等待 UI 线程上的操作完成,或者 需要获取 UI 线程上操作的返回值 时,InvokeAsync 是更好的选择。这在许多异步流程中非常常见。

5. 平台交互与依赖服务

  • 无法直接使用 Microsoft.Win32 等 Windows 特有的命名空间来打开文件对话框或访问注册表。
  • 避坑:
    • 使用 Avalonia 的抽象: 使用 TopLevel.GetTopLevel(this).StorageProvider 来异步打开文件对话框(这是跨平台的)。
    • 依赖注入: 对于平台特定的功能(如通知、系统信息),创建接口,并在平台相关的项目中实现它们,然后在 App 启动时通过 DI 容器注册和解析。

6. 内存泄漏 (Memory Leaks)

  • 坑: 忘记注销事件订阅、长时间存在的静态对象持有视图的引用等,会导致视图和ViewModel无法被垃圾回收。
  • 避坑:
    • 事件订阅: 如果某个对象生命周期比事件发布者长,记得在适当时候(如 Unloaded 事件中)使用 = 取消订阅。
    • 静态事件: 谨慎订阅静态事件,如果不注销,一定会泄漏。

7. UI 虚拟化 (UI Virtualization)

  • 坑: 在 StackPanelCanvas 中放置大量数据项,会导致所有项一次性渲染,造成UI卡顿和内存激增。Avalonia 的 ItemsControl 默认是虚拟化的

8. 命令 (Commands) 的使用

  • 坑: ICommandCanExecute 状态不会自动通知更新。
  • 避坑:
    • 手动通知: 在使用 RelayCommandDelegateCommand 时,在条件改变后手动调用 RaiseCanExecuteChanged() 方法。
    • 使用响应式编程: 可以使用 ReactiveCommand,它能够自动根据 IObservable<bool> 来管理 CanExecute 状态。

**8.1.**直接绑定到方法

Avalonia可以直接绑定到方法上而无需创建ICommand:

复制代码
<Window xmlns="https:///avaloniaui">
   ...
   <StackPanel Margin="20">
      <Button Command="{Binding DoTest}"
              CommandParameter="test">
              Run the example</Button>
   </StackPanel>
</Window>

namespace AvaloniaDemo.ViewModels
{
    public class MainWindowViewModel
    {
        public void DoTest(object msg)
        {
            Debug.WriteLine($"The action was called. {msg}");
        }
		//表示是否可执行
        public bool CanDoTest(object msg)
        {
            if (msg!=null) return !string.IsNullOrWhiteSpace( msg.ToString() );
            return false;
        }
    }
}

Avalonia默认会在绑定的方法上加上Can,用来查找是否定义了可执行方法。

9. 事件

官方文档:http://avaloniaui.net/docs/input/events

9.1.无法自动生成

1.直接在axaml中定义事件有时候不会成功,可以在后台中定义,例如:

复制代码
<Button x:Name="btn" Click="Btn_Click">Click Me</Button>
private void Btn_Click(object sender, RoutedEventArgs args)
{
   //...
}

若不成功可以:

复制代码
this.Get<Button>("btn").Click+=Btn_Click;

9.2.事件的更替

WPF中的MouseDown/Up事件和Preview事件

Avalonia中将Preview替换成了 Tapped 事件

PointerPressed ---> MouseDown

PointerReleased ---> MouseUp

为控件注册一个Preview事件:

复制代码
你的控件名.AddHandler(PointerReleasedEvent,事件名称, RoutingStrategies.Tunnel);

10.其他

10.1. 调试模式

Avalonia不支持热重载,如需热重载需要添加相应Nuget包,但对于大型项目不是很友好。

在主窗口对应axaml.cs里添加如下代码,在Debug模式下运行调试,进入程序后,F12进入调试模式

复制代码
#if DEBUG
        this.AttachDevTools();
#endif

开启Avalonia自带开发者工具Avalonia DevTools调试

10.2. 加载图像

在加载图像资源的时候,Avalonia没有WPF中处理图像的BitmapImage,Avalonia 并未内置可以直接添加到 ResourceDictionary 中的 Bitmap 对象。而且如果你尝试直接将 Avalonia 资源路径(以 avares:// 开头的路径)绑定给 Image 控件的 Source 属性,会发现图片无法正常显示,故原先加载图像资源的方式有所改变

复制代码
public class AssetBitmap(string url) : Bitmap(AssetLoader.Open(new Uri(url)));

自定义类,该类型派生自 Bitmap,并向外暴露一个构造函数。接收一个 Avalonia 资源路径用于初始化位图。

P.S.

对于Avalonia的样式资源,这里生成操作统一变为AvaloniaResource

10.3. Prism容器变化

由于原先采用的Unity容器,底层基于Prism.WPF,Avalonia无法使用,所以采用DryIoc容器。

基于Prism为Avalonia版本定制的框架(这里采用9.0.537.11130版本)

10.4. Prism区域加载方式

Prism 区域发现(Region Discovery)的机制在 Avalonia 中没有自动生效

在 WPF 中,能够自动扫描 XAML 树中的 prism:RegionManager.RegionName 属性,并创建相应的区域。但在 Avalonia 中,Prism 无法利用 Avalonia 依赖属性系统的特性来实现自动发现。

所以原先区域导航这个方法无法使用

复制代码
_regionManager.RequestNavigate(RegionName, ViewUri);

更改为

复制代码
_regionManager.RegisterViewWithRegion(RegionName, Type.GetType(ViewUri));

10.4 axaml定义,后台设定值

在WPF中,当你在xaml文件中定义完UI并设置x:Name就可以在后台中直接使用对象名称进行操作.

那是因为vs在你设计时自动生成了.g.i.cs文件(你可以在/obj中看到)

而avalonia中不同,你需要在后台中自己Get到这个UI对象(与Android类似): 例如:

axaml中定义一个名称为 TB_Title的TextBlock文本标签:

复制代码
<TextBlock x:Name="tb_test"
           HorizontalAlignment="Center"
           Foreground="White"
           FontSize="14" />

在cs中定义并更改标签内容:

复制代码
TextBlock tb_test= this.Get<TextBlock>("tb_test");
TB_Title.Text = "test";

10.5.初始化相关设置

由于Avalonia使用Prism容器为DryIoc,所以在App继承的类更改为PrismApplication,对应引用命名空间为

复制代码
using Prism.DryIoc;

需要重写CreateContainerExtension方法进行初始化容器

复制代码
protected override IContainerExtension CreateContainerExtension()
{
    var extension = new DryIocContainerExtension();
    ServiceLocator.SetLocatorProvider(() => new DryIocServiceLocator(extension.Instance));
    return extension;
}

同样对于Prism加载模块可以使用原先读取配置文件的形式,目前工程里采用的为AddModule的形式

复制代码
moduleCatalog.AddModule

10.6.IsVisible属性

Avalonia 并没有直接的 Visibility 枚举,而是使用 bool 类型的 IsVisible 属性

10.7.URI

这是最核心的区别。WPF 使用 pack:// 方案,而 Avalonia 引入了自己的一套方案。

  • WPF:
    • 使用 pack://application:,/ 方案来访问应用程序内部的资源。
  • Avalonia
    • 主要使用 avares:// 方案来访问嵌入式资源。

10.8.控件相关

Avalonia中CalendarDatePicker代替WPF中的DatePicker

DatePicker ---> CalendarDatePicker

Avalonia中没有MessageBox控件,需自己实现或者引用第三方开源组件

相关推荐
阿蔹4 分钟前
UI测试自动化-Web-Python-Selenium-2-元素操作、浏览器操作
前端·python·selenium·ui·自动化
谎言西西里23 分钟前
React hooks 之 一篇文章掌握 useState 和 useEffect 的核心机制
前端·react.js
Apifox.28 分钟前
Apifox 12 月更新| AI 生成用例同步生成测试数据、接口文档完整性检测、设计 SSE 流式接口、从 Git 仓库导入数据
前端·人工智能·git·ai·postman·团队开发
bjzhang7532 分钟前
使用 HTML + JavaScript 实现滑动验证码
前端·javascript·html
Fighting_p1 小时前
【预览word文档】使用插件 docx-preview 预览线上 word 文档
开发语言·c#·word
行走的陀螺仪1 小时前
使用uniapp,实现根据时间倒计时执行进度条变化
前端·javascript·uni-app·vue2·h5
科技D人生1 小时前
Vue.js 学习总结(19)—— Vue3 按钮防重复点击三种方案总结
前端·vue.js·uniapp·vue3 防重复提交·uniapp 防重复提交·前端防抖
指尖跳动的光1 小时前
前端视角-如何保证系统稳定性
前端
fruge1 小时前
2025全栈技术深耕与实践:从框架融合到工程落地
前端
秋4271 小时前
tomcat与web服务器
服务器·前端·tomcat