工具网站

常用工具

序号 名称 说明
1 即使设计 国内的UI设计,其他在线PS等网站很垃圾
2 转换工具 支持图像Ico(点击下载按钮无法下载时,双击图片名称,将图片另存为即可),电子书,视频,声音等,少量广告
3 Z-Library 拷贝到设备Kindle(已经不支持邮箱发送到设备Kindle),支持 azw3 格式,不支持 EPUB 格式
4 电子书格式转换工具-calibre 电子书制作、格式转换工具
5 音流(音乐播放器) 音流是一款 NAS 音乐播放器,可以连接到您部署在 NAS 上的音乐服务1,能让您在不同平台2享受一致的音乐盛宴。跨平台支持 可在 Android、iOS、macOS 和 Windows 上使用,并提供与平台一致的体验。

关注的文章

  1. 杂烩饭:一个搞运维的博主,内容:系统/容器/数据库等

CSharp

项目设置

应用程序设置(Settings.settings)

在WPF、Winform、Web项目中都有应用程序设置,以下以WPF为例。

创建和打开配置文件

1722478718891

配置文件设计窗口

1722478766425

  • 名称:变量名
  • 类型:字符串、整数、布尔值、集合等。
  • 范围:应用程序和用户
  • :默认值

程序发布后,设置的数据存储在:程序集.exe.config 文件中,如你的程序集为 MyTool,对应的文件名为:MyTool.exe.config,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    //...
    <userSettings>
        <MyTool.Properties.Settings>
            <setting name="Number" serializeAs="String">
            <value>1</value>
            </setting>    
        </MyTool.Properties.Settings>
    </userSettings>
    <applicationSettings>
    <MyTool.Properties.Settings>
        <setting name="Count" serializeAs="String">
        <value>10</value>
        </setting>   
    </MyTool.Properties.Settings>
    </applicationSettings>
</configuration>

分别设置了用户范围的变量 Number,和应用程序范围的变量 Count注意这里面修改用户设置的数据无效,但是可以修改应用程序的设置

用户范围设置:默认情况下,用户特定的设置保存在用户配置文件目录下的 user.config 文件中。这个文件的位置因操作系统不同而有所不同,一般情况下会在以下路径中找到:

Windows 7/8/10: C:\Users\<用户名>\AppData\Local\<公司名>\<应用程序名>\<版本号>\user.config

Windows XP: C:\Documents and Settings\<用户名>\Local Settings\Application Data\<公司名>\<应用程序名>\<版本号>\user.config

❕ 注:公司名通过项目属性查看。

⚠️ <span style="color:red;font-weight:bold;">注:修改程序根目录下的 “MyTool.exe.config” 配置文件无效

应用程序范围设置:如果设置是应用程序范围的(而不是用户特定的),那么它们会保存在应用程序的配置文件中,通常是 App.config(程序集.exe.config)Web.config(对于 Web 应用程序)。不过,这些文件中的设置是只读的,运行时的修改不会保存回这些文件。

修改设置


// 读取设置值
int number = Properties.Settings.Default.Number;

// 修改设置值
Properties.Settings.Default.Number = 2;

// 保存设置值
Properties.Settings.Default.Save();

保存后,新的设置值会写入 user.config 文件中,下次启动应用程序时会读取这个值。

总结

对比 应用程序范围 用户范围
存储位置 程序根目录:App.config(程序集.exe.config)Web.config(对于 Web 应用程序) C:\Users\<用户名>\AppData\Local\<公司名>\<应用程序名>\<版本号>\user.config
修改 App.config 文件有效 ✔️
可读 ✔️ ✔️
可写 ✔️

使用场景

动态配置数据,如用户偏好、应用程序配置参数等。

应用程序资源(Resources.resx)

用途:管理应用程序的静态资源和多语言资源。

存储位置:通常定义在 .resx 文件中,这些文件可以包含字符串、图像、图标、文件等资源。

类型:可以包含各种类型的资源,如字符串、图像、图标、文件等。

使用场景

  • 存储应用程序的静态文本,支持多语言本地化。
  • 存储应用程序所需的图像、图标等静态资源。
  • 组织和管理资源文件,方便维护和更新。

.resx 文件示例

定义资源

你可以在 Visual Studio 中添加 .resx 文件并定义资源。例如,添加一个名为 Resources.resx 的资源文件,然后添加一些字符串资源:

  • Greeting -> Hello
  • Farewell -> Goodbye

生成资源类

Visual Studio 会自动生成一个与 .resx 文件对应的资源类,你可以在代码中使用这些资源。假设你的命名空间是 MyApp,你可以这样使用:

using MyApp.Properties;

// 读取资源值
string greeting = Resources.Greeting;//注:若文件名称为:Resources.xx.resx  那么读取时为:Resources_xx.Greeting
string farewell = Resources.Farewell;

多语言支持(本地化) 为了支持多语言,你可以创建多个 .resx 文件,每个文件对应一种语言。例如:

  • Resources.resx(默认资源文件)
  • Resources.en-US.resx(美国英语资源文件)
  • Resources.fr-FR.resx(法国法语资源文件)

在不同语言的资源文件中,定义相同的键但值为对应语言的翻译。

运行时切换语言 通过更改应用程序的文化信息,可以在运行时切换语言:

using System.Globalization;
using System.Threading;
using MyApp.Properties;

// 切换到法国法语
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr-FR");

// 读取资源值
string greeting = Resources.Greeting; // 将会返回法语的问候语

应用程序资源与设置的区别总结

应用程序资源(Resources):

  • 用途:管理静态资源和支持多语言。
  • 存储位置:.resx 文件。
  • 类型:字符串、图像、图标、文件等。
  • 使用场景:静态文本、图像、图标、多语言支持等。

应用程序设置(Settings):

  • 用途:存储和管理应用程序配置数据和用户设置。
  • 存储位置:配置文件(如 App.config 或 user.config)。
  • 类型:字符串、整数、布尔值、集合等。
  • 使用场景:动态配置数据,如用户偏好、应用程序配置参数等。
  • 通过理解这些区别,你可以更好地管理和组织 WPF 应用程序中的资源和设置,确保应用程序的灵活性和可维护性。

时间格式化

DateTime 格式化

dateTime.ToString("yyyy-MM-DD hh:mm:ss");

TimeSpan 格式化

常用:timeSpan.ToString(@"dd\.hh\:mm\:ss"); 或则 timeSpan.ToString("dd\\.hh\\:mm\\:ss");区别是否使用转义符号。

using System;

public class Example
{
   public static void Main()
   {
      TimeSpan duration = new TimeSpan(1, 12, 23, 62);

      string output = null;
      output = "Time of Travel: " + duration.ToString("%d") + " days";
      Console.WriteLine(output);
      output = "Time of Travel: " + duration.ToString(@"dd\.hh\:mm\:ss");
      Console.WriteLine(output);

      Console.WriteLine("Time of Travel: {0:%d} day(s)", duration);
      Console.WriteLine("Time of Travel: {0:dd\\.hh\\:mm\\:ss} days", duration);
   }
}
// The example displays the following output:
//       Time of Travel: 1 days
//       Time of Travel: 01.12:24:02
//       Time of Travel: 1 day(s)
//       Time of Travel: 01.12:24:02 days

Linq

多个属性具有相同处理逻辑

使用 Expression 来实现

Expression:将强类型lambda表达式表示为表达式树形式的数据结构。该类不能被继承,当一个对象存在多个相同类型的属性,且这些属性控制的功能相同时,可以使用 Expression 来实现

  • 有一组 Person, 需要遍历每个person,根据属性 A 或者 B,判断是说 Hello 还是 Bye
  • Person
    public class Person
    {
        public string Name { get; set; }
        public bool pA { get; set; }
        public bool pB { get; set; }
    }

  • 创建 Person List
    class ExpressionTest
    {
        List<Person> persons = new List<Person>();
        public ExpressionTest()
        {
            persons.Add(new Person { Name = "Jerry", pA = true, pB = false });
            persons.Add(new Person { Name = "Shin", pA = true, pB = false });
        }
        ...
    }

常规解法

  • 分别写两个方法,根据属性A 或者 属性B 来判断
  internal class ExpressionTest
    {
        List<Person> persons = new List<Person>();
        public ExpressionTest()
        {
            persons.Add(new Person { Name = "Jerry", pA = true, pB = false });
            persons.Add(new Person { Name = "Shin", pA = true, pB = false });
        }

        /* 需求: 遍历 persons, 根据 pA 或者 pB 的值,判断每个人是说 hello 还是 bye
         * 常规写法: 分别些两个方法遍历判断
         * Expression: 通过表达式实现通用写法
         * 
         */
        #region 常规写法

        private void SayHelloByA()
        {
            foreach (var p in persons)
            {
                if (p.pA)
                    Console.WriteLine($"[{p.Name}]\tSay Hello!");
                else
                    Console.WriteLine($"[{p.Name}]\tSay Bye!");
            }
           
        }
        private void SayHelloByB()
        {
            foreach (var p in persons)
            {
                if (p.pB)
                    Console.WriteLine($"[{p.Name}]\tSay Hello!");
                else
                    Console.WriteLine($"[{p.Name}]\tSay Bye!");
            }

        }

        #endregion
       // 测试
       public void Test()
        {
            SayHelloByA();// 输出: Say Hello!
            SayHelloByB();// 输出: Say Bye!
        }
    }
  • 存在的问题 : 如果我们有多个属性 C D E F,那么我们就需要写多少方法,但是发现这些方法的逻辑都是一样的。总感觉很别扭,此时就要用到 Expression

Expression 写法

 internal class ExpressionTest
    {
        List<Person> persons = new List<Person>();
        public ExpressionTest()
        {
            persons.Add(new Person { Name = "Jerry", pA = true, pB = false });
            persons.Add(new Person { Name = "Shin", pA = true, pB = false });
        }
       
        #region  Expression
        public void SayHello(Expression<Func<Person, bool>> expr)
        {
            var valProp = (PropertyInfo)(((MemberExpression)expr.Body).Member);
            foreach (var p in persons)
            {
                bool isShow = (bool)valProp.GetValue(p, null);
                if (isShow)
                    Console.WriteLine($"[{p.Name}]\tSay Hello!");
                else
                    Console.WriteLine($"[{p.Name}]\tSay Bye!");
            }
        }
        #endregion
       // 测试
       public void Test()
        {
            SayHello(x => x.pA);// 输出: Say Hello!
            SayHello(x => x.pB);// 输出: Say Bye!
        }
    }
  • 通过表达式,让调用者来确定条件的值。

泛型基类

无法创建显示类型的基类问题 解决方案:将泛型参数作为构造函数的参数,调用的函数中不要包含泛型参数,同时将函数提升为接口(新增接口),转换为面向接口编程

需求场景

有多个数据结构,它们继承同一个基类,需要创建不同的 'Handler' 来处理不同的数据结构。

基类:

public class BaseClass{
    public string Name{get;set;}
}

子类A

public class AClass:BaseClass{
}

子类B

public class BClass:BaseClass{
}

Handler base

public abstract class BaseHandler<T>{

    public abstract void DoSomthing(T data);
   

}

Handler A

public class AHandler:BaseHandler<AClass>{

    public override void DoSomthing(T data)
    {
       Console.WriteLine($"A handel {data.Name}");
    }
}

Handler B

public class BHandler:BaseHandler<BClass>{
     public override void DoSomthing(T data)
    {
          Console.WriteLine($"B handel {data.Name}");
    }
}

处理逻辑(失败)

public void Handler(T data)
{
    //这里无法这样声明,因为泛型需要显示的指定类型,然后如果显示的指定类型,就无法使用到达我们多态的需求
    BaseHandler? = null;
    if(typeof(T) == typeof(AClass))
        return new AHandler();
    if(typeof(T) == typeof(BClass))
        return new BHandler();
    BaseHandler.DoSomthing(data);

}

处理逻辑解读: 无法这样声明,因为泛型需要显示的指定类型,然后如果显示的指定类型,就无法使用到达我们多态的需求


解决办法

  • 将多态的概念转变为面向接口编程
  • 将调用的函数提升为接口,并且接口不要包含泛型参数
  • 将参数放在构造函数中初始化
  • 使用工厂创建处理器

接口定义

public interface IHandler{
    void DoSomthing();
}

处理器基类实现接口,并定义包含泛型参数的构造函数

public abstract class BaseHandler<T>:IHandler{

    public T Data{get;set;}
    public abstract void DoSomthing();
   

}

Handler A 新增 含泛型参数的构造函数

public class AHandler:BaseHandler<AClass>{

    public AHandler(AClass data)
    {
        this.Data = data;
    }
    public override void DoSomthing(T data)
    {
       Console.WriteLine($"A handel {Data.Name}");
    }
}

Handler B 新增 含泛型参数的构造函数

public class BHandler:BaseHandler<BClass>{
     public BHandler(BClass data)
    {
        this.Data = data;
    }
     public override void DoSomthing()
    {
          Console.WriteLine($"B handel {Data.Name}");
    }
}

增加创建工厂

public class HandlerFactory
    {
        public static IHandler CreateHandler<T>(T data) where T : BaseClass 
        {
            if (typeof(T) == typeof(AClass))
            {
                return new AHandler(data as AClass);
            }
            if (typeof(T) == typeof(FlowPsdSingelOpen))
            {
               return new BHandler(data as BClass);
            }
            throw new ArgumentException("HandlerFactory: CreateHandler: Unsupported type: " + data.GetType());
        }
    }

处理逻辑(使用接口)

public void Handler(T data)
{
    //使用工厂创建
    BaseHandler= HandlerFactory.CreateHandler(data);
    BaseHandler.DoSomthing();

}

代码混淆

使用Obfuscar对.NET开发的程序进行混淆

  • NUGET 添加包
  • 配置文件, <Module file="$(InPath)\ObfuscarTest.dll" 中指定混淆的 dll ,这里好像不能为 exe
<?xml version="1.0" encoding="UTF-8"?>
<Obfuscator>
	<Var name="InPath" value=".\bin\Release\net8.0" />
	<Var name="OutPath" value=".\bin\Release\net8.0\Obfuscar" />
	<Var name="KeepPublicApi" value="true" />
	<Var name="HidePrivateApi" value="true" />
	<Var name="HideStrings" value="true" />
	<Var name="UseUnicodeNames" value="true" />
	<Var name="ReuseNames" value="true" />
	<Var name="RenameFields" value="true" />
	<Var name="RegenerateDebugInfo" value="true" />
	<Module file="$(InPath)\ObfuscarTest.dll" />
	
</Obfuscator>
  • 配置文件的属性设置为内容,较新复制
  • 项目属性中,事件里面添加 生产后事件 "$(Obfuscar)" Obfuscar.xml
  • 注意在 Release 目录下 ../ Obfuscar 中的 dll 就是混淆后的
  • 将exe 和 其他文件拷贝过来即可。

.Net Reactor 加密

免费加密软件

官网

慧都上提供的手册

开源项目

  1. 一款基于.Net WinForm的节点编辑器:纯GDI+绘制 使用方式非常简洁 提供了丰富的属性以及事件 可以非常方便的完成节点之间数据的交互及通知 大量的虚函数供开发者重写具有很高的自由性。

Nuget

.nuget 迁移到D盘

C 盘瘦身

.nuget 默认在C盘,用于缓存我们使用过的包,随着项目越来越多,文件夹越来越大,占用C盘空间。

参考 把.nuget文件夹从C盘移到其它盘

  • 查看当前缓存路径 dotnet nuget locals all --listglobal-packages 指向的就是当前的路径
  • 关闭当前的所有VS
  • 打开 %AppData%\NuGet\NuGet.Config 文件,假设将 .nuget 移动到 D 盘的 Users 文件夹下,添加以下配置 <add key="globalPackagesFolder" value="D:\Users\.nuget\packages" />
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
   //...
  </packageSources>
  <packageRestore>
    <add key="enabled" value="True" />
    <add key="automatic" value="True" />
  </packageRestore>
  <bindingRedirects>
    <add key="skip" value="False" />
  </bindingRedirects>
  <packageManagement>
    <add key="format" value="0" />
    <add key="disabled" value="False" />
  </packageManagement>
  <config>
     <add key="globalPackagesFolder" value="D:\Users\.nuget\packages" />
  </config>
</configuration>
  • .nuget 移动到 D 盘的 Users 文件夹下
  • 再次查看当前缓存路径 dotnet nuget locals all --list,确认修改成功

相关文章

发布包

  • 可以直接将包发布到官方网站上
  • 自建 Nuget 服务器,参考:docker-bagetter

Messagepack

速度超快的序列化工具。

Union 使用

  • 当存在多层多态时,在最基类上标注所有的子类
  • 中间层为抽象层也需要使用Union
/*Union应用所有的实现类*/
[MessagePack.Union(0, typeof(Foo1))]
[MessagePack.Union(1, typeof(Foo2))]
[MessagePack.Union(2, typeof(Bar1))]
public interface Root { }

[MessagePackObject]
[MessagePack.Union(0, typeof(Foo1))] //中间层使用Union,否则报错
[MessagePack.Union(1, typeof(Foo2))] //中间层使用Union,否则报错
public abstract class FooRoot : Root
{
    [Key(0)] public string F1 { get; set; }

}

[MessagePackObject]
[MessagePack.Union(0, typeof(Bar1))] //中间层使用Union,否则报错
public abstract class BarRoot : Root { }

[MessagePackObject]

public class Foo1 : FooRoot { [Key(1)] public string Name { get; set; } }
[MessagePackObject]

public class Foo2 : FooRoot { [Key(1)] public string Name { get; set; } }


[MessagePackObject]
public class Bar1 : BarRoot { [Key(0)] public string Name { get; set; } }



public class Test
{

    public static void TestMessagepack()
    {
        List<Root> arr = new List<Root>();
        arr.Add(new Foo1() {  F1 = "f" , Name = " nam"});
        arr.Add(new Bar1() {  Name = "b"});

        var bin = MessagePackSerializer.Serialize(arr);
        var list = MessagePackSerializer.Deserialize<List<Root>>(bin);
        foreach (var item in list)
        {
            Console.WriteLine("00");
        }
    }

}

Mpc

进行AOT编译时,请仔细检查输出信息,是否有错误,这一步导致的错误,在后面无法跟踪查找

MemoryPack

使用方式见官网,此处仅记录使用中出现的问题

Cysharp发布了一个新的序列化器Cysharp/内存包,日文:memorypack:日文

MemoryPackOrder

没有必要不要使用,当存在继承关系时,中途修改某个字段的 Order 将导致编号错乱,不易维护

默认序列化是按照定义的属性或者字段的顺序来序列化的,如果要自定义属性或者字段的序列化顺序,可以使用 MemoryPackOrder

    /*
    使用 MemoryPackOrder
    */
    [MemoryPackable(generateType: GenerateType.VersionTolerant, SerializeLayout.Explicit)]
    public partial class TestData
    {
        [MemoryPack.MemoryPackOrder(1)]
        public int  Num { get; set; }
        [MemoryPack.MemoryPackOrder(0)]
        public string Name { get; set; }

        [MemoryPack.MemoryPackIgnore]
        public string NoSeriProp { get; set; }
    }
  • SerializeLayout.Explicit: 标识自定义布局
  • 先序列化 Name ,后序列化 Num

注意事项: 在 ASP.NET 中使用时(HttpClient 网络传输),上述的结构没问题,但是如果不使用 MemoryPackOrder 时会报错,后面找到原因是必须给标记了 MemoryPackIgnore 属性的字段一个默认值才正常

    /*
    不使用 MemoryPackOrder
    */
    [MemoryPackable]
    public partial class TestData
    {
        public int  Num { get; set; }
        public string Name { get; set; }

        [MemoryPack.MemoryPackIgnore]
        public string NoSeriProp { get; set; } = string.Empty; //给定默认值
    }

与 Asp.net Core 一起使用

基础

优点

  • 支持多态
  • 包小
  • 可以嵌套等
  • 支持AOT(for unity)

使用默认设置(还包括 OpenAPI)创建 ASP.NET Core Web API (.NET 6-8) 项目。除了安装 MemoryPack 之外,还包括 ASP.NET Core 的包。

PM> Install-Package MemoryPack.AspNetCoreMvcFormatter

如下所示更改 Program.cs 中的 AddControllers。

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new MemoryPackInputFormatter());
    options.OutputFormatters.Insert(0, new MemoryPackOutputFormatter(true));
});

将 WeatherForecast 类设为部分类并添加 MemoryPackable 属性。

[MemoryPackable]
public partial class WeatherForecast
{
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string? Summary { get; set; }
}

1718327837738

application/x-memorypack可以看到它已经被添加到 Media type 中了。

测试调用该方法

1718327868816

它作为 application/x-memorypack 返回。二进制数据按原样在响应正文中输出。 当然,如果将Media type改为JSON,也会返回可读的数据。

它可以很容易地转换成 MemoryPack。

顺便说一句,JSON 的主体大小约为 493 字节,MemoryPack 的主体大小约为 150 字节。

1718327894976

1718327901785

WeatherForecast 并不准确,因为数据是随机的,但它肯定更小。嗯,很自然,字段名字符串会随着记录数的减少而减少,所以如果有几十个字段,而且名称都很长,减少的效果会相当显着。

这是令人惊奇的,因为流经此过程的数据量减少了。

配置既可以接收Json和二进制文件

new MemoryPackOutputFormatter(true)

如果未使用 checkContentType=true 生成 OutputFormatter,MemoryPack 将接管所有输出处理,因此即使您指定 JSON,也会返回二进制文件,您会感到失望。

支持客户端TypeScript

服务端是 ASP.NET Core,客户端却是 React 或 Vue。 使用 GenerateTypeScript 特性,详见官网。

[MemoryPackable]
[GenerateTypeScript]
public partial class Person
{
    public required Guid Id { get; init; }
    public required int Age { get; init; }
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required DateTime DateOfBirth { get; init; }
    public required Gender Gender { get; init; }
    public required string[] Emails { get; init; }
}

public enum Gender
{
    Male, Female, Other
}

HttpClient 中解析异常

给标记MemoryPackIgnore的属性指定默认值

在使用 HttpClient 网络传输时,出现以下情况

  • 当使用 MemoryPackOrder 时,被标记 MemoryPackIgnore 的属性不给默认值,使用正常,如下:
    /*
    使用 MemoryPackOrder
    */
    [MemoryPackable(generateType: GenerateType.VersionTolerant, SerializeLayout.Explicit)]
    public partial class TestData
    {
        [MemoryPack.MemoryPackOrder(1)]
        public int  Num { get; set; }
        [MemoryPack.MemoryPackOrder(0)]
        public string Name { get; set; }

        [MemoryPack.MemoryPackIgnore]
        public string NoSeriProp { get; set; }
    }
  • 不使用 MemoryPackOrder 时,被标记 MemoryPackIgnore 的属性不给默认值,使用异常,解析错误,必须如下给定一个默认值:
    /*
    不使用 MemoryPackOrder
    */
    [MemoryPackable]
    public partial class TestData
    {
        public int  Num { get; set; }
        public string Name { get; set; }

        [MemoryPack.MemoryPackIgnore]
        public string NoSeriProp { get; set; } = string.Empty; //给定默认值
    }

List 属性异常

Ver 1.21.1

现象描述 :定义一个类 A,从服务器获取 List<A> 时正常,从客户端向服务器 Post A 时失败,(这里没有验证从服务器获取 单个 A 和从客户端向服务器Post List<A> 的情况)

查找问题步骤

  1. 若类中存在被标记为 MemoryPackIgnore 的属性,确定给其指定了默认值
  2. 逐步将属性设置为 null 进行测试,如果都还有问题进行下一步
  3. 从新构建一个类,将原类中所有的属性拷贝过来,先进行一次测试,若存在问题,下一步
  4. 在构建的新类中,逐步删除复杂结构(List,[],枚举,其他类或者数据结构)的属性进行测试

找到的原因 :在 A 中类型为 List<B> 的属性,当存在数据时就会异常(根本原因不清楚),设置为null时就正常

解决办法

  1. 看存在异常的属性在当前传中是否会使用到,如果未使用到将其设置为 null
  2. 重新构建一个类,仅包含需要使用到的属性

服务端收到的消息滞后一条

第一条消息服务端没收到,客户端发送第二条时,服务端收到上一条的消息?

原因:

使用了SSL,加密后出现上述问题,不使用就正常,未测试具体原因。

Excel插件-EPPlus

  1. 使用前需要设置证书声明

  2. 一个文件中的sheet,索引是从0开始的

  3. 表里面的内容,行和列的索引都是从1开始的

  4. 当new ExcelPackage 时,若没有文件会直接创建

  5. 在 wpf 和 unity 端都比较好用, unity 端可以用这个来读取配置的excel文件,转换为Unity 多序列化文件,或者 json ,注意exce只是用来配置文件,真正使用的还是json等通用的文件格式,因为excel 在有些平台不支持,或者说使用的插件不支持,且excel文件比较大,冗余数据多

代码:

public static List<string[]> ReadAllInfo(int hostId)
{
    ExcelPackage.LicenseContext = LicenseContext.NonCommercial;//声明非商业证书

    FileInfo fileInfo = new FileInfo(excelPath);
    if(File.Exists(fileInfo.FullName) == false)
    {
        return null;
    }
    using (ExcelPackage ep = new ExcelPackage(fileInfo))
    {
        List<string[]> listArr = new List<string[]>();
        ExcelWorksheet ipSheet = ep.Workbook.Worksheets[hostId];
        if (ipSheet.Cells[2, 1].Value == null) return null;
        int workStaCount = int.Parse(ipSheet.Cells[2, 3].Value.ToString());
        int count = 3 + workStaCount;
        int row = 1;
        while (true)
        {
            if (ipSheet.Cells[row, 1].Value != null)
            {
                string[] rowData = new string[count];
                for (int i = 0; i < count; i++)//遍历列
                {
                    if (ipSheet.Cells[row, i + 1].Value != null)
                        rowData[i] = ipSheet.Cells[row, i + 1].Value.ToString();
                }
                listArr.Add(rowData);
                row++;
            }
            else
            {
                break;
            }
        }
        return listArr;
    }
}

WPF

使用 SVG

  • 通过 SvgToXaml 工具SVG 图片转为 xaml 资源

  • 打开工具,在底部选择 SVG 图片

alt text

  • 在弹出的窗口中,切换到 XAML 查看代码

alt text

  • 集成到资源字典,接下来我们可以把转换得到了xaml 集成到资源字典中,以下是直接放在 App.xaml 中(可创建一个资源文件来存放):
<Application x:Class="WpfApp3.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp3"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

        <DrawingImage x:Key="XX">
            <DrawingImage.Drawing>
                <DrawingGroup ClipGeometry="M0,0 V25 H120 V0 H0 Z">
                    <GeometryDrawing Brush="#FFFFFFFF" Geometry="F1 M120,25z M0,0z M0,25L120,25 120,0 0,0 0,25z" />
                    <DrawingGroup Opacity="1">
                        <GeometryDrawing Brush="#FFB0B0B0" Geometry="F1 M120,25z M0,0z M18.91,8.2C18.77,8.71,18.59,9.24,18.38,9.75L15.63,9.75 15.63,10.47 18.05,10.47C17.41,11.75 16.53,12.94 15.38,13.74 15.5,13.91 15.69,14.23 15.77,14.43 16.19,14.13 16.58,13.79 16.93,13.42L16.93,17.36 17.68,17.36 17.68,12.53C18.15,11.89,18.56,11.19,18.9,10.47L24.39,10.47 24.39,9.75 19.21,9.75C19.39,9.3,19.55,8.84,19.69,8.39L18.91,8.2z M20.98,10.99L20.98,12.92 18.73,12.92 18.73,13.62 20.98,13.62 20.98,16.46 18.33,16.46 18.33,17.16 24.38,17.16 24.38,16.46 21.73,16.46 21.73,13.62 24,13.62 24,12.92 21.73,12.92 21.73,10.99 20.98,10.99z M25.58,10.08L25.58,10.78 29.47,10.78 29.47,10.08 25.58,10.08z M25.98,11.35C26.21,12.48,26.42,13.95,26.46,14.93L27.09,14.82C27.03,13.83,26.82,12.38,26.58,11.24L25.98,11.35z M26.75,8.45C27.02,8.92,27.31,9.57,27.43,9.98L28.11,9.74C27.99,9.33,27.69,8.72,27.4,8.25L26.75,8.45z M28.3,11.11C28.17,12.34 27.9,14.1 27.64,15.16 26.82,15.36 26.05,15.53 25.47,15.65L25.65,16.4C26.69,16.14,28.1,15.78,29.43,15.44L29.36,14.75 28.28,15.01C28.53,13.96,28.81,12.43,29,11.25L28.3,11.11z M29.67,12.98L29.67,17.39 30.4,17.39 30.4,16.91 33.42,16.91 33.42,17.35 34.18,17.35 34.18,12.98 32.06,12.98 32.06,10.99 34.6,10.99 34.6,10.27 32.06,10.27 32.06,8.19 31.29,8.19 31.29,12.98 29.67,12.98z M30.4,16.21L30.4,13.69 33.42,13.69 33.42,16.21 30.4,16.21z M39.11,12.26C39.2,12.18,39.52,12.14,39.98,12.14L40.69,12.14C40.27,13.24,39.55,14.15,38.63,14.75L38.51,14.17 37.44,14.57 37.44,11.35 38.54,11.35 38.54,10.64 37.44,10.64 37.44,8.32 36.73,8.32 36.73,10.64 35.5,10.64 35.5,11.35 36.73,11.35 36.73,14.83C36.21,15.02,35.74,15.19,35.36,15.31L35.61,16.07C36.47,15.73,37.6,15.28,38.65,14.86L38.63,14.77C38.79,14.87 39.06,15.07 39.17,15.19 40.13,14.49 40.95,13.44 41.4,12.14L42.24,12.14C41.61,14.28 40.49,15.94 38.79,16.96 38.96,17.06 39.25,17.27 39.37,17.39 41.06,16.26 42.25,14.49 42.94,12.14L43.62,12.14C43.44,15.08 43.23,16.22 42.97,16.5 42.87,16.62 42.78,16.65 42.62,16.64 42.44,16.64 42.06,16.64 41.65,16.6 41.77,16.8 41.85,17.1 41.86,17.31 42.28,17.33 42.69,17.34 42.93,17.31 43.22,17.28 43.42,17.2 43.61,16.96 43.96,16.55 44.17,15.31 44.38,11.8 44.39,11.69 44.4,11.43 44.4,11.43L40.38,11.43C41.37,10.8,42.42,9.98,43.49,9.03L42.93,8.61 42.77,8.67 38.75,8.67 38.75,9.38 41.97,9.38C41.1,10.17 40.13,10.85 39.8,11.06 39.41,11.31 39.04,11.52 38.79,11.55 38.89,11.74 39.05,12.09 39.11,12.26z M48.75,13.81C49.55,13.98,50.57,14.33,51.13,14.61L51.44,14.1C50.88,13.84,49.87,13.51,49.07,13.35L48.75,13.81z M47.75,15.08C49.13,15.25,50.86,15.65,51.82,15.99L52.15,15.43C51.18,15.11,49.45,14.72,48.1,14.57L47.75,15.08z M45.84,8.64L45.84,17.4 46.56,17.4 46.56,16.98 53.42,16.98 53.42,17.4 54.17,17.4 54.17,8.64 45.84,8.64z M46.56,16.31L46.56,9.32 53.42,9.32 53.42,16.31 46.56,16.31z M49.14,9.52C48.64,10.34 47.78,11.12 46.92,11.63 47.08,11.73 47.34,11.96 47.45,12.08 47.75,11.88 48.06,11.64 48.37,11.37 48.67,11.69 49.04,11.99 49.44,12.26 48.59,12.66 47.63,12.96 46.74,13.14 46.87,13.28 47.03,13.57 47.1,13.75 48.08,13.52 49.13,13.15 50.08,12.64 50.91,13.09 51.86,13.43 52.81,13.64 52.9,13.46 53.09,13.2 53.23,13.07 52.35,12.91 51.47,12.64 50.69,12.28 51.44,11.79 52.07,11.22 52.49,10.54L52.06,10.29 51.95,10.32 49.36,10.32C49.51,10.13,49.65,9.94,49.77,9.74L49.14,9.52z M48.78,10.97L48.85,10.9 51.44,10.9C51.08,11.29 50.6,11.64 50.06,11.95 49.55,11.66 49.11,11.33 48.78,10.97z M59.27,8.35L59.27,16.17 55.51,16.17 55.51,16.92 64.5,16.92 64.5,16.17 60.06,16.17 60.06,12.19 63.81,12.19 63.81,11.44 60.06,11.44 60.06,8.35 59.27,8.35z M65.61,8.95C66.19,9.44,66.87,10.14,67.16,10.63L67.78,10.16C67.46,9.68,66.77,9,66.18,8.54L65.61,8.95z M69.46,8.5C69.22,9.39 68.8,10.27 68.26,10.86 68.44,10.95 68.76,11.15 68.9,11.26 69.13,10.98 69.35,10.63 69.55,10.24L71.03,10.24 71.03,11.7 68.2,11.7 68.2,12.37 70.01,12.37C69.84,13.68 69.43,14.63 67.93,15.16 68.09,15.3 68.31,15.58 68.39,15.77 70.07,15.11 70.57,13.96 70.76,12.37L71.79,12.37 71.79,14.69C71.79,15.45 71.96,15.67 72.71,15.67 72.86,15.67 73.54,15.67 73.69,15.67 74.32,15.67 74.52,15.35 74.59,14.08 74.38,14.03 74.07,13.92 73.93,13.78 73.9,14.83 73.86,14.97 73.61,14.97 73.47,14.97 72.92,14.97 72.82,14.97 72.56,14.97 72.53,14.94 72.53,14.69L72.53,12.37 74.51,12.37 74.51,11.7 71.78,11.7 71.78,10.24 74.09,10.24 74.09,9.59 71.78,9.59 71.78,8.24 71.03,8.24 71.03,9.59 69.85,9.59C69.98,9.29,70.09,8.97,70.18,8.65L69.46,8.5z M67.51,12.04L65.56,12.04 65.56,12.74 66.79,12.74 66.79,15.77C66.36,15.97,65.9,16.33,65.45,16.75L65.95,17.4C66.52,16.78 67.06,16.26 67.43,16.26 67.65,16.26 67.96,16.55 68.35,16.79 69.01,17.18 69.84,17.28 71,17.28 71.98,17.28 73.67,17.23 74.45,17.18 74.46,16.96 74.58,16.59 74.66,16.4 73.67,16.5 72.15,16.57 71.01,16.57 69.95,16.57 69.11,16.51 68.49,16.14 68.01,15.86 67.78,15.62 67.51,15.6L67.51,12.04z M76.77,8.21L76.77,10.21 75.46,10.21 75.46,10.91 76.77,10.91 76.77,13.04C76.24,13.2,75.75,13.34,75.36,13.45L75.55,14.18 76.77,13.79 76.77,16.48C76.77,16.61 76.72,16.65 76.6,16.66 76.48,16.66 76.09,16.67 75.66,16.65 75.76,16.86 75.85,17.17 75.88,17.36 76.52,17.36 76.91,17.35 77.16,17.22 77.41,17.1 77.5,16.89 77.5,16.48L77.5,13.55 78.66,13.17 78.56,12.48 77.5,12.81 77.5,10.91 78.69,10.91 78.69,10.21 77.5,10.21 77.5,8.21 76.77,8.21z M83.04,9.41C82.68,9.93 82.19,10.39 81.62,10.79 81.1,10.39 80.66,9.93 80.32,9.41L83.04,9.41z M78.96,8.73L78.96,9.41 79.6,9.41C79.97,10.08 80.46,10.66 81.04,11.16 80.26,11.63 79.38,11.98 78.53,12.19 78.67,12.34 78.85,12.62 78.93,12.8 79.84,12.53 80.77,12.13 81.6,11.6 82.38,12.14 83.29,12.55 84.28,12.81 84.38,12.61 84.59,12.33 84.74,12.18 83.8,11.98 82.94,11.64 82.2,11.18 82.99,10.58 83.66,9.83 84.09,8.95L83.64,8.7 83.51,8.73 78.96,8.73z M81.2,12.48L81.2,13.36 79.17,13.36 79.17,14.04 81.2,14.04 81.2,15.07 78.66,15.07 78.66,15.75 81.2,15.75 81.2,17.42 81.95,17.42 81.95,15.75 84.57,15.75 84.57,15.07 81.95,15.07 81.95,14.04 83.85,14.04 83.85,13.36 81.95,13.36 81.95,12.48 81.2,12.48z M94.27,8.74L85.97,8.74 85.97,17.1 94.52,17.1 94.52,16.38 86.71,16.38 86.71,9.47 94.27,9.47 94.27,8.74z M87.59,10.75C88.37,11.39 89.24,12.15 90.05,12.91 89.2,13.77 88.24,14.53 87.26,15.11 87.44,15.24 87.73,15.53 87.86,15.68 88.8,15.06 89.72,14.29 90.58,13.41 91.45,14.24 92.22,15.05 92.72,15.68L93.33,15.13C92.79,14.5 91.98,13.69 91.09,12.86 91.81,12.05 92.47,11.16 93.02,10.23L92.31,9.95C91.83,10.8 91.23,11.62 90.55,12.38 89.74,11.64 88.89,10.92 88.13,10.31L87.59,10.75z M100.38,8.57L100.38,9.78C100.38,10.51 100.22,11.4 99.23,12.06 99.38,12.15 99.66,12.4 99.76,12.54 100.85,11.81 101.08,10.69 101.08,9.8L101.08,9.22 102.48,9.22 102.48,11.1C102.48,11.78 102.61,12.04 103.28,12.04 103.4,12.04 103.89,12.04 104.03,12.04 104.22,12.04 104.43,12.03 104.54,11.99 104.52,11.84 104.5,11.59 104.49,11.41 104.37,11.44 104.15,11.45 104.02,11.45 103.9,11.45 103.46,11.45 103.34,11.45 103.2,11.45 103.17,11.38 103.17,11.11L103.17,8.57 100.38,8.57z M99.67,12.74L99.67,13.39 100.4,13.39 100.01,13.5C100.33,14.34 100.77,15.08 101.34,15.69 100.65,16.22 99.83,16.58 98.93,16.8 99.08,16.95 99.25,17.24 99.33,17.44 100.28,17.17 101.14,16.77 101.87,16.19 102.5,16.72 103.26,17.12 104.13,17.37 104.24,17.18 104.44,16.88 104.61,16.73 103.76,16.53 103.02,16.17 102.39,15.7 103.07,15 103.58,14.08 103.87,12.88L103.4,12.71 103.27,12.74 99.67,12.74z M100.63,13.39L102.97,13.39C102.72,14.12 102.34,14.73 101.85,15.23 101.32,14.71 100.91,14.09 100.63,13.39z M96.18,9.09L96.18,14.92 95.33,15.03 95.46,15.75 96.18,15.63 96.18,17.26 96.91,17.26 96.91,15.51 99.35,15.1 99.31,14.45 96.91,14.81 96.91,13.36 99.15,13.36 99.15,12.68 96.91,12.68 96.91,11.31 99.16,11.31 99.16,10.64 96.91,10.64 96.91,9.55C97.78,9.32,98.73,9.03,99.45,8.7L98.83,8.14C98.21,8.47,97.14,8.85,96.2,9.1L96.18,9.09z" />
                    </DrawingGroup>
                </DrawingGroup>
            </DrawingImage.Drawing>
        </DrawingImage>

    </Application.Resources>
</Application>

  • 前端使用,实现 TextBox 的水印功能(最好定义一个 image 的资源)
<Window x:Class="WpfApp3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp3"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <ImageBrush x:Key="watermark" ImageSource="{StaticResource XX}" AlignmentX="Center" Stretch="None" />
    </Window.Resources>
    <StackPanel>
        <TextBox Name="myTextBox" TextChanged="OnTextBoxTextChanged" Width="200" Height="25" Background="{StaticResource  watermark}" />
    </StackPanel>
</Window>

后端添加文本changed处理事件

  private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
  {
      if (sender is TextBox box)
      {
          if (string.IsNullOrEmpty(box.Text))
              box.Background = (ImageBrush)FindResource("watermark");
          else
              box.Background = null;
      }
  }

剪切板错误

❌ WPF剪切板问题-OpenClipboard HRESULT:0x800401D0 (CLIPBRD_E_CANT_OPEN))

错误示例:

1717641062860

原实现:

 Clipboard.SetText(content);

解决方法:

 Clipboard.SetDataObject(content);

参考(其中的处理方式不优)

WPF剪切板问题-OpenClipboard HRESULT:0x800401D0 (CLIPBRD_E_CANT_OPEN))

元素焦点问题

Focus() 函数常常设置失败

通过以下方式可设置成功 pwdBoxPWD 需要设置的UI元素

Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Render,
new Action(() => pwdBoxPWD.Focus()));

WPF hardware acceleration in RDP

在 .net 8.0 后出现的问题

调试模式正常运行,发布后无法运行,提示设置硬件加速

google 文章

解决方案:

在项目中配置此设置RuntimeHostConfigurationOption:

<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <RuntimeHostConfigurationOption Include="Switch.System.Windows.Media.EnableHardwareAccelerationInRdp" Value="true" />
  </ItemGroup>
</Project>

删除所有的文件,再生成

项目示例:

在某项目中,有一组菜单按钮,一个菜单对应一个页面,假设A按钮对应A页面,B按钮对应B页面,同时有个需求,在A页面,按下空格键后代码控制跳转到B页面。

出现问题:当点击A按钮后,然后按下空格键,不会跳到B页面。

调试结果:按下空格键后执行代码跳转到B页面后,会再次跳转到A页面。

分析原因

  • 点击A按钮,此时A按钮获得键盘焦点。
  • 按下空格键,执行跳转到B的代码逻辑,跳转到B页面。
  • 执行完成后,由于A按钮具有焦点会响应键盘,所以会执行A按钮的点击事件,既点击了A按钮,跳转到A页面。

Label 控件格式化

单独使用 ContentStringFormat

格式化日期

<Label Width="100" Height="30" Content="{Binding Path=DateTime.Now}" ContentStringFormat='{}{0:yyyy-MM-dd}'></Label>

格式化时间

<Label Width="100" Height="30" Content="{Binding Path=DateTime.Now}" ContentStringFormat='{}{0:hh:mm:ss}'></Label>

上下文菜单

在资源文件中定义菜单内容

在资源文件中绑定上下文

(1)定义菜单绑定的上下文 MenuViewModel.cs


namespace MenuDemo
{
    /// <summary>
    /// 菜单绑定上下文
    /// </summary>
    public  class MenuData
    {
        //这里可改为通知属性
       public string Name{get;set;} = "Name01";
       public string ItemName{get;set;}= "Item01";
    }
}

(2)定义页面绑定的上下文 MainWindowViewModel.cs


namespace MenuDemo
{
    /// <summary>
    /// 菜单绑定上下文
    /// </summary>
    public  class MainWindowViewModel
    {
        //这里可改为通知属性
       public MenuData Menu{get;set;} = new MenuData();
    }
}

(3)定义菜单资源文件方式一(每项都指定对象Menu):,路径: MenuViews.xaml

⚠️ 这里固定了 属性名称必须为Menu ,要解决参照: 使用附加属性绑定菜单的上下文


<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ContextMenu x:Key="_menu">
        <MenuItem Header="{Binding Menu.Name}" />
        <Separator />
        <MenuItem Header="{Binding Menu.ItemName}" />
    </ContextMenu>

</ResourceDictionary>

(4)定义菜单资源文件方式二(在根节点上指定 Menu):,路径: MenuViews.xaml

DataContext="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget.DataContext.Menu}"

因为 ContextMenu 不直接属于视觉树里面的元素,所以要特殊处理

不能这样 DataContext="{Binding Menu}"

⚠️ 这里固定了 属性名称必须为Menu,要解决参照: 使用附加属性绑定菜单的上下文


<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ContextMenu x:Key="_menu" DataContext="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget.DataContext.Menu}">
        <MenuItem Header="{Binding Name}" />
        <Separator />
        <MenuItem Header="{Binding ItemName}" />
    </ContextMenu>

</ResourceDictionary>

(5) MainWindow.xaml

<Window x:Class="MenuDemo.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MenuDemo"        
             xmlns:prism="http://prismlibrary.com/"   >
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/MenuDemo;component/MenuViews.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <Grid Margin="12">
         <Button  ContextMenu="{StaticResource _menu}" >
         </Button>

    </Grid>
</Window>

(6) MainWindow.cs

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();
       DataContext = new MainWindowViewModel();
    }
}

控制菜单的显示隐藏

需求描述

可禁止和激活菜单功能(即控制显示和不显示)

知识点

  • 不要使用 Visibility 来实现,因为右键的时都会创建 PopupRoot, 由于未显示,他们不会触发失去焦点,不会自动关闭,所以右键多个对象后,都会创建 PopupRoot,若此时激活菜单,那么界面上会出现很多菜单
  • 使用 ContextMenuOpening 来控制,
    • 若上下文菜单直接在 UserControl 或者 Windows 中定义,那么可直接添加 ContextMenuOpening的处理事件
    • 若菜单的条目在一个资源中定义,可使用一个静态变量来进行绑定,本节主要介绍这种方式。

控制菜单 Active 的静态脚本 ContextMenuHelper


namespace MenuDemo
{
    /// <summary>
    /// 上下文菜单帮助类
    /// </summary>
    public static class ContextMenuHelper
    {
        //菜单状态
        private static bool _menuActiveState;

        /// <summary>
        /// 提供此静态属性给 前端 ContextMenuOpening 进行绑定,不能直接绑定函数 ContextMenuOpeningHandler
        /// 使用案例
        /// 根节点引入命名空间: xmlns:local="clr-namespace:MenuDemo;assembly=MenuDemo"
        /// 在需要上下文菜单的元素节点使用 ContextMenuOpening="{x:Static local:ContextMenuHelper.ContextMenuOpeningHandler}"
        /// </summary>
        public static ContextMenuEventHandler ContextMenuOpening
        {
            get; internal set;
        }

        static ContextMenuHelper()
        {
            ContextMenuOpening = ContextMenuOpeningHandler;
        }

        //切换菜单功能,更新状态
        public static void SwitchActive(bool active)
        {
            _menuActiveState = active;
        }

        private static void ContextMenuOpeningHandler(object sender, ContextMenuEventArgs e)
        {
            e.Handled = !_menuActiveState;//根据变量判断是否取消事件。 _menuActiveState:false 时 e.Handled = true 表示取消
        }
    }
}

前端绑定,注意在根目录引入:xmlns:local="clr-namespace:MenuDemo;assembly=MenuDemo"

  <Button  ContextMenu="{StaticResource _menu}" 
                  ContextMenuOpening="{x:Static local:ContextMenuHelper.ContextMenuOpening}">
   </Button>

无法引用静态资源同时绑定上下文

这样能使用资源,但是无法绑定上下文

    <Button  DataContext="{Binding PlatformDemo}"
                ContextMenu="{StaticResource zt}" >

                <!--没有提供  ContextMenu.DataContext-->
    </Button>
                        

这样能绑定上下文,但是无法使用资源



    <Button>
     <Button.ContextMenu>
         <ContextMenu DataContext="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget.DataContext.Menu}">
          <!--只能在这里添加item 或者 绑定 ItemSources(推荐)-->
         </ContextMenu>
     </Button.ContextMenu>
 </Button>
                        

通过嵌套元素来绑定菜单的上下文

前面章节存在下面问题

⚠️ 这里固定了 属性名称必须为Menu

下面通过嵌套来处理这个问题,把菜单放在 Button 的子元素,如下放在 TextBlock 上,这样在子元素上绑定 DataContext 为 Menu

<Button  DataContext="{Binding PlatformDemo}">
    <Grid DataContext="{Binding Menu}"  ContextMenu="{StaticResource zt}">
            <TextBlock Text="Button 的子元素上来绑定菜单 " />
    <Grid/>
</Button>

上面只能应对简单情况,如果子元素上又有元素绑定 PlatformDemo 中的对象呢,如下 TextBlock 需要绑定 PlatformDemo 中的 SomeName

<Button  DataContext="{Binding PlatformDemo}">
    <Grid DataContext="{Binding Menu}"  ContextMenu="{StaticResource zt}">
            <TextBlock Text="{Binding SomeName}" />
    <Grid/>
</Button>

使用附加属性绑定菜单的上下文(推荐)

使用场景

  • 菜单内容在资源文件中定义
  • 菜单中的属性或者命令绑定了属性
  • 整个菜单需要绑定一个属性
  • 解决默认下不能同时引用资源又绑定菜单上下文的问题

附件属性脚本

using ESUI.Services.api;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Reflection;
using System.Diagnostics;
using System.Windows.Data;

namespace ESUI.Utilities
{
    /// <summary>
    /// 上下文菜单帮助类
    /// </summary>
    public static class ContextMenuHelper
    {
        #region 附加属性

        public static readonly DependencyProperty DataContextProperty =
       DependencyProperty.RegisterAttached(
           "DataContext",
           typeof(object),
           typeof(ContextMenuHelper),
           new PropertyMetadata(null, OnDataContextChanged));

        public static void SetDataContext(UIElement element, object value)
        {
            element.SetValue(DataContextProperty, value);
        }

        public static object GetDataContext(UIElement element)
        {
            return element.GetValue(DataContextProperty);
        }

        private static void OnDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is UIElement element && element is FrameworkElement frameworkElement)
            {
                // 获取绑定的值
                var contextMenu = frameworkElement.ContextMenu;
                if (contextMenu != null)
                {
                    contextMenu.SetBinding(ContextMenu.DataContextProperty,
                        new Binding { Source =  e.NewValue });
                }
            }
        }
        #endregion

        private static IESUIService _service;

        /// <summary>
        /// 提供此静态属性给 前端 xaml 进行绑定
        /// 使用案例
        /// 根节点引入命名空间: xmlns:esui_util="clr-namespace:ESUI.Utilities;assembly=ESUI"
        /// 在需要上下文菜单的元素节点使用 ContextMenuOpening="{x:Static esui_util:ContextMenuHelper.ContextMenuOpeningHandler}"
        /// </summary>
        public static ContextMenuEventHandler ContextMenuOpening
        {
            get; internal set;
        }

        static ContextMenuHelper()
        {
            ContextMenuOpening = ContextMenuOpeningHandler;
        }

        internal static void Init(IESUIService service)
        {
            _service = service;
        }
        private static void ContextMenuOpeningHandler(object sender, ContextMenuEventArgs e)
        {
            e.Handled = !_service.IsMenuActive;
        }
    }
}

菜单资源定义


<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ContextMenu x:Key="_menu">
        <MenuItem Header="{Binding Menu.Name}" />
        <Separator />
        <MenuItem Header="{Binding Menu.ItemName}" />
    </ContextMenu>

</ResourceDictionary>

前端引用资源

 <Button DataContext="{Binding PlatformDemo}" Content="{Binding Describe}"  
         esui_util:ContextMenuHelper.DataContext="{Binding ContextMenu.PlacementTarget.DataContext.Menu2, RelativeSource={RelativeSource Self}}"
         ContextMenu="{StaticResource _menu}"
         ContextMenuOpening="{x:Static esui_util:ContextMenuHelper.ContextMenuOpening}" >
 </Button>

  • 指定对象的上下文 DataContext="{Binding PlatformDemo}" PlatformDemo 中包含 Menu 属性
  • 附件属性的使用 esui_util:ContextMenuHelper.DataContext="{Binding ContextMenu.PlacementTarget.DataContext.Menu, RelativeSource={RelativeSource Self}}" ,其中的 Menu 名称就可以自定义了
  • 资源引用 ContextMenu="{StaticResource _menu}"
  • 控制菜单激活 ContextMenuOpening="{x:Static esui_util:ContextMenuHelper.ContextMenuOpening}"

动态查找资源(拓展,没必要)

/// <summary>
/// FindResource
/// </summary>
/// <param name="fileName"></param>
/// <param name="resKey"></param>
/// <exception cref="Exception"></exception>
private static T FindResource<T>(string fileName,string resKey)
{
    // 获取资源文件的路径
    string resourceFilePath = $"pack://application:,,,/ESUI;component/MenuViews/{fileName}.xaml";

    // 创建一个ResourceDictionary并加载该资源文件
    ResourceDictionary resourceDictionary = new ResourceDictionary();
    resourceDictionary.Source = new Uri(resourceFilePath, UriKind.Absolute);

    if (resourceDictionary.Count == 0)
    {
        throw new Exception($"“{resourceFilePath}” 中未定义资源");
    }

    if(resourceDictionary.Contains(resKey)) //注意 中包含了自身定义的资源也包含引入的
    {
        return (T)resourceDictionary[resKey];
    }
    else
    {

        throw new Exception($"“{resourceFilePath}” 中未找到 “{resKey}” 资源");
    }
 
}

    // 动态绑定菜单资源
  private static void ContextMenuOpeningHandler(object sender, ContextMenuEventArgs e)
  {
      if(_service.IsMenuActive)
      {
          FrameworkElement el = sender as FrameworkElement;

          // "fileName", "key" 两个值通过 tag 或者 uid 传进来
          el.ContextMenu = FindResource<ContextMenu>("fileName", "key");
          if (el.ContextMenu.DataContext == null)
          {
              string vmName = el.Tag == null ? "Menu" : el.Tag.ToString();
              BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
              var d = el.DataContext.GetType().GetProperty(vmName, bindingFlags);
              object vm = null;
              vm = d.GetValue(el.DataContext);
              el.ContextMenu.DataContext = vm;
          }
      }
      e.Handled = !_service.IsMenuActive;

       
  }

ListBox

常用于导航

自定义ItemContainerStyle

在使用ListBox的时候,发现鼠标移入到item上,会有一个很宽的背景,想把它去除,我们自己定义鼠标移入的状态,可以通过 ItemContainerStyle 来定义模板,以下实现一个理论考试的题目选择样式,有如下需求:

  • 题目有 未做、已做、正确、错误,四种状态,不同的状态显示不同的背景
  • 鼠标移入有状态变化(因为这里有四种状态,采用鼠标移入的时候,先死一个透明度40的遮罩,或者显示一个边框,否则要基于4种颜色来设计4种不同的over 色)
  • 注意:不能将 ItemContainerStyle 样式设定为: ItemContainerStyle="{x:Null}" ,会引起其他异常。
 <ListBox.ItemContainerStyle>
     <Style TargetType="ListBoxItem">
         <Setter Property="Padding" Value="0"/>
         <Setter Property="BorderThickness" Value="0"/>
         <Setter Property="BorderThickness" Value="0"/>
         <Setter Property="Template" >
             <Setter.Value>
                 <ControlTemplate >
                     <Border x:Name="_border"  Style="{StaticResource subnum_border}">
                         <Grid>
                             <TextBlock   Text="{Binding SubNum}" Style="{StaticResource subnum_text}"/>
                             <Border x:Name="_mask" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Visibility="Collapsed" 
                                 Opacity="0.4" Background="#FFF7E9C2"/>
                         </Grid>
                     </Border>
                     <ControlTemplate.Triggers>

                         <!--未做-->
                         <DataTrigger Binding="{Binding OperateState}" Value="0">
                             <Setter TargetName="_border" Property="Background" Value="#FFB9B9B9"/>
                         </DataTrigger>
                         <!--已做-->
                         <DataTrigger Binding="{Binding OperateState}" Value="1">
                             <Setter TargetName="_border" Property="Background" Value="#FF428DD2"/>

                         </DataTrigger>
                         <!--正确-->
                         <DataTrigger Binding="{Binding OperateState}" Value="2">
                             <Setter TargetName="_border" Property="Background" Value="Green"/>

                         </DataTrigger>
                         <!--错误-->
                         <DataTrigger Binding="{Binding OperateState}" Value="3">
                             <Setter TargetName="_border" Property="Background" Value="#FFAD1E1E"/>
                         </DataTrigger>

                         <!-- 鼠标悬停时,改变背景色  -->
                         <Trigger Property="IsMouseOver" Value="True">
                             <Setter TargetName="_mask" Property="Visibility" Value="Visible" />
                         </Trigger>
                     </ControlTemplate.Triggers>

                 </ControlTemplate>

             </Setter.Value>
         </Setter>
         <Style.Triggers>
             <Trigger Property="IsMouseOver" Value="True">
                 <!-- 当鼠标移入时重置背景色为透明 -->
                 <Setter Property="Background" Value="Transparent"/>
                 <!-- 其他的属性重置 -->
             </Trigger>
         </Style.Triggers>
     </Style>
 </ListBox.ItemContainerStyle>

设置ItemContainerStyle 为null(出现问题)

这种方式,我们可以单独定义我们的item 模板,如果不自定义模板,还需要,重置 ListItem的风格


  <!-- 自定义 ListBoxItem 的控件模板  去除自带的模板样式-->
  <Style TargetType="ListBoxItem">
      <Setter Property="Template">
          <Setter.Value>
              <ControlTemplate TargetType="ListBoxItem">
                  <ContentPresenter />
              </ControlTemplate>
          </Setter.Value>
      </Setter>
  </Style>

<ListBox ItemsSource="{Binding YourItems}"
         ItemTemplateSelector="{StaticResource MyTemplateSelector}"
         ItemContainerStyle="{x:Null}" />

ListView

表头动态显示和隐藏

在WPF中,表头不在VisualTree里面,无法通过绑定属性值动态控制列的显示隐藏,此示例通过附件属性实现该功能, 以 ListView为例,其他具有表头的组件

如DataGrid应类似处理。

  1. 添加附加属性
public class GridViewColumnVisibilityManager
{
    static void UpdateListView(ListView lv)
    {
        GridView gridview = lv.View as GridView;
        if (gridview == null || gridview.Columns == null) return;
        List<GridViewColumn> toRemove = new List<GridViewColumn>();
        foreach (GridViewColumn gc in gridview.Columns)
        {
            if (GetIsVisible(gc) == false)
            {
                toRemove.Add(gc);
            }
        }
        foreach (GridViewColumn gc in toRemove)
        {
            gridview.Columns.Remove(gc);
        }
    }

    public static bool GetIsVisible(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsVisibleProperty);
    }

    public static void SetIsVisible(DependencyObject obj, bool value)
    {
        obj.SetValue(IsVisibleProperty, value);
    }

    public static readonly DependencyProperty IsVisibleProperty =
        DependencyProperty.RegisterAttached("IsVisible", typeof(bool), typeof(GridViewColumnVisibilityManager), new UIPropertyMetadata(true));


    public static bool GetEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(EnabledProperty);
    }

    public static void SetEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(EnabledProperty, value);
    }

    public static readonly DependencyProperty EnabledProperty =
        DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(GridViewColumnVisibilityManager), new UIPropertyMetadata(false,
            new PropertyChangedCallback(OnEnabledChanged)));

    private static void OnEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        ListView view = obj as ListView;
        if (view != null)
        {
            bool enabled = (bool)e.NewValue;
            if (enabled)
            {
                view.Loaded += (sender, e2) =>
                {
                    UpdateListView((ListView)sender);
                };
                view.TargetUpdated += (sender, e2) =>
                {
                    UpdateListView((ListView)sender);
                };
                view.DataContextChanged += (sender, e2) =>
                {
                    UpdateListView((ListView)sender);
                };
            }
        }
    }
}
  1. 前端使用
  • 引入附加属性脚本的命名空间 xmlns:foo="clr-namespace:LotteryPredict.Control"
  • 在ListView里面使用附件属性 用 foo:GridViewColumnVisibilityManager.Enabled="true",启用附件属性的功能
  • GridViewColumn 中使用 foo:GridViewColumnVisibilityManager.IsVisible="{Bindg IsFullAuth}"控制该列
...
xmlns:foo="clr-namespace:LotteryPredict.Control"
...
 <ListView Grid.Row="1" ItemContainerStyle="{StaticResource CenterLVItemStyle}" ItemsSource="{Binding CurVersionCatalogs}"
            foo:GridViewColumnVisibilityManager.Enabled="True">
            ...
             <GridViewColumn Header="目录名称" DisplayMemberBinding="{Binding Name}" Width="110" 
                 foo:GridViewColumnVisibilityManager.IsVisible="{Binding IsFullAuth}"
                 ></GridViewColumn>
...

设置项目版本号

  • 可以在项目属性面板上设置(一般在包选项卡下),这种方式没找到产品版本号的设置
  • 可直接编辑项目文件,以下为编辑项目文件案例,这种方式可直接设置各种版本号

框架:. net 6.0

添加版本号,可以在测试的时候确定当前版本

  1. 可以在属性面板中设置(只看到了文件版本和程序集版本号的设置)
  2. 也可以直接在文件中编辑
<PropertyGroup>
  <OutputType>WinExe</OutputType>
  <TargetFramework>net6.0-windows</TargetFramework>
  <UseWPF>true</UseWPF>
  <ApplicationIcon>sc.ico</ApplicationIcon>
  <NeutralLanguage>zh-CN</NeutralLanguage>
  <Version>2.0.0</Version>
  <AssemblyVersion>2.0.0.0</AssemblyVersion>
  <FileVersion>2.0.0.0</FileVersion>
</PropertyGroup>

1717635499500

资源引用

WPF中的资源包含很多,如图片、字体、声音,以及样式、模板等很多,在使用的时候常常出现很多问题。

绑定图片路径

需求:程序发布后,可以更换图片

一定要使用绝对路径,相对路径不行

前端

<UserControl xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"  xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"  x:Class="OESTS.Modules.Login.Views.LoginView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"   
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
             xmlns:Control="clr-namespace:OESTS.Control;assembly=OESTS.Control"
             xmlns:core="clr-namespace:OESTS.Core.Events;assembly=OESTS.Core"
             prism:ViewModelLocator.AutoWireViewModel="True" Margin="0"
             Width="1280" Height="720">
    <UserControl.Background>
        <ImageBrush ImageSource="{Binding BackgroundImagesource}" />
    </UserControl.Background>

后端

   public class LoginViewModel : RegionViewModelBase
   {


       private string _backgroundImagesource;
       public string BackgroundImagesource
       {
           get { return _backgroundImagesource; }
           set { SetProperty(ref _backgroundImagesource, value); }
       }

       public LoginViewModel() 
       {
            //注意,一定要使用绝对路径,相对路径不行
           //加载背景图,在exe文件的 Assets/bg.png
           BackgroundImagesource = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"Assets/bg.png");
       }

   }

资源路径

在WPF开发中,对资源的管理非常重要,当我们存在多个项目时,最好是把所有的资源都放在一个基础项目中进行管理,方便维护。

正确的做法使用

pack://application:,,,/程序集名称;component/资源路径名

图片资源路径

存在两个项目

  • 基础项目名称:Demo.Infrastructure
  • 主项目 Demo

所有的资源在 Demo.Infrastructure 中定义

  • 正确的使用
    <UserControl.Background>
        <!---->
        <ImageBrush ImageSource="pack://application:,,,/
        Demo.Infrastructure;component/images/login_bg.png"/>
    </UserControl.Background>
  • 错误的使用(不加 pack://application:,,,)此时为相对路径,在编辑模式下可以看到图片,但是运行后不显示
 <UserControl.Background>
        <!---->
        <ImageBrush ImageSource="/
        Demo.Infrastructure;component/images/login_bg.png"/>
    </UserControl.Background>
  • 错误的使用(不加 pack://application:,,, 和命名空间)
 <UserControl.Background>
        <!---->
        <ImageBrush ImageSource="images/login_bg.png"/>
    </UserControl.Background>

字体资源路径

可以下载阿里巴巴的字体文件,下载后将ttf文件拷贝到项目中


<TextBlock Text="&#xe657;" FontFamily="{StaticResource iconfont}" FontSize="14" Foreground="Red" VerticalAlignment="Center" />
<TextBlock Text="&#xe657;" FontFamily="/OESTS.Modules.Login;component/fonts/#iconfont" FontSize="14" Foreground="Red" VerticalAlignment="Center" />
<TextBlock Text="&#xe657;" FontFamily="/fonts/#iconfont" FontSize="14" Foreground="Red" VerticalAlignment="Center"/>

LoadComponent使用的路径

加载组件函数,可以加载一个xaml文件,这里页存在一个资源路径的问题,此方法仅能使用相对路径,不能使用绝对路径

案例:需求加载一个流文档控件的文件资源,

文件名为flowdoc.xaml,如下

<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
                xmlns:sys="clr-namespace:System;assembly=mscorlib"
                TextOptions.TextFormattingMode="Display" >
    <Paragraph>
        <Run Text="加载测试" ></Run>
    </Paragraph>
</FlowDocument>
  • 当只有一个项目时,若文件存在项目的resources文件中,加载方式如下
  FlowDocument doc = (FlowDocument)Application.LoadComponent(new Uri("/resources/flowdoc.xaml", UriKind.Relative));
  • 当存在多个项目,如使用了Prism框架,在一个模块项目中加载时(模块项目的程序集为:DemoModule)
//正确的做法
FlowDocument doc = (FlowDocument)Application.LoadComponent(
new Uri("/DemoModule;component/resources/flowdoc.xaml",
UriKind.Relative));

//错误的做法,不能加pack://application:,,,,加了后为绝对路径
FlowDocument doc = (FlowDocument)Application.LoadComponent(
new Uri("pack://application:,,,DemoModule;component/resources/flowdoc.xaml", 
UriKind.Relative));

资源的引用方式

  • 本地添加模板目录:分别创建模板对象。
  • App.xaml中添加模板目录,创建全局模板对象,导致大家的数据相同。
  • 对于样式、转换器、模板选择器等,在App.xaml添加目录比较合适。
  • 对于模板的使用,在本地添加模板目录不会出现问题。
  • 如果两者都添加,本地添加的起作用。

场景描述: 当前有两个页面(AView,BView)的布局相同,数据不同,定义了一个通用模板为 ContentTemplate, 定义了通用模板,key 为 contentView

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ScreenControlE" >
    <Grid x:Key="contentView">

       <TextBlock Text="{Binding Path=Content}" FontSize="14" Foreground="Red" VerticalAlignment="Center" />

    </Grid>
</ResourceDictionary>

A、B页面在本地添加模板目录

  • A、B页面的前端(这里使用了Prsim框架),在本地添加了资源目录,并引用 contentView,此时两个页面显示正常,分别显示自己的内容

<UserControl x:Class=" ScreenControlE.Views.NCCView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"       
             prism:ViewModelLocator.AutoWireViewModel="True">
    <UserControl.Resources>
        <ResourceDictionary Source="/Themes/ContentTemplate.xaml"/>
    </UserControl.Resources>
    <Grid>
        <ContentControl x:Name="ncc_contentCrl" Content="{StaticResource contentView }"/>
    </Grid>
</UserControl>

在App.xaml中添加模板的目录

  • App.xaml
<prism:PrismApplication x:Class="ScreenControlE.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:ScreenControlE"
             xmlns:prism="http://prismlibrary.com/" >
    <Application.Resources>
		<ResourceDictionary Source="/Themes/ContentTemplate.xaml"/>

    </Application.Resources>
</prism:PrismApplication>

  • A、B页面的前端(这里使用了Prsim框架),直接引用 contentView

<UserControl x:Class=" ScreenControlE.Views.NCCView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"       
             prism:ViewModelLocator.AutoWireViewModel="True">
 
    <Grid>
        <ContentControl x:Name="ncc_contentCrl" Content="{StaticResource contentView }"/>
    </Grid>
</UserControl>

  • 两个页面显示了相同的内容,在App.xaml中添加的相当于创建了一个全局变量的对象

  • ViewModel(A为例)

public class AViewModel : ViewModelBase
{
    public string Content { get; set; } = "A模板"
}

Prism 框架

IDialogService 集成 MaterialDesign

需求:通过 Prism 的对话框服务来管理 MaterialDesign 的 DialogHost 功能,实现既有Prism的框架注入服务,又具备 MaterialDesign 的界面,同时保持 Prism 的默认对话框功能(可拖动移动)。

Prism 8.0 和 9.0 接口变动

Prism 8.0 到 9.0 的命名空间以及 Dialog的接口有变动,需要分别处理

接口 Prism 8.0 Prism 9.0
IDialogService 命名空间:Prism.Services.Dialogs
  • void Show(string name, IDialogParameters parameters, Action callback);
  • void Show(string name, IDialogParameters parameters, Action callback, string windowName);
  • void ShowDialog(string name, IDialogParameters parameters, Action callback);
  • void ShowDialog(string name, IDialogParameters parameters, Action callback, string windowName);
命名空间:Prism.Dialogs
  • void ShowDialog(string name, IDialogParameters parameters, DialogCallback callback);
备注:其他的接口改为扩展来提供。
IDialogAware 命名空间:Prism.Dialogs
  • string Title { get; }
  • bool CanCloseDialog();
  • void OnDialogClosed();
  • void OnDialogOpened(IDialogParameters parameters);
  • event Action RequestClose;
命名空间:Prism.Dialogs
  • bool CanCloseDialog();
  • void OnDialogClosed();
  • void OnDialogOpened(IDialogParameters parameters);
  • DialogCloseListener RequestClose { get; }
备注:这里主要是将 RequestClose 从事件更改为 属性

集成过程

IDialogAware 接口,注意命名空间以及 RequestClose 在 9.0 中的变动

定义 DialogServiceExtensions

IDialogService 扩展,该脚本在8.0 和 9.0 中仅命名空间名称不一样(注意将 using Prism.Services.Dialogs; 改为 using Prism.Dialogs;)。

 public static class DialogServiceExtensions
 {
     public static void ShowDialogHost(this IDialogService dialogService, string name,
      string windowName)
     {
         if (!(dialogService is MaterialDialogService materialDialogService))
             throw new NullReferenceException("DialogService must be a MaterialDialogService");

         materialDialogService.ShowDialogHost(name, windowName, null, null
             );
     }


     /// <summary>
     /// Shows a modal dialog using a <see cref="MaterialDesignThemes.Wpf.DialogHost"/>.
     /// </summary>
     /// <param name="dialogService"></param>
     /// <param name="name">The name of the dialog to show.</param>
     /// <param name="parameters">The parameters to pass to the dialog.</param>
     /// <param name="callback">The action to perform when the dialog is closed.</param>
     /// <exception cref="NullReferenceException">Thrown when the dialog service is not a MaterialDialogService</exception>
     public static void ShowDialogHost(this IDialogService dialogService, string name, IDialogParameters parameters, Action<IDialogResult> callback)
     {
         if (!(dialogService is MaterialDialogService materialDialogService))
             throw new NullReferenceException("DialogService must be a MaterialDialogService");

         materialDialogService.ShowDialogHost(name, parameters, callback);
     }

     /// <summary>
     /// Shows a modal dialog using a <see cref="MaterialDesignThemes.Wpf.DialogHost"/>.
     /// </summary>
     /// <param name="dialogService"></param>
     /// <param name="name">The name of the dialog to show.</param>
     /// <param name="parameters">The parameters to pass to the dialog.</param>
     /// <param name="callback">The action to perform when the dialog is closed.</param>
     /// <param name="windowName">The name of the <see cref="MaterialDesignThemes.Wpf.DialogHost"/> that will contain the dialog control</param>
     /// <exception cref="NullReferenceException">Thrown when the dialog service is not a MaterialDialogService</exception>
     public static void ShowDialogHost(this IDialogService dialogService, string name,
         IDialogParameters parameters, Action<IDialogResult> callback, string windowName)
     {
         if (!(dialogService is MaterialDialogService materialDialogService))
             throw new NullReferenceException("DialogService must be a MaterialDialogService");

         materialDialogService.ShowDialogHost(name, windowName, parameters, callback);
     }
 }

定义 MaterialDialogService

8.0 和 9.0 定义不同:

Prism 8.0 定义 MaterialDialogService

Prism 9.0 定义 MaterialDialogService

App.xaml.cs 中注册

  protected override void RegisterTypes(IContainerRegistry containerRegistry)
  {
    //other...
      containerRegistry.Register<IDialogService, MaterialDialogService>();

  }

前端容器

<UserControl x:Class="OESTS.Modules.Share.Views.MainLayout"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"    
              xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
              xmlns:core="clr-namespace:OESTS.Core;assembly=OESTS.Core" 
              xmlns:Selectors="clr-namespace:OESTS.Modules.Share.Selectors" 
             prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
        <materialDesign:DialogHost Identifier="WinName" >
            <ContentControl  HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0 1 0 0"
                 prism:RegionManager.RegionName="{x:Static core:RegionNames.WorkRegion}" />
        </materialDesign:DialogHost>

    </Grid>
</UserControl>

后端调用


/*弹出班级选项面板
* ContentView: 用户控件视图的名称
* WinName: 前端定义的 DialogHost Identifier
*/
_dialogService.ShowDialogHost("ContentView", parameters, (r) =>
{
    dialogResult = r;

}, "WinName");

Prism 8.0 定义 MaterialDialogService

MaterialDialogService

 public class MaterialDialogService : DialogService
 {
     private readonly IContainerExtension _containerExtension;

     public MaterialDialogService(IContainerExtension containerExtension) : base(containerExtension)
     {
         _containerExtension = containerExtension;
     }

     public void ShowDialogHost(string name, IDialogParameters parameters, Action<IDialogResult> callback) =>
         ShowDialogHost(name, null, parameters, callback);

     public void ShowDialogHost(string name, string dialogHostName, IDialogParameters parameters, Action<IDialogResult> callback)
     {
         if (parameters == null)
             parameters = new DialogParameters();

         var content = _containerExtension.Resolve<object>(name);
         if (!(content is FrameworkElement dialogContent))
         {
             throw new NullReferenceException("A dialog's content must be a FrameworkElement");
         }

         AutowireViewModel(dialogContent);

         if (!(dialogContent.DataContext is IDialogAware dialogAware))
         {
             throw new ArgumentException("A dialog's ViewModel must implement IDialogAware interface");
         }

         var openedEventHandler = new DialogOpenedEventHandler((sender, args) =>
         {
             dialogAware.OnDialogOpened(parameters);
         });
         var closedEventHandler = new DialogClosedEventHandler((sender, args) =>
         {
             dialogAware.OnDialogClosed();
         });

         dialogAware.RequestClose += res =>
         {
             if (DialogHost.IsDialogOpen(dialogHostName))
             {
                 callback?.Invoke(res);
                 DialogHost.Close(dialogHostName);
             }
         };

         var dispatcherFrame = new DispatcherFrame();
         if (dialogHostName == null)
         {
             _ = DialogHost.Show(dialogContent, openedEventHandler, null, closedEventHandler)
                 .ContinueWith(_ => dispatcherFrame.Continue = false); ;
         }
         else
         {
             _ = DialogHost.Show(dialogContent, dialogHostName, openedEventHandler, null, closedEventHandler)
                 .ContinueWith(_ => dispatcherFrame.Continue = false);
         }

         try
         {
             // tell users we're going modal
             ComponentDispatcher.PushModal();

             Dispatcher.PushFrame(dispatcherFrame);
         }
         finally
         {
             // tell users we're going non-modal
             ComponentDispatcher.PopModal();
         }

         dialogAware.RequestClose -= callback;
     }

     private static void AutowireViewModel(object viewOrViewModel)
     {
         if (viewOrViewModel is FrameworkElement view && view.DataContext is null && ViewModelLocator.GetAutoWireViewModel(view) is null)
         {
             ViewModelLocator.SetAutoWireViewModel(view, true);
         }
     }
 }

Prism 9.0 定义 MaterialDialogService

MaterialDialogService

 public class MaterialDialogService : DialogService
 {
     private readonly IContainerExtension _containerExtension;

     public MaterialDialogService(IContainerExtension containerExtension) : base(containerExtension)
     {
         _containerExtension = containerExtension;
     }

     public void ShowDialogHost(string name, IDialogParameters parameters, Action<IDialogResult> callback) =>
         ShowDialogHost(name, null, parameters, callback);

     public void ShowDialogHost(string name, string dialogHostName, IDialogParameters parameters, Action<IDialogResult> callback)
     {
         if (parameters == null)
             parameters = new DialogParameters();

         var content = _containerExtension.Resolve<object>(name);
         if (!(content is FrameworkElement dialogContent))
         {
             throw new NullReferenceException("A dialog's content must be a FrameworkElement");
         }

         AutowireViewModel(dialogContent);

         if (!(dialogContent.DataContext is IDialogAware dialogAware))
         {
             throw new ArgumentException("A dialog's ViewModel must implement IDialogAware interface");
         }
         var openedEventHandler = new DialogOpenedEventHandler((sender, args) =>
         {
             dialogAware.OnDialogOpened(parameters);
         });
         var closedEventHandler = new DialogClosedEventHandler((sender, args) =>
         {
             dialogAware.OnDialogClosed();
         });

         Action<IDialogResult> requestCloseHandler = (r) =>
         {
             if (DialogHost.IsDialogOpen(dialogHostName))
             {
                 callback?.Invoke(r);
                 DialogHost.Close(dialogHostName);
             }
         };
         DialogUtilities.InitializeListener(dialogAware, requestCloseHandler);


         var dispatcherFrame = new DispatcherFrame();
         if (dialogHostName == null)
         {
             _ = DialogHost.Show(dialogContent, openedEventHandler, null, closedEventHandler)
                 .ContinueWith(_ => dispatcherFrame.Continue = false); ;
         }
         else
         {
             _ = DialogHost.Show(dialogContent, dialogHostName, openedEventHandler, null, closedEventHandler)
                 .ContinueWith(_ => dispatcherFrame.Continue = false);
         }

         try
         {
             // tell users we're going modal
             ComponentDispatcher.PushModal();

             Dispatcher.PushFrame(dispatcherFrame);
         }
         finally
         {
             // tell users we're going non-modal
             ComponentDispatcher.PopModal();
         }

     }

     private static void AutowireViewModel(object viewOrViewModel)
     {
         if (viewOrViewModel is FrameworkElement view && view.DataContext is null && ViewModelLocator.GetAutoWireViewModel(view) is null)
         {
             ViewModelLocator.SetAutoWireViewModel(view, true);
         }
     }
 }

注入模块中的服务

  1. 服务是在模块中注册
  2. 不能在主窗体的 MainWindowViewModel 中注入服务,会报错,需要在导航的子页面中去注入。
  3. 在Loaded 事件中导航到子页面

以使用 ESUI 模块为例。

(1)创建一个WPF项目,并引用和店家模块

using ESUI;
using ESUITest.Services;
using ESUITest.Views;
using Prism.Ioc;
using Prism.Modularity;
using Prism.Regions;
using Prism.Unity;
using System;
using System.Windows;

namespace ESUITest
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : PrismApplication
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterSingleton<IESUITestService, ESUITestService>();
            containerRegistry.RegisterForNavigation<MainView>();
        }

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            Uri aero = new Uri("/PresentationFramework.Classic, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/classic.xaml", UriKind.Relative);
            Resources.MergedDictionaries.Add(Application.LoadComponent(aero) as ResourceDictionary);

        }
        protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
        {
            moduleCatalog.AddModule<ESUIModule>();
        }
      
    }
}

MainWindow.xaml

<Window x:Class="ESUITest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ESUITest"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        mc:Ignorable="d"
        Title="{Binding Title}" Height="900" Width="1600">

    <Grid>
        <ContentControl prism:RegionManager.RegionName="{x:Static local:RegionNames.ContentRegion}" />
    </Grid>
    
</Window>

MainWindow.cs

using Prism.Regions;
using System.Windows;

namespace ESUITest.Views
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly IRegionManager _regionManager;

        public MainWindow(IRegionManager regionManager)
        {
            InitializeComponent();
            this.Loaded += MainWindow_Loaded;
            _regionManager = regionManager;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            _regionManager.RequestNavigate(RegionNames.ContentRegion, "MainView");
        }
    }
}


MainViewModel.cs

using ESUI.MenuViewModels;
using ESUI.Mvvm;
using ESUI.Services.api;
using ESUITest.Services;
using Prism.Regions;

namespace ESUITest.ViewModels
{
    internal class MainViewModel : BindableBase, IDestructible, INavigationAware, IConfirmNavigationRequest
    {
        private ATS_SystemMenuVM _mainMenu;
        public ATS_SystemMenuVM MainMenu
        {
            get { return _mainMenu; }
            set { SetProperty(ref _mainMenu, value); }
        }

        public MainViewModel(IRegionManager regionManager, IESUITestService eSUITestService, IESService esService) : base(regionManager)
        {
            MainMenu = esService.CreateATS_SystemMenuVM("系统");
        }
    }
}

DevExpress

官网

文档手册

版本记录

DevExPressFramework4.8.net 7.0.net 8.0
v23.1×
v23.2(还未发布)

安装和激活

官网下载进行安装,国内慧都(代理商)下载地址 上下载,目前为23.1版本,还不能支持 .net8.0,如果下面的网址打不开,直接在官网下载最新的即可。

下载(群晖下载)后使用科技进行解锁使用,运行下图中的程序,选择devexpress的版本和vs的版本

1722506797754

创建项目

vs中直接创建DevExpress的模板项目,可以选择使用的.net版本

1722507105165

案例

DevExpress 安装时,提供了一系列案例(包含源码和编译好的exe)

运行案例程序

  • 第一种:在 VS 中直接运行案例程序:

DevExpress 案例程序

  • 第二种:在开始菜单中运行案例exe程序(操作略)。

查看案例源码

每个案例窗口上可以查看源码或者直接打开源码解决方案,如下图:

alt text

同时也可以通过开始菜单,所有程序->DevExpress xx.x 中打开源码路径

DevExpress demo source code

路径默认在 公共用户 的文档目录下:

C:\Users\Public\Documents\DevExpress Demos 23.2

汉化

网络上教程DevExpress:手把手教你用官方汉化资源做汉化

防丢失记录

  • 进入官网 https://www.devexpress.com/MyAccount/,使用账号登录

  • 点击 支持(SUPPORT $ DOCS)

  • 点击本地化 (Localization)

    alt text

  • 选择当前使用的 DevExpress 版本和对应的语言

  • 中文包相关说明

    • zh-CN:简体中文
    • zh-hans:简体中文,和zh-CN等价,在net 6.0中发布时,默认已经有一个 zh-hans包,我们只需要使用这个就可以了
    • zh-hant:繁体中文

    alt text

  • 点击下载,大概半个小时以邮件的方式发送连接给我们

  • 收到邮件后下载(邮件中的附件地址有过期时间),解压后包含源码文件夹 src 和已经发布的汉化文件 dll, 拷贝需要的dll到自己的工程中,如:zh-hans

  • 源码可自定义汉化,并且处理一些原本未汉化的内容。

汉化失败处理

官方提供的汉化包,有些没有汉化,可以通过提供的源码自己更改汉化。

(1)通过邮件附件中附带的src源码工程添加缺少的汉化,注意工程中勾选了生产签名,取消签名即可。

(2)通过 资源设置 来配置汉化。

Windows 窗口,最小化按钮汉化失败处理对比:

版本:23.2.4 wpf

修改前

1722507003938

修改后

1722507009226

功能模块

SplashScreen

软件启动时的加载界面

模板自带的启动画面

版本: 23.2

软件启动时的加载界面,使用官方主题模板创建项目,会有一个启动界面,但是不能修改界面上的公司、版权、版本以及logo等信息,原因是模板中使用的默认画面。

 static App()
 {
     CompatibilitySettings.UseLightweightThemes = true;
     ApplicationThemeHelper.UpdateApplicationThemeName();
     //SplashScreenManager.CreateThemed().ShowOnStartup(); //这里直接默认创建了,但是不能修改信息
 }

自定义启动加载画面

根据官方文档(失败)

自定义启动加载画面,根据官网文档以及下载github上的模板参考后还是没有成功,后续再研究

SplashScreenManagerService | WPF Controls | DevExpress Documentation

根据脚本中的说明

**版本:**23.2

在创建SplashScreen后,在xaml脚本中有段说明

<!-- To show the splash screen on the application startup, add the following code to App.xaml.cs:

static App() {

  var splashScreenViewModel = new DXSplashScreenViewModel() { Title = "MyApplication" };

  SplashScreenManager.Create(() => new SplashScreen(), splashScreenViewModel).ShowOnStartup();

}

Refer to the following articles for more information:

  [docs.devexpress.com/WPF/401685/controls-and-libraries/windows-and-utility-controls/splash-screen-manager?v=20.1#show-splash-screen](http://docs.devexpress.com/WPF/401685/controls-and-libraries/windows-and-utility-controls/splash-screen-manager?v=20.1#show-splash-screen)

  [docs.devexpress.com/WPF/401692/mvvm-framework/services/predefined-set/splashscreenmanagerservice?v=20.1#show-splash-screen-from-view-model](http://docs.devexpress.com/WPF/401692/mvvm-framework/services/predefined-set/splashscreenmanagerservice?v=20.1#show-splash-screen-from-view-model)

NOTE: SplashScreenWindow uses its own theme mechanism to speed up initialization.

For this reason, the current theme will not be applied to any DevExpress or standard controls placed inside the splash screen.

To apply the default theme mechanism to the splash screen content, set AllowTheming="True" for SplashScreenWindow. -->

根据说明在App.cs中添加以下内容

   DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(47, 1);
   defaultInterpolatedStringHandler.AppendLiteral("Copyright © ");
   defaultInterpolatedStringHandler.AppendFormatted(DateTime.Today.Year);
   defaultInterpolatedStringHandler.AppendLiteral(" 上海田之金计算机科技有限公司.\nAll rights reserved.");
   string copyright = defaultInterpolatedStringHandler.ToStringAndClear();

   var splashScreenViewModel = new DXSplashScreenViewModel()
   {
       //Title = "维护工具",
       Status = "加载中...",
       //Logo = new Uri("pack://application:,,,/logo2.png"), //logo在模板中设置了
       //Logo = new Uri("../logo.png", UriKind.Relative),
       Copyright = copyright,
       //Subtitle = Application.ResourceAssembly.GetName().Version.ToString()
   };
   SplashScreenManager.Create(() => new Views.SplashScreen(), splashScreenViewModel).ShowOnStartup();

注意

  • Logo 图片的属性要设置为 资源,设置为内容,发布后不显示
  • 发布时,选择单文件发布,可减少文件数量,但是注意Logo图片要设置为 资源。

1722507276944

GridControl

(1) 行模块中绑定按钮

注意当我们点击行中的某个按钮,把当前行绑定的对象作为参数传递给命令函数时:

  • WPF 自带或者使用(PRISM)的写法 CommandParameter="{Binding}",在 DevExpress 框架中要这样写 CommandParameter="{Binding RowData.Row}"

前端 ItemsPerson 集合

 <dxg:GridControl Grid.Row="1" Name="grid" ShowBorder="False" ItemsSource="{Binding Items}" AutoGenerateColumns="None">
     <dxg:GridControl.View>
         <dxg:TableView x:Name="view"
                        ShowHorizontalLines="True"
                        ShowGroupPanel="False"
                        AllowSorting="False"
                        AllowColumnFiltering="False"
                        AllowHorizontalScrollingVirtualization="True"
                     />
     </dxg:GridControl.View>
     <dxg:GridControl.Columns>
         <dxg:GridColumn FieldName="Id" Header="Id"/>
         <dxg:GridColumn FieldName="Name" Header="Name"/>
         <dxg:GridColumn FieldName="Age" Header="Age"/>
         <dxg:GridColumn  Header="操作" AllowEditing="False">
             <dxg:GridColumn.CellTemplate>
                 <DataTemplate>
                     <dx:SimpleButton Content="删除" Width="100" 
                                      Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=dxg:GridControl} }" 
                                      CommandParameter="{Binding RowData.Row}"/>
                 </DataTemplate>
             </dxg:GridColumn.CellTemplate>
         </dxg:GridColumn>
     </dxg:GridControl.Columns>
 </dxg:GridControl>

后端写法一、使用 Command 特性,注意:此时前端以 Command 结尾,但是后端不需要此结尾

[Command]
 public async Task Delete(Persion data)
 {
    //todo
 }

后端写法二、使用 DelegateCommand ,和其他框架一致。

public DelegateCommand<Person> DeleteCommand { get; set; }
public MainViewModel()
{
    DeleteCommand = new DelegateCommand<Person>((r) => { 

        //todo:

     });
}

RichEditControl

富文本控件:可以渲染长文和图片。可以读取 mht/rtf/odt 等格式的文件,但是使用后,如下效果:

  • rtf: 显示正常(注意段落中不要打空格,否则会直接换行)
  • mht: 中文显示 ?号,英文和图片正常显示(未解决)
  • odt:直接没有渲染出来

使用描述(RTF):

  1. 通过 Word 编辑好文本后,转化为 tft 格式
  2. 后端加载文件
using DevExpress.Mvvm;
using DevExpress.Mvvm.POCO;
using Microsoft.Win32;
using System.Collections.ObjectModel;
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Windows.Media;
using System.IO;
using System.Linq;
using TsimTool.Properties;
using System.Windows;

namespace TsimTool.ViewModels
{
    public class ToolDescriptionViewModel : ViewModelBase
    {
        private string _text;

        /// <summary>
        /// 
        /// </summary>
        public string Text
        {
            get { return _text; }
            set { _text = value; }
        }

        public ToolDescriptionViewModel() {

            Text = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"Docs/工具说明.rtf"));
        }

    }
}

  1. 前端绑定(注意要使用rtf转换器),感觉控件是根据绑定的文本内容格式来自动设定控件的内容格式,但是要手动指定转换器
<UserControl xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"  
             xmlns:dxre="http://schemas.devexpress.com/winfx/2008/xaml/richedit"  
             xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm" 
             xmlns:ViewModels="clr-namespace:TsimTool.ViewModels" 
             xmlns:dxe1="http://schemas.devexpress.com/winfx/2008/xaml/docking/visualelements"
             xmlns:dxp="http://schemas.devexpress.com/winfx/2008/xaml/printing"  
             x:Class="TsimTool.Views.ToolDescriptionView"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="1000">
    <UserControl.Resources>
        <dxre:MhtToContentConverter x:Key="MhtToContentConverter"/>
        <dxre:RtfToContentConverter x:Key="RtfToContentConverter"/>
    </UserControl.Resources>
    <UserControl.DataContext>
        <ViewModels:ToolDescriptionViewModel/>
    </UserControl.DataContext>
    <dxre:RichEditControl x:Name="richEditControl"  
    ReadOnly="True" ActiveViewType="Simple" ShowBorder="False" 
    HorizontalScrollBarVisibility="Collapsed" HorizontalAlignment="Stretch"
    Content="{Binding Path=Text, Converter={StaticResource RtfToContentConverter}}"/>

</UserControl>

托盘应用开发

设置启动时不显示窗体

通过重写 OnLoad 来实现

protected override void OnLoad(EventArgs e)
     {
         base.OnLoad(e);
         this.Visible = false;
         this.ShowInTaskbar = false;
     }

异常

在构造函数中添加 this.ShowInTaskbar = false; 写了这句,导致窗体一直显示,出现异常

设置托盘图标

注意事项

  • NotifyIcon 必须设置图标,否则不会显示
  • ContextMenuStrip:作为菜单,可设置菜单勾选
 // 创建 NotifyIcon。
         this.notifyIcon1 = new NotifyIcon();
         this.notifyIcon1.Icon = this.Icon;
         this.notifyIcon1.Visible = true;
         this.notifyIcon1.MouseDoubleClick += NotifyIcon1_MouseDoubleClick;
         // 创建 ContextMenuStrip。
         this.notifyIconMenuStrip = new ContextMenuStrip();
         // 创建并初始化 ToolStripMenuItem 对象。
         ToolStripMenuItem showFormItem = new ToolStripMenuItem("显示窗体");
         showFormItem.Click += (object? sender, EventArgs e) =>
         {
             NotifyIcon1_MouseDoubleClick(sender, null);
         };

         ToolStripMenuItem item2 = new ToolStripMenuItem("开机自启动") { CheckOnClick = true, Checked = true };
         item2.CheckedChanged += (object? sender, EventArgs e) =>
         {
             bool isAuto = ((ToolStripMenuItem)sender).Checked;
             SetAutoStart(isAuto);
         };

         ToolStripMenuItem item3 = new ToolStripMenuItem("退出");
         item3.Click += (object? sender, EventArgs e) =>
         {
             Application.Exit();  // 退出应用程序
         };
         // 将 ToolStripMenuItem 对象添加到 ContextMenuStrip 的 Items 集合中。
         this.notifyIconMenuStrip.Items.Add(showFormItem);
         this.notifyIconMenuStrip.Items.Add(item2);
         this.notifyIconMenuStrip.Items.Add(item3);
         // ContextMenu 属性设置当右键点击系统托盘图标时显示的菜单。
         notifyIcon1.ContextMenuStrip = this.notifyIconMenuStrip;

其他开机启动方式(不推荐)

  • 拷贝快捷方式到启动菜单
  • 添加任务几乎

设置气泡消息

  • 设置消息标题
  • 设置消息内容
  • 设置消息图标
  • 设置显示时间
  • 没找到在那里更改程序集的名称,如案例中为 TSCT 它就显示TSCT
notifyIcon1.BalloonTipIcon = ToolTipIcon.Info;
notifyIcon1.BalloonTipTitle = "提示";
notifyIcon1.BalloonTipText = "培训系统被控端已启动";
notifyIcon1.ShowBalloonTip(3000);

设置开机启动

重要部分!!!

实现向其他软件一样的可以通过界面操作设置软件是否开机启动,采用的方式为修改注册表,有几个注意事项

  • 不要设置软件为管理员权限启动,这样开机启动无效(有些电脑管理员启动也有效。暂时搞不懂)
  • 修改当前用户的注册表(Registry.CurrentUser),不要修改本机的注册表(Registry.LocalMachine),否则会要求管理员权限,从而导致启动失败
string R_startPath = Application.ExecutablePath;
         try
         {
             RegistryKey R_currentUser = Registry.CurrentUser; //不要使用本地,本地的无法开机启动
             RegistryKey? R_run = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
             if (autoStart)
             {
                 if (R_run == null)
                     R_run = Registry.CurrentUser.CreateSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
                 R_run.SetValue(Properties.Resources.RegistryKey, R_startPath);
                 R_run.Close();
                 OnMsgEventHandler(MsgType.Info, "软件设置自动启动成功!");
             }
             else
             {
                 if (R_run != null)
                 {
                     R_run.DeleteValue(Properties.Resources.RegistryKey);
                     R_run.Close();
                     OnMsgEventHandler(MsgType.Info, "软件禁止自动启动成功!");
                 }
             }
         }
         catch (Exception e)
         {
             OnMsgEventHandler(MsgType.Info, $"设置软件的开机状态:{autoStart} 出现异常!" + e.Message);
             MessageBox.Show("设置软件的开机状态异常,如那件正常启动,但是下次可能不会自动启动", "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
         }

全部代码

可能会存在其他类的引用,可自行判断

using System.Windows.Forms;
 public partial class Form1 : Form
 {
     private NotifyIcon notifyIcon1;

     private ContextMenuStrip notifyIconMenuStrip;
     // 右键菜单
     private ContextMenuStrip itemMenuStrip;
     public Form1()
     {
         InitializeComponent();
         InitForm();
         InitTrayIcon();
         /*
          软件每次启动,强制设置为开机启动,启动后可手动禁止开机启动
          */
         SetAutoStart(true);
     }

     private void InitForm()
     {
         OnMsgEventHandler(MsgType.Info, "软件启动");

         this.Text = Properties.Resources.AppTitle + $" V{GetVersion()}";
         this.Visible = false;
         this.StartPosition = FormStartPosition.CenterScreen;
         //this.ShowInTaskbar = false; //此语句导致窗体会显示,必须注释
         Program.OnMsgEvent += OnMsgEventHandler;
         // 初始化右键菜单
         itemMenuStrip = new ContextMenuStrip();
         ToolStripMenuItem copyMenuItem = new ToolStripMenuItem("复制");
         copyMenuItem.Click += CopyMenuItem_Click;
         itemMenuStrip.Items.Add(copyMenuItem);

         // 绑定右键菜单到ListBox
         listBox1.ContextMenuStrip = itemMenuStrip;

         listBox1.DrawMode = DrawMode.OwnerDrawVariable;
         listBox1.MeasureItem += ListBox1_MeasureItem;
         listBox1.DrawItem += listBox1_DrawItem;
         listBox1.HorizontalScrollbar = true; // 设置横向滚动条为可见
     }


     private void OnMsgEventHandler(MsgType type, string message)
     {
         MsgInfo msg = new MsgInfo() { Message = DateTime.Now.ToString("HH:mm:ss: ") + message, Type = type };
         // 判断是否需要在UI线程中执行
         if (this.InvokeRequired)
         {
             // 使用Invoke在UI线程中执行
             this.Invoke(new Action(() =>
             {
                 listBox1.DrawMode = DrawMode.OwnerDrawFixed;
                 this.listBox1.Items.Add(msg);
             }));
         }
         else
         {
             // 如果已在UI线程中,直接更新UI
             this.listBox1.Items.Add(msg);
         }
     }


     private void ListBox1_MeasureItem(object? sender, MeasureItemEventArgs e)
     {
         e.ItemHeight = (int)e.Graphics.MeasureString(listBox1.Items[e.Index].ToString(), listBox1.Font, listBox1.Width).Height;
     }

     private void listBox1_DrawItem(object? sender, DrawItemEventArgs e)
     {
         if (e.Index < 0) return;
         e.DrawBackground();
         MsgInfo info = (MsgInfo)listBox1.Items[e.Index];
         Color color = Color.White;
         switch (info.Type)
         {
             case MsgType.Error:
                 color = Color.Red;
                 break;
             case MsgType.Warning:
                 color = Color.Yellow;
                 break;
             case MsgType.Info:
                 break;
         }

         e.DrawBackground();
         //e.Graphics.DrawString(info.Message, e.Font, new SolidBrush(color), new PointF(e.Bounds.X, e.Bounds.Y));
         e.Graphics.DrawString(info.Message, e.Font, new SolidBrush(color), e.Bounds);

         // 获取文本的实际宽度,并设置为横向滚动条的最大值
         float textWidth = e.Graphics.MeasureString(info.Message, e.Font).Width;
         if (textWidth > listBox1.HorizontalExtent)
         {
             listBox1.HorizontalExtent = (int)textWidth;
         }

         e.DrawFocusRectangle();
     }


     private void button1_Click(object sender, EventArgs e)
     {
         listBox1.Items.Clear();
         listBox1.HorizontalExtent = 0;
     }

     private void button2_Click(object sender, EventArgs e)
     {
         if (listBox1.Items.Count > 10)
         {
             for (int i = listBox1.Items.Count - 10; i > -1; i--)
             {
                 listBox1.Items.RemoveAt(i);
             }
         }
         UpdateHorizontalScrollBar();
     }

     // 更新横向滚动条的宽度
     private void UpdateHorizontalScrollBar()
     {
         int maxWidth = 0;

         // 计算当前列表中最长文本的宽度
         foreach (MsgInfo item in listBox1.Items)
         {
             int itemWidth = (int)listBox1.CreateGraphics().MeasureString(item.Message, listBox1.Font).Width;
             if (itemWidth > maxWidth)
             {
                 maxWidth = itemWidth;
             }
         }

         // 将最长文本的宽度设置为横向滚动条的最大值
         if (maxWidth > listBox1.HorizontalExtent)
         {
             listBox1.HorizontalExtent = maxWidth;
         }
     }

     private void CopyMenuItem_Click(object? sender, EventArgs e)
     {
         // 获取选定项的文本
         string selectedText = "";

         foreach (var item in listBox1.SelectedItems)
         {
             selectedText += ((MsgInfo)item).Message.ToString() + Environment.NewLine;
         }

         // 将文本放入剪贴板
         Clipboard.SetText(selectedText);
     }

     private void InitTrayIcon()
     {
         // 创建 NotifyIcon。
         this.notifyIcon1 = new NotifyIcon();
         this.notifyIcon1.Icon = this.Icon;
         this.notifyIcon1.Visible = true;
         this.notifyIcon1.MouseDoubleClick += NotifyIcon1_MouseDoubleClick;
         // 创建 ContextMenuStrip。
         this.notifyIconMenuStrip = new ContextMenuStrip();
         // 创建并初始化 ToolStripMenuItem 对象。
         ToolStripMenuItem showFormItem = new ToolStripMenuItem("显示窗体");
         showFormItem.Click += (object? sender, EventArgs e) =>
         {
             NotifyIcon1_MouseDoubleClick(sender, null);
         };

         ToolStripMenuItem item2 = new ToolStripMenuItem("开机自启动") { CheckOnClick = true, Checked = true };
         item2.CheckedChanged += (object? sender, EventArgs e) =>
         {
             bool isAuto = ((ToolStripMenuItem)sender).Checked;
             SetAutoStart(isAuto);
         };

         ToolStripMenuItem item3 = new ToolStripMenuItem("退出");
         item3.Click += (object? sender, EventArgs e) =>
         {
             Application.Exit();  // 退出应用程序
         };
         // 将 ToolStripMenuItem 对象添加到 ContextMenuStrip 的 Items 集合中。
         this.notifyIconMenuStrip.Items.Add(showFormItem);
         this.notifyIconMenuStrip.Items.Add(item2);
         this.notifyIconMenuStrip.Items.Add(item3);
         // ContextMenu 属性设置当右键点击系统托盘图标时显示的菜单。
         notifyIcon1.ContextMenuStrip = this.notifyIconMenuStrip;
         ShowStartInfo();
     }

     private void NotifyIcon1_MouseDoubleClick(object? sender, MouseEventArgs e)
     {
         this.ShowInTaskbar = true;
         this.Show();  // 显示窗体
         this.WindowState = FormWindowState.Normal;  // 恢复窗体正常大小
     }
     private void UpdateNoticyIconText(bool isAutoStart)
     {
         string yes = isAutoStart ? "是" : "否";
         this.notifyIcon1.Text = $"{Properties.Resources.AppTitle}\n端口:{Program.port}\n开机启动:{yes}";
     }

     private void ShowStartInfo()
     {
         notifyIcon1.BalloonTipIcon = ToolTipIcon.Info;
         notifyIcon1.BalloonTipTitle = "提示";
         notifyIcon1.BalloonTipText = "培训系统被控端已启动";
         notifyIcon1.ShowBalloonTip(3000);
     }

     protected override void OnLoad(EventArgs e)
     {
         base.OnLoad(e);
         this.Visible = false;
         this.ShowInTaskbar = false;
     }

     protected override void OnFormClosing(FormClosingEventArgs e)
     {
         if (e.CloseReason == CloseReason.UserClosing)
         {
             e.Cancel = true;  // 取消关闭窗体
             this.Hide();  // 隐藏窗体
         }
     }


     //开机启动
     private void SetAutoStart(bool autoStart)
     {
         string R_startPath = Application.ExecutablePath;
         try
         {
             RegistryKey R_currentUser = Registry.CurrentUser; //不要使用本地,本地的无法开机启动
             RegistryKey? R_run = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
             if (autoStart)
             {
                 if (R_run == null)
                     R_run = Registry.CurrentUser.CreateSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
                 R_run.SetValue(Properties.Resources.RegistryKey, R_startPath);
                 R_run.Close();
                 OnMsgEventHandler(MsgType.Info, "软件设置自动启动成功!");
             }
             else
             {
                 if (R_run != null)
                 {
                     R_run.DeleteValue(Properties.Resources.RegistryKey);
                     R_run.Close();
                     OnMsgEventHandler(MsgType.Info, "软件禁止自动启动成功!");
                 }
             }
         }
         catch (Exception e)
         {
             OnMsgEventHandler(MsgType.Info, $"设置软件的开机状态:{autoStart} 出现异常!" + e.Message);
             MessageBox.Show("设置软件的开机状态异常,如那件正常启动,但是下次可能不会自动启动", "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
         }
         UpdateNoticyIconText(autoStart);
     }

     private string GetVersion()
     {
         return Assembly.GetExecutingAssembly().GetName().Version.ToString();
     }
 }

Devexpress

DocumetManager

界面提示设置的是ContainerControl ,但是要设置ClientControl才正常

alt text

Asp.Net

控制器

访问路径问题

假设在本机访问,端口为 80, 代码示例,配置了两个接口 Get1Get2

    [ApiController]
    [Route("[controller]")]
    public class TestController : ControllerBase
    {

        //使用 http://localhost/Test/Get1 访问
        [HttpGet("Get1")] 
        public async Task<int> Get1()
        {
            //...
        }
        //使用 http://localhost/Get2 访问
        [HttpGet("/Get2")]
        public async Task<int> Get2()
        {
            //...
        }
    }
  • 当 HttpGet 中的路由地址以 / 开头,那么地址不会加上控制器的名称,如:http://localhost/Get2
  • 当 HttpGet 中的路由地址不以 / 开头,那么地址会自动加上控制器的名称,如:http://localhost/Test/Get1

全局异常捕获

默认的异常提示

2024-09-18 17:47:51.7860 error:An unhandled exception has occurred while executing the request.

希望的异常提示,最好是有栈跟踪,或者具有明确的错误信息。

通过控制器的过滤器捕获

具有栈跟踪

定义过滤器

 public class CustomExceptionFilter : IExceptionFilter
 {
     private readonly ILogger<CustomExceptionFilter> _logger;

     // 注入日志服务
     public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger)
     {
         _logger = logger;
     }
     public void OnException(ExceptionContext context)
     {
         var exception = context.Exception;

         _logger.LogError("控制器异常:"+exception.ToString());
         // 记录异常
         context.Result = new RedirectToActionResult("Error", "Home", null);
         context.ExceptionHandled = false; // 标记异常未处理,让全局获取
     }
 }

注册

    // 注册全局异常过滤器
    builder.Services.AddControllersWithViews(options =>
    {
        options.Filters.Add<CustomExceptionFilter>(); // 全局添加过滤器
    });


捕获控制器的过滤器无法处理的

无栈跟踪,提供错误信息

    builder.Services.AddProblemDetails(options =>
            options.CustomizeProblemDetails = (context) =>
            {
                logger.Error($"全局异常捕获:Title: {context.ProblemDetails.Title},Detail: {context.ProblemDetails.Detail}");

            }
        );

部署 IIS

说明:使用IIS部署,通常是 http 协议,后面要通过 Nginx 来代理访问,因为 Nginx 配置证书等比较方便

官网手册

以 Windows Server 为例:

先决条件

  • NET Core SDK 安装在开发计算机上。如果发布的程序勾选了独立运行,可以不安装SDK。
  • 安装IIS,自行网上查找资料。
  • 在 IIS 服务器上安装 .NET Core 托管捆绑包。 捆绑包可安装 .NET Core 运行时、.NET Core 库和 ASP.NET Core 模块。 该模块允许 ASP.NET Core 应用在 IIS 后面运行。
  • 上面安装的托管捆绑包应该包含了 AspNetCoreModuleV2

发布Asp.NET程序

发布到文件夹。

不要发布单文件格式,好像无法运行。

创建站点

创建应用程序池:在IIS管理器中创建一个新的应用程序池,注意:

  • 当部署Blazor项目时:设置.NET CLR版本为“No Managed Code”,正常运行
  • 当部署 Web API 项目,并且从 Blazor 项目中跨域访问时(注:API项目中已经配置了跨域策略),这里要选择一个版本,否则会因 跨域问题 无法访问。

1723516732615

创建网站:设置网站名称(仅显示作用),选择自己创建的应用程序池,选择程序目录,设置端口,IP地址和主机名不用填,默认就行

1723516895463

使用 Nginx部署

问题汇总

【开发】以一种访问权限不允许的方式做了一个访问套接字的尝试

经验:多改几个端口尝试,且不要用一些特殊的端口

网上说是防火墙、权限或者端口被占用导致的,解决办法

  • 以管理员权限运行VS
  • 检查端口是否被占用,查询某个端口 netstat -ano | findstr :7000,或者使用 netsh interface ipv4 show excludedportrange protocol=tcp 查询端口是否被占用

特殊情况

防火墙全部关闭、已经使用管理员运行了VS、检查端口 7000 也没被占用,但是未解决问题:

改变端口尝试,并且如7001、7002等也不行,改为7340解决了问题。

【部署】有的浏览器不能访问

环境

  • 阿里云域名
  • 阿里云免费云解析
  • 阿里云服务器(带固定公网IP)
  • 未备案

阿里云售后给出的答复是,网站未备案,详细回复如下:

您好,www.xxx.com看解析到阿里云大陆主机,未在阿里云备案导致备案拦截无法访问

网站托管在中国内地(大陆)的服务器上,您需根据所在省市的管局规则进行备案申请。当您使用阿里云中国内地(大陆)节点服务器时,您可以在PC端或移动端的阿里云ICP代备案系统中提交ICP备案申请,审核通过便可开通网站访问服务。 https://help.aliyun.com/document_detail/61819.html

备案后

网站正常访问。

【部署】重启IIS服务可能存在访问异常

错误信息: Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery|An exception was thrown while deserializing the token. Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The antiforgery token could not be decrypted.

当IIS重启过程中,客户端通过刷新浏览器发起请求,由于两次请求的服务不一样,导致出现此错误信息,应该不影响。

SignalR

Signal Sample# SignalR

Hub

配置Hub

若要注册 SignalR 中心所需的服务,请调用 Program.cs 中的 AddSignalR:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

若要配置 SignalR 终结点,请调用同时在 Program.cs 中的 MapHub:

app.MapRazorPages();
app.MapHub<ChatHub>("/Chat");

app.Run();

⚠️ 备注

ASP.NET Core SignalR 服务器端程序集现在随 .NET Core SDK 一起安装。 有关详细信息,请参阅共享框架中的 SignalR 程序集。

创建和使用Hub

通过声明从 Hub 继承的类来创建中心。 将 public 方法添加到类,使其可从客户端调用:

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
        => await Clients.All.SendAsync("ReceiveMessage", user, message);
}

⚠️ 备注

中心是暂时性的:

  • 请勿将状态存储在中心类的属性中。 每个中心方法调用都在新的中心实例上执行。
  • 请勿通过依赖关系注入直接实例化中心。 若要从应用程序中的其他位置向客户端发送消息,请使用 IHubContext。
  • 调用依赖于保持活动状态的中心的异步方法时请使用 await。 例如,如果在没有 await 的情况下进行调用,则 Clients.All.SendAsync(...) 这类方法会失败,并且中心方法会在 SendAsync 完成之前完成。

上下文对象

Hub 类具有一个 Context 属性,该属性包含具有连接相关信息的以下属性:

属性说明解释
ConnectionId获取连接的唯一 ID(由 SignalR 分配)。 每个连接有一个连接 ID。每次调用方法时,Hub都是新示例
UserIdentifier获取用户标识符。 默认情况下,SignalR 使用与连接关联的 ClaimsPrincipal 中的 ClaimTypes.NameIdentifier 作为用户标识符。
User获取与当前用户关联的 ClaimsPrincipal。
Items获取可用于在此连接范围内共享数据的键/值集合。 数据可以存储在此集合中,会在不同的中心方法调用间为连接持久保存。
Features获取连接上可用的功能的集合。 目前,在大多数情况下不需要此集合,因此未对其进行详细记录。
ConnectionAborted获取一个 CancellationToken,它会在连接中止时发出通知。

Hub.Context 还包含以下方法:

方法说明解释
GetHttpContext返回连接的 HttpContext,如果连接不与 HTTP 请求关联,则返回 null。 对于 HTTP 连接,可以使用此方法获取 HTTP 标头和查询字符串等信息。每次调用方法时,Hub都是新示例
Abort中止连接。

客户端对象

Hub 类具有一个 Clients 属性,该属性包含适用于服务器与客户端之间的通信的以下属性:

属性说明解释
All对所有连接的客户端调用方法
Caller对调用了中心方法的客户端调用方法哪个客户端发过来的,就发送给哪个
Others对所有连接的客户端调用方法(调用了方法的客户端除外)除了调用者,都发送

Hub.Clients |还包含以下方法:

方法说明解释
AllExcept对所有连接的客户端调用方法(指定连接除外)
Client对连接的一个特定客户端调用方法
Clients对连接的多个特定客户端调用方法
Group对指定组中的所有连接调用方法需要一个组id
GroupExcept对指定组中的所有连接调用方法(指定连接除外)
Groups对多个连接组调用方法
OthersInGroup对一个连接组调用方法(不包括调用了中心方法的客户端)
User对与一个特定用户关联的所有连接调用方法
Users对与多个指定用户关联的所有连接调用方法

以上表中的每个属性或方法都返回具有 SendAsync 方法的对象。 SendAsync 方法接收要调用的客户端方法的名称和任何参数。

Client 和 Caller 方法返回的对象还包含一个 InvokeAsync 方法,该方法可用于等待来自客户端的结果。

向客户端发送消息

若要对特定客户端发出调用,请使用 Clients 对象的属性。 在下面的示例中,有三个中心方法:

  • SendMessage 使用 Clients.All 将消息发送到所有连接的客户端。
  • SendMessageToCaller 使用 Clients.Caller 将消息发送回调用方。
  • SendMessageToGroup 将消息发送给 SignalR Users 组中的所有客户端。
public async Task SendMessage(string user, string message)
    => await Clients.All.SendAsync("ReceiveMessage", user, message);

public async Task SendMessageToCaller(string user, string message)
    => await Clients.Caller.SendAsync("ReceiveMessage", user, message);

public async Task SendMessageToGroup(string user, string message)
    => await Clients.Group("SignalR Users").SendAsync("ReceiveMessage", user, message);

强类型Hub

使用 SendAsync 的缺点在于,它依赖于字符串来指定要调用的客户端方法。 如果客户端中的方法名称拼写错误或缺失,则这会使代码可能出现运行时错误。

使用 SendAsync 的替代方法是使用 Hub 将 Hub 类设为强类型。 在下面的示例中,ChatHub 客户端方法提取到名为 IChatClient 的接口中:

public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
}

此接口可用于将上面的 ChatHub 示例重构为强类型:

public class StronglyTypedChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
        => await Clients.All.ReceiveMessage(user, message);

    public async Task SendMessageToCaller(string user, string message)
        => await Clients.Caller.ReceiveMessage(user, message);

    public async Task SendMessageToGroup(string user, string message)
        => await Clients.Group("SignalR Users").ReceiveMessage(user, message);
}

使用 Hub 可以对客户端方法进行编译时检查。 这可防止由于使用字符串而导致的问题,因为 Hub 只能提供对接口中定义的方法的访问。 使用强类型 Hub 会禁止使用 SendAsync。

⚠️ 备注

Async 后缀不会从方法名称中去除。 除非使用 .on('MyMethodAsync') 定义客户端方法,否则不要使用 MyMethodAsync 作为名称。

客户端结果

除了对客户端进行调用外,服务器还可以从客户端请求结果。 这要求服务器使用 ISingleClientProxy.InvokeAsync,并且客户端从其 .On 处理程序返回结果。

有两种方法可以在服务器上使用 API,第一种方法是在中心方法中对 Clients 属性调用 Client(...) 或 Caller:

public class ChatHub : Hub
{
    public async Task<string> WaitForMessage(string connectionId)
    {
        var message = await Clients.Client(connectionId).InvokeAsync<string>(
            "GetMessage");
        return message;
    }
}

第二种方法是对 IHubContext 的实例调用 Client(...):

async Task SomeMethod(IHubContext<MyHub> context)
{
    string result = await context.Clients.Client(connectionID).InvokeAsync<string>(
        "GetMessage");
}

强类型中心还可以从接口方法返回值:

public interface IClient
{
    Task<string> GetMessage();
}

public class ChatHub : Hub<IClient>
{
    public async Task<string> WaitForMessage(string connectionId)
    {
        string message = await Clients.Client(connectionId).GetMessage();
        return message;
    }
}

客户端在其 .On(...) 处理程序中返回结果,如下所示:

.NET 客户端

hubConnection.On("GetMessage", async () =>
{
    Console.WriteLine("Enter message:");
    var message = await Console.In.ReadLineAsync();
    return message;
});

Typescript 客户端

hubConnection.on("GetMessage", async () => {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("message");
        }, 100);
    });
    return promise;
});

Java 客户端

hubConnection.onWithResult("GetMessage", () -> {
    return Single.just("message");
});

更改中心方法的名称 默认情况下,服务器中心方法名称是 .NET 方法的名称。 若要更改特定方法的此默认行为,请使用 HubMethodName 属性。 调用方法时,客户端应使用此名称,而不是 .NET 方法名称:

[HubMethodName("SendMessageToUser")]
public async Task DirectMessage(string user, string message)
    => await Clients.User(user).SendAsync("ReceiveMessage", user, message);

将服务注入Hub

中心构造函数可以接受 DI 中的服务作为参数,这些参数可以存储在类的属性中,以便在中心方法中使用。

为不同的中心方法注入多个服务或作为编写代码的替代方法时,中心方法也可以接受来自 DI 的服务。 默认情况下,如果可能,将从 DI 检查和解析中心方法参数。

services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();

// ...

public class ChatHub : Hub
{
    public Task SendMessage(string user, string message, IDatabaseService dbService)
    {
        var userName = dbService.GetUserName(user);
        return Clients.All.SendAsync("ReceiveMessage", userName, message);
    }
}

如果不需要隐式解析来自服务的参数,请使用 DisableImplicitFromServicesParameters 禁用它。 若要在中心方法中显式指定从 DI 解析的参数,请使用 DisableImplicitFromServicesParameters 选项,并使用 [FromServices] 属性或自定义属性,该属性在应从 DI 解析的中心方法参数上实现 IFromServiceMetadata。

services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
services.AddSignalR(options =>
{
    options.DisableImplicitFromServicesParameters = true;
});

// ...

public class ChatHub : Hub
{
    public Task SendMessage(string user, string message,
        [FromServices] IDatabaseService dbService)
    {
        var userName = dbService.GetUserName(user);
        return Clients.All.SendAsync("ReceiveMessage", userName, message);
    }
}

⚠️ 备注

此功能使用 IServiceProviderIsService,通过 DI 实现来实现(可选)。 如果应用的 DI 容器不支持此功能,则不支持将服务注入中心方法。

依赖项注入中的键控服务支持

密钥服务是指使用密钥注册和检索依赖项注入 (DI) 服务的机制。 通过调用 AddKeyedSingleton (或 AddKeyedScoped 或 AddKeyedTransient)来注册服务,与密钥相关联。 使用 [FromKeyedServices] 属性指定密钥来访问已注册的服务。 以下代码演示如何使用键化服务:

using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

app.MapRazorPages();
app.MapHub<MyHub>("/myHub");

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

public class MyHub : Hub
{
    public void SmallCacheMethod([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }

    public void BigCacheMethod([FromKeyedServices("big")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}

为连接处理事件 SignalR 中心 API 提供 OnConnectedAsync 和 OnDisconnectedAsync 虚拟方法来管理和跟踪连接。 替代 OnConnectedAsync 虚拟方法可在客户端连接到中心时执行操作(例如将它添加到组):

public override async Task OnConnectedAsync()
{
    await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
    await base.OnConnectedAsync();
}

替代 OnDisconnectedAsync 虚拟方法可在客户端断开连接时执行操作。 如果客户端有意断开连接(例如通过调用 connection.stop()),则 exception 参数将设置为 null。 但是,如果客户端由于错误(例如网络故障)而断开连接,则 exception 参数将包含描述故障的异常:

public override async Task OnDisconnectedAsync(Exception? exception)
{
    await base.OnDisconnectedAsync(exception);
}

无需在 OnDisconnectedAsync 中调用 RemoveFromGroupAsync,系统会自动为你进行处理。

处理错误 在中心方法中引发的异常会发送到调用方法的客户端。 在 JavaScript 客户端上,invoke 方法会返回 JavaScript Promise。 客户端可以将 catch 处理程序附加到返回的承诺,或使用和 try/catch 和 async/await 来处理异常:

try {
  await connection.invoke("SendMessage", user, message);
} catch (err) {
  console.error(err);
}

当中心引发异常时,连接不会关闭。 默认情况下,SignalR 将向客户端返回一般错误消息,如以下示例所示:

输出

Microsoft.AspNetCore.SignalR.HubException: An unexpected error occurred invoking 'SendMessage' on the server.

意外异常通常包含敏感信息,例如在数据库连接失败时触发的异常中会包含数据库服务器的名称。 作为安全措施,SignalR 在默认情况下不会公开这些详细错误消息。 有关为何禁止显示异常详细信息的详细信息,请参阅 ASP.NET Core SignalR 中的安全注意事项一文。

如果必须将异常情况传播到客户端,请使用 HubException 类。 如果在中心方法中引发 HubException,则 SignalR 会将整个消息发送到客户端(未修改):

public Task ThrowException()
    => throw new HubException("This error will be sent to the client!");

⚠️ 备注

SignalR 仅将异常的 Message 属性发送到客户端。 异常中的堆栈跟踪和其他属性不可供客户端使用。

从中心外部发送

由于Hub每次调用都是新的示例,也就意味着Hub中的方法触发是被动的,只有客户端主动调用,服务端无法主动调用。那要实现服务端向客户端主动发送数据。

SignalR 中心是用于向连接到 SignalR 服务器的客户端发送消息的核心抽象。 你也可以使用 IHubContext 服务从应用中的其他位置发送消息。 本文介绍如何访问 SignalRIHubContext 以从中心外部向客户端发送通知。

⚠️ 备注

IHubContext 用于将通知发送到客户端,而非用于调用 Hub 上的方法。

获取 IHubContext 实例

在 ASP.NET Core SignalR 中,你可以通过依赖项注入来访问 IHubContext 实例。 你可以将 IHubContext 实例注入控制器、中间件或其他 DI 服务。 使用该实例向客户端发送消息。

在控制器中注入 IHubContext 实例

通过将 IHubContext 实例添加到构造函数,可以将其注入控制器:

public class HomeController : Controller
{
    private readonly IHubContext<NotificationHub> _hubContext;

    public HomeController(IHubContext<NotificationHub> hubContext)
    {
        _hubContext = hubContext;
    }
}

获权访问 IHubContext 实例后,就像在中心本身一样调用客户端方法:

public async Task<IActionResult> Index()
{
    await _hubContext.Clients.All.SendAsync("Notify", $"Home page loaded at: {DateTime.Now}");
    return View();
}

在中间件中获取 IHubContext 实例

访问中间件管道中的 IHubContext,如下所示:

app.Use(async (context, next) =>
{
    var hubContext = context.RequestServices
                            .GetRequiredService<IHubContext<ChatHub>>();
    //...
    
    if (next != null)
    {
        await next.Invoke();
    }
});

⚠️ 备注

当从 Hub 类外部调用客户端方法时,没有与该调用关联的调用方。 因此,无法访问 ConnectionId、Caller 和 >Others 属性。

需要将用户映射到连接 ID 并保留该映射的应用可以执行以下操作之一:

将单个或多个连接的映射保留为组。 有关详细信息,请参阅SignalR 中的组。 通过单一实例服务保留连接和用户信息。 有关详细信息,请参阅将服务注入中心。 单一实例服务可以使用任何存储方法,>例如: 字典中的内存中存储。 永久性外部存储。 例如,使用 Azure.Data.Tables NuGet 包的数据库或 Azure 表存储。 在客户端之间传递连接 ID。

从 IHost 获取 IHubContext 实例

从 Web 主机访问 IHubContext 对于与 ASP.NET Core 之外的区域集成很有用,例如,使用第三方依赖项注入框架:

    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();
            var hubContext = host.Services.GetService(typeof(IHubContext<ChatHub>));
            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => {
                    webBuilder.UseStartup<Startup>();
                });
    }

注入强类型 HubContext

若要注入强类型 HubContext,请确保中心继承自 Hub。 使用 IHubContext<THub, T> 接口而不是 IHubContext 进行注入。

public class ChatController : Controller
{
    public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }

    public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)
    {
        _strongChatHubContext = chatHubContext;
    }

    public async Task SendMessage(string user, string message)
    {
        await _strongChatHubContext.Clients.All.ReceiveMessage(user, message);
    }
}

有关详细信息,请参阅强类型中心。

在泛型代码中使用 IHubContext

注入的 IHubContext 实例可以强制转换为 IHubContext,而无需指定泛型 Hub 类型。

class MyHub : Hub
{ }

class MyOtherHub : Hub
{ }

app.Use(async (context, next) =>
{
    var myHubContext = context.RequestServices
                            .GetRequiredService<IHubContext<MyHub>>();
    var myOtherHubContext = context.RequestServices
                            .GetRequiredService<IHubContext<MyOtherHub>>();
    await CommonHubContextMethod((IHubContext)myHubContext);
    await CommonHubContextMethod((IHubContext)myOtherHubContext);

    await next.Invoke();
}

async Task CommonHubContextMethod(IHubContext context)
{
    await context.Clients.All.SendAsync("clientMethod", new Args());
}

此操作在以下情况下十分有用:

  • 编写不引用应用正在使用的特定 Hub 类型的库。
  • 编写可应用于多个不同 Hub 实现的泛型代码

用户和组

SignalR 允许将消息发送到与特定用户关联的所有连接,以及指定的连接组。

SignalR 中的用户

SignalR 中的单个用户可以与一个应用建立多个连接。 例如,用户可以在桌面和手机上进行连接。 每台设备都有一个单独的 SignalR 连接,但它们都与同一个用户关联。 如果向用户发送消息,则与该用户关联的所有连接都会收到消息。 可以通过中心内的 Context.UserIdentifier 属性访问连接的用户标识符。

默认情况下,SignalR 使用与连接关联的 ClaimsPrincipal 中的 ClaimTypes.NameIdentifier 作为用户标识符。 若要自定义此行为,请参阅使用声明自定义标识处理。

通过将用户标识符传递给中心方法中的 User 函数,向特定用户发送消息,如以下示例所示:

⚠️ 备注

用户标识符区分大小写。

public Task SendPrivateMessage(string user, string message)
{
    return Clients.User(user).SendAsync("ReceiveMessage", message);
}

SignalR 中的组

组是与名称关联的连接集合。 你可以将消息发送到组中的所有连接。 建议通过组发送到一个或多个连接,因为组由应用程序管理。 一个连接可以是多个组的成员。 组非常适合聊天应用程序之类的应用,其中每个聊天室都可以表示为一个组。

添加或删除组的连接

可通过 AddToGroupAsync 和 RemoveFromGroupAsync 方法在组中添加或删除连接:

public async Task AddToGroup(string groupName)
{
    await Groups.AddToGroupAsync(Context.ConnectionId, groupName);

    await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
}

public async Task RemoveFromGroup(string groupName)
{
    await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);

    await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has left the group {groupName}.");
}

多次将用户添加到组是安全的,如果组中已存在用户,不会引发异常。

重新连接时不会保留组成员身份。 重新建立连接后,需要重新加入组。 无法计算组的成员数,因为如果将应用程序扩展到多台服务器,则无法获取此信息。

组保留在内存中,因此在服务器重启时不会保留。 对于需要保留组成员身份的方案,请考虑 Azure SignalR 服务。 有关详细信息,请参阅 Azure SignalR

若要在使用组时保护对资源的访问,请使用 ASP.NET Core 中的身份验证和授权功能。 如果仅当凭据对组有效时才将用户添加到该组,则发送到该组的消息将仅发送给授权用户。 但是,组不是一项安全功能。 身份验证声明具有组不具备的功能,例如到期和吊销。 如果撤销用户对组的访问权限,应用必须从组中显式删除该用户。

⚠️ 备注

组名称区分大小写。

Asp.NET 证书

生成证书

CSDN

Jwt

密钥的类型

密钥通常是一个字符串,通常是用作 HMAC(例如 HS256、HS384、HS512)算法的密钥。这个密钥可以是任何字符串,但为了确保安全性,通常会选择一个随机生成的、足够复杂的字符串。

密钥的长度

密钥的长度要求取决于你所使用的加密算法:

  • HS256 : 建议至少使用 32 字节(256 位)长度的密钥。
  • HS384 : 建议至少使用 48 字节(384 位)长度的密钥。
  • HS512 : 建议至少使用 64 字节(512 位)长度的密钥。

短于这些推荐长度的密钥虽然可以使用,但会降低安全性,增加被破解的风险。

示例代码

创建TOKEN

 private string CreateToken(User user)
 {
     List<Claim> claims = new List<Claim>
     {
         new Claim(ClaimTypes.NameIdentifier, user.Id),
         new Claim(ClaimTypes.Name, user.Account),
         new Claim(ClaimTypes.Role, user.Role)
     };
     var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8
         .GetBytes(_authSettingsOpt.Value.SecurityKey));

    // var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); //这个需要更长的密钥,同时要牺牲一点性能
     var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

     var token = new JwtSecurityToken(
             claims: claims,
             expires: DateTime.Now.AddDays(_authSettingsOpt.Value.ExpireDays),
             //expires: DateTime.Now.AddSeconds(5),
             signingCredentials: creds);

     var jwt = new JwtSecurityTokenHandler().WriteToken(token);

     return jwt;
 }

总结

  • 密钥应该是一个随机生成的、复杂的字符串。
  • 密钥的长度应根据所使用的算法来确定,建议至少达到算法要求的位数,以确保安全性。

使用强密码生成器或专门的密钥管理工具来生成和存储这些密钥,可以进一步提升安全性。

Balzor

不要复制Razor文件

当复制一个界面作为一个新的界面时,配置了新的 @page ,但是无法路由到此界面,最后新建页面解决,感觉复制页面容易出现Bug,通过新建,把内容复制过来比较靠谱。

注入服务

页面中中注入服务

不能通过构造函数注入

服务不加 ? 会有警告

[Inject][NotNull] NavigationManager? nav { get; set; }

服务中注入其他服务

最好通过构造函数注入,如 “A” 服务依赖 “B” 服务,在 “A” 中通过构造函数注入 “B”服务,通过属性注入的方式可能会导致注入失败。

脚本函数

OnInitializedAsync

页面加载进行初始化。

⚠️该函数异步执行,其他函数可能在此函数执行完成后就执行了,所以注意数据的使用

Page

路由问题

在ASP.NET Core Razor页面上,我们可以执行以下操作来更改默认的基于文件夹的路由:

@page"/student-list"

希望 将 /student-list 定义为常量,以便在其他地方共用(如授权策略,菜单导航等),避免重复编写字符

public const string Myroute ="/student-list";
@page @Constants.Routes.Myroute

编译时出现错误:The 'page' directive expects a string surrounded by double quotes.

总结:网上有一些解决办法,但是都比较复杂,没必要使用,就按照标注的使用双引号字符直接编写。

Display特性

在ASP.NET Core中,Display属性是用来定义模型的某个属性在视图中显示的格式。它通常在使用Entity Framework Core的模型类中使用,并且通常与Data Annotations模型进行配合使用。

定义一个枚举,每个值添加Display特性,在使用枚举集合绑定到下拉列表上的时候,视图上显示Display的名称,如果想获取我们选择的值对应的名称,按下面方式实现


 public enum MajorType
 {
     [Display(Name = "运营专业")]
     YunYing = 0,
     [Display(Name = "车辆专业")]
     CheLiang = 1,
     [Display(Name = "通号专业")]
     TongHao = 1,
     [Display(Name = "机电专业")]
     JiDian = 1,
     [Display(Name = "供电专业")]
     GongDian = 1,
     [Display(Name = "工务专业")]
     GongWu = 1,
 }
private string GetMajorText(MajorType type)
{

    // 获取枚举类型的字段信息  
    FieldInfo? field = type.GetType().GetField(type.ToString());
    if(field == null) return type.ToString();
    // 检查字段是否有Display特性  
    DisplayAttribute? attribute = Attribute.GetCustomAttribute(field, typeof(DisplayAttribute)) as DisplayAttribute;

    // 返回Display特性的Name属性值,如果没有找到Display特性,则返回枚举值的名称  
    return attribute?.Name ?? type.ToString();
  
}

BlazorInputFile-不支持同步读取

BlazorInputFile - Synchronous reads are not supported

Blazor 中上传文件,不支持同步读取,仅支持异步,如下方法中上传文件:

private async Task Upload(InputFileChangeEventArgs e)
{
    MemoryStream ms = new MemoryStream();
    e.File.OpenReadStream().CopyTo(ms);
    var bytes = ms.ToArray();
    UploadFile file = new UploadFile
    {
        FileName = e.File.Name,
        FileContent = bytes,
        Size = e.File.Size,
        ContentType = e.File.ContentType
    };
        await Http.PostAsJsonAsync<UploadFile>("/api/uploadfile", file);
        await OnInitializedAsync();
}

报错:

ortunately I keep getting an error/exception from BlazorInputFile which says "Synchronous reads are not supported".

解决办法: 将 OpenReadStream 的复制改为 异步复制:

MemoryStream ms = new MemoryStream();
await e.File.OpenReadStream().CopyToAsync(ms);

如下写法在非Blazor程序中正确,但是在Blazor中不行:

MemoryStream ms = new MemoryStream();
await e.File.OpenReadStream().CopyTo(ms);

//or
MemoryStream ms =  e.File.OpenReadStream();

Blazor 文件上传说明文档

阿里云对象存储(OSS)接口

  • 在阿里云的对象存储接口中,只提供了同步方法,导致用户认为,无法在 Blazor 中使用

  • 上传文件只能先将客户端的资源保存到服务端,然后使用服务端的本地路径进行上传,不能使用 Stream 上传,否则报 “数组越界”

Bootstrp Blazor

登出组件

登出组件 Logout

使用参看官网文档,这里讨论下使用 LogoutLink 的注意事项,必须配置自己的 Url 属性值,如果不配置,直接跳转到默认的 /Account/Login ,出现找不到页面404错误

<Logout ImageUrl="./images/Argo.png" DisplayName="Administrators" UserName="Admin" class="bg-primary">
    <LinkTemplate>
        <a href="#">
            <i class="fa-solid fa-user"></i><span>个人中心</span>
        </a>
        <a href="#">
            <i class="fa-solid fa-gear"></i><span>设置</span>
        </a>
        <LogoutLink Url="/logouts" />
    </LinkTemplate>
</Logout>

LogoutLink 配置了 Url,表示点击登出时,路由到 /logouts 页面

此时需要配置一个登出页面来处理登出逻辑,包含:清除登录数据,自动跳转到登录页:

@namespace xxx
@page "/logouts"
@inherits LayoutComponentBase
你已退出登录!
using Blazored.LocalStorage;
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components;

namespace TSIM.Web.Components
{
    /// <summary>
    /// 
    /// </summary>
    public sealed partial class AccountLogout : LayoutComponentBase
    {
#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
        [Inject] NavigationManager nav { get; set; }
        [Inject] ILocalStorageService _localStorage { get; set; }
        [Inject] AuthenticationStateProvider authStateProvider { get; set; }

#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。

        /// <summary>
        /// OnInitialized 方法
        /// </summary>
        protected async override void OnInitialized()
        {
            await this._localStorage.RemoveItemAsync("authToken");/清除登录数据
            await this.authStateProvider.GetAuthenticationStateAsync();
            nav.NavigateTo("/login", true);//导航到登录页面
        }  
    }
}

表格页面

当页面中有表格时,如果出现垂直滚动条错误,采用以下解决方式

整个页面使用 <div> 包裹,可添加 class

.page-user {
    height: calc(100vh - 162px);
}

如果希望表格底部的页码正好显示,可以给 table 定义类来解决(自行调整93px 的值),如:

.table-users {
    height: calc(100% - 93px);
}css

页面:

@page "/users"
@attribute [TabItemOption(Text = "花名册")]
<PageTitle>花名册</PageTitle>

<div class="page-user">
    <h1>花名册</h1>

    <p>只读数据 - 更改每页显示数量体验固定表头功能</p>

    <Table TItem="Foo" class="table-users">
        <TableColumns>
           ...
        </TableColumns>
    </Table>
</div>

更改组件的样式

通过组件属性 AdditionalAttributes 来实现,组件都提供了 classstyle 两种方式来更改。

在后端定义一个属性 AdditionalAttributes,分别加了 classstyle 两个key和对应的值

public Dictionary<string, object> AdditionalAttributes { get; set; } = 
new Dictionary<string, object>() 
{ 
	{ "style", "border:none;bacground-color:red;" } ,
	{ "class", "custom-cls" }
};

style : 直接编写样式字符串

class :值为定义的css类名,这里定义类名为 custom-cls 的样式,设置颜色为红色

.custom-cls { color:red; }

前端假设设置ListView组件,直接绑定我们的属性

<ListView  AdditionalAttributes="@AdditionalAttributes" >
</ListView>

发现class设置的有些值可能无效,可能是优先级的原因,我们添加class的优先级并没有原有的高,但是style设置的值都是有效的。

对象绑定

对于框架封装的组件,如Table、ListView等,有时需要手动查询数据,此时需要获得组件对象,通过 @ref来绑定对象,以ListView为例:

前端通过 @ref 绑定对象

<ListView @ref="listview" >
</ListView>

后端定义对象属性,并通过对象调用手工查询方法来更新组件内容

private ListView<VideoCollModel> listView { get; set; } 

//如果是表格
//private Table<VideoCollModel> table { get; set; } 

//通过对象查询更新内容
listView.QueryAsync();

本地化资源缺失错误

System.Resources.MissingManifestResourceException”(位于 System.Private.CoreLib.dll 中)

状态:⚠️未解决⚠️

由于当前提示的错误不影响使用,没继续研究。

组件相关章节

异常通常发生在 .NET 程序无法找到指定的资源文件时。这通常出现在使用本地化、资源文件或卫星程序集的应用程序中。

ChatGPT 回复

Unity

Enum

自定义特性实现

定义特性

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace EnumTest
{
  public class EnumFlags : PropertyAttribute
  {
    public EnumFlags() { }
  }

  #if UNITY_EDITOR
  [CustomPropertyDrawer( typeof( EnumFlags ) )]
  public class EnumFlagsPropertyDrawer : PropertyDrawer
  {
    public override void OnGUI( Rect position, SerializedProperty property, GUIContent label )
    {
      property.intValue = EditorGUI.MaskField( position, label, property.intValue, property.enumNames);
    }
  }
#endif
}

定义枚举,定义枚举需标记1,2,4...,一次2的次方,代码如下:

public enum MyEnum
{
    A=1,
    B=2,
    V=4
}

使用特性标记属性面板多选

[EnumFlags]
public MyEnum val;

判断两个枚举值是否包含相同

public bool IsSelectEnumType(MyEnum e1, MyEnum e2)
{         
    return ((int)e1 & (int)e2) != 0 ;
}

Flags实现

使用DLL

在开发过程中,我们需要将一些dll放入Pulgins文件夹下,这时最好使用对应Unity版本的dll,并不是dll的版本越高越好,不同版本在引用Unity中的dll的路径不同,具体参照下表

版本dll路径备注
以前版本C:\Program Files\Unity\Editor\Data\Mono\lib\mono\2.0没有使用HUB安装的以前的版本
Unity2020.3.xEditor\Data\MonoBleedingEdge\lib\mono\unityjit在对应版本的 Editor

开发中,我们可能需要使用到如下的dll

  • I18N.CJK.dll
  • I18N.dll
  • Mono.Data.Sqlite.dll
  • System.Data.dll

注:某些dll是可以在Unity安装的目录找到的,有些不能,这时需要在官网上看是否有对应Unity的版本,不要一味的最求最新版本

多场景加载

异步加载时,需要设置 asyncOp.allowSceneActivation = false;

加载完成后,需要设置 asyncOp.allowSceneActivation = true;

注意,此时被加载的场景并不是活跃场景,我们一般都是需要把被加载的场景设置为活跃场景

//...
  SceneManager.sceneLoaded += SceneLoadHandler;
//...

private void SceneLoadHandler(Scene scene, LoadSceneMode mode)
{
    Debug.Log("激活场景: " + scene.name);
    SceneManager.SetActiveScene(scene);
}
//...
  SceneManager.sceneLoaded -= SceneLoadHandler;
//...

证书问题崩溃

查看崩溃日志:C:\Users\ABYTE\AppData\Local\Temp\Unity\Editor\Crashes,找到最近一次崩溃的文件夹,查看 Editor.log 文件,若出现 [Licensing::Client] Error: Code 10 while verifying Licensing Client signature 这种错误信息。

删除 C:\ProgramData\Unity\Unity_lic.ulf ,重新激活即可。

  • 注意,在使用 "+="时,要匹配使用 “-=”,否则响应会叠加

激活场景的目的:多场景时,引擎会使用活跃场景的天空盒

Dictionary的key 为 null导致报错

ArgumentNullException: Value cannot be null.

Parameter name: key
  at System.Collections.Generic.Dictionary`2[TKey,TValue].FindEntry (TKey key) [0x00008] in <695d1cc93cca45069c528c15c9fdd749>:0 
  at System.Collections.Generic.Dictionary`2[TKey,TValue].ContainsKey (TKey key) [0x00000] in <695d1cc93cca45069c528c15c9fdd749>:0 

Dictionary 的 key 值不能为null.

查看代码追踪 key 的来源,进行修改

注意此问题: 在编辑模式下,应该也会报错,但是有些时候莫名不会提示你错误,但是在发布后会报错 所以在使用 字典 时,最好对key进行判断**

挂载脚本顺序导致依赖失败

Unity版本:Unity2017.3.0

使用情景:组件依赖问题,自定义了两个脚本,其中A依赖B,但是当停止Play时,报错:CAN'T REMOVE HIGHLIGHTER (SCRIPT) BECAUSE HIGHLIGHTERREGISTER (SCRIPT) DEPENDS ON IT。

解决办法:更改两个脚本的组件顺序,先是依赖项,后为被依赖项。

1721199810487

改为

1721199816545

FAILED TO LOAD MONO

Unity版本:Unity2017.1.0

解决办法:文件夹不能包含中文名,之前发现有的版本在中文路径下可以,所以觉得后面的版本也应该可以,导致花了很多时间,最后还是中文路径的问题。

Unity打包后画面灰色、重影、带错误的彩色现象。

Unity版本:Unity2017.1.0

解决办法:更新显卡

Unity编译时间过长

可能有很多问题导致的

  • 项目过大,资源过多,没办法,或者优化资源(不要有重复的资源)
  • VS 影响的,关闭VS ,删除 .vs文件夹,打开VS时,重新生成

将所有配置关联场景导致内存爆了

某次项目,配置文件都是使用“ScriptableObject”的方式来配置,为了方便读取,就把它关联的启动场景(Launch,一直存在)上,导致在移动端启动就闪退。 (1)刚开始将所有的配置文件(ScriptableObject)全部挂在到启动场景(Launch)上,导致在移动端启动时,直接闪退。 原因: 所有的配置文件占用大量内存,启动时内存不够用了,直接闪退 解决办法: (1)按需加载,不要将太多的资源直接关联到场景中,应该按需加载 (2)手动进行Gc回收,可能导致系统卡顿,且存在问题,最好是在退出任务的时候,进下gc回收

链接

UI

UI边缘锯齿严重

1.没有开启canvas上的抗锯齿选项,project setting里也要开启抗锯齿,选择2x,4x,8x

2.图片在导入的时候质量被压缩或者应用的时候被缩放。重新设置图片的格式,使图片的质量更高。

3.美术出图的时候,每张图的边缘最好都要留几个空白的像素单位。边缘是产生锯齿的源头,这样被锯齿的部分就是透明的。

UI

Toggle

需求: 第一次不选中任何选项,当点击任一个选项后,自动只选中一个

  • 默认将ToggleGroup allowSwiechOff 勾选

  • 调用 toggleGroup.SetAllTogglesOff(false);

动画

问题汇总

动画播放失败

如在使用 timeline 时,拖动 timeline 动画正常,但是点击 Play 后动画未播放。

解决

游戏对象是否勾选了 static

Camera(相机)

1718097515607

运行的CameraTag 应该为 MainCamera

Projecttion

Rendering

1718097530253

  • Renderer

  • PostProcession

  • Anti-aliasing

  • StopNan

  • Dithering:

  • RenderShadows

  • Priority

  • OpaqueTexture

  • DepthTexture

  • CullingMask:剔除遮罩,勾选要渲染的层,注意:如果不勾选,不仅不渲染,且不会响应射线事件

  • OcclusionCulling

Envrionment

Output

Stack

PhysicsRaycaster

用于射线检测,可对场景中的数据进行交互

  • EventMask:这里勾选需要交互的层级,同时注意在Camera->Renderer->CullingMask 中也要勾选响应的层级
  • MaxRayIntersections: 最大射线距离

向量

1 基础知识

1.1 向量叉积

若有向量a、b 则向量的叉积为:

数学上定义:

a×b=|a||b|sin<a,b>  # 使用弧度计算

Unity 对应函数:

 public static Vector3 Cross(Vector3 lhs, Vector3 rhs);

三维坐标中

  • 结果是一个向量(法向量),方向:根据右手定则,四指从a的方向向b旋转,大拇指的方向就是结果方向
  • 当a是单位向量时,计算b终点到a所在直线的距离

二维坐标中

  • 叉积等于由向量A和向量B构成的平行四边形的面积

1.2 向量点积

若有向量a、b 则向量的点积为:

数学定义:

a·b=|a||b|cos<a,b>  # 使用弧度计算

Unity对应函数:

public static float Dot(Vector3 lhs, Vector3 rhs);
  • 结果是一个标量。
  • 常用于计算 两个向量的夹角 :当a、b向量的模都为1(向量归一化),得到的为a、b向量的余玄值,求反余玄,得到夹角(弧度)

1.3 弧度和度

通常我们说两个向量的角度为30度,30指的是 , 弧度:180度对应的 弧度 是 Π,Unity里为常量: UnityEngine.Mathf.PI

  • Unity里使用向量的方法都是使用弧度计算,而我们通常配置都是使用度(度在描述时更形象,一圈就是360度),所以需要将度转换为弧度

Unity中提供了转换的比例常数

  • 度转弧度:Mathf.Deg2Rad,等价于 180/(UnityEngine.Mathf.PI)
  • 弧度转度:Mathf.Rad2Deg,等价于 (UnityEngine.Mathf.PI)/180

举例:求Sin(30)的值

var v = Mathf.Sin(30*Mathf.Deg2Rad)

2 常用函数

2.1 两向量的夹角

var cosValue= Vector3.Dot(player.forward.normalized, (target.position - player.position).normalized);
float rad = Mathf.Acos(cosValue);//反余玄求夹角的弧[-1,1]
var deg = rad * Mathf.Rad2Deg;//弧度转度

2.2 判断目标点相对玩家的方位

设:

  • 玩家为: player(Transform)
  • 目标为: target(Transform)

2.2.1 前后判断

  • 原理说明:通过玩家正前方向量(forward)与玩家到目标构成的向量之间的夹角来判断,>90度(余玄值<0)在后方,<90度 (余玄值>0) 在前方
  • 使用知识:点乘
//非归一化
var value = Vector3.Dot(player.forward,target.position - player.position);
//value > 0: 前方
//value < 0: 后方

或者

//归一化,得到的值范围:[-1,1]
var value = Vector3.Dot(player.forward.normalized, (target.position - player.position).normalized);
//value > 0: 前方
//value < 0: 后方

2.2.2 左右判断

与“前后判断”原理相同,仅需要使用 player.right 进行计算即可

//归一化,得到的值范围:[-1,1]
var value = Vector3.Dot(player.right.normalized, (target.position - player.position).normalized);
//value > 0: 右方
//value < 0: 左方

2.2.3 上下判断

与“前后判断”原理相同,仅需要使用 player.up 进行计算即可

//归一化,得到的值范围:[-1,1]
var value = Vector3.Dot(player.up.normalized, (target.position - player.position).normalized);
//value > 0: 上方
//value < 0: 下方

3 需求案例

3.1 判断对象是否在玩家视野内

分析

  • 视野是以相机为起点的一个圆锥体
  • 视野的正前方为 forward方向
  • 求夹角,用点积

求解 假设玩家对象为player(transform),视野的大小为 filedView,从角色位置出发,任意一个向量和正前方的向量的 夹角 <= filedView * 0.5 ,那么该向量上的点都在视野内。

private bool Check(Transform player, int filedView)
{
    Vector3 playerFoward = player.forward;// 角色正前方的向量
    Vector3 player2TagVec = transform.position - player.position;//角色到目标点的向量
    //向量归一化后求两个向量的点积(标量)
    var v = Vector3.Dot(playerFoward.normalized, player2TagVec.normalized);
    //然后反余玄求到夹角的弧度,这里的结果是要给-1 到 1 的弧度。
    float rad = Mathf.Acos(v);
    var deg = rad * Mathf.Rad2Deg;//弧度转度
    return deg <= filedView * 0.5f;//filedView:player 前方锥形角度
}

存在问题

  1. 若仅仅通过角度来判断,且这个角度是固定的,当角色与目标点较近时,此时目标点在视野外,但是此时判断就很奇怪,所以应将角度 与 “玩家和目标点的距离” 同时用于判断
//1)假设玩家和目标点的距离(水平面上的距离)为1m时,角度判断为30度
//2)那么用于实际判断的 filedView = 30/距离
//3)当玩家与目标越来越近时,应该使用更大的距离来判断
//4)这个距离应该在0-160度之间(自己可以改动这个范围)

    private bool CheckInFiledView(Transform player, int fieldView)
    {
        Vector3 playerFoward = player.forward;// 角色正前方的向量
        Vector3 player2TagVec = transform.position - player.position;//角色到目标点的向量
        var cosValue = Vector3.Dot(playerFoward.normalized, player2TagVec.normalized);//向量归一化后求两个向量的点积(标量),即两向量的夹角的余玄值
        if (cosValue < 0) return false;// > 90 为 负值,表示 目标在角色后面
        float rad = Mathf.Acos(cosValue);//反余玄求夹角的弧[-1,1]
        float disOnPlane = player.DistanceToTargetOnHorPlane(transform);//求玩家和目标的水平面上的距离
        float newFiled = fieldView / disOnPlane;
        newFiled = Mathf.Clamp(newFiled, 0f, 60f);
        var deg = rad * Mathf.Rad2Deg;//弧度转度
        bool r = deg <= newFiled * 0.5f;//filedView:player 前方锥形角度
        // if (r)
        //     Debug.Log($"激活的相机:{player.name}  目标: {name}  v: {cosValue}    deg: {deg}  newFiled{newFiled} ");
        return r;
    }

3.2 怪物看向玩家

要点:控制怪物仅围绕Y轴旋转,否则怪物会倾斜

如在场景中有一个ui箭头指示操作,此时需要玩家看向箭头时,永远是箭头的正确面

private void lookat(Transform self, Transform target)
    {
        //look的方向 起点 - 终点
        Vector3 dir = target .position - self.position;
        //设置y轴上为0,不偏移
        dir.y = 0;
        //四元数插值运算
        self.rotation = Quaternion.Slerp(self.transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime);
    }

3.3 VR中在玩家面前显示UI

分析

  • 一个位置坐标 + 一个向量 = 从这个位置,以向量方向移动向量模的距离后的新的位置点
  • ui 始终垂直水平面,不能前后左右倾斜

求解

   Transform player; //玩家对象
    
    public void (Transform ui)
    {
        Vector3 forward = player.forward;
        forward.y = 0;//这样得到的就是一个平行水平面的一个向量
        var vMove = forward.normalized;//归一化
        
        Vector3  pos= player.position - vMove * 2;//正前方2米的位置,我们可以设置这个参数

        ui.SetPositionAndRotation(pos, Quaternion.LookRotation(v));

        //ui.SetPositionAndRotation(pos - Vector3.up * 0.2f, Quaternion.LookRotation(v)); //也可以调整高度      
    }    

收集了网上的一些帖子,以便需要时自己看

Unity2020

烘焙常见问题及解决

关注的博主

程美博主

Material(材质)

通过改变材质的属性改变Gameobject的显示

在项目中,经常需要控制改变某些对象的显示状态,如红绿灯的控制,假设现在有个3个红绿灯,有三种状态,红、绿、黄,要控制他们显示不同的颜色

更改材质实现

  • 一个材质是一个shader的实例,当改变材质的状态时,如我们在属性面板直接改变材质的颜色或者贴图等,那么会导致使用了此材质的所有对象都同步改变

  • 创建三个材质,分别为 红 绿 黄

  • 此时根据需求替换对应的材质即可

通过材质属性块实现

个人理解:材质属性块应该是材质中的一个属性实例,也就是说材质对该属性,仅是关联引用,并没有关联具体的值,当改变值时,引用不变,因此可以实现不同的游戏对象,使用一个材质,他们同时对某个属性引用,但是可以通过设置不同的值来实现不同的表现方式

实现代码


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MaterialTest : MonoBehaviour
{
    public Color color;
    void Start()
    {
        var render = GetComponent<Renderer>();
        MaterialPropertyBlock block = new MaterialPropertyBlock();
        render.GetPropertyBlock(block);
        block.SetColor("_BaseColor",color);
        render.SetPropertyBlock(block);
    }

    

经验记录

  1. 某些材质的SurfaceTpye设置为Transparent,导致渲染前后错乱,应改为Opaque

参考

  • https://developer.aliyun.com/article/432429

Awake

    private void Awake()
    {
        Debug.Log("Awake");
    }
  • 引擎首先调用的函数
  • 游戏对象被隐藏时,不会执行
  • enabel: 启用与否都会执行
  • 脚本执行顺序:Awake->...

OnEnable

    private void OnEnable()
    {
        Debug.Log("OnEnable");
    }

  • 游戏对象被隐藏时,不会执行
  • enabel = true 才会执行
  • 脚本执行顺序:Awake->OnEnable->...

Start

说明

  • 当游戏对象显示后调用

  • 仅调用一次,即来回切换游戏对象显示隐藏时,仅在第一次显示时被调用

  • 当在Awake函数设置 gameobject.setactive(false); 时,不会被调用

  • 游戏对象被隐藏时,不会执行

  • enabel = true 才会执行

  • 脚本执行顺序:Awake->OnEnable->Start->...

一个案例说明

  • 在 Awake 里面设置 对象隐藏,运行后,Awake被执行,此时 Start 不会被调用
  • 通过点击一个按钮来 调用gameobject.setactive(true) 显示对象
  • 此时要注意,onclick函数里的代码要完全执行完成后,才会执行start
  • 通常的 误解是 只要 gameobject.setactive(true) start就会立刻被调用

下面代码示例输出: 运行后:awake 点击调用onclick: for i = 0 for i = 1 onclick end start

这里就导致了start在其他函数后面调用了,容易出问题


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestStart : MonoBehaviour
{
    private void Awake()
    {
        gameObject.SetActive(false);
        Debug.Log("awake");
    }
    void Start()
    {
        Debug.Log("start");
    }

    public void OnClick()
    {
        gameObject.SetActive(true);
        for(int i = 0; i<100;i++)
        {
            Debug.Log("for i = "+ i);
        }
        Debug.Log("onclick end");

    }


}

一个错误示例

public class Test:MonoBehaviour
{
    bool state;
    private void Awake()
    {
        gameObject.SetActive(false);
    }
    private void Start()
    {
        state = true;
    }
    //点击调用
    public void onclick()
    {
        gameObject.SetActive(true);
        Debug.Log(state);
    }
}

上面代码中,我在onclick函数里面,预期的输出是 true,但是实际结果是 false, 因为在运行后,wake将 游戏对象设置为未激活状态,导致,star未被执行,而在onclick时,虽然激活了游戏对象,但是当前代码域未执行完,不会调用start,所以先输出 false,然后才执行start ,赋值为 true。

Update

    private void Update()
    {
        Debug.Log("Update");
    }

  • 游戏对象被隐藏时,不会执行
  • enabel = true 才会执行
  • 脚本执行顺序:Awake->OnEnable->Start->Update->...

OnBecameVisible


    private void OnBecameVisible()
    {
        Debug.Log("OnBecameVisible");
    }

  • 游戏对象被隐藏时,不会执行
  • enabel 不影响调用
  • 脚本所在的游戏对象需要meshRenderder组件 且 enable =true
  • 当进入相机视野内时调用
  • 在编辑模式 Scene 窗口,进入窗口范围时调用
  • 若一开始就在相机范围内,脚本执行顺序:Awake->OnEnable->Start->Update->Update->OnBecameVisible->... (注意:在调用两次 Update后调用这个函数)

OnBecameInvisible


    private void OnBecameInvisible()
    {
        Debug.Log("OnBecameInvisible");
    }
  • 游戏对象被隐藏时,不会执行
  • enabel 不影响调用
  • 脚本所在的游戏对象需要meshRenderder组件 enable =true
  • 当离开相机视野内时调用
  • 在编辑模式 Scene 窗口,离开窗口范围时调用

OnTriggerEnter/OnTriggerExit


    private void OnTriggerEnter(Collider other)
    {
        Debug.Log("OnTriggerEnter");
    }

    private void OnTriggerExit(Collider other)
    {
        Debug.Log("OnTriggerExit");
    }


     //一般情况下不用,耗性能
     private void OnTriggerStay(Collider other) {
        Debug.Log("OnTriggerStay");
    }

  • 一般用于检车玩家进入某个触发区域
  • enabel 不影响调用
  • 游戏对象需要显示
  • 两个对象至少有个挂载 刚体(Rigidbody) 组件,两个都挂也会触发
  • 两个对象都要挂载 Collider 组件,至少有个设置IsTrigger = true
  • 两个对象可以挂载多个 Collider,但是不能同时存在 IsTrigger = false的 Collider (A/B上不能同时存在)
A
Collider数量
A 勾选IsTrigger数量 B
Collider数量
B 勾选IsTrigger数量 触发结果
11 11 触发 1 次Enter/Exit
22 11 触发 2 次Enter/Exit
11 22 触发 2 次Enter/Exit
22 22 触发 4 次Enter/Exit

注意事项

  • 角色进入触发器中,若此时隐藏掉触发器对象,不会触发Exit方法

OnDisable

    private void OnDisable()
    {
        Debug.Log("OnDisable");
    }
  • 游戏对象需要显示
  • 当设置 enable = false 时触发
  • 对象删除时触发
  • 场景卸载时触发

OnDestroy

    private void OnDestroy()
    {
        Debug.Log("OnDestroy");
    }
  • 游戏对象需要显示
  • enabel 不影响调用
  • 对象删除时触发
  • 场景卸载时触发

Timeline

PlayableDirector

导演,挂在场景游戏对象上的一个组件

  • Playable :Timeline编辑的资源,类型为 “TimelineAsset”
  • UpdateMethod :更新方式,默认 GameTime
  • PlayOnAwake :在游戏对象Wake时,是否播放Timeline
  • WrapMode :循环模式
  • InitialTime :初始化时间,若设置了时间,会等待这个时间之后才播放
  • Bindings :绑定的轨道数据

TimelineAsset

编辑时间线数据时,存储的数据

获取轨道集合

timelineAsset.GetOutputTracks()

通过轨道获取timeline资源( track.timelineAsset ),但是可能返回null,导致异常,可能的原因是复制timeline导致的,如果出现异常,可以通过 遍历打印出每个轨道的名字来检测

public TimelineAsset timelineAsset
{
    get
    {
        var node = this;
        while (node != null)
        {
            if (node.parent == null)
                return null;
            var seq = node.parent as TimelineAsset;
            if (seq != null)
                return seq;
            node = node.parent as TrackAsset;
        }
        return null;
    }
}

TrackAsset(轨道)

编辑时间线数据时,存储的数据

  • name :轨道名称

  • Start(float) :开始时间

  • End(float) :结束时间

  • duration(float) :轨道长度

  • muted(bool) :是否静音,静音轨道从生成的PlayableGraph中被排除,所以应先静音,再 buildGraph

  • timelineAsset(TimelineAsset) :这个指向 时间线资源,实际上对应的是 轨道的 根 父节点

  • parent(PlayableAsset) :轨道的父节点(轨道的拥有者),轨道是可以嵌套的, TrackGroup 可以包含其他 轨道,所有轨道的最终父节点应为 TimelineAsset,即我们编辑的资源

  • clips(TimelineClip[]) :轨道片段数组

获取轨道绑定的对象

var bind = director.GetGenericBinding(track);//需要使用Director来获取,  得到的是一个UnityEngine.Object  

重写CreateTrackMixer

 public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
 {
    //graph: timelineasset构建的图
    //go: 挂载 director 的游戏对象
    //inputCount: 片段数量
     return base.CreateTrackMixer(graph, go, inputCount);
 }

混合器

描述

根据片段的权重来控制某个参数的百分比,可用于解决以下需求:

(1)动画轨道(可以和controller 混合使用)

(2)解决淡进淡出问题

(3)解决监听播放一个片段结束问题(这里可以避免因暂停timeline导致误认为播发结束的问题)

淡进淡出案例

使用 CanvasGroup 组件来实现效果

public class CanvasGroupTrack : TrackAsset
{
    public float easeduration = 0.5f;
    public bool useSettingForAllClips;
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        //graph: timelineasset构建的图
        //go: 挂载 director 的游戏对象
        //inputCount: 片段数量
        if (useSettingForAllClips) UpdateEaseDuration();
        return ScriptPlayable<CanvasGroupMixerBehaviour>.Create(graph, inputCount);
    }

    private void UpdateEaseDuration()
    {
        foreach (var clip in GetClips())
        {
            clip.easeInDuration = easeduration;
            clip.easeOutDuration = easeduration;
        }
    }
}


/// <summary>
/// 注意事项:
/// 1 gameObject.activeSelf 必须为true 否则会导致 初次设置 淡进淡出 报错,提示就是获取不到 reacTransform 
/// 2 当淡出后,必须设置canvasgroup 不可交互,否则会挡住其他的交互
/// </summary>
public class CanvasGroupMixerBehaviour : PlayableBehaviour
{
    CanvasGroup canvasGroup;

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        if (canvasGroup == null)
        {
            MonoBehaviour bind = playerData as MonoBehaviour;
            if (bind == null) return;
            if (bind.gameObject.activeSelf == false)
            {
                bind.gameObject
                    .SetActive(true); //注意:滚到绑定的对象必须设置为 active 状态,否则会导致 初次设置 淡进淡出 报错,提示就是获取不到 reacTransform 
            }

            canvasGroup = bind.GetComponent<CanvasGroup>();
            if (canvasGroup == null)
            {
                Debug.LogWarning("CanvasGroupMixerBehaviour:轨道使用淡进淡出效果,但是缺少 CanvasGroup组件");
                return;
            }
        }

        int inputCount = playable.GetInputCount(); //这是当前轨道上所有片段的数量,注意不是当前帧

        //通过每个片段当前的权重和来设置 canvasGroup alpha值
        float inputWeight = 0;
        for (int i = 0; i < inputCount; i++)
        {
            float curWeight = playable.GetInputWeight(i);
            if (curWeight > 0)
                inputWeight += curWeight;
        }
        canvasGroup.alpha = inputWeight;
        var b = inputWeight > 0.05f; //设置使用0.05的阈值来设定而非0,避免极限情况的出现
        if (canvasGroup.blocksRaycasts != b)
        {
            canvasGroup.blocksRaycasts = b;
        }
    }
}

注意事项:在这个案例中,场景中对轨道绑定的CanvasGroup对象默认为“未激活”状态,每次运行的时候报错,最后在首次给CanvasGroup赋值时,将对象激活显示出来,解决问题。

1721813944567

扩展功能

复制Timeline资源,解决绑定丢失

已在UKit中添加了工具处理,下面可作为原理参照。

_ ** 应用场景:复制已经配置好的TimelineA,改为TimelineB,发现B上的轨道绑定丢失 **_

配置 TimelineA

1721813994579

在场景中复制TimelineA,改为 TimelineB

1721814013936

复制资源 TimelineA,改为 TimelineB,并托给场景中 TimelineB 的Director,此时发现绑定丢失

1721814028802

观看下图分析,它隐藏绑定了A的,且绑定正确,B上的丢失了,我们就要去找到这部分配置,修改,使用Notepad++工具

1721814045040

  • 通过 TimelineA.playable.meta 文件找到 TimelineA 资源文件的 guid:56b3a74b657957d4cae6622c4a199001

  • 通过 TimelineB.playable.meta 文件找到 TimelineB 资源文件的 guid:c4cdb66148603ca4080d5eeb2b49d526

  • 打开 TimelineAssetCopyTest.unity 场景文件,然后找到 c4cdb66148603ca4080d5eeb2b49d526的位置

1721814094360

  • 我们只需要将下方的B的 guid 替换上方的A的 guid,并删除下方 fileID 为 0 的绑定,就可以了
PlayableDirector:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 1410287964}
  m_Enabled: 1
  serializedVersion: 3
  m_PlayableAsset: {fileID: 11400000, guid: c4cdb66148603ca4080d5eeb2b49d526, type: 2}
  m_InitialState: 1
  m_WrapMode: 2
  m_DirectorUpdateMode: 1
  m_InitialTime: 0
  m_SceneBindings:
  - key: {fileID: 8672370563209045018, guid: c4cdb66148603ca4080d5eeb2b49d526, type: 2}
    value: {fileID: 1191359100}
  - key: {fileID: -1281548762813716947, guid: c4cdb66148603ca4080d5eeb2b49d526, type: 2}
    value: {fileID: 1489575779}
  m_ExposedReferences:
    m_References: []
--- !u!4 &1410287966
  • 改后截图

1721814115192

注意事项

开发过程中的经验总结

  • 冗余绑定,复制Timeline会将原有的轨道绑定复制过来,需要删除掉不必要的信息
  • Timeline资源最好不要复制
  • 编辑器模式下正常,发布后若出现Timeline不能播放,有可能是某些轨道有问题(track.timelineAsset == null),编写测试找到轨道,然后新建相同的轨道功能,删除原有轨道

问题与解决方案

此处的问题解决后会将解决方案加入的注意事项中

发布后Timeline不播放

  • 版本 Unity2020.3.40f
  • Timeline版本是最新的
  • 问题详情:在编辑器模式下,播放正常,发布后Timeline不播放(无论是安卓端还是PC端)

第1次处理

  • 删除Timeline的冗余
  • 隐藏其他Timeline

未找到根本的原因,使用下面的方式可以正常播放,但是总是不靠谱

  1. 在发布前,将timeline的播放针头托到起始位置
  2. 有种情况是即使在起始位置,也会有问题,需要再次打开这个场景,然后再发布,不知道到底什么原因

第2次处理

调试处理过程记录

  1. 开始发布的是安卓版本,不是很好调试,后面发布PC端的 Debug 版本来查看下报错信息

  2. 报错信息是TimelinePlayable(插件内置类)中的 下面165行 存在 null 引用,判断是 timelineasset 为 null

1721814195696

  1. 从截图看出是某个 TrackTimelineAsset 为空, TimelineAseet 是创建的timeline资源

  2. 现在就是要找到是哪个轨道的 TimelineAsset 为 null,编写测试代码

1721814204519

第一行是打印测试的名字,第二行如果为空会报错

  1. 打包运行后日志如下,是一个 Teach_模式选择开关高亮 的轨道 为 null,且两个不能播放Timeline的都是因为这个同名的轨道

1721814235064

  1. 查看轨道,该轨道是内置的 PlayableTrack, 自己扩展编写的 Asset 和 behaviour

轨道

1721814216815

属性

1721814247671

  1. 检查是否是因为这个轨道有问题引起的,发现其他使用了此轨道的Timelien正常,排除这个原因
  2. 修改轨道名称,将中文改为英文,看看是不是因为轨道名称引起的, 发现正常了
  3. 修改轨道名称,将中文改为英文,播放针头托离起点,发布后运行, 还是报错
  4. 新建( 不要复制 )相同功能的轨道,并新建( 不要复制 )对应片段,删除原有轨道,针头拖离起点, 发布运行正常
  5. 总结: 有可能是这个轨道创建的时候,脚本未编写完成,导致这个轨道异常

第3次处理

使用第二次的方式还是为解决,通过调试代码找不到错误轨道

通过构建时,勾选 “development build” 得知是在 track.TimelineAsset 获取为空时,出现问题

  • 当前Timeline上应该有GroupTrack,将组内的轨道拖动到组外,有可能的异常,如果拖到的是异常的轨道,会连带其他正常的轨道一起到组外,需要逐个的尝试

  • 逐个的删除轨道,一般也是组内的轨道,若异常会出现图情况

1721814267535

错误信息如下

MissingReferenceException: The object of type 'AnimationTrack' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
UnityEngine.Object.get_name () (at <c7a29af87eea4c208ef711c9671e7831>:0)
UnityEditor.Timeline.TimelineTrackErrorGUI.DrawMissingTrackHeader (UnityEngine.Rect headerRect, UnityEditor.Timeline.WindowState state) (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/treeview/TrackGui/TimelineTrackErrorGUI.cs:76)
UnityEditor.Timeline.TimelineTrackErrorGUI.Draw (UnityEngine.Rect headerRect, UnityEngine.Rect contentRect, UnityEditor.Timeline.WindowState state) (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/treeview/TrackGui/TimelineTrackErrorGUI.cs:46)
UnityEditor.Timeline.TimelineTreeView.OnRowGUI (UnityEngine.Rect rowRect, UnityEditor.IMGUI.Controls.TreeViewItem item, System.Int32 row, System.Boolean selected, System.Boolean focused) (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/treeview/TimelineTreeView.cs:185)
UnityEditor.IMGUI.Controls.TreeViewController.DoItemGUI (UnityEditor.IMGUI.Controls.TreeViewItem item, System.Int32 row, System.Single rowWidth, System.Boolean hasFocus) (at <43587c69e7d54192b46754a593b0061f>:0)
UnityEditor.IMGUI.Controls.TreeViewController.IterateVisibleItems (System.Int32 firstRow, System.Int32 numVisibleRows, System.Single rowWidth, System.Boolean hasFocus) (at <43587c69e7d54192b46754a593b0061f>:0)
UnityEditor.IMGUI.Controls.TreeViewController.OnGUI (UnityEngine.Rect rect, System.Int32 keyboardControlID) (at <43587c69e7d54192b46754a593b0061f>:0)
UnityEditor.Timeline.TimelineTreeViewGUI.OnGUI (UnityEngine.Rect rect) (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/treeview/TimelineTreeViewGUI.cs:152)
UnityEditor.Timeline.TimelineWindow.DrawTracksGUI (UnityEngine.Rect clientRect, UnityEditor.Timeline.TimelineModeGUIState trackState) (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/Window/TimelineWindow_TrackGui.cs:183)
UnityEditor.Timeline.TimelineWindow.TracksGUI (UnityEngine.Rect clientRect, UnityEditor.Timeline.WindowState state, UnityEditor.Timeline.TimelineModeGUIState trackState) (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/Window/TimelineWindow_TrackGui.cs:47)
UnityEditor.Timeline.TimelineWindow.TrackViewsGUI () (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/Window/TimelineWindow_Gui.cs:178)
UnityEditor.Timeline.TimelineWindow.SequencerGUI () (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/Window/TimelineWindow_Gui.cs:270)
UnityEditor.Timeline.TimelineWindow.DoLayout () (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/Window/TimelineWindow_Gui.cs:153)
UnityEditor.Timeline.TimelineWindow.OnGUI () (at Library/PackageCache/com.unity.timeline@1.4.8/Editor/Window/TimelineWindow.cs:227)
UnityEditor.HostView.InvokeOnGUI (UnityEngine.Rect onGUIPosition, UnityEngine.Rect viewRect) (at <43587c69e7d54192b46754a593b0061f>:0)
UnityEditor.DockArea.DrawView (UnityEngine.Rect viewRect, UnityEngine.Rect dockAreaRect) (at <43587c69e7d54192b46754a593b0061f>:0)
UnityEditor.DockArea.OldOnGUI () (at <43587c69e7d54192b46754a593b0061f>:0)
UnityEngine.UIElements.IMGUIContainer.DoOnGUI (UnityEngine.Event evt, UnityEngine.Matrix4x4 parentTransform, UnityEngine.Rect clippingRect, System.Boolean isComputingLayout, UnityEngine.Rect layoutSize, System.Action onGUIHandler, System.Boolean canAffectFocus) (at <033e99bd4b59428a814373dfbafe317f>:0)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)

GUI Error: You are pushing more GUIClips than you are popping. Make sure they are balanced.
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

  • Ctr+Z 后如下图所示

1721814291143

  • 总结办法:1. 随意的将轨道在组外和组内拖动,查看是否正常;2. 逐个删除轨道,查看是否异常, 将出现的异常的组轨道以及异常轨道删除重新制作

Steam VR

1 安装

Steamworks 文献库

2 按键绑定

将自己的配置设置为默认配置。

  1. Unity中 Window->SteamVR Input,打开设置面板

  2. 点击 open binding ui

1718164102854

  1. 如果显示 “这些是此游戏开发这发布的官方案件设置”,那么说明使用的不是你的配置

1718164117949

  1. 点击 编辑,弹出面板后,点击 替换为默认配置

1718164149485

  1. 填写你的信息,点击保存

1718164159028

  1. 最后显示为你的配置,此时发布可以了

1718164172236

3 组件说明

无论是与模型还是UI的交互,无论是直接使用手柄还是射线的交互方式,交互对象(如果对象本身没有,那么子级上要有)需要有Collider,一般为BoxCollider,交互分手势交互和射线(手柄发送射线)交互两种情况,一般情况下,我们都用射线来交互,如和UI交互,大部分模型(如,仿真检查设备操作),手势操作的包含如:拉动滑块,转动轮盘等情况。

手势交互

3.1 Interactable

1718164181331

  • 标识对象是可交互的

  • 当手柄触碰到对象时,显示轮廓高亮

  • 不能实现抓取和投掷等功能

  • 通过继承该组件重写OnPointerClick函数来写自己的业务逻辑,或者直接注册 PointerClick 事件来写自己的业务逻辑

3.2 Throwable

投掷功能组件:添加这个组件时,会自动添加依赖组件Interactable(可交互的) 和 Rigibody(释放时,物理重力)

3.3 SteamvR_LaserPointer

官方提供的,在CameraRig预制体上的一个组件,用于实现激光交互,我们可以迁移到Player上

1718164209485

用于射线交互

  • Pose 保持默认,会自动关联

  • InteractWithUI: 默认

  • Active: 设置无效(源代码未使用)

  • Color: 射线默认颜色,这里设置为白色,透明度注意设置为255

  • Thickness: 射线宽度,默认

  • Click Color: 点击颜色

  • Holder: Pointer的父级, 设置无效,源代码中会自动创建

  • Pointer: 设置无效,源代码中会自动创建一个Cube

  • AddRigidBody: 射线上添加刚体属性,默认

  • Reference: 设置无效,源代码中未使用

3.4 SteamVR_LoadLevel

属性面板

SteamVR_LoadLevel属性面板

所有用到的贴图,检查是否被压缩,全部改为不压缩

  • LevelName:场景名称

  • InternalProcessPath:加载时要启动的进程(不用写)

  • InternalProcessArgs:进程参数

  • Load Addtive:场景是否是添加模式,不勾选就是从当前场景切换到目标场景

  • LoadAsync: 是否为异步加载,一般勾选

  • LoadingScreen: 加载背景

  • ProgressBarEmpty:加载进度条的背景

  • ProgressBarFull:加载进度条的进度

  • LoadingScreenWidthInMeters:加载背景的宽度(单位m)

  • ProgressBarWidthInmeters:加载进度条的宽度(单位m)

  • LoadingScreenDistance: 加载背景的距离,0代表无限远的地方,这里可以不设置,而采用下面两个来设置

  • LoadingScreenTransform:在场景中创建一个空对象,位置代表背景的位置

  • ProgressBarTransform:在常见中创建一个空对象,摆放和背景一个相对的位置,方便调整进度条在背景的某个位置,避免遮挡背景上的文字

  • Front:这6张图,代表天空盒的6个面

  • Back:

  • Right:

  • Top:

  • Bottom:

  • BackgroundColor:背景颜色

  • ShowGrid:是否显示网格

  • Fade Out Time:淡出时间

  • Fade In Time:淡进时间

  • Post Load Settle Time: 场景载入后,还需要等待的时间,为了能够很好的观看加载界面

  • Loading Screen Fade In Time: 加载背景淡入的时间 保持默认

  • Loading Screen Fade Out Time: 加载背景淡出的时间 保持默认

  • Auto Trigger On Enable: 脚本激活就触发加载

注意事项

(1)组件一直处于Loadind状态,导致无法加载

启动launch场景,自动加载 Hall 场景,这个时候如果在加载其他场景时,无效,代码查看是因为 static laoding 一直为 true

解决方案

加载Hall直接使用SceneManager.LoadScene("Hall",Mode.Addtive)来加载

另外使用组件时,使用的是异步加载,不知道换同步是否存在问题

3.5 Teleport

3.5.1 配置规则

传送涉及到两个预制体和一个组件

  • Teleporting(预制体) :必要的预制体,拖到场景,没有不行,提供传送的机制逻辑等

  • TeleportPoint(预制体) :传送点,定位传送

  • TeleportArea(组件) :传送区域,区域传送

- 注意:TeleportPoint 预制体的碰撞体默认是属于UI Layer,所以 Teleporting 检测的层级一定要包含 UI 层,或者你自定义

3.5.2 Teleporting

1718163458139

主要关注图上属性配置,其他的是特效,声音以及调试的配置

  • TeleportAction : 动作配置

  • TraceLayerMask : 检测的图层,如:手柄发送射线传送时,射线被某个物体遮挡,此时,可以将这个物体设置一个独立的图层,然后,这里不勾选这个图层,射线就会穿过这个物体

  • FloorFixupTraceLayerMask : 将哪些图层识别为地面,和下面的Distace属性共同使用

  • FloorFixupMaximunTraceDistance :一个容差,用于角色在传送后,角色始终在地面上而不是在传送区域所在的平面上,举例:传送区域高于地面0.5,若 FloorFixupTraceLayerMask 设置的是nothing,那么传送后,角色就会高于地面0.5 ;若 FloorFixupTraceLayerMask设置了地面图层,那么会根据这里的distace,假设为1,会在1这个范围内查找地面,如果找到,就设置角色到地面上,这里1>0.5所以,会找到地面图层,会将角色传送到地面上

URP材质替换

在URP渲染模式下:DestinationReticle 对象的材质,默认是 TeleportPointHighlighted,在编辑模式下还正常,但是发布后会出现无线显示传送位置标记的问题,需要替换为URP的材质,如下图:

1718164319587

3.5.3 TeleportPoint

1718164329451

  • Lockked :勾选,将无法传送到此区域,可通过代码来控制此变量,动态解锁传送区域

  • MarkerActive :勾选,运行时,传送区域不会显示,当按下传送健的时候才显示出来,否则,一开时就显示

  • TeleportType :传送到本地或者是切换场景

  • Title :传送的名称,在游戏当中显示

  • SwitchToScene :场景名称,需要“TeleportType” 为 “SwitchToNewScene”

  • PlayerSpawnPoint :角色初始点,勾选,角色最开始会出现在这个位置

4 Player

角色组件

  • 这是一个单例,场景中仅能存在一个,因此该对象在场景切换时不会不销毁,会一直存在

  • 使用技巧:现在一个空场景放置一个Player,启动空场景后立刻加载场景,这样切换场景时,Player一直存在

  • 或者若你的初始场景是一直存在,加载场景是以“Addtive”的方式,那么可将Player放置在初始场景

5 组件扩展

5.1 激光交互响应组件(LaserHand)

该组件依赖 SteamVR_LaserPointer,并监听其的 PointerInPointerOutPointerClick 三个事件,以实现对对象的指针移入、移出和点击三种交互事件的处理

代码

using UnityEngine;
using UnityEngine.EventSystems;
using Valve.VR.Extras;

namespace LFramework
{
    /// <summary>
    /// 激光射线交互,挂手柄上
    /// </summary>
    [RequireComponent(typeof(SteamVR_LaserPointer))]
    public class LaserHand : MonoBehaviour
    {
        // LaserPointer组件
        private SteamVR_LaserPointer _laser;
        // 是否开启激光指针交互
        public bool isEnabled = true;

        private void Awake()
        {
            // 获取到LaserPointer组件
            _laser = GetComponent<SteamVR_LaserPointer>();
            // 如果不为空
            if (_laser != null)
            {
                // 如果不开启激光指针交互
                if (!isEnabled)
                {
                    // 激光指针不显示
                    _laser.enabled = false;
                    return;
                }
                else // 如果开启激光指针交互
                {
                    _laser.PointerIn += LaserOnPointerIn;
                    _laser.PointerOut += LaserOnPointerOut;
                    _laser.PointerClick += LaserOnPointerClick;
                }
            }
        }

        private void LaserOnPointerIn(object sender, PointerEventArgs e)
        {

            IPointerEnterHandler _pointerEnterHandler = e.target.gameObject.GetComponent<IPointerEnterHandler>();
            if (_pointerEnterHandler != null)
            {
                _laser.color.a = 1;// showColor;
                _pointerEnterHandler.OnPointerEnter(new PointerEventData(EventSystem.current));
            }
        }

        private void LaserOnPointerOut(object sender, PointerEventArgs e)
        {
            IPointerExitHandler _pointerExitHandler = e.target.gameObject.GetComponent<IPointerExitHandler>();
            if (_pointerExitHandler != null)
            {
                _laser.color.a = 0;
                _pointerExitHandler.OnPointerExit(new PointerEventData(EventSystem.current));
            }
        }

        private void LaserOnPointerClick(object sender, PointerEventArgs e)
        {
            IPointerClickHandler _pointerClickHandler = e.target.gameObject.GetComponent<IPointerClickHandler>();
            if (_pointerClickHandler != null)
            {
                Debug.Log("点击了..." + e.target.gameObject.name);
                _pointerClickHandler.OnPointerClick(new PointerEventData(EventSystem.current));
            }
        }

        private void OnDestroy()
        {
            if (_laser != null && isEnabled)
            {
                _laser.PointerIn -= LaserOnPointerIn;
                _laser.PointerOut -= LaserOnPointerOut;
                _laser.PointerClick -= LaserOnPointerClick;
            }
        }
    }
}

对UI的交互

  • 和普通开发模式一样,是对Button组件的交互

  • Button按钮上添加BoxCollider组件,用于射线检测

  • 注意交互对象所在的Layer(层级),是否在射线检测的层级设置里面

对模型的交互

  • 模型对象上添加BoxCollider组件,用于射线检测

  • 注意交互对象所在的Layer(层级),是否在射线检测的层级设置里面

  • 在模型上挂载一个组件,该组件根据需求实现:IPointerEnterHandler IPointerExitHandler IPointerClickHandler 三个接口

代码忽略。

6 UI

6.1 设置

  1. 将Canvas 设置为 World Space 模式

  2. Scale 均设置为 0.001,长宽都设置为1(这个没什么影响)

  3. 其他和普通的UI设置一样

6.2 UI被模型遮挡

  1. 使用网上的 uioverlay shader制作材质,赋给 image text rawimage 等

  2. 对于 TextmeshProUGUI, 它有自己的 overlay shader,我们主要将我们制作的字体材质复制一份,作为ovlerlay材质,将shader换为 Distance filed Overlay shader

  3. 问题:玩家的手部模型,和射线也会被 设置了Overlay的UI覆盖,导致 无法操作 这种UI,目前还没解决,网上的人很狗屎,没人提这个问题

7 问题汇总

世界坐标中的Canvas在相机距离改变后被遮挡

图中的箭头是个UI,当相机离它近时,它正常显示在黄色标签的前面,如下图

1721199092376

当相机拉远点时,箭头被标签挡住了

1721199097929

解决办法:将Cavans的Order in Layer 设置为1,要大于0

1721199106664

HTC VIVE 定位器更新固件后闪烁红灯

官方回复(失败)

首先联系官方,根据回复指导操作后,未解决问题,以下是官方的回复:

以下是1.0 定位器重置固件的详细操作步骤。

1. 从电源插座上拔下电源适配器。

2. 从基站背面拔下电源适配器。

3. 使用官方自带 USB 线缆(手柄充电线)将基站与计算机连接。

4. 按住“频道键”不放。

5. 将电源适配器连接到基站背面,然后将另一端插入电源插座。

6. 在计算机上,您将看到一个新设备显示为CRP DISABLD.

7. 打开它并删除文件 firmware.bin.

8. 在计算机上,转到SteamVR的安装文件夹。

9. 找到到固件版本为436的文件夹。文件夹路径如下: VIVE\Steam\steamapps\common\SteamVR\tools\lighthouse\firmware\lighthou setx\archive\htc2.0

10. 复制 lighthousetxhtc_2.0-436-2016-09-20.bin 到 CRP DISABLD 中。

11. 等候约 1分钟,然后拔下USB线缆。

网友方法(成功)

注意:若修复成功,定位器会显示一个感叹号,提示更新,千万不要更新,就是因为更新出现的问题,我们的操作就是回退固件。 {.is-warning}

在SteamVR安装目录下找到以下两个文件,安装目录:D:\Program Files (x86)\Steam\steamapps\common\SteamVR\tools\lighthouse\firmware\lighthouse_tx\archive\htc_2.0

lighthouse_tx_htc_2_0-calibration-rescue-244.bin lighthouse_tx_htc_2_0-244-2016-03-12.bin

附件根据自己的具体路径查找

  1. 将好的定位器先断电

  2. 在基站未通电情况下,将其通过micro-B USB(手柄充电线)传输线连接到PC。

  3. 按住基站背后的模式键并插入电源线.

  4. 一旦PC端确认为USB大容量存储设备(USB mass storage device),才可以释放模式键。

  5. 被连接的基站储存设备名为“CRP_DISABLED”,打开后包含一个文件“firmware.bin”,删除它。

  6. 将“lighthouse_tx_htc_2_0-calibration-rescue-244.bin”文件复制到基站的储存空间上。

  7. 复制完成后,拔掉电源线。

  8. 等几秒,然后再次插上电源。在此过程中不要按模式键。几秒后基站应该会快速的闪烁绿灯或者红灯。绿灯表示修复成功。

  9. 如果它闪烁红灯,这表示不能自动修复,请您送修。

  10. 再此拔下电源。

  11. 重复步骤1到7,但第5步复制文件改为“lighthouse_tx_htc_2_0-244-2016-03-12.bin"。

  12. 完整后基站就恢复正常了,讲其频道设置为“A”并单独跟踪(另一个基站不通电)来确认运行情况。一旦确认工作正常,再打开另一个基站。

其他:

  • 若单独使用一个定位器:设置为A模式

  • 若使用两个定位器(有线连接):AB模式

  • 若使用两个定位器(无线连接):BC模式

URP渲染模式发布后无法显示传送位置

需要手动替换下 DestinationReticle 的 材质,具体见 Teleporting章节

发布后手柄无响应

  1. 发布位置不能有中文

  2. 按键配置需要替换为默认的配置,操作见 相应章节

左右眼渲染不同,视野模糊

通过 Edit->ProjectSettings->XR Plug-in Managerment 下的 openvr设置为多通道

通过设置这个解决问题,但是,项目报错 “IsMatrixValid(matrix)”

以下是一个解决方案,待测试

https://forum.unity.com/threads/ismatrixvalid-matrix-error-spam-when-splitting-up-steamvr-eyes.425474/

新建一个脚本,挂在初始场景上

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Management;
using Valve.VR;
namespace LFramework
{
    public class XRFix : MonoBehaviour
    {
        List<XRDisplaySubsystem> displays = new List<XRDisplaySubsystem>();
        private void Awake()
        {
            //StartXR();
            //SteamVR.Initialize();
        }
        public void StartXR()
        {
            XRGeneralSettings.Instance.Manager.InitializeLoaderSync();
            XRGeneralSettings.Instance.Manager.StartSubsystems();
        }
        private void Update()
        {
            EnforceStereoTexture();
        }
        void EnforceStereoTexture()
        {
            displays.Clear();

            SubsystemManager.GetInstances(displays);
            foreach (var displaySubsystem in displays)
            {
                if (displaySubsystem.running)
                {
                    displaySubsystem.textureLayout = XRDisplaySubsystem.TextureLayout.SeparateTexture2Ds;
                }
            }
        }
    }
}

非多样采集的贴图绑定到多样采集器上的错误

版本:untiy 2020.3.40

VR开发

❌错误提示:A non-multisampled texture being bound to a multisampled sampler. Disabling in order to avoid undefined behavior. Please enable the bindMS flag on the texture.

报错说明:将非多样采集的贴图绑定到多样采集器上,为了避免未知的结果应该禁用它,请激活贴图的bindMS

报错的原因

在进行VR开发时,场景结构是一个原始场景一直存在,通过Addtive的方式加载其他场景,但是第二次加载的时候就会报错,信息显示是SteamVR_loadlevel的脚本中调用了Shader.WarmupAllShaders();

1721198690359

查找资料: 1 设置抗锯齿(MSAA)导致的,网上说的是要关闭,但是我关闭后,第一次就报错

1721198700183

其他资料 https://docs.unity3d.com/Manual/shader-variant-collections.html

https://github.com/ValveSoftware/unity-xr-plugin/issues/88

https://forum.unity.com/threads/a-non-multisampled-texture-spams-the-console.1143884/

https://blog.csdn.net/u013716859/article/details/124644214

3.目前先注释调这个代码看看

资源优化

EditorSetting

LayerCollisionMatrix

层级碰撞矩阵,没必要每层都去检测碰撞

检查Physics设置中的LayerCollisionMatrix设置

  • 这会增加CPU的负担, 应该取消勾选那些没有必要的格子

  • 检查Editor -> Project Settings -> Physics -> Layer Collision Matrix的设置

  • 在Physics设置中LayerCollisionMatrix中的格子不应该都勾选上

检查Physics2D中LayerCollisionMatrix设置

  • 这会增加CPU的负担, 应该取消勾选那些没有必要的格子

  • 检查Editor -> Project Settings -> Physics2D -> Layer Collision Matrix的设置

  • 在Physics2D设置中LayerCollisionMatrix中的格子不应该都勾选上

FBX

检查读/写标志

开启FBX资源的读/写标志会导致双倍的内存占用,一般禁用掉写

检查动画资源压缩方式

动画资源使用最佳压缩方式可以提高加载效率,查看Inspector -> Animation Tab -> Anim. Compression选项

检查动画资源的OptimizeGameObjects选项

动画资源应该勾选OptimizeGameObjects选项来减少CPU消耗,查看Inspector -> Rig Tab -> Optimize Game Objects选项

检查FBX资源顶点数

FBX资源资源中有太多的顶点, 请检查是否必要,500以下,Custom Parameters: vertexCountLimit : 500

Animation

检查动画曲线精度

动画曲线精度过高会增加动画占用内存; 此规则仅面向以文本格式序列化的*.anim文件中的浮点精度 Custom Parameters: precision : 5 用文本编辑器打开.anim动画文件,修改m_EditorCurves::curve::m_Curve下的float值的精度。建议用脚本直接将此文件中所有float精度都调小。

自己编写的优化工具:工具

Prefab

检查Skinned Mesh Renderer

启用Skinned Motion Vectors会使渲染器同时使用当前帧和上一帧的蒙皮网络来渲染目标的动画以提高精度,从而需要双倍大小的缓冲区并占用双倍的显存

Scene

检查场景中的Audio Listener

一个场景不应包含多个Audio Listener 检查场景中所有GameObject下的Audio Listener组件

检查场景中mesh collider

Mesh Collider可以在场景中提供更精细化的碰撞检测,随之而来也会消耗大量计算资源,建议审慎使用。 检查场景中所有GameObject下的Mesh Collider组件

检查场景渲染设置

在移动平台,建议在渲染设置中关闭对雾的渲染以节省计算资源。 检查Window -> Rendering -> Lighting Settings -> Scene -> Other Settings -> Fog选项

检查Canvas中的component数量

包含太多组件的Canvas可能会影响UI刷新的性能,并进一步影响应用的帧率。

检查场景Animator组件中的cullingMode

场景中Animator组件的cullingMode是AlwaysAnimate会增加CPU使用率。

Shader

检查Shader中纹理数量

Shader中过多的纹理可能会增加GPU消耗 Custom Parameters: textureCountLimit : 3

Texture

检查纹理读/写标记

开启纹理资源的读/写标志会导致双倍的内存占用 检查Inspector -> Advanced -> Read/Write Enabled选项

检查Mipmap标记

未压缩的纹理资源启用Mipmap标志会增加内存占用 检查Inspector -> Advanced -> Generate Mip Maps选项

  • 未压缩的纹理应该禁用mipmap

  • 类型为Sprite的纹理应当禁用mipmap

Android平台纹理压缩格式

检查Android平台的纹理压缩格式

如果希望对各平台统一设置压缩格式,检查Inspector -> Default -> Format选项; 如果希望为Android平台单独设置,打开旁边的Android选项卡,勾选Override for Android并检查下面的Format选项

纹理资源大小2的幂次

大小非2的幂次的纹理资源将无法使用ETC1和PVRTC压缩格式。在导入时自动伸缩为2的幂次也可能会导致内存占用或者贴图质量问题。

检查Inspector -> Advanced -> Non-Power of 2选项. 建议使用原始大小为2的幂次的贴图.

检查纯色纹理

纯色纹理的使用可能可以由一些设置来代替。由于某些情况下纯色纹理是必不可少的,此警告仅会在所使用的纹理较大(大于设定值, 默认为16x16)时才会触发。

Custom Parameters: heightThreshold : 16widthThreshold : 16

检查重复纹理

检查重复纹理

检查精灵图纹理填充率

填充率是精灵图分割后的有效面积与总面积的比率,较低的精灵图纹理填充率会导致显存的浪费。

Custom Parameters: fillRateThreshold : 0.5onlyCheckSprite : True

尝试重新编排精灵图,尽量缩小总面积以提高填充率

Script

避免分配托管内存

以下方法会导致分配托管内存,给出了替代的方法

问题解决方案
Component.GetComponentsInChildren()替代:GetComponentsInChildren(bool includeInactive, List result)
AnimationCurve.keys替代:AnimationCurve.AddKey()/MoveKey()/RemoveKey()
Resources.FindObjectsOfTypeAll()Resources.FindObjectsOfTypeNonAlloc()
Object.FindObjectsOfType()尽量避免在频繁更新的代码中调用此方法。理想情况下,该方法应该只在初始化期间使用,如果需要重用结果,则应该缓存结果。
Linq 会分配大量的托管内存强烈建议不要在任何频繁更新的代码中使用Linq。完全禁止在项目中使用它,或者将它限制在初始化代码中,并少量使用它。
Component.GetComponentsInParent()替代:GetComponentsInParent(bool includeInactive, List results)
Texture2D.GetPixels()替代:Texture2D.GetRawTextureData()

减少CPU使用

以下方法会导致CPU性能的问题,给出了替代的方法

问题解决方案
AddComponent()尽量避免在频繁更新的代码中获得此属性。更喜欢从预制件实例化游戏对象将所有必要的组件。
Instantiate()操作昂贵,避免经常使用,内存池处理
反射删除依赖于反射的代码,或尽量减少反射的使用,特别是在初始化之外。
空方法任何空的MonoBehaviour魔法方法都将包含在构建中并执行,移除任何空的方法

检查空的MonoBehavior Update方法

MonoBehavior脚本不应具有空的Update方法

检查OnGUI方法

由于内存使用率高,不应使用OnGUI方法

IMGUI是过时的UI系统,仅建议在开发调试时使用。

Unity中的特殊文件夹

文件夹

Editor

Editor文件夹可以在Assets目录下,也可以在子目录里,只要名子叫Editor就可以。例如目录Assets/Editor和Assets/Scripts/Editor是一样的,无论多少个叫Editor的文件夹都可以 Editor文件夹下存放的所有资源文件或脚本文件都不会被打进包中,并且脚本也只能在编辑时使用。一般会把一些编辑器扩展的脚本放在这里,或者是一些编辑时用的DLL

Editor Default Resources

必须放在Assets目录下,可以把编辑器扩展用到的一些资源放在这里,比如图片、文本文件、等等。它和Editor文件夹一样都不会被打到包中,仅仅用于开发时使用 通过 EditorGUIUtility.Load 可以读取到该文件夹下的资源

using UnityEngine;
using UnityEditor;

public class LoadExample
{
    [MenuItem("Examples/Load Editor Texture Example")]
    static void loadExample()
    {
        Texture tex  = (Texture)EditorGUIUtility.Load("aboutwindow.mainheader");
        Debug.Log("Got: " + tex.name + " !");

        Renderer r = GameObject.Find("Cube").GetComponent<Renderer>();
        r.sharedMaterial.mainTexture = tex;
    }
}

Gizmos

必须放在Assets目录下,可以将绘制在场景中的Gizmo素材放在这个文件夹下,使用Gizmos.DrawIcon绘制的时候可以直接传入图片路径,打包时会打到包中

Plugins

可以将sdk、dll库文件、插件等放在这里,打完包后就会自动将这些文件打到包中

Resources

❗❗❗ 项目开发时不要使用它 ❗❗❗

它只是用来快速构建demo

Resources文件夹可以在Assets目录下,也可以在子目录里,只要名子叫Resources就可以,Resources文件夹下的所有资源不管你用还是不用都会被打进包中 可以使用Resource.Load读取此文件夹下的资源 因为Resource文件夹下的所有资源都会被打进包中,所以一般开发时使用AssetDatabase.LoadAssetAtPath,它可以读取Assets目录下的任意文件夹下的资源,只能在编辑器下使用,它的路径是Assets/xx/xx.xxx,需要注意路径带文件的后缀名

StreamingAssets

这个文件夹下的资源也都会被打进包中,它和Resources的区别是Resources会压缩文件,但是StreamingAssets不会压缩而是原封不动的打包进去。并且它是一个只读的文件夹,就是程序运行时只能读不能写。它在各个平台下的路径是不同的 使用Application.streamingAssetsPath会根据当前的平台选择对应的路径

其他文件夹: 如果是没有用到的资源,就不会被打到包中,如果是用到了的资源就会被打到包中

路径

Application.dataPath

  • 这里是列表文本Assets文件夹路径,
  • 只在编辑器模式下有效,打包后该路径不存在

Application.streamingAssetsPath

StreamingAssets文件夹路径,不同平台读取方式不同,详细见图:

平台 File读取 UnityWebRequest读取
PC ✔️ 无前缀 ✔️
安卓 ✔️ 加前缀:jar:file://
IOS ✔️ 无前缀 ✔️ 加前缀:file://
Mac ✔️ 无前缀 ✔️ 加前缀:file://

UnityWebRequest读取代码示例:

/// <summary>
/// 使用UnityWebRequest请求时的地址(根据各个平台处理)
/// 注:这个路径不能使用File来使用
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string GetRequestURL(string fileName)
{

    string path = Path.Combine(Application.streamingAssetsPath, fileName);

#if UNITY_ANDROID && !UNITY_EDITOR
            path ="jar:file://" + ptah;
#elif UNITY_EIDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE //苹果下是这样
            path ="file://"+ ptah;

// #else // UNITY_EDITOR || UNITY_STANDALONE_WIN || UNITY_IPHONE 这些的路径一样
//             request = new UnityWebRequest(Application.streamingAssetsPath + "/" + fileName);
#endif
    return path;
}

Application.persistentDataPath

这个文件夹在不同平台下的路径不同

平台 路径 读写方式
PC 与普通文件路径相同 File直接读写
安卓 /data/data/xxx.xxx.xxx/files File直接读写(安卓端唯一可以用File读写的路径)
安卓手机连接电脑,可通过 Android/data/com.xxx.xxx查看
IOS Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Documents File直接读写

读写案例

    public UnityEngine.UI.Text infoText;
    public void FileRead()
    {
        System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(Application.persistentDataPath);
        infoText.text = "";
        foreach (FileInfo file in di.GetFiles())
        {
            //Debug.Log(file.Name);
            infoText.text += file.Name.ToString();
            StreamReader sr = file.OpenText();
            infoText.text += ":" + sr.ReadToEnd()+"\n";
            sr.Close();
        }
    }
    public void FileWrite()
    {
        string fileName = "Test.txt";
        string path = Path.Combine(Application.persistentDataPath, fileName);

        if (!File.Exists(path))
        {
            File.WriteAllText(path, "Persistent path test!");
            infoText.text = "添加了 Test.txt 文件 ";
        }
        else
        {
           infoText.text = "已经存在 Test.txt,已删除 ";
           File.Delete(path);
        }
    }

itextShap插创建字体案例

itextShap 插件是用于创建PDF文档的,但是需要使用我们自己的字体。

  • 创建字体函数的方法
public static BaseFont CreateFont(string name, string encoding, bool embedded);//底层应该是通过 System.IO.File 来访问创建的
  • 在PC或者IOS上,我们只需要将字体放到 StreamingAssets 文件夹可正常使用接口创建
  • 在安卓端,是不能通过 System.Io.File 访问 StreamingAssets 的,唯一可以访问的是 PersistentData 文件夹,所以我们需要在程序首次启动的时候,将StreamingAssets 中的字体文件 拷贝到 PersistentData
     const string SIMHEI = "SIMHEI.TTF";
    /// <summary>
    /// 安卓下调用
    /// 将自己使用的字体从 StreamingAssets中复制到 persisentData 中 
    /// StreamingAssets 中的 在 安卓端 被压缩了 不能直接通过  IO读取
    /// </summary>
    private static void CopyFontToPersistentData()
    {
        string path = Path.Combine(Application.persistentDataPath, SIMHEI);
        //如果持久化目录下没有配置文件,先从streamingAssets里复制一份到持久化目录
        if (!File.Exists(path))
        {
            //先通过 UnityWebRequest 将 streamingAssetsPath 中的 字体读取出来
            UnityWebRequest request = UnityWebRequest.Get(Path.Combine(Application.streamingAssetsPath, SIMHEI));
            request.SendWebRequest();
            while (request.isDone == false) { }
            //写入 persistentDataPath
            File.WriteAllBytes(path, request.downloadHandler.data);
        }

    }

注意事项

该路径常用来存储档案的注意事项

  • 卸载软件后该路径会被 删除 ,再次安装数据被重置了(安卓端测试)
  • 不卸载软件,直接重新安装,原数据还在(安卓端测试)
  • 在PC端,有些时候是免安装版本,该路径是在程序所在的文件夹外,所以不会被删除
  • 总结以上情况:在版本升级中,若对存储的数据结构进行改变了,那应先删除原来的数据,否则程序要出错

Application.temporaryCachePath

基本未使用过,未深入研究。

平台 路径 读写方式
PC - -
安卓 /data/data/xxx.xxx.xxx/cache -
IOS Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Library/Caches -

发布 Window 平台

USB调试

Unity版本:Unity2020.3.38

BuildSettings

以Windows端为例

1721891706918

  • DevelopmentBUild:勾选后激活下面的选项

  • ScriptDebugging: 脚本调试,输入日志

  • ScirptsOnlyBuild: 当你调试后想更新版本时,若只是更改了代码,那么勾选这个,并会出现两个选项 patchpathc and run ,构建时相当于只更新代码那块,加快构建速度,在移动端,要取消 strip unityEngine code

Playersettings

  • Mono 环境下,不用更改

    1721891713790

  • IL2CPP 环境下,需要切换成Debug,否则不能显示日志信息

    1721891732543

发布 WebGL 平台

模板

压缩

参考:

官网压缩设置手册

服务器配置手册

压缩格式

从 WebGL Player Settings 窗口(菜单:Edit > Project Settings > Player ),然后选择 WebGL 并展开 Publishing Settings 部分),一般选压缩格式 Gzip

1722581090754

压缩方法描述
gzip这是默认选项。gzip 文件比 Brotli 文件更大,但构建速度更快,且所有浏览器都基于 HTTP 和 HTTPS 实现此格式的本机支持。
BrotliBrotli 压缩提供最佳压缩比。Brotli 压缩文件小于 gzip,但需要更长的压缩时间,因此会增加发布版本的迭代时间。Chrome 和 Firefox 仅原生支持基于 HTTPS 的 Brotli 压缩。
Disabled禁用压缩。如果要在后期处理脚本中实现您自己的压缩,请使用此选项。如果计划在托管服务器上使用静态压缩,也应该使用此选项。

有关浏览器对所选压缩方法的支持的更多信息,请参阅有关 WebGL 浏览器兼容性的文档。

Web 服务器配置

参考

文档上提供了服务器配置示例,但是未设置成功,还是提示要配置gzip

解压缩回退

解压缩回退选项使 Unity 能够自动将 JavaScript 解压缩器嵌入到您的构建中。该解压缩器与您选择的压缩方法相对应,它在浏览器无法解压缩内容时执行解压缩。

Enable decompression fallback

Player Settings 窗口启用解压缩回退(菜单:Edit > Project Settings > Player ,然后选择 WebGL 并展开 Publishing Settings 部分):

如果启用解压缩回退,Unity 会向构建文件添加一个 .unityweb 扩展名。 如果不熟悉服务器配置,或者无法使用服务器配置,应考虑使用 Decompression Fallback

Note : Enabling decompression fallback results in a large loader size and a less efficient loading scheme for the build files.

Disable decompression fallback

The Decompression Fallback option is disabled by default. Therefore, by default, build files include an extension that corresponds to the compression method you select.

有两种压缩方法可供选择:gzip 或 Brotli。有关更多信息,请参阅压缩格式部分。

To enable browsers to natively decompress Unity build files while they’re downloading, you need to configure your web server to serve the compressed files with the appropriate HTTP headers. This is called native browser decompression. It’s faster than the JavaScript decompression fallback, which can reduce your application’s startup time.

原生浏览器解压缩的设置过程取决于 Web 服务器。有关代码示例,请参阅服务器配置代码示例

内容编码标头

Content-Encoding 标头告诉浏览器 Unity 用于压缩文件的压缩类型。这样,浏览器可以原生解压缩文件。

将 Content-Encoding 响应标头设置为在 Player Settings 中选择的压缩方法。

压缩方法文件扩展名响应标头
gzip.gzContent-Encoding: gzip
Brotli.brContent-Encoding: br

WebAssembly 串流(更高级别的标头)

WebAssembly streaming allows the browser to compile the WebAssembly code while it’s still downloading the code. This significantly improves loading times.

为了进行 WebAssembly 串流编译,服务器需要返回 application/wasm MIME 类型的 WebAssembly 文件。 要使用 WebAssembly 串流,需要使用带 Content-Type: application/wasm 响应标头的 WebAssembly 文件。 Content-Type 标头告诉服务器内容是什么媒体类型。对于 WebAssembly 文件,该值应该设置为 application/wasm

文件扩展名响应标头
.wasm、.wasm.gz、.wasm.brContent-Type: application/wasm

Note : WebAssembly streaming doesn’t work together with JavaScript decompression when the Decompression Fallback option is enabled. The downloaded WebAssembly file must first go through the JavaScript decompressor because the browser can’t stream it during download.

附加标头

If your file contains JavaScript, you should add the application/javascript Content-Type header. Some servers might include this automatically, while others don’t.

文件扩展名响应标头
.js、.js.gz、js.brContent-Type: application/javascript

缓存

发布Unity WebGL项目时,实现资源缓存可以显著提高应用的加载速度和用户体验。以下是一些常见的方法来实现资源缓存:

使用Unity自带的缓存功能

Unity提供了一些内置功能来实现资源缓存:

  • 使用PlayerSettings设置压缩和缓存策略 : 在 PlayerSettings > WebGL > Publishing Settings中,可以设置压缩格式(如gzip或brotli)和启用数据缓存。

使用浏览器缓存

浏览器缓存是最常见的资源缓存方法:

  • 设置HTTP头 : 配置服务器来设置适当的HTTP缓存头。可以设置 Cache-ControlExpiresETag头来控制资源的缓存策略。 示例:
Cache-Control: max-age=31536000, public

使用Workbox实现缓存

安装Workbox :

npm install workbox-cli --global

创建Workbox配置文件:在你的项目根目录创建一个Workbox配置文件,例如workbox-config.js,配置文件内容如下

module.exports = {
  globDirectory: 'build/',
  globPatterns: [
    '**/*.{html,js,css,wasm,data}'
  ],
  swDest: 'build/sw.js',
  runtimeCaching: [{
    urlPattern: /\.(?:html|js|css|wasm|data)$/,
    handler: 'CacheFirst',
    options: {
      cacheName: 'my-game-cache',
      expiration: {
        maxEntries: 50,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 缓存30天
      },
    },
  }],
   maximumFileSizeToCacheInBytes: 50 * 1024 * 1024, // 设置为50MB
};

这里指定了要缓存的文件类型(如.html、.js、.css、.wasm、.data、.mem和.framework.js)。

生成Service Worker:在项目根目录运行以下命令,生成Service Worker文件:

workbox generateSW workbox-config.js

这将在Build目录中生成一个名为sw.js的Service Worker文件。

注册Service Worker:在Unity WebGL项目的HTML文件(通常是index.html)中注册Service Worker。在或标签中添加以下代码:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
      navigator.serviceWorker.register('/Build/sw.js').then(function(registration) {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
      }, function(err) {
        console.log('ServiceWorker registration failed: ', err);
      });
    });
  }
</script>

部署项目:将整个Build目录上传到你的Web服务器。确保你的服务器支持HTTPS,因为Service Worker需要在HTTPS环境下才能工作。

配置服务器缓存:为确保最佳性能,配置你的Web服务器以支持长时间缓存。以下是一个Nginx服务器的示例配置:

server {
  listen 80;
  server_name yourdomain.com;
  root /path/to/your/build/directory;

  location / {
    try_files $uri $uri/ /index.html;
  }

  location ~* \.(?:manifest|appcache|html?|xml|json)$ {
    expires -1;
  }

  location ~* \.(?:css|js|wasm|data|mem|framework.js)$ {
    expires 1y;
    access_log off;
  }

  location ~* \.(?:png|jpg|jpeg|gif|ico|svg|webp)$ {
    expires 1y;
    access_log off;
  }
}

UnityWebRequest

https://docs.unity3d.com/cn/current/Manual/webgl-networking.html

下面错误的使用方式

❌错误使用:在 pc 端可以,但是在 webgl 端要阻塞。

while(!wwww.isDone){}

1722582220211

报错汇总

GrpcWebSocketBridge:插件只能使用 .net standard.21


<span style="color:red;font-weight:bold;"> Uncaught RuntimeError: null function or function signature mismatch

原因:

Addressables 打包未成功,出现的原因是莫名奇妙的少了 ContentPachking$Loading 这部分,需要点击 **Add Schema**添加

打包后需要在本地选择 Use Exsiting build 模式下测试是否加载成功。

1722582624459


<span style="color:red;font-weight:bold;">Cannot create FMOD::Sound instance for clip " " (FMOD error: An error occured that wasn't supposed to. Contact support. )

**使用场景:**对音频资源使用Addressables打包,然后通过AssetReference 的方式引用。

解决方式:(未解决) 相关的音频需要设置为流式加载。 补充:已经设置为Streaming 又出现相同的问题。

1722582895900

发布 Android 平台

1 开发

2 调试

2.1 使用插件Android Logcat

在插件包中安装该插件,详情可以看插件的文档

2.2 UnityRemot 5

这个感觉没得上面一个好用,且不是真正在手机端运行,可能和实际情况有不符合的情况

收集的文章

  • https://blog.csdn.net/vegetable_haker/article/details/126864565

3 发布

3.1 发布设置

前提条件

  1. 包体不能大于2G,否则无法安装

3.2 发布报错

  1. 发布失败,提示IL2CPP .... 一推错误。

    • 情况1:开始勾选了development build ,发布失败,取消勾选,发布成功,再勾选,也会发布成功
  2. 编辑器下运行正常,发布后失败

    • 是否时使用了Addressables, 是否已经打AB包
  3. repositories.cfg

File C:\Users\Lenovo.android\repositories.cfg could not be loaded. UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

  1. NDK问题
Exception: Unity.IL2CPP.Building.BuilderFailedException: C:\Program Files\Unity\Hub\Editor\2020.3.40f1c1\Editor\Data\PlaybackEngines\AndroidPlayer\NDK\toolchains\llvm\prebuilt\windows-x86_64\bin\clang++ @"C:\Users\Lenovo\AppData\Local\Temp\tmp77D3.tmp" -o "E:\Projects\CWSS\Library\il2cpp_android_arm64-v8a\il2cpp_cache\linkresult_744F7712D2E44CF47D673819F69A85D6\libil2cpp.so" -shared -Wl,-soname,libil2cpp.so -Wl,--no-undefined -Wl,-z,noexecstack -Wl,--gc-sections -Wl,--build-id -stdlib=libc++ -static-libstdc++ -target aarch64-linux-android21 -Wl,--wrap,sigaction "C:\Program Files\Unity\Hub\Editor\2020.3.40f1c1\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\il2cpp\Development\StaticLibs\arm64-v8a\baselib.a" -llog -rdynamic -fuse-ld=bfd.exe

  1. 【Unity3D IL2CPP】构建失败,异常:Unity.IL2CPP.Building.BuilderFailedException
异常提示:预编译头文件来自编译器的早期版本,或者预编译头为 C++ 而在 C 中使用它(或相反)

删除项目目录Library文件夹下il2cpp_cache和Il2cppBuildCache文件夹缓存,然后尝试重新打包。

发布 IOS 平台

Unity官方文档 - iOS 构建过程详解

上架说明

  1. 注册一个苹果开发者账号,可以个人注册,或者公司注册,公司比较复杂,个人比较简单,个人的年费为688

  2. 需要一个Mac,主要用于申请一些证书和使用Xcode软件(上架必备)

  3. 请求证书,以及制作配置文件

  4. Unity Build IOS, 此过程在 windows上和mac上都可以进行,结果是得到一个 Xcode项目工程

  5. 在 mac 上使用xcode 打开 上一步的项目设置分发

  6. 测试分发:发布测试版本,就是不用上架审核,但是有限制

  7. 上架分发,需要审核,终极目标。

苹果开发者账号

注册

首先需要开发者账号,且按年付费,具体注册流程如下:

  1. 手机下载 developer app 进行注册申请,在手机上下载app安装

  2. 注意填写信息时,完全按照身份证上的填写,按照英文的格式填写,最好就是找个已经注册好的看下格式来填写(我还是失败了,最后是人工客服完成的,使用电话的方式,她帮你解决;使用邮件会来回折腾,还是不行)

  3. 客户帮你解决后,回到app,退出再登录,才能继续注册流程,选择个人注册,直到付完费用688,订阅后,马上取消订阅,避免到时候忘了(若上架的账号没有继续订阅,未安装过此app的无法在商店找到此应用,安装过的还是可以找打)

  4. 此时,在app上显示一个灰色的注册,说是我即将收到一个邮件

  5. 收到邮件,上传身份证,正反面都要上传,完成激活,此时应该才是注册完成(给的邮件说是带有照片的,我就只上传照片的,最后人工后,说两面都要上传)

  6. 总结:注册很狗屎,每次错也不晓得哪里错,不停的试,最后都是人工处理。

注意事项(坑)

  1. 看siki学院的视频,以为付费完就注册完了,实际上还要上传身份证

  2. 我也的确收到邮件了,叫我上传身份证,但是我以为是之前注册失败,叫我上传,有几次失败就是叫上传,结果还是不行,所以我也没理

  3. 我就还在等其他的邮件,在网页端登录,叫我继续注册流程,很郁闷。

续费

订阅续费在手机上操作:设置/账号头像(名称)/订阅。即可看到已经停用的 Apple Developer ,点击续订即可。

网上(ChatGpt)说的在开发者官网上的会员详情里面续订不行,就没有续订按钮。

一、钥匙串创建证书

先创建好后面步骤要使用。

  1. 在 mac 上 打开钥匙串app

  2. 在屏幕左上角:钥匙串访问-》证书助理-》从证书颁发机构请求证书

    证书助理

  3. 请求是:选择“存储到磁盘”,点击继续,保存即可

  4. 注意这个证书,只能在本设备上使用,若其他电脑要使用这个证书,需要转换,方法待补充

二、证书

  1. 登录开发者网站,进入Developer-账户

  2. 选择证书(注意:如果官网存在协议更新,需要重新查看协议,并同意后,才能出现证书的相关操作,否者证书下没有可选操作)。 证书

  3. 点击 +alt text

  4. Software 下选择证书类型,在 Services 下选择需要的服务,点击继续。

    • Apple Development: 苹果所有终端的开发测试证书,包含 mac ios
    • Apple Distribution: 苹果所有终端的上架发布测试证书,包含 mac ios
    • IOS App Development:仅 ios 端的开发测试证书
    • IOS Distribution (App Store and Ad Hoc):ios 端 的 上架商店证书
    • 其他同理
  5. 上传之前使用钥匙串申请的证书,点击继续。

  6. 下载申请好的证书,然后双击后安装到钥匙串中。

  7. 最后我们在证书列表里面可以看到当前生气的证书,注:钥匙串申请的证书可以创建测试证书和分发证书。

  8. 证书会过期(当开发者账号订阅过期时,证书也会过期)

三、Identifiers(标识符)

  1. 在证书、标识符和描述文件页面,左侧选择 Identifers ,点击 Identifiers +
  2. 选择 App IDs
  3. 选择 APP
  4. Regieste an App ID, 输入 bundle identifier,这个需要记住,后面在Unity工程设置中需要使用
  5. 设置 App Services,这个根据需要选择,比如推送、支付等,点击 continue
  6. 点击 register,注册完成
  7. 在 Identifers 列表里面可以看到当前注册的 App ID,注:这个 App ID 会在后面的描述文件中使用到

四、Devices(设备)

作用,收集需要参加测试手机的UDID号,添加到这里,在打包测试的时候,只有添加了的设备才能安装测试 上架商店就不需要这个

获取设备UDID 通过下面的地址,在浏览器打开后,提示下载配置文件,然后安装,获取,发送给指定邮箱 https://www.pgyer.com/tools/udid

五、Profiles(描述文件)

  1. 点击 + 。
  2. Register a New Provisioning Profile:选择类型,根据当前的测试设备和类型(测试/分发)来选择。
  3. Generate a Provisioning Profile:选择 App ID,Provisioning Profile Configuration 默认 选择 No,点击 continue.
  4. 选择一个证书,点击继续。
  5. 选择测试的设备,点击继续。
  6. 填写描述文件名称,点击生成。
  7. 下载描述文件,备用。
  8. 在 Profiles 列表里面可以看到当前生成的描述文件,注:这个描述文件会在后面的打包中使用到。

六、Unity项目设置

此处的设置可以在xcode里面修改,所以如果填写错误,不用再重新发布。

  1. 可以在window上或者mac上进行
  2. 设置公司名称
  3. 设置product name:上架审核时,这个名称必须 app store connect上创建的app 名称相同,否审核不过
  4. 设置version:只能包含"数字"和“.”,其他都是非法的,可以在xcode里面修改。
  5. 设置bundle identifier: 这个必须和后面创建的证书时一致,可以在xcode里面修改。
  6. 设置 Target device: 根据你的实际需求来,如果仅在iphone上运行,那么就选 iPhone only,否则在上架审核时,需要你上传其他设备的分辨率截图,多出很多事情,可以在xcode里面修改。
  7. 目标最小ios版本,这个根据情况设置,可以在xcode里面修改。

七、Xcode项目设置

version: 15.4

官网文档

  1. 将Unity工程打包成Xcode工程,可以在Windows打包,也可以在mac上打包(未知原因:在mac上无法打开工程,一致卡在fbx的导入)
  2. 双击Xcode文件,打开工程
  3. 选中TARGETS 下的 第一个

(1)Geneeral

  1. Suported Destinations:设置支持的设备,这里删除掉ipad的设备,如果这里支持iPad,上架审核的时候,会叫你整ipad的截图,很多事情。

  2. Minimum Deployments:设置最小的IOS版本。

  3. Identity:主要设置下面两个选项

    • Bundle Identifier:证书里面填写的id,unity 中配置的一致。
    • Version:设置版本号,一种方式就是在unity里面设置好,另外一种方式就是在xcode 里面设置,这个只能包含 “数字”、“.”, 如 “1.0.1”, 不能有字母或者空格,如果发布的是测试版本,版本号可以不变,如果是上架审核,版本号增加比较好,不增加不会报错。
  4. 设置 app icon ,这个在上架的时候必须有

(2)Signing&Capabilities

Bundle Identifiers: 同创建证书时的一致

在 all 标签下,选择profile,这里如果是构建测试的就import 测试的 配置,就是前面下载下来的,同理选择分发上架类型的,如果找不到文件,重启下xcode.

错误信息:

alt text

解决(根据提示,xcode 11 及之后都要 apple distribution):

alt text

如果连接手机测试,此处要这样配置

alt text

(3)Resources Tages

暂无配置

(4)Info

Custom iOS Target Properties:

  • App Transport Securiy Settings

    • Allow Arbitrary LoadsYes,(这样就允许了http 未加密的方式访问网站,否则会报错)
  • 【新版xcode 没有这个】设置不使用出口合规证明 (仅在上架审核的时候需要) <key>ITSAppUsesNonExemptEncryption</key><false/>

(5)Build Settings

  • User-Defined

    • Eenable Bitcode : 设置为 false (打包的时候说是不赞成这个,不让通过)

(6)连接手机测试

  • 需要手机和电脑连接,直接将app安装到手机上,适用于调试方式。
  • 在xcode上点击菜单:Product->Run。

(7)Xcode 操作

菜单

  1. 菜单:Produr/Run: 直接连接手机,安装到手机运行
  2. 菜单:ProdurArechive: 归档,用于构建 ipa 文件、上架商店,通过Testflight 测试
  3. 菜单:Window/Organizer: 打开归档的窗口

左侧导航栏。

  1. Unity 中的 StreamingAssets 下的资源对应 Data/Raw,当需要修改这些资源时,不需要重新打包 data/raw

八、分发APP

该章节说明怎么进行 Testflight 测试、第三方分发Ipa包测试、以及最终上架苹果商店。

ipa文件不能直接通过苹果手机安装,需要通过分发平台进行分发安装。

Xcode 操作文档(英文)

App Store Connect 帮助 - 中文文档: add app,

各种运行方式

运行方式 分发类型 说明
手机直接连接 调试
分发ipa app store connect/testfliht only/ad hoc 都可以,仅是在中途将ipa 文件导出保存下来 构建之前要获取目标设备udid号进行注册
可以使用蒲公英云平台分发
不适合非注册人员安装
这种方式似乎不需要支付688费用,未确定
testflight app store connect/testfliht only都可以 要运行的手机首先要在构建之前手机设备udid号进行注册
可以使用蒲公英云平台分发
app store app store connect 正常用户使用

(1)创建您的应用程序的存档

要使用任何分发方法,请先创建应用程序的存档。存档是应用程序的构建,包括调试信息,Xcode存储在捆绑包中。Xcode根据您为分发选择的分发配置重新打包存档的内容。

在Xcode项目的主窗口中,从Scheme工具栏菜单中选择要构建的方案和运行目标。然后,选择 Produnct>Archive ,为您选择的设备类构建该方案中包含的目标,并创建出现在档案管理器中的存档。

档案组织者的屏幕截图,显示已选择的档案和分发应用程序按钮。

您可以通过选择 Window>Organizer 直接打开档案管理器。如果您想确认您的应用程序已准备好在未提交的情况下提交到TestFlight或App Store,请选择您的存档,然后单击 Validate App 。Xcode将对应用程序执行有限的自动初始验证并提供反馈。 对于使用Mac Catalyst构建的Mac应用程序,请为iPad和Mac版本创建单独的存档。为Mac版本创建存档时,请选择“我的Mac”作为运行目标。

(2)选择一种分发方法

您可以导出存档或将其上传到App Store Connect。如果您导出存档,您可以在App Store之外分发(通过第三方平台分发Ipa)。否则,将存档上传到App Store Connect,以通过TestFlight或App Store分发。

从Xcode的 Organizer 窗口中,在边栏中选择“存档”,然后单击“分发应用程序”。

Archives Organizer的屏幕截图,显示选择分发方法对话框,并选择预配置的TestFlight和App Store选项。

选择以下选项之一,使用推荐的设置进行分发:

TestFlight & App Store

通过TestFlight分发并提交到App Store的默认设置。使用此选项更新存档中内容的构建号,执行自动代码签名,并使用符号上传应用程序。

TestFlight Internal Only

通过TestFlight分发的默认设置,并限制对团队的访问。使用此选项可防止应用程序的开发构建被提交到App Store。

Release Testing 发布测试

默认设置是在发布前分发要测试的版本。使用此选项执行类似于App Store分发选项的自动代码签名,并导出以安装在您的团队在App Store Connect注册的设备上。这种分发方法不适用于为Mac构建的应用程序。

Enterprise(企业)

分配给组织成员的默认设置。如果您是Apple Developer企业计划的一部分,请使用此选项。这种分发方法不适用于为Mac构建的应用程序。

Direct Distribution(直接分销)

直接分发macOS应用程序的默认设置。使用此选项对开发人员ID应用程序进行公证,以便直接分发。这种分发方法仅适用于为Mac构建的应用程序。

Debugging 调试

分发版本进行调试的默认设置。使用此选项导出版本,在您的团队在App Store Connect注册的设备上安装和调试。这使沙盒测试环境能够为一些支持它们的功能提供支持。

选择分发选项后,单击分发按钮。Xcode开始处理、打包和上传。单击末尾的链接以访问App Store Connect上应用程序的构建页面,或单击导出按钮以在本地访问资产。

笔记 在您首次将应用程序上传到App Store之前,请创建一个应用程序记录以在App Store Connect中注册您的应用程序。如果您还没有这样做,Xcode会要求您提> 供为您创建此记录所需的信息。有关更多信息,请参阅创建应用程序记录

(3)创建自定义发行版

要开始自定义分发,允许您配置自己的设置,请单击自定义选项。

分发方法选择步骤的屏幕截图,显示所选的临时分发。

从以下分发方法中选择:

App Store Connect

使用TestFlight或通过App Store进行分发。

Ad Hoc(临时)

分发到您在App Store Connect中注册的有限数量的设备。有关分发到您注册的设备的更多信息,请参阅将应用程序分发到注册设备

Enterprise(企业)

如果您是Apple Developer Enterprise Program的一部分,并准备将应用程序发布给组织中的用户,请分发给您的组织成员。

Developer ID(开发人员ID)

在App Store之外分发由苹果公证或使用开发人员ID签名的macOS应用程序。这种分发方法仅适用于为Mac构建的应用程序。

Development(开发)

分发到您在App Store Connect中注册的有限数量的设备。有关分发到您注册的设备的更多信息,请参阅将应用程序分发到注册设备

Copy App(复制应用程序)

在没有代码签名的情况下分发macOS应用程序。这种分发方法仅适用于为Mac构建的应用程序。

如果您选择App Store Connect或Developer ID作为分发方法,您也会选择一个目标选项。您可以选择将构建版本上传到App Store,或将构建版本导出到本地以稍后上传。

分发流程的屏幕截图,显示目标选项以及上传或导出选项。上传被选中

在TestFlight或App Store上分发应用程序时,请选择如何管理符号和构建数字:

分发流程的屏幕截图显示App Store Connect分发选项,带有复选框,用于上传应用程序的符号,管理版本和构建版本号,以及仅限TestFlight内部测试。选中“上传应用程序的符号”和“管理版本和构建数字”复选框

Strip Swift symbols 剥离Swift符号

通过从Swift标准库中剥离符号来减小应用程序的大小。仅当您的项目嵌入了swift库时,此设置才可用。

Upload your app’s symbols 上传您的应用程序的符号

允许苹果为您提供符号崩溃日志和其他诊断信息。符号日志将日志中的内存地址替换为人类可读的函数名和行号。这些符号在测试您的应用程序与苹果产品和服务的兼容性方面也很有用。

Manage version and build number 管理版本和构建号

允许Xcode更新存档中所有内容的构建号。

TestFlight internal testing only 仅限TestFlight内部测试

准备通过TestFlight分发应用程序,并限制对您团队的访问。使用此选项可防止应用程序的开发构建被提交到App Store。

在选择涉及代码签名的分发方法时,请选择代码签名方法。

分发流程的屏幕截图显示了自动管理签名和手动管理签名分发的复选框。选中自动管理签名复选框

选择“自动管理签名”允许Xcode为您管理签名。要手动对应用程序进行签名,您可以使用签名证书。有关共享签名证书的信息,请参阅将您的代码签名身份与Apple Developer Portal同步。

当使用Ad Hoc或Development选项进行自我分发打包时,请选择是启用还是禁用App Thinning并配置按需资源设置。有关应用程序稀薄和按需资源的更多信息,请参阅减少应用程序的大小和进行高级优化以进一步减少应用程序的大小。

显示临时分发设置的分发流程屏幕截图。该对话框包括应用程序瘦身弹出式菜单,其中选项为“无”,其他选项包括一个带有“包括无线安装清单”标签的复选框

(4)分发测试版

要分发应用程序的测试版,以提供即将发布的版本的预览,请选择与您的测试资源一致的分发方法:

  • 使用TestFlight将应用程序的测试版分发给内部和外部测试人员。TestFlight应用程序允许受邀用户安装、测试版测试、提供反馈并获取应用程序的更新。苹果为您分发测试版,您可以在App Store Connect中管理构建版本和用户。要了解更多信息,请参阅TestFlight概述

    在 Xcode 上传后,在网站上查看状态,如下图

    alt text

  • 将测试版分发到开发人员帐户中的注册设备。仅当您可以保留部分有限的开发设备用于beta测试时,才选择此选项。要了解更多信息,请参阅将应用程序分发到注册设备

  • 对于macOS应用程序,在通过App Store分发应用程序之前,先将经过苹果公证的构建版本分发给测试人员。要了解更多信息,请参阅在分发前对macOS软件进行公证

App Store Connect的屏幕截图显示了从Xcode上传的构建版本

(5)在App Store上发布

测试最终版本后,将其提交到App Review,然后在App Store上提供。有关发布过程的更多信息,请参阅发布应用程序的概述

转到App Review查看App Store和人机界面指南。有关特定平台的指导,请参阅立即提交您的应用程序

您可能需要在App Store Connect中输入其他信息,然后才能将应用程序提交到App Review。在您的应用程序上传或发布后,您无法更改其中一些元数据,因此仔细选择设置很重要。有关此元数据的更多信息,请转到App Store Connect帮助中的必需、可本地化和可编辑属性。

如果您使用TestFlight分发测试版,并输入了App Store发布所需的其他信息,只需将App Store Connect中显示的最后一个版本提交到App Review即可。 App Store Connect的屏幕截图,显示版本信息和提交以供审核。

如果您没有使用TestFlight分发最终构建版本,请准备应用程序分发并创建应用程序的存档。在继续之前验证存档并修复任何验证错误。然后,将其上传到App Store Connect,并等待它通过App Store Connect验证测试。

要将构建提交到App Review,请转到Submit for Review。

(6)注意事项

有关注意事项可参考:官方文档

  1. 添加App时,填写app名称和unity里面的名称最好一样,两个名称不同或者相差很大,导致审核不通过,这里写的名称是商店里软件显示的名称,unity里面的名称,是安装后在手机桌面显示的名称,提交后,苹果会提示里两个名称不一样,会导致用户找不到下载的软件。

1718095073433

  1. 上传完成后,在 App store connect 里的TestFlight 标签页查看,这个时候还不能在app store 页查看,因为还没提交

  2. 在xcode 上传后,并不能立即进行testflight 测试,一般要显示“已批准/通知测试人员”才可以开始测试。

  3. 管理 APP ID 不能参加测试,也就是开发者账户不能参加测试。

  4. 提交构建版本,多个版本的时候选择最新的版本

  5. 除了可选填的,都是必须填写的

  6. 隐审核流程:私项填写完成后需要 发布

  7. 审核流程:各种网址(技术网址,隐私网址),填写公司网址,或者个人网址即可

  8. 构建版本大小要求

  9. 审核流程:截屏规范,用一个手机截图后,ps处理成对应分辨率的,上传失败会提示里,应该上传什么分辨率

  10. 审核流程:只要你有登录界面,那么需要在testflight -》 测试信息中,勾选 “需要登录”,且输入登录审核的账号信息,即使输入任何账号和密码都可以,也需要勾选需求要登录,然后明确的输入一个账号和密码,否者审核不过。

若是企业定制版本的软件,不需要上架商店,而是希望通过内部分发的方式,参考:分发类型

使用非公开发方式发布

(7)问题汇总

  1. 提示邀请已被撤销或无效,请要求开发者发出新的邀请。

    看是否使用的是管理 APP ID。

  2. 此Beta 版本现在不接受任何新的测试人员

    应该是该APP还未被批准使用TestFlight测试,首次提交一般上传后需要1天(头天下午上传,第二天10点可以)左右的时间,会收到一封邮件提示已批准使用TestFlight测试,然后可以发送邀请链接了,后面提交很快,基本上提交等待几分钟就可以测试。

  3. 构建时提示禁用 bitcode

    参考文章:在 Xcode中 选择 buildsettings, 搜索 bitcode,设置为 false

九、蒲公英分发

ipa文件不能直接通过苹果手机安装,需要通过分发平台进行分发安装。

  1. Product->Archive,等待编译完成
  2. 编译完成后,Xcode会自动打开“Organizer”窗口
  3. 你可以选择导出的配置文件和导出选项,然后点击“Export”按钮,选择“Save for Ad Hoc Deployment”或“Save for Enterprise Deployment”
  4. 最后选择保存路径和文件名,完成IPA文件的生成。
  5. 将ipa通过蒲公英分发平台分发app
  6. 打开蒲公英分发平台网站,注册账号上传ipa(名字最短那个文件) 分发app

若提示需要钥匙串的密码,输入开机密码。并非用户账户密码。

十、错误汇总

PC端正常,构建IOS后无法进入场景

情况说明:

当前同个项目会发布4个软件(APP),前面两个正常,后面两个加载场景后无法进入场景

后面两个场景有5个任务场景,当发布一个的时候又正常

错误提示

MissingMethodException: Default constructor not found for type UnityEngine.ResourceManagement.AsyncOperations.ProviderOperation`1[[UnityEngine.AddressableAssets.Initialization.ResourceManagerRuntimeData, Unity.Addressables, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]
  at System.RuntimeType.CreateInstanceMono (System.Boolean nonPublic) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Activator.CreateInstance (System.Type type, System.Boolean nonPublic) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.ResourceManagement.Util.LRUCacheAllocationStrategy.New (System.Type type, System.Int32 typeHash) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.ResourceManagement.ResourceManager.CreateOperation[T] (System.Type actualType, System.Int32 typeHash, UnityEngine.ResourceManagement.Util.IOperationCacheKey cacheKey, System.Action`1[T] onDestroyAction) [0x00000] in <00000000000000000000000000000000>:0 

尝试解决方案:参考文章

创建自定义包

官网:创建自定义包

文章参看:

创建包

手动创建

根据官网文档创建 官网:创建自定义包

插件创建

推荐(创建比较快速)

插件名称:软件包开发

状态:Unity2019.4 之前的官网文档中显示为预览插件,之后官网手册中移除了,但是还在更新中

可按插件文档 中描述进行操作。

下面为简要记录步骤:

  • 创建一个空项目
  • 包管理器窗口中添加插件,地址:com.unity.upm.develop 导入开发扩展包,不同的Unity版本导入的插件版本可能不同
  • 点击 “+” ->"Create Package" ,输入包名,点击“Add”
  • 创建后是在 Packages 目录下
  • 注意:作者名是你登录Unity的名称,显示得有点奇怪,自行修改文件夹名、类命名空间、包id信息等
  • 个人发现无法编写示例(因为不在Assets下)【有可能没找对方法】,解决方法是把整个包移动到 Assets下操作

使用Git存储

  • 整个包的源码放在主分支上
  • 将包目录下的分割推送到指定分支
  • 给分支创建标签来管理包,便于包的版本管理
  • 在Unity中通过引入git url 地址来获取包

文件结构

  • 包文件放在Assets目录下,进行开发,里面的设置还是包的规范要求
  • 开发完成后通过命令上传包,以及打版本

一、⚠️先更新 package.json 文件 中的版本信息

一、⚠️先更新 package.json 文件 中的版本信息

一、⚠️先更新 package.json 文件 中的版本信息

"version": "0.1.0",

二、更新 CHANGELOG (日志)

# 更新日志
All notable changes to this package will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2024-11-14

### This is the first release of *\<Cwss.Shared\>*.

*Short description of this release*

三、提交到主分支

git add * 

四、提交信息

git commit -m'提交信息' 

五、推送到主分支

git push 

六、分离到分支 Assets/UKitunp 根据实际情况修改

git subtree split --prefix=Assets/UKit --branch upm 

七、打标签和package.json版本一致

git tag 1.0.0 upm 

八、推送标签

git push origin upm --tags 

注意:

  • 不要使用其他命令或者方式创建分支
  • master分支作为开发分支,其他分支作为版本分支

git 扩展

列出本地标签

git tag -l

删除 0.1.0 标签,其他标签同理(一个一个的删除)

git tag -d 0.1.0

删除远程标签

假设我们在仓库中有一个名为 prod1.0 的远程标签。我们可以使用带有 --delete 选项的 git push 命令删除远程标签。

git push --delete origin prod1.0

结果:

To https://github.com/myrepos/prod.git
 - [deleted]         prod1.0

有时,我们可能有一个与分支同名的标签。在这种情况下,我们需要使用带有 refs 语法的 git push 命令而不是 --delete 选项,如下所示。

git push origin :refs/tags/prod1.0

结果:

To https://github.com/myrepos/prod.git
 - [deleted]         prod1.0

删除分支:https://www.php.cn/tool/git/493215.html

UniTask

官网

RunOnThreadPool

❌❌❌ RunOnThreadPool 不支持 WEBGL❌❌❌

在WebGL平台上,这个方法可能不起作用,因为WebGL不支持标准的线程池或异步编程模型。

如下示例:

private async UniTask Execute()
{
    await UniTask.Delay(1000);
    Debug.Log($"Delay 1000!");
    UniTask.RunOnThreadPool(async e =>
    {
        await UniTask.Delay(2000); //在PC上这里可以执行,但是在WEBGL中,这里不会执行
        Debug.Log($"Delay 2000!");
    },true).Forget();
}

       

更新后

private async UniTask Execute(ICommand command, EventDataBase evt)
{
    await UniTask.Delay(1000);
    Debug.Log($"Delay 1000!");
    MyDelay().Forget();
}

private async UniTaskVoid MyDelay()
{
    await UniTask.Delay(2000);
    Debug.Log($"Delay 2000!");
}

CancellationTokenSource

  1. 每次使用要 new

  2. Cancel 和 Dispose

    CancellationTokenSource  Cancel = new CancellationTokenSource();

    UniTask.Delay(5000, cancellationToken: Cancel.Token).ContinueWith(() =>
    {
        Debug.Log("执行了");
    }).Forget();

Cancel: 不会输出 执行了

Dispose: 会输出 执行了

包:Sqlite

第一人称控制,跳跃时,角色一直往天上飞

角色模型加了碰撞体,角色节点要设置为 postprocessing 层级,如下图所示

1718094417048

Addressables

使用说明

https://blog.csdn.net/linxinfa/article/details/122390621?spm=1001.2014.3001.5501

AddressablesGroups 窗口

注意事项

  1. 不用时关闭它,当打开状态下,选中场景中的对象,对属性面板进行改动时,会导致取消对场景中对象的选中

  2. 调试android时,当我们仅更改代码,勾选 scripts only build 以加快发布,不要卸载手机上的应用,因为资源不会再重新构建

Unity代码剥离导致ResourceManagerRuntimeData为空

Unity代码剥离为High,代码剥离,但我们遇到了 Addressables 包的问题。由于使用反射,ProviderOperation 被剥离。导致ResourceManagerRuntimeData为空

大概报错如下:

MissingMethodException: Default constructor not found for type UnityEngine.ResourceManagement.AsyncOperations.ProviderOperation`1[[UnityEngine.AddressableAssets.Initialization.ResourceManagerRuntimeData, Unity.Addressables, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]
  at System.RuntimeType.CreateInstanceMono (System.Boolean nonPublic) [0x0007b] in <71d402037f2643fe94cabbbe0d22d04d>:0
  at System.RuntimeType.CreateInstanceSlow (System.Boolean publicOnly, System.Boolean skipCheckThis, System.Boolean fillCache, System.Threading.StackCrawlMark& stackMark) [0x00009] in <71d402037f2643fe94cabbbe0d22d04d>:0
  at System.RuntimeType.CreateInstanceDefaultCtor (System.Boolean publicOnly, System.Boolean skipCheckThis, System.Boolean fillCache, System.Threading.StackCrawlMark& stackMark) [0x00027] in <71d402037f2643fe94cabbbe0d22d04d>:0
  at System.Activator.CreateInstance (System.Type type, System.Boolean nonPublic) [0x00020] in <71d402037f2643fe94cabbbe0d22d04d>:0
  at System.Activator.CreateInstance (System.Type type) [0x00000] in <71d402037f2643fe94cabbbe0d22d04d>:0
  at UnityEngine.ResourceManagement.Util.LRUCacheAllocationStrategy.New (System.Type type, System.Int32 typeHash) [0x00055] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\ResourceManager\Util\ResourceManagerConfig.cs:134
  at UnityEngine.ResourceManagement.ResourceManager.CreateOperation[T] (System.Type actualType, System.Int32 typeHash, System.Int32 operationHash, System.Action`1[T] onDestroyAction) [0x00032] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\ResourceManager\ResourceManager.cs:358
  at UnityEngine.ResourceManagement.ResourceManager.ProvideResource (UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation location, System.Type desiredType) [0x000d1] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\ResourceManager\ResourceManager.cs:260
  at UnityEngine.ResourceManagement.ResourceManager.ProvideResource[TObject] (UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation location) [0x00001] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\ResourceManager\ResourceManager.cs:287
  at UnityEngine.AddressableAssets.Initialization.InitializationOperation.CreateInitializationOperation (UnityEngine.AddressableAssets.AddressablesImpl aa, System.String playerSettingsLocation, System.String providerSuffix) [0x0009a] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\Initialization\InitializationOperation.cs:42
  at UnityEngine.AddressableAssets.AddressablesImpl.InitializeAsync (System.String runtimeDataPath, System.String providerSuffix) [0x0008d] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\AddressablesImpl.cs:280
  at UnityEngine.AddressableAssets.AddressablesImpl.InitializeAsync () [0x00013] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\AddressablesImpl.cs:289
  at UnityEngine.AddressableAssets.AddressablesImpl.get_InitializationOperation () [0x00013] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\AddressablesImpl.cs:306
  at UnityEngine.AddressableAssets.AddressablesImpl.LoadContentCatalogAsync (System.String catalogPath, System.String providerSuffix) [0x0004c] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\AddressablesImpl.cs:296
  at UnityEngine.AddressableAssets.Addressables.LoadContentCatalogAsync (System.String catalogPath, System.String providerSuffix) [0x00001] in D:\Project\Designer\Designer\Library\PackageCache\com.unity.addressables@1.1.9\Runtime\Addressables.cs:231
  at Ubiant.Common.Adressables.AddressablesLoader+<InitializeAddressables>d__2.MoveNext () [0x00020] in D:\Project\Designer\Designer\Assets\Scripts\Runtime\Addressables\AddressablesLoader.cs:49
  at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00027] in <1cc3984764e64bdeb5a66b5d2a3bc7e1>:0
UnityEngine.MonoBehaviour:StartCoroutineManaged2(IEnumerator)
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)

解决方案:

在Assets文件夹里面添加 link.xml文件,内容如下:


 
<linker>
    <assembly fullname="Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" preserve="all">
    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.LegacyResourcesProvider" preserve="all" />
    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider" preserve="all" />
    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider" preserve="all" />
    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider" preserve="all" />
    <type fullname="UnityEngine.ResourceManagement.AsyncOperations" preserve="all" />
    </assembly>
    <assembly fullname="Unity.Addressables, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" preserve="all">
    <type fullname="UnityEngine.AddressableAssets.Addressables" preserve="all" />
    </assembly>
</linker>

若项目中已经存在 link.xml 直接追加即可。

参考文章:

终极曲线解决方案

文档

  • 官方文档,edge浏览器有时不能翻译为中文,使用google浏览器翻译中文,结合英文阅读比较好

列车控制

  • 列车组件:TrainManager,依赖组件:CurvySpine(轨道路径)

  • 车厢组件:CarManager

  • 列车由车厢和贯通道的遮蓬拼接而成,实际控制的是车厢,贯通到根据两个切面自动跟随绑定的车厢运动

  • 运行原理:控制每个车厢在轨道上的距离,根据当前列车速度计算出单位时间上的距离,更新列车位置

  • 列车启动:列车状态变为加速状态,列车速度为0变为列车运行最大速度,列车状态更新为匀速状态,此时,列车不再加速

  • 列车刹车:列车状态变为刹车状态,刹车是从当前速度,直到速度为0,列车状态更新为None,此时需要一个刹车距离来求得需要多大的加速度,

  • 列车停车:列车状态变为None状态,列车的速度立即设置为0,列车停在当前位置

  • 切换运行方向:调换运行方向,切换控制组件的Direction属性,同时切换检测刚体

  • 列车检测刚体:在列车的头尾各有一个检测刚体,用于检测轨道上的“刹车触发器”和“停车触发器”,列车上行运行时启用上行检测刚体,下行运行时启用下行检测刚体

注意事项,列车的根节点不会产生位移,只有根节点下的车厢节点会产生移动

连接点

应用场景:轨道道岔切换

创建连接点

  • 官网文档 https://curvyeditor.com/documentation/splines/connections

  • 简要创建举例

创建:

1718094508590

结果:

1718094532126

连接点控制

https://curvyeditor.com/documentation/splines/connections

使用连接组件,可设置到达连接点后,运行路径

官方文档描述:

仅样条控制器。

这允许您指定样条控制器在到达连接时应如何移动。您可以设置以下行为之一:

  • 当前文本样条线:控制器将继续在当前样条线上移动,就好像没有连接一样。

  • Follow Up Spline:控制器将继续在包含 Follow-Up 的样条线上移动(如果有),否则将继续在当前样条线上移动。

  • 随机样条线:控制器继​​续在随机选择的连接样条线之一上移动。

  • Follow Up 否则 Random:控制器将继续在包含 Follow-Up 的样条线上移动(如果有的话),否则将继续在随机选择的连接样条线之一上移动。

  • 自定义:定义您自己的连接样条线选择逻辑。您可以通过分配具有继承自 ConnectedControlPointsSelector 的组件的 GameObject 来实现此目的。

道岔控制的应用

Dotween

官网:http://dotween.demigiant.com/

Tween (Tweener)

属性

回调(事件监听)

  • 在补间完成时调用(包括循环),每到达终点一次调用一次
public TweenCallback onComplete;
  • 暂停事件,若 autoKill set to FALSE,播放完成后触发
public TweenCallback onPause;
  • 播放时调用
public TweenCallback onPlay;
  • onUpdate
public TweenCallback onUpdate;
  • onStepComplete
public TweenCallback onStepComplete;
  • onKill
public TweenCallback onKill;
  • 倒播到起始位置的回调,监听,注意,若 Tween 已经是倒播到起始位置,再调用倒播接口,不会触发此事件
public TweenCallback onRewind;

扩展

补间的播放等是通过扩展里面的方法来实现的

  • Play()

播放:首次播放时使用

  • Restart()

重新启动:再次播放时使用

  • PlayBackwards()

倒播:回退补间时使用

案例

实现一个侧边目录窗口,点击按钮,从左侧往右出现,并停住,点击关闭按钮,回退回去

代码

         [SerializeField] GameObject flowBtnGameobject; //打开面板的按钮

         [SerializeField] RectTransform showStopPos;// 面板目标位置
        
         Tweener teachFlowTweener;//流程面板动画
         /// <summary>
         /// 打开教学目录
         /// </summary>
         public void OnClickTeachCatalogue()   
         {
             if (teachFlowTweener == null)//为空,首次播放
             {                                
                 //teachFlowPlane.ShowUpdateStep(gameModel.timelineStepId.Value);//TODO:面板显示逻辑处理,替换为自己的
                 teachFlowTweener = teachFlowPlane.transform.DOMoveX(showStopPos.position.x, 0.5f).SetAutoKill(false).SetEase(Ease.InOutFlash).Pause(); //首次播放
                 teachFlowTweener.Play();
                 teachFlowTweener.onRewind += onRewindHandler; //监听倒播结束事件,为了显示打开面板的按钮
             }
             else
             {
                 if (teachFlowTweener.IsPlaying()) return;
                //teachFlowPlane.ShowUpdateStep(gameModel.timelineStepId.Value);//TODO:面板显示逻辑处理,替换为自己的
                 teachFlowTweener.Restart();//重新启动 和 play 同效果
             }
             flowBtnGameobject.SetActive(false); //隐藏按钮
        
         }
        //关闭面板的方法
        public void OnCloseFlowPanel()
        {
            if (teachFlowTweener.IsPlaying()) return;
            teachFlowTweener.PlayBackwards();//倒播关闭

        }
   

        //面板回到初始位置后调用
        private void onRewindHandler()
        {
            flowBtnGameobject.SetActive(true);//显示打开按钮
        }

        private void OnDestroy()
        {
            teachFlowTweener.onComplete -= onRewindHandler; //删除监听

        }

问题

  1. 软件刚启动时,执行某个 Save 操作,但是文件夹中未发现序列化成功,很奇怪,目前解决方法是,在之后的某个操作后进行Save

https://github.com/egametang/ET

https://www.taikr.com/goods/show/554?targetId=1053&preview=0

Unity 新的输入系统

使用说明

插件设置

(1) InputSystemPackage 设置界面,需要设置为 Dynamic Update模式,这样操作虚拟控件时才不卡顿

1718094580288

(2)FirstPersionController,第一人称控制器,目前

1718094594239

问题和解决方案

:bangbang: Type of instance in array does not match expected type

现象 当退出游戏的时候会出现这个报错

解决办法 InputSystem社区 回退到 1.3.0 版本

学习任何一个插件,最好的方式就是去仔细阅读它的官方文档,先原文(英语)阅读一遍,然后再阅读译文,一遍不够,再来一遍!!!

MagicOnion

知识无国界,作者是一个日本大佬,非常崇拜,Unitask,MemoryPack 等都是它写的

官网:https://github.com/Cysharp/MagicOnion, 原为阅读和翻译阅读

我认为最合适的轻便服务器,提供RPC,实时通信,用在WPF unity上都不错,WPF或者.net的桌面端开发看官网文档就行了,下面主要记录Unity方面的开发

Unity开发过程

项目说明

使用此框架实现一个 CS 架构的系统

  1. 工程目录结构设计,项目文件夹结构如下,便于后期的插件注册

- TSIM.Server //服务器解决方案
    TSIM.Server //服务器项目
    TSIM.Shared //共享接口库
+ TSIM.Unity //Unity项目

Server项目

  1. 创建 AspNetCore.APP空项目(net7.0)

  2. 在Nuget下载包:

  • MagicOnion.Server

  • Grpc.AspNetCore

  1. 版本管理:gitee,国内最大的开源版本库(适合非游戏类型的工程管理)

Shared项目

  1. 创建 库项目 (netstandard2.0),2.1 grpc没有支持

  2. 在Nuget下载包:

  • MagicOnion.Abstractions

  • MagicOnion.Shared

  • MessagePack.UnityShims:这个是对Unity的支持,提供了Unity里面的结构类型

Unity项目

  1. Unity 版本: Unity2022

  2. 版本管理:svn,(群晖上自建的SVN服务器)

项目搭建过程

MagicOnion UnityPackge 包使用

该包在发布版本下载UnityPackge ,导入到Unity项目中

https://github.com/cysharp/MagicOnion/releases

Grpc library for Unity client

Grpc 在 netstandard2.0 时候,提供了对Unity的支持,但是当出现跨平台的 .net 之后,就移除对Unity的支持了,MagicOnion的作者正在实现这部分的支持。

目前的处理方式,通过MagicOnion 案例中的 Unity项目,复制拷贝过来

问题

  • 开发环境:Mac M1,在Window上正常,mac上不正常

  • Unity版本:2023.1.4f1c1 silicon

  • 错误信息

DllNotFoundException: grpc_csharp_ext assembly:<unknown assembly> type:<unknown type> member:(null)

解决过程

  • 相关问题的连接

https://github.com/grpc/grpc/issues/30068

https://github.com/grpc/grpc/issues/25223

  • 简要说明:就是grpc 不支持 unity 了,他们觉得维护成本很高,不再支持, unity intel 版本不存在问题,但是 silicon 版本可能缺少本机库的支持

  • nugetforunity 不是官方出品,即使使用这个插件将nuget里面的包倒入unity ,可能存在问题。

  • 最好的状态:就是magiconion作者大佬,实现支持,期待中

MessagePack

下载地址

https://github.com/neuecc/MessagePack-CSharp#unity

MagicOnion code generator (for IL2CPP)

MagicOnion 默认仅支持UnityEditor 或者 非 IL2CPP,也就是不能发布:windows-il2cpp、 安卓、苹果这些版本,显然是不行的。如果要支持还需要做其他的工作,将我们的Shared项目生产客户端并且注册到Unity项目,生成客户端们这里分别对 MessagePack 和 MagicOnion 都要操作

准备工作

  1. 安装 dotnetcore3 runtime,这里下载3.1就可以了

  2. 使用vs 非预览版,在使用vs2022 preview版本时,出现错误,换成非预览版正常

操作过程

  1. 安装 MessagePack.Generator

安装分全局安装和本地安装。

  • 全局安装
dotnet tool install --global MessagePack.Generator
  • 本地安装,在服务器解决方案的根目录,打开cmd

1718094651927

  • 执行下面的命令

dotnet new tool-manifest 
dotnet tool install MessagePack.Generator
dotnet tool install MagicOnion.Generator

  • 若提示如下错误
C:\Users\ABYTE\AppData\Local\Temp\8e12065c-69cb-4bf3-92cf-7fbfc1dd389c\restore.csproj : error NU1301: 无法加载源 https://dot
net.myget.org/F/roslyn/api/v3/index.json 的服务索引。
无法还原工具包。
工具“messagepack.generator”安装失败。此故障可能由以下原因导致:

* 你尝试安装预览版,但未使用 --version 选项来指定该版本。
* 已找到具有此名称的包,但是它不是 .NET 工具。
* 无法访问所需的 NuGet 源,这可能是由于 Internet 连接问题导致。
* 工具名称输入错误。

有关更多原因(包括强制包命名),请访问 https://aka.ms/failure-installing-tool
  • 提示我们需要指定一个版本,通过 dotnet tool search 包名 来搜索包
F:\000-TZJ\TSIM\MagicOnion-main\samples\ChatApp>dotnet tool search messagepack.generator
包 ID                       最新版本         作者                  下载          已验证
---------------------------------------------------------------------------
messagepack.generator      2.5.124      neuecc,aarnott      100885
tapper.generator           1.8.3        nenoNaninu          6792
  • 安装指定版本的包
dotnet tool install messagepack.generator --version 2.5.124
  • 查看命令信息
dotnet mpc 
  • 安装 mpcmoc 的截图信息

1718094674431

  • json文件

1718094681088

  1. 根据命令提示,配置Shared项目
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>   
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MagicOnion.Abstractions" Version="5.1.8" />
    <PackageReference Include="MagicOnion.Shared" Version="5.1.8" />
    <PackageReference Include="MessagePack.UnityShims" Version="2.5.124" />
  </ItemGroup>

	<Target Name="RestoreLocalTools" BeforeTargets="GenerateMessagePack;GenerateMagicOnion">
		<Exec Command="dotnet tool restore" />
	</Target>
	<Target Name="GenerateMessagePack" AfterTargets="Build">
		<PropertyGroup>
			<_MessagePackGeneratorArguments>-i ./TSIM.Shared.csproj -o ../../TSIM.Unity/Assets/Scripts/Generated/MessagePack.Generated.cs</_MessagePackGeneratorArguments>
		</PropertyGroup>
		<Exec Command="dotnet tool run mpc $(_MessagePackGeneratorArguments)" />
	</Target>
	<Target Name="GenerateMagicOnion" AfterTargets="Build">
		<PropertyGroup>
			<_MagicOnionGeneratorArguments>-i ./TSIM.Shared.csproj -o ../../TSIM.Unity/Assets/Scripts/Generated/MagicOnion.Generated.cs</_MagicOnionGeneratorArguments>
		</PropertyGroup>
		<Exec Command="dotnet tool run dotnet-moc $(_MagicOnionGeneratorArguments)" />
	</Target>
</Project>

注意:TSIM.Unity/Assets/Scripts/Generated/MagicOnion.Generated.cs 此路径要对应Unity项目和Shared项目的相对路径,应该也可以写绝对路径,目的就是将 Shared项目 生成到 Unity项目中

  1. 上面将 Shared项目生成客户端,下面将注册到Unity中

在unity项目中新建脚本InitialSettings,下面的脚本包含了注册,以及Grpc的通道设置(且是必须的)

using System.IO;
using Grpc.Core;
#if USE_GRPC_NET_CLIENT
using Grpc.Net.Client;
#endif
using MagicOnion.Unity;
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

namespace Assets.Scripts
{
    class InitialSettings
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        static void RegisterResolvers()
        {
            // NOTE: Currently, CompositeResolver doesn't work on Unity IL2CPP build. Use StaticCompositeResolver instead of it.
            StaticCompositeResolver.Instance.Register(
                MagicOnion.Resolvers.MagicOnionResolver.Instance,
                MessagePack.Resolvers.GeneratedResolver.Instance,
                BuiltinResolver.Instance,
                PrimitiveObjectResolver.Instance
            );
        
            MessagePackSerializer.DefaultOptions = MessagePackSerializer.DefaultOptions
                .WithResolver(StaticCompositeResolver.Instance);
        }

        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        public static void OnRuntimeInitialize()
        {
            // Initialize gRPC channel provider when the application is loaded.
            GrpcChannelProviderHost.Initialize(new DefaultGrpcChannelProvider(new GrpcCCoreChannelOptions(new[]
            {
                // send keepalive ping every 5 second, default is 2 hours
                new ChannelOption("grpc.keepalive_time_ms", 5000),
                // keepalive ping time out after 5 seconds, default is 20 seconds
                new ChannelOption("grpc.keepalive_timeout_ms", 5 * 1000),
            })));

            // NOTE: If you want to use self-signed certificate for SSL/TLS connection
            //var cred = new SslCredentials(File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "server.crt")));
            //GrpcChannelProviderHost.Initialize(new DefaultGrpcChannelProvider(new GrpcCCoreChannelOptions(channelCredentials: cred)));

            // Use Grpc.Net.Client instead of C-core gRPC library.
            //GrpcChannelProviderHost.Initialize(new GrpcNetClientGrpcChannelProvider(new GrpcChannelOptions() { HttpHandler = ... }));
        }
    }
}

Unity端使用共享库

服务端可以通过项目引用直接使用共享库,但是Unity端因为依赖不同不能直接使用共享库dll,需要直接使用共享库的源代码,官方的解决方案是,通过 源码链接来实现,没搞懂,直接将代码考本到Unity,作为一个本地包来导入

  1. 在 Unity项目的 Pacakges 文件夹下,创建 TSIM.Shared 文件夹(TSIM 是我的项目名称),创建 package.json 和 程序定义文件 TSIM.Shared.Unity.asmdef

package.json

{
  "name": "com.abyte.tsim.shared.unity",
  "version": "1.0.0",
  "displayName": "TSIM.Shared.Unity",
  "description": "服务端-客户端共享源文件,由于MessagePack所依赖的库不同,不能使用dll,处理方式在服务端写完后,拷贝到Unity",
  "unity": "2020.3"
}

问题和处理

其他电脑连接服务器失败

确保服务器的配置为 0.0.0.0,表示监听所有IP的连接

{
  "Kestrel": {
    "Endpoints": {
      "Grpc": {
        "Url": "http://0.0.0.0:7000",
        "Protocols": "Http2"
      },
      "Https": {
        "Url": "https://0.0.0.0:7001",
        "Protocols": "Http1AndHttp2"
      },
      "Http": {
        "Url": "http://0.0.0.0:7002",
        "Protocols": "Http1"
      }
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

安卓端打包错误 找不到grpc_csharp_ext.dll

  • Unity版本:Unity2022 / Unity2023

  • 设置:IL2CPP ARM64

1718094706135

原因:

DLL文件设置需要根据项目设置同步调整

1718094712023

Nativefileso

商店资源

!!注意:插件已经停止更新,Unity 商店已经下架

发布 AndroidIOS 需要不同的设置,参考教程

Nativefileso

Github 上的资源

功能:跨平台的文件操作插件,是一个国外大牛在github上的unity插件,提供调用应用程序分享功能,如:微信分享,你不用去注册微信的开放平台。直接可以分享到微信,钉钉等。非常强大

说明

仓库地址

注意事项:

  1. 在发布安卓端时,删除掉 Mac os, IOS对应的插件,否则打包会报错
  2. Windows 发布, 提示 .forms.dll 警告

最后说明:使用起来不是很满意,用 Unity商城里面的 NativeShare 代替

Android 设置

NativeShare 不再需要在 Android 上进行任何手动设置。如果您使用的是旧版本的插件,则需要<provider ... />从AndroidManifest.xml中删除 NativeShare 。

作为参考,旧文档可在以下位置找到:https://github.com/yasirkula/UnityNativeShare/wiki/Manual-Setup-for-Android

iOS 设置

有两种方法可以在 iOS 上设置插件:

a. 自动设置:在Unity中( 可选)在项目设置/yasirkula/Native Share中勾选自动设置,并添加更改照片库使用说明描述:“该应用程序需要访问照片才能将媒体保存到其中” b. 手动设置:参见:https://github.com/yasirkula/UnityNativeShare/wiki/Manual-Setup-for-iOS

Newtonsoft

  • 功能:json序列化工具
  • 注意事项:必须使用 for unity 版本的,否则在打包时(特别是打包il2cpp)会报错

github:https://github.com/SaladLab/Json.Net.Unity3D 下载:https://github.com/SaladLab/Json.Net.Unity3D/releases 保险起见,下载这个完整的包

版本后续可能会更新,下载最新的即可。

New UIWidgets

UI插件,功能模板非常强大

  • 提供了大量的容器组件,如列表,树等
  • 容器组件支持拖拽,但是此时鼠标按下内容区域无法滚动视图,若需要滚动,移除支持拖拽的组件
  • 支持定位,如打开面板的时候希望定位到某个位置,Unity内置功能里面很垃圾,搞了半天,效果不好,这个组件里提供了方便的接口,如定位到我选择的item的位置,定位到最后等,注意定位到最后的位置并不是1,需要求出最后一个item的位置

TextMeshPro_UGUI的支持

若要对TMPRO_UGUI的支持,需要添加 脚本定义符号

UIWIDGETS_TMPRO_SUPPORT

添加自定义程序集

当项目定义了不同的程序集时,如对UI相关的功能自定义了程序集,此时需要使用New UI Widgets插件,需要为New UI Widgets 添加程序集,包含运行时和编辑器两个。

  1. 添加运行时程序集

    • 运行时程序集文件

    运行时程序集文件

    • 运行时程序集属性,需要添加相应的依赖

    运行时程序集属性,需要添加相应的依赖

  2. 添加编辑器程序集

    • 编辑器程序集

    1721187463878

    • 编辑器程序集属性

    1721187474618

部件生成器(Widgets Generator)

官方文档

操作说明

创建一个数据类,如果不需要执行数据更新,普通类即可,若需要数据更新,需要实现INotifyPropertyChanged IObservable,其中IObservable具有更少的内存分配,脚本模板如下:

继承 INotifyPropertyChanged 接口示例脚本:

public class ListViewIconsItemDescription : INotifyPropertyChanged
{
   [SerializeField]
   string name;
   public string Name
   {
      get
      {
         return name;
      }
      set
      {
         if (name != value)
         {
            name = value;
            Changed("Name");
         }
      }
   }
   public event PropertyChangedEventHandler PropertyChanged;
   protected void Changed(string propertyName)
   {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
   }
   ...
}

继承 IObservable 接口示例脚本:

public class ListViewIconsItemDescription : IObservable
{
   [SerializeField]
   string name;
   public string Name
   {
      get
      {
         return name;
      }
      set
      {
         if (name != value)
         {
            name = value;
            Changed();
         }
      }
   }
   public event OnChange OnChange;
   protected void Changed()
   {
      OnChange?.Invoke();
   }
   ...
}

右键脚本,创建>New UI Widgets>Widgets Gennerator 注意需要鼠标选中数据类右键

自定义程序集时生成部件处理

当使用自定义程序集时,生成部件无法创建预制体和案例场景,因为缺少对 new ui widgets Editor的引用,处理方式:为我们的程序集添加相关依赖。

程序集TSIM.View 是项目中的View层程序集,该程序集需要添加 Nuw(new ui widgets 的 运行时程序集)的依赖,如图所示:

添加对Editor的依赖

当在生成部件时,由于还需要依赖编辑器里面的内容,所以还需要添加 Nuw.Editor(new ui widgets 的 编辑器程序集)的依赖,并在生成完成后,删除依赖,否则发布时存在问题。

添加对Editor的依赖

特别提醒:生成后删除依赖中的 Nuw.Editor

表格中item添加不参与布局的子对象

在生成一个排名表格的需求中,需求将自己的排名item添加一个不同的背景,于是创建了一个子对象图片组件,不参与布局,当是自己时,显示这个对象。

运行时表格header报错找不到子对象,header下的子对象需要和item下的数量相同。

在header下添加一个不参与布局的对象即可,且他们的顺序要一致。

列表

  1. 自动选择指定条目接口:

    void SelectNode(TreeNode<TItem> node)
    
  2. 自动滚动显示选择的条目

    • 移这里是列表文本动到指定Item:void ScrollTo(TItem item);

    • 移动到指定Item(带动画):void ScrollToAnimated(TItem item);

  3. 移动到指定索引值:

    void ScrollTo(int index);
    
  4. 移动到指定滚动位置:

    void ScrollToPosition(float position);
    
  5. 移动到指定位置:

    void ScrollToPosition(Vector2 position);
    

树控件

注意:对于树控件,默认未展开的节点,不会被选择,所以如果需要选中,一种方式是先展开节点,另外一种是在属性面板取消勾选 ** Deselect Collapsed Nodes **

展开父级示例代码:

 TreeNode<ICatalogueItem> pNode = node.Parent;                      
 while (pNode != null)
 {
     pNode.IsExpanded = true;
     pNode = pNode.Parent;
 }

在开发中遇到一个问题,用Timeline制作一个教学流程,然后根据时间节点设计了目录,使用树控件来显示目录,能够点击跳转。但是在这个流程中第 6 章节 和 第 3 章节的内容是一样的,配置Timeline 时就没配置 第6章节的,但是在目录中需要有,点击第6章节,跳转到第3章节的内容,这个时候就导致了 节点的id相同,所以自动选择的时候出现错乱,需要做特殊处理

虚拟化

开启比较好,否则,在树控件中,如果大量节点导致拖动很卡顿

EasyLayout

布局组件,对应引擎自带的 VerticalLoayout 和 HorizontalLayout

插件自带使用的这个,不使用这个会导致代码更新滚动视图位置无效

1718094735942

  • Main Axis: 搞不懂这个属性,似乎没影响
  • Sikp Inactive: 勾选时,被隐藏的子对象不会参加布局,此时和引擎自带的功能一直,否则即使隐藏,也会占位置

输入框组件

1718094742970

  • Character Limit: 设置字符的个数,用于在设置密码、或者激活码等,可以显示个数
  • Content Type: 数据类型: 如激活码,只能输入整数 Integer Number
  • Caret Blink Rate: 光标闪烁的频率
  • Caret Width: 光标宽度: 默认的宽度比较小,基本看不见,建议这里设置大点
  • Custom Caret Color: 光标颜色

ListViewCustomBase

这个基类当中有一个属性:ListType , 当你对面板进行布局的时候,可能需要重写这个变量的值来满足布局要求 ListViewCustom<TComponent, TItem> 继承于它。 也就是基本上所有的容器都可以使用这个功能

 public class TaskCatalogueTreeView : TreeViewCustom<CatalogueComponent, ICatalogueItem>
 {
    //需要重写
    public override ListViewType ListType { get =>ListViewType.ListViewWithVariableSize; }
    //..
}

已知问题:在某些Unity版本的命名空间中,无法使用结构(Strcut)或接口(Interface)类型,需要使用Class


TreeViewCustom<TComponent, TItem>

  • 树形控件视图,一一般我们要编写自己的组件来继承此组件
  • TCompoenet: 树形控件中每个节点上挂载的 monobehavior组件,需要继承:TreeViewComponentBase
  • Item: TCompoenet对应的数据接口,需要继承 INotifyPropertyChanged
  • 根据插件自带的例子,参照实现接口和组件

INotifyPropertyChanged Support

插件使用的ObservableList提供了对数据类型的INotifyPropertyChanged接口的支持,因此如果属性更新并引发PropertyChanged事件,那么视图将被更新。

如果您想在项目数据更改时自动更新集合小部件(如ListView、TileView、Table),那么你需要添加 INotifyPropertyChanged 实现到你的数据类型。

  • 例子
public class ListViewIconsItemDescription : INotifyPropertyChanged
{
    [SerializeField]
    string name;
    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
            Changed("Name");
        }
    }
    public event PropertyChangedEventHandler PropertyChanged = (x, y) => { };
    protected void Changed(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    ...
}
  • 通过这种方式,界面的第一个将被更新为新的名称
ListView.DataSource[0].Name = "New name";
  • 可以通过属性 ObserveItems 来禁用通知行为
ListView.DataSource.ObserveItems = false;
// name displayed with the widget will be not changed
ListView.DataSource[0].Name = "New name";

案例

制作目录章节的树形案例

  • 存在节点嵌套
  • 当节点是一个组(类似文件夹)节点时,应显示 三角图标,打开时,箭头向下,关闭时,箭头向右
  • 当节点是一个数据(类似文件)节点时,不显示图标

TeachFlowCatalogue 脚本:

数目录组件

 public class TeachFlowCatalogue : TreeViewCustom<CatalogueComponent, ICatalogueItem>
    {
        public override ListViewType ListType { get => ListViewType.ListViewWithVariableSize; }//布局需要
    }

1718094755079

FAQ

ListView中修改Item的显示状态(文本颜色)失败

答:ListView 使用的 DataSource 来绑定数据并显示,如果要改变状态的显示,那么你要改变DataSource的源数据,需要注意两点

  • 数据类型不能是Struct 和 interface,必须是class
  • 需要提供 INotifyPropertyChanged 支持,详见 INotifyPropertyChanged 的介绍
  • 提示:通过单独修改Item的Color是无效的,因为源数据没改变

NodeGraphProcessor

  • 功能 节点编辑器插件,用于配置任务流程,需要自己编写逻辑节点和处理流程。这是github上一个外国人基于Unity UIElement编写的。

  • 安装 ● 通过 UPM 安装插件 ● 更新指定版本

  • 样式扩展 在Resources目录下创建 PortViewTypes.uss,这个文件的名称是固定的,在这里面添加自己的样式

outflow 更改无效,不知原因
.Port_FlowLink {
    --port-color: #FFFF30;
}

.Port_ActionLink {
    --port-color: #FFFF30;
}
#contents #top .inflow #connector,
#contents #top .outflow #connector {
    border-radius: 0px;
}
  • 插件存在问题 ● 1.3.0 版本:图形界面很卡,回退到了1.2.0 ● 大半年没更新了,不过基本满足自己的需求了

生成PDF的插件

列举了几个在.net下使用的插件,但是最后只有ItextSharp使用成功了

iTextSharp

说明

进入官网的时候,提示itextsharp已经停止维护,请使用itext7,然后又去了解itext7,,但是最后发现要使用itext7需要很多dll,并且还报错,没成功,最后还是使用iTextSharp,主要是在网上可以找到使用列子。

  • 官网 :https://github.com/itext/itextsharp
  • 教程 :http://t.zoukankan.com/springsnow-p-13156025.html (实用)https://cloud.tencent.com/developer/article/1013339

使用手册

安装

  1. 使用vs nuget安装 ITextsharp后,在工程的packages文件夹下拷贝 BouncyCastle.Crypto.dll 和 itetshrp.dll到Pluins 文件夹下

  2. 拷贝Unity对应版本下的 I18N.dll 和 I18.West.dll(这两个没有在安卓端报错),对应路径(假设为Unity2019.3.15f): Unity\Hub\Editor\2019.3.15f1\Editor\Data\MonoBleedingEdge\lib\mono\unityjit 下的i18N dll

  3. 最后的dll如下图

1718094807127

字体

itextsharp 需要配置字体,使用file读取字体的,在PC IOS 上,直接将字体丢在 stremingassets下就可以了,但是 android 下 不支持 file 读取 stremingassets,解决办法是将 字体复制到 persentAssets下

报错处理

  1. Encoding 1252 data could not be found

    打包安卓端(确保路径正确),发现报错 NotSupportedException: Encoding 1252 data could not be found 在百度或者国内找了一辈子没解决问题,还是google:https://forum.unity.com/threads/notsupportedexception-encoding-1252-data-could-not-be-found-2020-2-3-f1.1058102/ . 解决方法:https://answers.unity.com/questions/1756912/invalid-il-code-in-build.html ,就是要添加 I18n ,按照以下的方式添加(就是要使用 Unity\Hub\Editor\2019.3.15f1\Editor\Data\MonoBleedingEdge\lib\mono\unityjit 下的i18N dll,最后要添加的dll如下:):

Okay nevermind, just had to snoop around a little more. The I18N dlls that I was using came from the Unity\Hub\Editor\2019.3.15f1\Editor\Data\MonoBleedingEdge\lib\mono\unity folder, and using the ones in the Unity\Hub\Editor\2019.3.15f1\Editor\Data\MonoBleedingEdge\lib\mono\unityjit worked. I don't understand why but it works anyways !

1718094807127

  1. 打包安卓时,未生成PDF,导致程序未执行后续逻辑

    • 没有显示生成的PDF,确定PDF有问题

    • 通过手机USB连接电脑,在电脑上查看手机的文件夹,定位到 包文件夹,找到生成的 pdf ,打不开,说明 未成功生成 pdf

    • 对每次 document.Add(element); 进行try 捕获错误,得到 1.not.found.as.a.resource?,定位是哪个 element 出现问题发现在下面语句时出现错误,

//绘制一条横线
elements.Add(new Chunk(new iTextSharp.text.pdf.draw.LineSeparator(0.0F, 100.0F, BaseColor.BLACK, Element.ALIGN_LEFT, 1)));

删除了这条语句

未清除的地方:第一次会报错,但是后续有时候又不报错

itext7

  • 官网 :https://github.com/itext/itext7-dotnet/releases/tag/7.2.2

  • Unity教程(失败) : https://www.ngui.cc/article/show-124951.html

  • 官方手册 https://kb.itextpdf.com/home/it7kb/examples/itext-7-jump-start-tutorial-chapter-2

  • 绘图教程,易懂 https://blog.csdn.net/u012397189/article/details/76726576

注意:需要安装

  • itext7

  • itext7.bouncy-castle-adapter(这个在编程的时候并没有体现出来需要,但是没有在加载文件的时候就会报错)

Questpdf

官网 :https://github.com/QuestPDF/QuestPDF

这个官方文档比较清楚,比较好用,但是在Unity端要报错,用不了,在.net端倒是比较好用,如在WPF上使用

PDF Renderer(渲染)

Unity 商店中的一款插件,收费较高,目前最新版本 5.51,自己目前最新为 5.30

注 在 csdn上有个资源为 5.51 是假的

导入包后有相关文档 ApiDocumentation.chmDocumentation.pdf

新输入系统(new input system)报错

  1. 若项目使用新输入系统, PDFViewerPage.cs 脚本会报错,原因是插件默认使用的是旧输入系统的输入方式

  2. PDF Renderer 文件夹的程序集定义文件中,添加 InputSystem 的依赖

{
    "name": "Paroxe.PDFRenderer",
    "rootNamespace": "",
    "references": [
        "Unity.InputSystem"
    ],
    "includePlatforms": [],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}
  1. 打开 PDFViewerPage.cs,修改脚本

    //添加新输入系统的命名空间
    using UnityEngine.InputSystem;
    //....


    //大概在265行
    //Vector2 pointerPosition = Input.mousePosition;//注释
    Vector2 pointerPosition = Mouse.current.position.ReadValue();

构建

  1. 先确认build平台相关的文件是否存在,不存在需要重新导入,或者拖入文件夹

  2. 其他平台的文件可能会影响构建,需要暂时删除

VSCODE 环境配置

  • 安装 .net core

  • 安装 .net framework 4.7.1 这个根据提示来,在vscode 提示里面搜索 version=,下载地址:https://dotnet.microsoft.com/zh-cn/download/dotnet-framework

  • 安装各种插件包

  • 在mac上始终不行,仔细看c#扩展

输入图片说明

1. Vue3简介

1.1. 【性能的提升】

  • 打包大小减少41%

  • 初次渲染快55%, 更新渲染快133%

  • 内存减少54%

1.2.【 源码的升级】

  • 使用Proxy代替defineProperty实现响应式。

  • 重写虚拟DOM的实现和Tree-Shaking

1.3. 【拥抱TypeScript】

  • Vue3可以更好的支持TypeScript

1.4. 【新的特性】

  1. Composition API(组合API):

    • setup

    • refreactive

    • computedwatch

      ......

  2. 新的内置组件:

    • Fragment

    • Teleport

    • Suspense

      ......

  3. 其他改变:

    • 新的生命周期钩子

    • data 选项应始终被声明为一个函数

    • 移除keyCode支持作为 v-on 的修饰符

      ......

2. 创建Vue3工程

2.1. 【基于 vue-cli 创建】

点击查看官方文档

备注:目前vue-cli已处于维护模式,官方推荐基于 Vite 创建项目。

## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version

## 安装或者升级你的@vue/cli 
npm install -g @vue/cli

## 执行创建命令
vue create vue_test

##  随后选择3.x
##  Choose a version of Vue.js that you want to start the project with (Use arrow keys)
##  > 3.x
##    2.x

## 启动
cd vue_test
npm run serve

2.2. 【基于 vite 创建】(推荐)

vite 是新一代前端构建工具,官网地址:https://vitejs.cnvite的优势如下:

  • 轻量快速的热重载(HMR),能实现极速的服务启动。
  • TypeScriptJSXCSS 等支持开箱即用。
  • 真正的按需编译,不再等待整个应用编译完成。
  • webpack构建 与 vite构建对比图如下: webpack构建 vite构建
## 1.创建命令
npm create vue@latest

## 2.具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript?  Yes
## 是否添加JSX支持
√ Add JSX Support?  No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development?  No
## 是否添加pinia环境
√ Add Pinia for state management?  No
## 是否添加单元测试
√ Add Vitest for Unit Testing?  No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality?  Yes
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting?  No

自己动手编写一个App组件

<template>
  <div class="app">
    <h1>你好啊!</h1>
  </div>
</template>

<script lang="ts">
  export default {
    name:'App' //组件名
  }
</script>

<style>
  .app {
    background-color: #ddd;
    box-shadow: 0 0 10px;
    border-radius: 10px;
    padding: 20px;
  }
</style>

安装官方推荐的vscode插件:

Snipaste_2023-10-08_20-46-34 image-20231218085906380

总结:

  • Vite 项目中,index.html 是项目的入口文件,在项目最外层。
  • 加载index.html后,Vite 解析 <script type="module" src="xxx"> 指向的JavaScript
  • Vue3**中是通过 **createApp 函数创建一个应用实例。

2.3. 【一个简单的效果】

Vue3向下兼容Vue2语法,且Vue3中的模板中可以没有根标签

<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
  </div>
</template>

<script lang="ts">
  export default {
    name:'App',
    data() {
      return {
        name:'张三',
        age:18,
        tel:'13888888888'
      }
    },
    methods:{
      changeName(){
        this.name = 'zhang-san'
      },
      changeAge(){
        this.age += 1
      },
      showTel(){
        alert(this.tel)
      }
    },
  }
</script>

3. Vue3核心语法

3.1. 【OptionsAPI 与 CompositionAPI】

  • Vue2API设计是Options(配置)风格的。
  • Vue3API设计是Composition(组合)风格的。

Options API 的弊端

Options类型的 API,数据、方法、计算属性等,是分散在:datamethodscomputed中的,若想新增或者修改一个需求,就需要分别修改:datamethodscomputed,不便于维护和复用。

1.gif2.gif

Composition API 的优势

可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。

3.gif4.gif

说明:以上四张动图原创作者:大帅老猿

3.2. 【拉开序幕的 setup】

setup 概述

setupVue3中一个新的配置项,值是一个函数,它是 Composition API “表演的舞台,组件中所用到的:数据、方法、计算属性、监视......等等,均配置在setup中。

特点如下:

  • setup函数返回的对象中的内容,可直接在模板中使用。
  • setup中访问thisundefined
  • setup函数会在beforeCreate之前调用,它是“领先”所有钩子执行的。
<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
  </div>
</template>

<script lang="ts">
  export default {
    name:'Person',
    setup(){
      // 数据,原来写在data中(注意:此时的name、age、tel数据都不是响应式数据)
      let name = '张三'
      let age = 18
      let tel = '13888888888'

      // 方法,原来写在methods中
      function changeName(){
        name = 'zhang-san' //注意:此时这么修改name页面是不变化的
        console.log(name)
      }
      function changeAge(){
        age += 1 //注意:此时这么修改age页面是不变化的
        console.log(age)
      }
      function showTel(){
        alert(tel)
      }

      // 返回一个对象,对象中的内容,模板中可以直接使用
      return {name,age,tel,changeName,changeAge,showTel}
    }
  }
</script>

setup 的返回值

  • 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
  • 若返回一个函数:则可以自定义渲染内容,代码如下:
setup(){
  return ()=> '你好啊!'
}

setup 与 Options API 的关系

  • Vue2 的配置(datamethos......)中可以访问到 setup中的属性、方法。
  • 但在setup不能访问到Vue2的配置(datamethos......)。
  • 如果与Vue2冲突,则setup优先。

setup 语法糖

setup函数有一个语法糖,这个语法糖,可以让我们把setup独立出去,代码如下:

<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="changName">修改名字</button>
    <button @click="changAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
  </div>
</template>

<script lang="ts">
  export default {
    name:'Person',
  }
</script>

<!-- 下面的写法是setup语法糖 -->
<script setup lang="ts">
  console.log(this) //undefined
  
  // 数据(注意:此时的name、age、tel都不是响应式数据)
  let name = '张三'
  let age = 18
  let tel = '13888888888'

  // 方法
  function changName(){
    name = '李四'//注意:此时这么修改name页面是不变化的
  }
  function changAge(){
    console.log(age)
    age += 1 //注意:此时这么修改age页面是不变化的
  }
  function showTel(){
    alert(tel)
  }
</script>

扩展:上述代码,还需要编写一个不写setupscript标签,去指定组件名字,比较麻烦,我们可以借助vite中的插件简化

  1. 第一步:npm i vite-plugin-vue-setup-extend -D
  2. 第二步:vite.config.ts
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

export default defineConfig({
  plugins: [ VueSetupExtend() ]
})
  1. 第三步:<script setup lang="ts" name="Person">

3.3. 【ref 创建:基本类型的响应式数据】

  • **作用:**定义响应式变量。
  • 语法:let xxx = ref(初始值)
  • **返回值:**一个RefImpl的实例对象,简称ref对象refref对象的value属性是响应式的
  • 注意点:
    • JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。
    • 对于let name = ref('张三')来说,name不是响应式的,name.value是响应式的。
<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
  </div>
</template>

<script setup lang="ts" name="Person">
  import {ref} from 'vue'
  // name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
  let name = ref('张三')
  let age = ref(18)
  // tel就是一个普通的字符串,不是响应式的
  let tel = '13888888888'

  function changeName(){
    // JS中操作ref对象时候需要.value
    name.value = '李四'
    console.log(name.value)

    // 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
    // name = ref('zhang-san')
  }
  function changeAge(){
    // JS中操作ref对象时候需要.value
    age.value += 1 
    console.log(age.value)
  }
  function showTel(){
    alert(tel)
  }
</script>

3.4. 【reactive 创建:对象类型的响应式数据】

  • 作用:定义一个响应式对象(基本类型不要用它,要用ref,否则报错)
  • 语法:let 响应式对象= reactive(源对象)
  • **返回值:**一个Proxy的实例对象,简称:响应式对象。
  • 注意点:reactive定义的响应式数据是“深层次”的。
<template>
  <div class="person">
    <h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
    <h2>游戏列表:</h2>
    <ul>
      <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
    <h2>测试:{{obj.a.b.c.d}}</h2>
    <button @click="changeCarPrice">修改汽车价格</button>
    <button @click="changeFirstGame">修改第一游戏</button>
    <button @click="test">测试</button>
  </div>
</template>

<script lang="ts" setup name="Person">
import { reactive } from 'vue'

// 数据
let car = reactive({ brand: '奔驰', price: 100 })
let games = reactive([
  { id: 'ahsgdyfa01', name: '英雄联盟' },
  { id: 'ahsgdyfa02', name: '王者荣耀' },
  { id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
  a:{
    b:{
      c:{
        d:666
      }
    }
  }
})

function changeCarPrice() {
  car.price += 10
}
function changeFirstGame() {
  games[0].name = '流星蝴蝶剑'
}
function test(){
  obj.a.b.c.d = 999
}
</script>

3.5. 【ref 创建:对象类型的响应式数据】

  • 其实ref接收的数据可以是:基本类型对象类型
  • ref接收的是对象类型,内部其实也是调用了reactive函数。
<template>
  <div class="person">
    <h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
    <h2>游戏列表:</h2>
    <ul>
      <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
    <h2>测试:{{obj.a.b.c.d}}</h2>
    <button @click="changeCarPrice">修改汽车价格</button>
    <button @click="changeFirstGame">修改第一游戏</button>
    <button @click="test">测试</button>
  </div>
</template>

<script lang="ts" setup name="Person">
import { ref } from 'vue'

// 数据
let car = ref({ brand: '奔驰', price: 100 })
let games = ref([
  { id: 'ahsgdyfa01', name: '英雄联盟' },
  { id: 'ahsgdyfa02', name: '王者荣耀' },
  { id: 'ahsgdyfa03', name: '原神' }
])
let obj = ref({
  a:{
    b:{
      c:{
        d:666
      }
    }
  }
})

console.log(car)

function changeCarPrice() {
  car.value.price += 10
}
function changeFirstGame() {
  games.value[0].name = '流星蝴蝶剑'
}
function test(){
  obj.value.a.b.c.d = 999
}
</script>

3.6. 【ref 对比 reactive】

宏观角度看:

  1. ref用来定义:基本类型数据对象类型数据

  2. reactive用来定义:对象类型数据

  • 区别:
  1. ref创建的变量必须使用.value(可以使用volar插件自动添加.value)。

    自动补充value
  2. reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。

  • 使用原则:
  1. 若需要一个基本类型的响应式数据,必须使用ref
  2. 若需要一个响应式对象,层级不深,refreactive都可以。
  3. 若需要一个响应式对象,且层级较深,推荐使用reactive

3.7. 【toRefs 与 toRef】

  • 作用:将一个响应式对象中的每一个属性,转换为ref对象。
  • 备注:toRefstoRef功能一致,但toRefs可以批量转换。
  • 语法如下:
<template>
  <div class="person">
    <h2>姓名:{{person.name}}</h2>
    <h2>年龄:{{person.age}}</h2>
    <h2>性别:{{person.gender}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changeGender">修改性别</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref,reactive,toRefs,toRef} from 'vue'

  // 数据
  let person = reactive({name:'张三', age:18, gender:'男'})
	
  // 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
  let {name,gender} =  toRefs(person)
	
  // 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
  let age = toRef(person,'age')

  // 方法
  function changeName(){
    name.value += '~'
  }
  function changeAge(){
    age.value += 1
  }
  function changeGender(){
    gender.value = '女'
  }
</script>

3.8. 【computed】

作用:根据已有数据计算出新数据(和Vue2中的computed作用一致)。

<template>
  <div class="person">
    姓:<input type="text" v-model="firstName"> <br>
    名:<input type="text" v-model="lastName"> <br>
    全名:<span>{{fullName}}</span> <br>
    <button @click="changeFullName">全名改为:li-si</button>
  </div>
</template>

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

  let firstName = ref('zhang')
  let lastName = ref('san')

  // 计算属性——只读取,不修改
  /* let fullName = computed(()=>{
    return firstName.value + '-' + lastName.value
  }) */


  // 计算属性——既读取又修改
  let fullName = computed({
    // 读取
    get(){
      return firstName.value + '-' + lastName.value
    },
    // 修改
    set(val){
      console.log('有人修改了fullName',val)
      firstName.value = val.split('-')[0]
      lastName.value = val.split('-')[1]
    }
  })

  function changeFullName(){
    fullName.value = 'li-si'
  } 
</script>

3.9.【watch】

  • 作用:监视数据的变化(和Vue2中的watch作用一致)
  • 特点:Vue3中的watch只能监视以下四种数据
  1. ref定义的数据。
  2. reactive定义的数据。
  3. 函数返回一个值(getter函数)。
  4. 一个包含上述内容的数组。

我们在Vue3中使用watch的时候,通常会遇到以下几种情况:

* 情况一

监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。

<template>
  <div class="person">
    <h1>情况一:监视【ref】定义的【基本类型】数据</h1>
    <h2>当前求和为:{{sum}}</h2>
    <button @click="changeSum">点我sum+1</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref,watch} from 'vue'
  // 数据
  let sum = ref(0)
  // 方法
  function changeSum(){
    sum.value += 1
  }
  // 监视,情况一:监视【ref】定义的【基本类型】数据
  const stopWatch = watch(sum,(newValue,oldValue)=>{
    console.log('sum变化了',newValue,oldValue)
    if(newValue >= 10){
      stopWatch()
    }
  })
</script>

* 情况二

监视ref定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。

注意:

  • 若修改的是ref定义的对象中的属性,newValueoldValue 都是新值,因为它们是同一个对象。

  • 若修改整个ref定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。

<template>
  <div class="person">
    <h1>情况二:监视【ref】定义的【对象类型】数据</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref,watch} from 'vue'
  // 数据
  let person = ref({
    name:'张三',
    age:18
  })
  // 方法
  function changeName(){
    person.value.name += '~'
  }
  function changeAge(){
    person.value.age += 1
  }
  function changePerson(){
    person.value = {name:'李四',age:90}
  }
  /* 
    监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视
    watch的第一个参数是:被监视的数据
    watch的第二个参数是:监视的回调
    watch的第三个参数是:配置对象(deep、immediate等等.....) 
  */
  watch(person,(newValue,oldValue)=>{
    console.log('person变化了',newValue,oldValue)
  },{deep:true})
  
</script>

* 情况三

监视reactive定义的【对象类型】数据,且默认开启了深度监视。

<template>
  <div class="person">
    <h1>情况三:监视【reactive】定义的【对象类型】数据</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>
    <hr>
    <h2>测试:{{obj.a.b.c}}</h2>
    <button @click="test">修改obj.a.b.c</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {reactive,watch} from 'vue'
  // 数据
  let person = reactive({
    name:'张三',
    age:18
  })
  let obj = reactive({
    a:{
      b:{
        c:666
      }
    }
  })
  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changePerson(){
    Object.assign(person,{name:'李四',age:80})
  }
  function test(){
    obj.a.b.c = 888
  }

  // 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
  watch(person,(newValue,oldValue)=>{
    console.log('person变化了',newValue,oldValue)
  })
  watch(obj,(newValue,oldValue)=>{
    console.log('Obj变化了',newValue,oldValue)
  })
</script>

* 情况四

监视refreactive定义的【对象类型】数据中的某个属性,注意点如下:

  1. 若该属性值不是【对象类型】,需要写成函数形式。
  2. 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。

结论:监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。

<template>
  <div class="person">
    <h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changeC1">修改第一台车</button>
    <button @click="changeC2">修改第二台车</button>
    <button @click="changeCar">修改整个车</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {reactive,watch} from 'vue'

  // 数据
  let person = reactive({
    name:'张三',
    age:18,
    car:{
      c1:'奔驰',
      c2:'宝马'
    }
  })
  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changeC1(){
    person.car.c1 = '奥迪'
  }
  function changeC2(){
    person.car.c2 = '大众'
  }
  function changeCar(){
    person.car = {c1:'雅迪',c2:'爱玛'}
  }

  // 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
  /* watch(()=> person.name,(newValue,oldValue)=>{
    console.log('person.name变化了',newValue,oldValue)
  }) */

  // 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
  watch(()=>person.car,(newValue,oldValue)=>{
    console.log('person.car变化了',newValue,oldValue)
  },{deep:true})
</script>

* 情况五

监视上述的多个数据

<template>
  <div class="person">
    <h1>情况五:监视上述的多个数据</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changeC1">修改第一台车</button>
    <button @click="changeC2">修改第二台车</button>
    <button @click="changeCar">修改整个车</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {reactive,watch} from 'vue'

  // 数据
  let person = reactive({
    name:'张三',
    age:18,
    car:{
      c1:'奔驰',
      c2:'宝马'
    }
  })
  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changeC1(){
    person.car.c1 = '奥迪'
  }
  function changeC2(){
    person.car.c2 = '大众'
  }
  function changeCar(){
    person.car = {c1:'雅迪',c2:'爱玛'}
  }

  // 监视,情况五:监视上述的多个数据
  watch([()=>person.name,person.car],(newValue,oldValue)=>{
    console.log('person.car变化了',newValue,oldValue)
  },{deep:true})

</script>

3.10. 【watchEffect】

  • 官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。

  • watch对比watchEffect

    1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同

    2. watch:要明确指出监视的数据

    3. watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。

  • 示例代码:

    <template>
      <div class="person">
        <h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1>
        <h2 id="demo">水温:{{temp}}</h2>
        <h2>水位:{{height}}</h2>
        <button @click="changePrice">水温+1</button>
        <button @click="changeSum">水位+10</button>
      </div>
    </template>
    
    <script lang="ts" setup name="Person">
      import {ref,watch,watchEffect} from 'vue'
      // 数据
      let temp = ref(0)
      let height = ref(0)
    
      // 方法
      function changePrice(){
        temp.value += 10
      }
      function changeSum(){
        height.value += 1
      }
    
      // 用watch实现,需要明确的指出要监视:temp、height
      watch([temp,height],(value)=>{
        // 从value中获取最新的temp值、height值
        const [newTemp,newHeight] = value
        // 室温达到50℃,或水位达到20cm,立刻联系服务器
        if(newTemp >= 50 || newHeight >= 20){
          console.log('联系服务器')
        }
      })
    
      // 用watchEffect实现,不用
      const stopWtach = watchEffect(()=>{
        // 室温达到50℃,或水位达到20cm,立刻联系服务器
        if(temp.value >= 50 || height.value >= 20){
          console.log(document.getElementById('demo')?.innerText)
          console.log('联系服务器')
        }
        // 水温达到100,或水位达到50,取消监视
        if(temp.value === 100 || height.value === 50){
          console.log('清理了')
          stopWtach()
        }
      })
    </script>
    

3.11. 【标签的 ref 属性】

作用:用于注册模板引用。

  • 用在普通DOM标签上,获取的是DOM节点。

  • 用在组件标签上,获取的是组件实例对象。

用在普通DOM标签上:

<template>
  <div class="person">
    <h1 ref="title1">尚硅谷</h1>
    <h2 ref="title2">前端</h2>
    <h3 ref="title3">Vue</h3>
    <input type="text" ref="inpt"> <br><br>
    <button @click="showLog">点我打印内容</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref} from 'vue'
	
  let title1 = ref()
  let title2 = ref()
  let title3 = ref()

  function showLog(){
    // 通过id获取元素
    const t1 = document.getElementById('title1')
    // 打印内容
    console.log((t1 as HTMLElement).innerText)
    console.log((<HTMLElement>t1).innerText)
    console.log(t1?.innerText)
    
		/************************************/
		
    // 通过ref获取元素
    console.log(title1.value)
    console.log(title2.value)
    console.log(title3.value)
  }
</script>

用在组件标签上:

<!-- 父组件App.vue -->
<template>
  <Person ref="ren"/>
  <button @click="test">测试</button>
</template>

<script lang="ts" setup name="App">
  import Person from './components/Person.vue'
  import {ref} from 'vue'

  let ren = ref()

  function test(){
    console.log(ren.value.name)
    console.log(ren.value.age)
  }
</script>


<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
  import {ref,defineExpose} from 'vue'
	// 数据
  let name = ref('张三')
  let age = ref(18)
  /****************************/
  /****************************/
  // 使用defineExpose将组件中的数据交给外部
  defineExpose({name,age})
</script>

3.12. 【props】

// 定义一个接口,限制每个Person对象的格式
export interface PersonInter {
 id:string,
 name:string,
    age:number
   }
   
// 定义一个自定义类型Persons
export type Persons = Array<PersonInter>

App.vue中代码:

<template>
	<Person :list="persons"/>
</template>
  
<script lang="ts" setup name="App">
  import Person from './components/Person.vue'
  import {reactive} from 'vue'
    import {type Persons} from './types'
  
    let persons = reactive<Persons>([
     {id:'e98219e12',name:'张三',age:18},
      {id:'e98219e13',name:'李四',age:19},
       {id:'e98219e14',name:'王五',age:20}
     ])
   </script>
  

Person.vue中代码:

<template>
<div class="person">
 <ul>
     <li v-for="item in list" :key="item.id">
        {{item.name}}--{{item.age}}
      </li>
    </ul>
   </div>
   </template>
  
<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
import {type PersonInter} from '@/types'
  
  // 第一种写法:仅接收
// const props = defineProps(['list'])
  
  // 第二种写法:接收+限制类型
// defineProps<{list:Persons}>()
  
  // 第三种写法:接收+限制类型+指定默认值+限制必要性
let props = withDefaults(defineProps<{list?:Persons}>(),{
     list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
  })
   console.log(props)
  </script>

3.13. 【生命周期】

  • 概念:Vue组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子

  • 规律:

    生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。

  • Vue2的生命周期

    创建阶段:beforeCreatecreated

    挂载阶段:beforeMountmounted

    更新阶段:beforeUpdateupdated

    销毁阶段:beforeDestroydestroyed

  • Vue3的生命周期

    创建阶段:setup

    挂载阶段:onBeforeMountonMounted

    更新阶段:onBeforeUpdateonUpdated

    卸载阶段:onBeforeUnmountonUnmounted

  • 常用的钩子:onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)

  • 示例代码:

    <template>
      <div class="person">
        <h2>当前求和为:{{ sum }}</h2>
        <button @click="changeSum">点我sum+1</button>
      </div>
    </template>
    
    <!-- vue3写法 -->
    <script lang="ts" setup name="Person">
      import { 
        ref, 
        onBeforeMount, 
        onMounted, 
        onBeforeUpdate, 
        onUpdated, 
        onBeforeUnmount, 
        onUnmounted 
      } from 'vue'
    
      // 数据
      let sum = ref(0)
      // 方法
      function changeSum() {
        sum.value += 1
      }
      console.log('setup')
      // 生命周期钩子
      onBeforeMount(()=>{
        console.log('挂载之前')
      })
      onMounted(()=>{
        console.log('挂载完毕')
      })
      onBeforeUpdate(()=>{
        console.log('更新之前')
      })
      onUpdated(()=>{
        console.log('更新完毕')
      })
      onBeforeUnmount(()=>{
        console.log('卸载之前')
      })
      onUnmounted(()=>{
        console.log('卸载完毕')
      })
    </script>
    

3.14. 【自定义hook】

  • 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin

  • 自定义hook的优势:复用代码, 让setup中的逻辑更清楚易懂。

示例代码:

  • useSum.ts中内容如下:

    import {ref,onMounted} from 'vue'
    
    export default function(){
      let sum = ref(0)
    
      const increment = ()=>{
        sum.value += 1
      }
      const decrement = ()=>{
        sum.value -= 1
      }
      onMounted(()=>{
        increment()
      })
    
      //向外部暴露数据
      return {sum,increment,decrement}
    }		
    
  • useDog.ts中内容如下:

    import {reactive,onMounted} from 'vue'
    import axios,{AxiosError} from 'axios'
    
    export default function(){
      let dogList = reactive<string[]>([])
    
      // 方法
      async function getDog(){
        try {
          // 发请求
          let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
          // 维护数据
          dogList.push(data.message)
        } catch (error) {
          // 处理错误
          const err = <AxiosError>error
          console.log(err.message)
        }
      }
    
      // 挂载钩子
      onMounted(()=>{
        getDog()
      })
    	
      //向外部暴露数据
      return {dogList,getDog}
    }
    
  • 组件中具体使用:

    <template>
      <h2>当前求和为:{{sum}}</h2>
      <button @click="increment">点我+1</button>
      <button @click="decrement">点我-1</button>
      <hr>
      <img v-for="(u,index) in dogList.urlList" :key="index" :src="(u as string)"> 
      <span v-show="dogList.isLoading">加载中......</span><br>
      <button @click="getDog">再来一只狗</button>
    </template>
    
    <script lang="ts">
      import {defineComponent} from 'vue'
    
      export default defineComponent({
        name:'App',
      })
    </script>
    
    <script setup lang="ts">
      import useSum from './hooks/useSum'
      import useDog from './hooks/useDog'
    	
      let {sum,increment,decrement} = useSum()
      let {dogList,getDog} = useDog()
    </script>
    

4. 路由

4.1. 【对路由的理解】

image-20231018144351536

4.2. 【基本切换效果】

  • Vue3中要使用vue-router的最新版本,目前是4版本。

  • 路由配置文件代码如下:

    import {createRouter,createWebHistory} from 'vue-router'
    import Home from '@/pages/Home.vue'
    import News from '@/pages/News.vue'
    import About from '@/pages/About.vue'
    
    const router = createRouter({
    	history:createWebHistory(),
    	routes:[
    		{
    			path:'/home',
    			component:Home
    		},
    		{
    			path:'/about',
    			component:About
    		}
    	]
    })
    export default router
    
  • main.ts代码如下:

    import router from './router/index'
    app.use(router)
    
    app.mount('#app')
    
  • App.vue代码如下

    <template>
      <div class="app">
        <h2 class="title">Vue路由测试</h2>
        <!-- 导航区 -->
        <div class="navigate">
          <RouterLink to="/home" active-class="active">首页</RouterLink>
          <RouterLink to="/news" active-class="active">新闻</RouterLink>
          <RouterLink to="/about" active-class="active">关于</RouterLink>
        </div>
        <!-- 展示区 -->
        <div class="main-content">
          <RouterView></RouterView>
        </div>
      </div>
    </template>
    
    <script lang="ts" setup name="App">
      import {RouterLink,RouterView} from 'vue-router'  
    </script>
    

4.3. 【两个注意点】

  1. 路由组件通常存放在pagesviews文件夹,一般组件通常存放在components文件夹。

  2. 通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载

4.4.【路由器工作模式】

  1. history模式

    优点:URL更加美观,不带有#,更接近传统的网站URL

    缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。

    const router = createRouter({
      	history:createWebHistory(), //history模式
      	/******/
    })
    
  2. hash模式

    优点:兼容性更好,因为不需要服务器端处理路径。

    缺点:URL带有#不太美观,且在SEO优化方面相对较差。

    const router = createRouter({
      	history:createWebHashHistory(), //hash模式
      	/******/
    })
    

4.5. 【to的两种写法】

<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>

<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>

4.6. 【命名路由】

作用:可以简化路由跳转及传参(后面就讲)。

给路由规则命名:

routes:[
  {
    name:'zhuye',
    path:'/home',
    component:Home
  },
  {
    name:'xinwen',
    path:'/news',
    component:News,
  },
  {
    name:'guanyu',
    path:'/about',
    component:About
  }
]

跳转路由:

<!--简化前:需要写完整的路径(to的字符串写法) -->
<router-link to="/news/detail">跳转</router-link>

<!--简化后:直接通过名字跳转(to的对象写法配合name属性) -->
<router-link :to="{name:'guanyu'}">跳转</router-link>

4.7. 【嵌套路由】

  1. 编写News的子路由:Detail.vue

  2. 配置路由规则,使用children配置项:

    const router = createRouter({
      history:createWebHistory(),
    	routes:[
    		{
    			name:'zhuye',
    			path:'/home',
    			component:Home
    		},
    		{
    			name:'xinwen',
    			path:'/news',
    			component:News,
    			children:[
    				{
    					name:'xiang',
    					path:'detail',
    					component:Detail
    				}
    			]
    		},
    		{
    			name:'guanyu',
    			path:'/about',
    			component:About
    		}
    	]
    })
    export default router
    
  3. 跳转路由(记得要加完整路径):

    <router-link to="/news/detail">xxxx</router-link>
    <!-- 或 -->
    <router-link :to="{path:'/news/detail'}">xxxx</router-link>
    
  4. 记得去Home组件中预留一个<router-view>

    <template>
      <div class="news">
        <nav class="news-list">
          <RouterLink v-for="news in newsList" :key="news.id" :to="{path:'/news/detail'}">
            {{news.name}}
          </RouterLink>
        </nav>
        <div class="news-detail">
          <RouterView/>
        </div>
      </div>
    </template>
    

4.8. 【路由传参】

query参数

  1. 传递参数

    <!-- 跳转并携带query参数(to的字符串写法) -->
    <router-link to="/news/detail?a=1&b=2&content=欢迎你">
    	跳转
    </router-link>
    				
    <!-- 跳转并携带query参数(to的对象写法) -->
    <RouterLink 
      :to="{
        //name:'xiang', //用name也可以跳转
        path:'/news/detail',
        query:{
          id:news.id,
          title:news.title,
          content:news.content
        }
      }"
    >
      {{news.title}}
    </RouterLink>
    
  2. 接收参数:

    import {useRoute} from 'vue-router'
    const route = useRoute()
    // 打印query参数
    console.log(route.query)
    

params参数

  1. 传递参数

    <!-- 跳转并携带params参数(to的字符串写法) -->
    <RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink>
    				
    <!-- 跳转并携带params参数(to的对象写法) -->
    <RouterLink 
      :to="{
        name:'xiang', //用name跳转
        params:{
          id:news.id,
          title:news.title,
          content:news.title
        }
      }"
    >
      {{news.title}}
    </RouterLink>
    
  2. 接收参数:

    import {useRoute} from 'vue-router'
    const route = useRoute()
    // 打印params参数
    console.log(route.params)
    

备注1:传递params参数时,若使用to的对象写法,必须使用name配置项,不能用path

备注2:传递params参数时,需要提前在规则中占位。

4.9. 【路由的props配置】

作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件)

{
	name:'xiang',
	path:'detail/:id/:title/:content',
	component:Detail,

  // props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件
  // props:{a:1,b:2,c:3}, 

  // props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件
  // props:true
  
  // props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件
  props(route){
    return route.query
  }
}

4.10. 【 replace属性】

  1. 作用:控制路由跳转时操作浏览器历史记录的模式。

  2. 浏览器的历史记录有两种写入方式:分别为pushreplace

    • push是追加历史记录(默认值)。
    • replace是替换当前记录。
  3. 开启replace模式:

    <RouterLink replace .......>News</RouterLink>
    

4.11. 【编程式导航】

路由组件的两个重要的属性:$route$router变成了两个hooks

import {useRoute,useRouter} from 'vue-router'

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

console.log(route.query)
console.log(route.parmas)
console.log(router.push)
console.log(router.replace)

4.12. 【重定向】

  1. 作用:将特定的路径,重新定向到已有路由。

  2. 具体编码:

    {
        path:'/',
        redirect:'/about'
    }
    

5. pinia

5.1【准备一个效果】

pinia_example

5.2【搭建 pinia 环境】

第一步:npm install pinia

第二步:操作src/main.ts

import { createApp } from 'vue'
import App from './App.vue'

/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'

/* 创建pinia */
const pinia = createPinia()
const app = createApp(App)

/* 使用插件 */{}
app.use(pinia)
app.mount('#app')

此时开发者工具中已经有了pinia选项

5.3【存储+读取数据】

  1. Store是一个保存:状态业务逻辑 的实体,每个组件都可以读取写入它。

  2. 它有三个概念:stategetteraction,相当于组件中的: datacomputedmethods

  3. 具体编码:src/store/count.ts

    // 引入defineStore用于创建store
    import {defineStore} from 'pinia'
    
    // 定义并暴露一个store
    export const useCountStore = defineStore('count',{
      // 动作
      actions:{},
      // 状态
      state(){
        return {
          sum:6
        }
      },
      // 计算
      getters:{}
    })
    
  4. 具体编码:src/store/talk.ts

    // 引入defineStore用于创建store
    import {defineStore} from 'pinia'
    
    // 定义并暴露一个store
    export const useTalkStore = defineStore('talk',{
      // 动作
      actions:{},
      // 状态
      state(){
        return {
          talkList:[
            {id:'yuysada01',content:'你今天有点怪,哪里怪?怪好看的!'},
         		{id:'yuysada02',content:'草莓、蓝莓、蔓越莓,你想我了没?'},
            {id:'yuysada03',content:'心里给你留了一块地,我的死心塌地'}
          ]
        }
      },
      // 计算
      getters:{}
    })
    
  5. 组件中使用state中的数据

    <template>
      <h2>当前求和为:{{ sumStore.sum }}</h2>
    </template>
    
    <script setup lang="ts" name="Count">
      // 引入对应的useXxxxxStore	
      import {useSumStore} from '@/store/sum'
      
      // 调用useXxxxxStore得到对应的store
      const sumStore = useSumStore()
    </script>
    
    <template>
    	<ul>
        <li v-for="talk in talkStore.talkList" :key="talk.id">
          {{ talk.content }}
        </li>
      </ul>
    </template>
    
    <script setup lang="ts" name="Count">
      import axios from 'axios'
      import {useTalkStore} from '@/store/talk'
    
      const talkStore = useTalkStore()
    </script>
    

5.4.【修改数据】(三种方式)

  1. 第一种修改方式,直接修改

    countStore.sum = 666
    
  2. 第二种修改方式:批量修改

    countStore.$patch({
      sum:999,
      school:'atguigu'
    })
    
  3. 第三种修改方式:借助action修改(action中可以编写一些业务逻辑)

    import { defineStore } from 'pinia'
    
    export const useCountStore = defineStore('count', {
      /*************/
      actions: {
        //加
        increment(value:number) {
          if (this.sum < 10) {
            //操作countStore中的sum
            this.sum += value
          }
        },
        //减
        decrement(value:number){
          if(this.sum > 1){
            this.sum -= value
          }
        }
      },
      /*************/
    })
    
  4. 组件中调用action即可

    // 使用countStore
    const countStore = useCountStore()
    
    // 调用对应action
    countStore.incrementOdd(n.value)
    

5.5.【storeToRefs】

  • 借助storeToRefsstore中的数据转为ref对象,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只会将数据做转换,而VuetoRefs会转换store中数据。
<template>
	<div class="count">
		<h2>当前求和为:{{sum}}</h2>
	</div>
</template>

<script setup lang="ts" name="Count">
  import { useCountStore } from '@/store/count'
  /* 引入storeToRefs */
  import { storeToRefs } from 'pinia'

	/* 得到countStore */
  const countStore = useCountStore()
  /* 使用storeToRefs转换countStore,随后解构 */
  const {sum} = storeToRefs(countStore)
</script>

5.6.【getters】

  1. 概念:当state中的数据,需要经过处理后再使用时,可以使用getters配置。

  2. 追加getters配置。

    // 引入defineStore用于创建store
    import {defineStore} from 'pinia'
    
    // 定义并暴露一个store
    export const useCountStore = defineStore('count',{
      // 动作
      actions:{
        /************/
      },
      // 状态
      state(){
        return {
          sum:1,
          school:'atguigu'
        }
      },
      // 计算
      getters:{
        bigSum:(state):number => state.sum *10,
        upperSchool():string{
          return this. school.toUpperCase()
        }
      }
    })
    
  3. 组件中读取数据:

    const {increment,decrement} = countStore
    let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)
    

5.7.【$subscribe】

通过 store 的 $subscribe() 方法侦听 state 及其变化

talkStore.$subscribe((mutate,state)=>{
  console.log('LoveTalk',mutate,state)
  localStorage.setItem('talk',JSON.stringify(talkList.value))
})

5.8. 【store组合式写法】

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
import {reactive} from 'vue'

export const useTalkStore = defineStore('talk',()=>{
  // talkList就是state
  const talkList = reactive(
    JSON.parse(localStorage.getItem('talkList') as string) || []
  )

  // getATalk函数相当于action
  async function getATalk(){
    // 发请求,下面这行的写法是:连续解构赋值+重命名
    let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
    // 把请求回来的字符串,包装成一个对象
    let obj = {id:nanoid(),title}
    // 放到数组中
    talkList.unshift(obj)
  }
  return {talkList,getATalk}
})

6. 组件通信

Vue3组件通信和Vue2的区别:

  • 移出事件总线,使用mitt代替。
  • vuex换成了pinia
  • .sync优化到了v-model里面了。
  • $listeners所有的东西,合并到$attrs中了。
  • $children被砍掉了。

常见搭配形式:

image-20231119185900990

6.1. 【props】

概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子

  • 父传子:属性值是非函数
  • 子传父:属性值是函数

父组件:

<template>
  <div class="father">
    <h3>父组件,</h3>
		<h4>我的车:{{ car }}</h4>
		<h4>儿子给的玩具:{{ toy }}</h4>
		<Child :car="car" :getToy="getToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	// 数据
	const car = ref('奔驰')
	const toy = ref()
	// 方法
	function getToy(value:string){
		toy.value = value
	}
</script>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
		<h4>我的玩具:{{ toy }}</h4>
		<h4>父给我的车:{{ car }}</h4>
		<button @click="getToy(toy)">玩具给父亲</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	const toy = ref('奥特曼')
	
	defineProps(['car','getToy'])
</script>

6.2. 【自定义事件】

  1. 概述:自定义事件常用于:子 => 父。
  2. 注意区分好:原生事件、自定义事件。
  • 原生事件:
    • 事件名是特定的(clickmosueenter等等)
    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:
    • 事件名是任意名称
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!
  1. 示例:

    <!--在父组件中,给子组件绑定自定义事件:-->
    <Child @send-toy="toy = $event"/>
    
    <!--注意区分原生事件与自定义事件中的$event-->
    <button @click="toy = $event">测试</button>
    
    //子组件中,触发事件:
    this.$emit('send-toy', 具体数据)
    

6.3. 【mitt】

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

安装mitt

npm i mitt

新建文件:src\utils\emitter.ts

// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

/*
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被触发',value)
  })

  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 创建并暴露mitt
export default emitter

接收数据的组件中:绑定事件、同时在销毁前解绑事件:

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被触发',value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('send-toy')
})

【第三步】:提供数据的组件,在合适的时候触发事件

import emitter from "@/utils/emitter";

function sendToy(){
  // 触发事件
  emitter.emit('send-toy',toy.value)
}

注意这个重要的内置关系,总线依赖着这个内置关系

6.4.【v-model】

  1. 概述:实现 父↔子 之间相互通信。

  2. 前序知识 —— v-model的本质

    <!-- 使用v-model指令 -->
    <input type="text" v-model="userName">
    
    <!-- v-model的本质是下面这行代码 -->
    <input 
      type="text" 
      :value="userName" 
      @input="userName =(<HTMLInputElement>$event.target).value"
    >
    
  3. 组件标签上的v-model的本质::moldeValueupdate:modelValue事件。

    <!-- 组件标签上使用v-model指令 -->
    <AtguiguInput v-model="userName"/>
    
    <!-- 组件标签上v-model的本质 -->
    <AtguiguInput :modelValue="userName" @update:model-value="userName = $event"/>
    

    AtguiguInput组件中:

    <template>
      <div class="box">
        <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
    		<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
        <input 
           type="text" 
           :value="modelValue" 
           @input="emit('update:model-value',$event.target.value)"
        >
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['modelValue'])
      // 声明事件
      const emit = defineEmits(['update:model-value'])
    </script>
    
  4. 也可以更换value,例如改成abc

    <!-- 也可以更换value,例如改成abc-->
    <AtguiguInput v-model:abc="userName"/>
    
    <!-- 上面代码的本质如下 -->
    <AtguiguInput :abc="userName" @update:abc="userName = $event"/>
    

    AtguiguInput组件中:

    <template>
      <div class="box">
        <input 
           type="text" 
           :value="abc" 
           @input="emit('update:abc',$event.target.value)"
        >
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['abc'])
      // 声明事件
      const emit = defineEmits(['update:abc'])
    </script>
    
  5. 如果value可以更换,那么就可以在组件标签上多次使用v-model

    <AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
    

6.5.【$attrs 】

  1. 概述:$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。

  2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

    注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
		<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	let a = ref(1)
	let b = ref(2)
	let c = ref(3)
	let d = ref(4)

	function updateA(value){
		a.value = value
	}
</script>

子组件:

<template>
	<div class="child">
		<h3>子组件</h3>
		<GrandChild v-bind="$attrs"/>
	</div>
</template>

<script setup lang="ts" name="Child">
	import GrandChild from './GrandChild.vue'
</script>

孙组件:

<template>
	<div class="grand-child">
		<h3>孙组件</h3>
		<h4>a:{{ a }}</h4>
		<h4>b:{{ b }}</h4>
		<h4>c:{{ c }}</h4>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>
		<button @click="updateA(666)">点我更新A</button>
	</div>
</template>

<script setup lang="ts" name="GrandChild">
	defineProps(['a','b','c','d','x','y','updateA'])
</script>

6.6. 【$refs、$parent】

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

    属性说明
    $refs值为对象,包含所有被ref属性标识的DOM元素或组件实例。
    $parent值为对象,当前组件的父组件实例对象。

6.7. 【provide、inject】

  1. 概述:实现祖孙组件直接通信

  2. 具体使用:

    • 在祖先组件中通过provide配置向后代组件提供数据
    • 在后代组件中通过inject配置来声明接收数据
  3. 具体编码:

    【第一步】父组件中,使用provide提供数据

    <template>
      <div class="father">
        <h3>父组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="money += 1">资产+1</button>
        <button @click="car.price += 1">汽车价格+1</button>
        <Child/>
      </div>
    </template>
    
    <script setup lang="ts" name="Father">
      import Child from './Child.vue'
      import { ref,reactive,provide } from "vue";
      // 数据
      let money = ref(100)
      let car = reactive({
        brand:'奔驰',
        price:100
      })
      // 用于更新money的方法
      function updateMoney(value:number){
        money.value += value
      }
      // 提供数据
      provide('moneyContext',{money,updateMoney})
      provide('car',car)
    </script>
    

    注意:子组件中不用编写任何东西,是不受到任何打扰的

    【第二步】孙组件中使用inject配置项接受数据。

    <template>
      <div class="grand-child">
        <h3>我是孙组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="updateMoney(6)">点我</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
      import { inject } from 'vue';
      // 注入数据
     let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
      let car = inject('car')
    
```

6.8. 【pinia】

参考之前pinia部分的讲解

6.9. 【slot】

1. 默认插槽

img

父组件中:
        <Category title="今日热门游戏">
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <!-- 默认插槽 -->
            <slot></slot>
          </div>
        </template>

2. 具名插槽

父组件中:
        <Category title="今日热门游戏">
          <template v-slot:s1>
            <ul>
              <li v-for="g in games" :key="g.id">{{ g.name }}</li>
            </ul>
          </template>
          <template #s2>
            <a href="">更多</a>
          </template>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <slot name="s1"></slot>
            <slot name="s2"></slot>
          </div>
        </template>

3. 作用域插槽

  1. 理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定)

  2. 具体编码:

    父组件中:
          <Game v-slot="params">
          <!-- <Game v-slot:default="params"> -->
          <!-- <Game #default="params"> -->
            <ul>
              <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
            </ul>
          </Game>
    
    子组件中:
          <template>
            <div class="category">
              <h2>今日游戏榜单</h2>
              <slot :games="games" a="哈哈"></slot>
            </div>
          </template>
    
          <script setup lang="ts" name="Category">
            import {reactive} from 'vue'
            let games = reactive([
              {id:'asgdytsa01',name:'英雄联盟'},
              {id:'asgdytsa02',name:'王者荣耀'},
              {id:'asgdytsa03',name:'红色警戒'},
              {id:'asgdytsa04',name:'斗罗大陆'}
            ])
          </script>
    

7. 其它 API

7.1.【shallowRef 与 shallowReactive 】

shallowRef

  1. 作用:创建一个响应式数据,但只对顶层属性进行响应式处理。

  2. 用法:

    let myVar = shallowRef(initialValue);
    
  3. 特点:只跟踪引用值的变化,不关心值内部的属性变化。

shallowReactive

  1. 作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的

  2. 用法:

    const myObj = shallowReactive({ ... });
    
  3. 特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。

总结

通过使用 shallowRef()shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。

7.2.【readonly 与 shallowReadonly】

readonly

  1. 作用:用于创建一个对象的深只读副本。

  2. 用法:

    const original = reactive({ ... });
    const readOnlyCopy = readonly(original);
    
  3. 特点:

    • 对象的所有嵌套属性都将变为只读。
    • 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
  4. 应用场景:

    • 创建不可变的状态快照。
    • 保护全局状态或配置不被修改。

shallowReadonly

  1. 作用:与 readonly 类似,但只作用于对象的顶层属性。

  2. 用法:

    const original = reactive({ ... });
    const shallowReadOnlyCopy = shallowReadonly(original);
    
  3. 特点:

    • 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。

    • 适用于只需保护对象顶层属性的场景。

7.3.【toRaw 与 markRaw】

toRaw

  1. 作用:用于获取一个响应式对象的原始对象, toRaw 返回的对象不再是响应式的,不会触发视图更新。

    官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

    何时使用? —— 在需要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象

  2. 具体编码:

    import { reactive,toRaw,markRaw,isReactive } from "vue";
    
    /* toRaw */
    // 响应式对象
    let person = reactive({name:'tony',age:18})
    // 原始对象
    let rawPerson = toRaw(person)
    
    
    /* markRaw */
    let citysd = markRaw([
      {id:'asdda01',name:'北京'},
      {id:'asdda02',name:'上海'},
      {id:'asdda03',name:'天津'},
      {id:'asdda04',name:'重庆'}
    ])
    // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
    let citys2 = reactive(citys)
    console.log(isReactive(person))
    console.log(isReactive(rawPerson))
    console.log(isReactive(citys))
    console.log(isReactive(citys2))
    

markRaw

  1. 作用:标记一个对象,使其永远不会变成响应式的。

    例如使用mockjs时,为了防止误把mockjs变为响应式对象,可以使用 markRaw 去标记mockjs

  2. 编码:

    /* markRaw */
    let citys = markRaw([
      {id:'asdda01',name:'北京'},
      {id:'asdda02',name:'上海'},
      {id:'asdda03',name:'天津'},
      {id:'asdda04',name:'重庆'}
    ])
    // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
    let citys2 = reactive(citys)
    

7.4.【customRef】

作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制。

实现防抖效果(useSumRef.ts):

import {customRef } from "vue";

export default function(initValue:string,delay:number){
  let msg = customRef((track,trigger)=>{
    let timer:number
    return {
      get(){
        track() // 告诉Vue数据msg很重要,要对msg持续关注,一旦变化就更新
        return initValue
      },
      set(value){
        clearTimeout(timer)
        timer = setTimeout(() => {
          initValue = value
          trigger() //通知Vue数据msg变化了
        }, delay);
      }
    }
  }) 
  return {msg}
}

组件中使用:

8. Vue3新组件

8.1. 【Teleport】

  • 什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
<teleport to='body' >
    <div class="modal" v-show="isShow">
      <h2>我是一个弹窗</h2>
      <p>我是弹窗中的一些内容</p>
      <button @click="isShow = false">关闭弹窗</button>
    </div>
</teleport>

8.2. 【Suspense】

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
    • 使用Suspense包裹组件,并配置好defaultfallback
import { defineAsyncComponent,Suspense } from "vue";
const Child = defineAsyncComponent(()=>import('./Child.vue'))
<template>
    <div class="app">
        <h3>我是App组件</h3>
        <Suspense>
          <template v-slot:default>
            <Child/>
          </template>
          <template v-slot:fallback>
            <h3>加载中.......</h3>
          </template>
        </Suspense>
    </div>
</template>

8.3.【全局API转移到应用对象】

  • app.component
  • app.config
  • app.directive
  • app.mount
  • app.unmount
  • app.use

8.4.【其他】

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from

  • keyCode 作为 v-on 修饰符的支持。

  • v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync。

  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化。

  • 移除了$on$off$once 实例方法。

  • 移除了过滤器 filter

  • 移除了$children 实例 propert

    ......

Vue中使用的包

Rust

教程

安装

Rust 编程语言

Windows上安装

下载安装文件进行安装:rustup-init.exe

Linux Mac上安装

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

更新和卸载

通过 安装 Rust 后rustup,更新到新发布的版本就很容易了。从 shell 运行以下更新脚本:

rustup update

要卸载 Rust 和rustup,请从 shell 运行以下卸载脚本:

rustup self uninstall

Cargo

Cargo Book

常用命令

注意命令全是小写,错误示范:Cargo run hello❌

功能/命令 说明
创建项目

cargo new hello_cargo
创建项目
构建项目

cargo build --release
此命令在`target/debug/hello_cargo(或 Windows 上的target\debug\hello_cargo.exe )`中创建一个可执行文件,而不是在当前目录中。由于默认构建是调试构建,因此 Cargo 将二进制文件放在名为debug的目录中。
构建发布

cargo build
当您的项目最终准备好发布时,您可以使用cargo build --release优化来编译它。此命令将在target/release而不是target/debug中创建可执行文件。
运行项目

cargo run
编译和运行一键执行
检查项目

cargo check
此命令可以快速检查您的代码,以确保它可以编译但不会产生可执行文件
测试

cargo test
测试

项目结构文件夹(目标表)

examples 目录

官网 exampels 目录

  • 在项目根目创建 examples
  • 在此文件中只能包含 rs 脚本文件,且包含 main 函数
  • 示例:examples/hello.rs, 运行示例 cargo run --example hello
  • 可以指定crate-type字段以将示例编译为库(没研究):

[[example]] name = "foo" crate-type = ["staticlib"]

注:这里仅能实现一些简单的脚本功能,如果要编写一些完整的项目示例,使用 工作区

tests 目录

集成测试

没研究

benches 目录

集成测试

基准测试

工作区

通过配置工作区来实现,官方文档

详细功能见官网,此处介绍使用工作区配置子项目,使用场景为:在学习rust的适合,使用的一个仓库,但是中途很多练习项目,此时就可以通过工作区来配置。

首先创建父级项目:

cargo new rust_examples

然后在 rust_examples 下创建 hello 等其他一系列子项目,

切换到 rust_examples

cd rust_examples

创建hello

cargo new hello

项目目录结构如下:

1721616093714

配置方式:

首先在 rust_examples 项目的 Cargo.toml 文件中配置 workspace 工作区成员,

[package]
name = "rust_examples"
version = "0.1.0"
edition = "2021"

[workspace]
members = [
    "hello","projects/hello1",,"projects/*"
]
[dependencies]
  • hello: 代表在根目(Cargo.toam 所在目录)下
  • projects/hello1: 在 projects 下的hello1
  • projects/* : 在 projects 下的所有

运行hello项目:

cargo run --package hello

其他命令如 build 均可以用,但是构建的文件在 rust_examples 的 target文件夹下。

问题汇总

Cargo config deprecated

当构建或者运行时,显示警告:

warning: `C:\Users\ABYTE\.cargo\config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`

构建的GUI程序,运行时会出现控制台窗口

当使用 iced 来构建GUI程序,启动时出现控制台窗口,可以在main.rs文件的顶部添加以下代码来去除该控制台窗口:

#![windows_subsystem = "windows"]

国内源

子节跳动镜像

.cargo 迁移到D盘

C 盘瘦身

.cargo 包含了我们下载的很多库文件,后期会越来越大。

在 Windows 系统上,将 .cargo 文件夹从默认位置(例如 C:\Users\ABYTE.cargo)移动到 D 盘后,需要调整环境变量以让 Rust 工具链(如 rustup)能够找到新的路径。请按照以下步骤操作:

移动 .cargo 文件夹:将 .cargo 文件夹从 C:\Users\ABYTE\ 移动到 D 盘,例如 D:.cargo。

设置环境变量:将新的 .cargo 路径添加到环境变量中。

打开“系统属性”窗口:右键点击“此电脑” → “属性” → “高级系统设置” → “环境变量”。 在“用户变量”或“系统变量”中找到 CARGO_HOME。如果没有此变量,点击“新建”添加一个,名称为 CARGO_HOME,值为 D:.cargo。 如果已经存在 CARGO_HOME,将其路径修改为 D:.cargo。 检查并更新路径:在同一窗口下检查 Path 变量,确保 D:.cargo\bin 被添加到了 Path 中。如果没有,则添加一个新条目 D:.cargo\bin。

验证配置:打开新的命令提示符(cmd)或 PowerShell,运行以下命令确认 rustup 已识别新的路径:


rustup --version
cargo --version

如果一切配置正确,这些命令应能够正常工作。

相关文章

Tauri

UI 框架

调试

前端调试

第一种方式:通过 Ctr+shift+I 来打开 DevTools 调试工具,和浏览器的 F12 调试浏览器相同

第二种方式:前端的调试可以通过点击链接在浏览器上进行渲染,按下 F12 来调试。

后端调试

根据 Tauri 官网文档进行配置

Vue

  • 路由

Element-plus

  • 国际化

异常汇总

执行后不出现窗口

可能的原因是还没编译完,编译完会出现 Finished

> rttve@0.1.0 tauri
> tauri dev

    Running BeforeDevCommand (`npm run dev`)

> rttve@0.1.0 dev
> vite


  VITE v5.4.8  ready in 589 ms

  ➜  Local:   http://localhost:1420/
    Info Watching F:\000-Labyte\Rust\tauri\rttve\src-tauri for changes...
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 36s

failed to read plugin permissions: failed to read file: 系统找不到指定

执行 npm run tauri dev 后 提示如下错误:

failed to read plugin permissions: failed to read file: 系统找不到指定

删掉 target 文件夹,重新运行

相关开源项目

Slint

UI 框架

Slint 官网首页

.slint 和 rust 分离

国内B站UP主代码,比较详细

快速开始

手动创建项目

创建项目:

cargo new slint_component

Cargo.toml 中添加依赖 slint slint-build,两者的版本号相同

[package]
name = "slint_component"
version = "0.1.0"
edition = "2021"

[dependencies]
slint = "1.7.0"

[build-dependencies]
slint-build = "1.7.0"

创建组件文件夹 ui(或者命名为 component),用于放置 slint文件,在文件中创建 Window.slint 文件,

import { AboutSlint, Button, VerticalBox } from "std-widgets.slint";
component Demo {

    VerticalBox {
        alignment: center;
    

        Text {
            text: "Hello World!";
            font-size: 24px;
            horizontal-alignment: center;
        }
        AboutSlint {
            preferred-height: 150px;
        }
        HorizontalLayout { alignment: LayoutAlignment.center; Button { text: "O1K!"; } }
    }
}

export component MainWindow inherits Window {
    width: 800px;
    height: 600px;
    title: "slint 测试";
    Demo{
    }
}

在根目(与src同层级)创建 build.rs 文件:

fn main(){
    slint_build::compile("src/ui/Window.slint").unwrap();
}

在 src/main.rs 中添加如下代码:

⚠️slint::include_modules!(); 提示错误 failed to load file UNRESOLVED_ENV_VAR,但是不影响运行

slint::include_modules!();
fn main() {
    MainWindow::new().unwrap().run().unwrap();
}

使用 Slint Rust 模板

类似于WPF中的 prim 框架模板一样,自动创建项目文件结构

首先确保安装了 cargo-generate

cargo install cargo-generate

使用模板创建

cargo generate --git https://github.com/slint-ui/slint-rust-template

稍等下,提示输入项目名称:

PS F:\000-SHING\Rust\rust_examples> cargo generate --git https://github.com/slint-ui/slint-rust-template
 Project Name: slint-02
 project-name: slint-02 ...
 Generating template ...
 Moving generated files into: `F:\000-SHING\Rust\rust_examples\slint-02`...
 Initializing a fresh Git repository
 Done! New project created F:\000-SHING\Rust\rust_examples\slint-02
PS F:\000-SHING\Rust\rust_examples> cargo run --package slint-02

创建后文档结构:

1721877459941

✅ 使用模板方式创建后: slint::include_modules!(); 提示错误 failed to load file UNRESOLVED_ENV_VAR 问题修复了,前面手动创建的项目的错误也消除了。

Iced

UI 框架

api不稳定,一天一个样,不适合。

ECS

Specs

官方手册

B站视频

  • 纯ECS框架
  • 轻量
  • 入门应该较容易

Bevy_ecs

官网

国外博主编写的使用指南

  • 星数最多
  • 包含了ECS和GUI部分
  • 入门较难

mdBook

注意:国内有很多个人翻译的中文版本,时间比较久远,还是看官网的英文版本或者翻译为中文来查看。

常用命令

执行构建:即编译生产网站静态文件,不支持实时更新,--open 可选:自动打开浏览器

mdbook build --open

运行服务:即当在编辑器中改动后,浏览器实时更新,--open 可选:自动打开浏览器

mdbook serve --open

渲染器

章节折叠渲染器配置

在book.toml中配置

[output.html.fold]
enable = true # 启用
level = 0  # 0:打开网站时,全部关闭,1:所有第一层级的展开,2:第二层级的展开...

预处理

主题

设置界面字体

  • theme/css/general.css 中的 :rootBody 可整体调整左侧章节目录和整文的字体大小

  • theme/css/chrome.css 中的 .sidebar 可整体调整左侧章节目录字体大小,两者结合,可满足相关需求

设置整文行间距

theme/css/general.css:

.content p { line-height: 1.5em; }
.content ol { line-height: 1.5em; }
.content ul { line-height: 1.5em; }

目录插件PageToc

PageToc:比较简单容易配置的目录插件,但是显示感觉怪怪的,在加上目录后,文章内容还是居中,就导致布局不合理

可自定义标题目录,未配置成功,该主题在Pagetoc的基础上添加了自定义功能,可配置文章内容全屏显示,但是配置后运行不起来,即使克隆作者的仓库也运行不起来。

直接使用开源仓库

可以直接克隆一些开源的仓库,作为模板进行编写。

Rust语言圣经

  • 支持章节收缩
  • 支持较好的目录
  • 支持评论
  • 不支持中文搜索,本仓库也使用了它的上述功能,自己实现了中文搜索。

中文支持

网页的工具栏等都是英文显示,在国内使用需要更改为中文。

  1. 设置网页为中文

在国内使用应该使用中文

  • book.toml中设置为中文 language = "zh-CN",这仅仅是说明我们的网页使用的是中文
[book]
authors = ["LIXINGJUN"]
language = "zh-CN"
multilingual = false
src = "src"
title = "成都轨道学院培训楼使用指南"
description = "该文档仅用于成都轨道学院培训楼使用,禁止外传。"
  1. 设置工具栏为中文
  • 工具栏默认提示为英文,如下图中的目录切换:

1716796122067

  • 这需要通过修改主题来实现,在默认创建的书中没有主题的配置,根据官网文档,通过执行命令 mdbook init --theme 来创建默认的主题文件,然后通过修改文件中内容来修改.
  • 通过修改 index.hbs 文件中的对应内容

1716796153384

1716796162203

1716796171171

1716796178418

  1. 支持中文搜索

说明: 默认不支持中文搜索,也就是在搜索框总输入中文,搜索不到结果,具体见社区: Rust社区支持中文说明,总结以下的处理方式:

  1. 安装指定插件
cargo install mdbook-mermaid
cargo install mdbook-plantuml
  1. 本地创建 assets,在社区中指示的仓库下载指定的文件到 assets
  2. 配置 book.toml
[output.html]
mathjax-support = true
additional-css = ["assets/mermaid.css", "assets/print.css", "assets/all-page.css"]
additional-js = ["assets/fzf.umd.js", "assets/elasticlunr.js", "assets/mermaid.min.js", "assets/import-html.js","assets/searcher.js"]
  1. 配置成功案例 https://gitee.com/shtzj/userguide.git 项目设置。

搜索结果描述的修改

如下图显示,我们搜索到内容后提示有多少个结果,默认是英文显示的,要改为中文,这里通过 index.hbs无法修改,因为这个数据是动态显示的,在后端实现。

1716796189226

通过在渲染输出中,通过字符串查找,在 seracher.js 文件中找到了响应的代码位置,简单的操作,就是每次我们只要将对应的地方改为中文即可,但是存在一个问题,每次构建都会覆盖改好的内容。

原代码

    function formatSearchMetric(count, searchterm) {
        if (count == 1) {
            return count + " search result for '" + searchterm + "':";
        } else if (count == 0) {
            return "No search results for '" + searchterm + "'.";
        } else {
            return count + " search results for '" + searchterm + "':";
        }
    }

希望的代码

    function formatSearchMetric(count, searchterm) {
        if (count == 1) {
            return count + " 个搜索结果 '" + searchterm + "':";
        } else if (count == 0) {
            return "无搜索结果 '" + searchterm + "'.";
        } else {
            return count + " 个搜索结果 '" + searchterm + "':";
        }
    }

临时解决方式(不理想)

  1. seracher.js 先改好,拷贝到 assets 文件夹下;
  2. 配置 book.toml 文件,如下图增加 searcher.js 的引用
[output.html]
mathjax-support = true
additional-css = ["assets/mermaid.css", "assets/print.css", "assets/all-page.css"]
additional-js = ["assets/fzf.umd.js", "assets/elasticlunr.js", "assets/mermaid.min.js", "assets/import-html.js","assets/searcher.js"]
  1. 每次构建后,会在输出目录的assts下,创建 searcher.js文件,但是同时根目也有相同的文件,需要删除根目下的 searcher.js 文件,否则导致点击 搜索按钮无效。

章节跳转

本页跳转

[跳转到章节](#chapter-1)
 
...
 
<a name="chapter-1"></a>
# 章节1
这里是章节1的内容。

跨页跳转

假设 a.mdb.md 在同一个目录下

a内容:

<a name="chapter-1"></a>
# 章节1
这里是章节1的内容。

<a name="chapter-2"></a>
# 章节2
这里是章节2的内容。

b 中跳转到 a 的章节2

[跳转到 a 的章节2](a.md#chapter-2)

在一个全新的电脑上搭建Mdbook环境

若要运行此仓库为例

安装

  1. 下载rust
  2. 安装 mdbook, 命令 cargo install mdbook
  3. 安装预处理器: 命令 cargo install mdbook-mermaid
  4. 安装预处理器: 命令 cargo install mdbook-plantuml

去除评论

  • \theme\index.hbs 中删除 218 行 <div id="giscus-container"></div>
  • custom3.js 中删除 82行到157行

注意事项

  • 章节的节点不要关联两个相同的文档,特别是两个相邻的,会导致点击下一章节无法跳转到下一章节(因为下一章节的链接和当前一样)

异常汇总

(1)部署github章节显示空白

  1. 在本地测试所有内容正常,部署到Github上后, csharp 文件夹下的所有章节都显示空白,将csharp改为 c-sharp 后显示正常。

Ptyhon

模块

一个.py文件就是一个模块,模块的作用是为了封装规范代码,创建模块就是创建一个.py文件,导入模块:import 模块名; from 模块名 import member

导入模块时,程序搜索模块的顺序为:

  1. 在当前目录(即执行Python脚本文件所在的目录)下查找

  2. 到PYTHONPATH(环境变量)下的每个目录中查找

  3. 到Python的默认安装目录查找

以上目录的具体位置保存在标准模块sys.path变量中,可以通过以下代码输出查看

import sys
print(sys.path)

如果要导入的模块不在以上目录中,会报错:ModuleNotFoundError: No module named 'xxxx'

模块的目录结构

1719555305558

SizeModule.py 中的代码

width = 100
height = 200

SizeModule.py 同目录的 test.py 调用方法(直接导入即可)

import SizeModule
print(SizeModule.width)
print(SizeModule.height)

如果我们要在外层 Test.py 中导入 TestPackge 文件夹下的 SizeModule.py 模块,需要先将包的路径加入到 sys.path 中。

第一种方式:根据当前路径将包路径加入到sys.path

import sys
import os

# 输出当前文件的绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
# 将需要导入模块代码文件相对于当前文件目录的绝对路径加入到sys.path中
sys.path.append(os.path.join(current_dir, ".."))

from TestPackge import SizeModule
print(SizeModule.width)

第二种方式:将工作目录添加到sys.path中,然后根据层级关系导入

import sys
import os
sys.path.append(os.getcwd()) # 工作目录,项目根目录
import 模块和包.TestPackge.SizeModule as size
print(size.width)

创建一个包就是创建一个文件夹,文件夹下创建 __init__.py 文件,里面可以为空,如果写了逻辑,导入包时会执行,在介绍模块的目录结构时,实际上就是一个包的目录结构,导入包中的模块(举例一种方式)

from TestPackge import SizeModule # TestPackge: 包, SizeMoudle:模块
from TestPackge import SizeModule as newName  # TestPackge: 包, SizeMoudle:模块 ,导入为新的名称

SQL Server

不是有效的安装文件夹(未解决)

系统:Windows Server 2022 Datacenter

数据库版本:Sql Server 2022

下载安装的时,默认安装了命名示例,需要增加一个默认示例,通过安装中心-》安装-》向现有安装添加功能,选择安装文件夹后,出现:

1721350378227

IP地址远程连接

SQL Server相关配置

  1. 启动混合模式登录:打开 SQL Server Management Studio (SSMS),连接到 SQL Server 实例,在对象资源管理器中,右键点击服务器名称,选择“属性”。在“安全性”页面上,选择“SQL Server 和 Windows 身份验证模式”

    1721354379160

  2. 配置 SQL Server 允许远程连接:在“连接”页面上,'远程服务器连接’选择“允许远程连接到此服务器”。

    1721354442672

  3. 启动 SQL Server Browser 服务(必须启动):SQL Server Browser 服务用于为 SQL Server 实例提供实例名称解析,确保此服务在 Windows Server 上是启动的,打开 SQL Server Configuration Manager(SQL Server 配置管理器)。

    1721355014528

    如果此处无法启动,先到 服务 窗口中启动对应的服务,然后再启动

    1721355285786

  4. 配置网络和 TCP/IP,打开 SQL Server Configuration Manager,在左侧导航窗格中,展开 SQL Server 网络配置,再次点击对应的协议展开,如“MSSQLSERVER 的协议”。在“协议”页面上,确保“TCP/IP”是启用的。

    1721355721120

  5. 设置端口(默认1433,应该可忽略):右键点击“TCP/IP”,选择“属性”。在“IP 地址”选项卡上,为 IPAll 设置 TCP 端口为 1433(或者设置为 SQL Server 配置的其他端口)。注意,使用了特定的 IP 地址,确保在 TCP 动态端口和 TCP 静态端口中没有设置冲突(默认的一般都没有冲突)。

    1721355860096

  6. 重启服务

    1721355777952

设置防火墙(阿里云服务器忽略)

基于 `1433` 端口,添加入站出站规则(具体操作略)。

注:阿里云的防火墙是禁用的,通过安全组来配置。

阿里云添加安全组

在 云服务器 ECS -> 安全组 上,添加入方向和出方向的访问规则。

入方向规则(出方向相同):

1721356321240

参考

CSDN:阿里云 WindowsServer 使用之 配置 SQL Server 允许远程连接

MySql

Redis

MongoDb

Windows

重启

在 Windows 上安装 MongoDB 后,你可以按照以下步骤重启 MongoDB 服务:

使用命令提示符(适用于服务安装)

打开命令提示符:以管理员身份运行命令提示符。

停止 MongoDB 服务:

net stop MongoDB

启动 MongoDB 服务:

net start MongoDB

重启 MongoDB 服务(可以直接使用此命令代替上述两个命令, 有时候会报错无法识别 &&):

net stop MongoDB && net start MongoDB

使用 Windows 服务管理器

打开服务管理器:

按 Win + R,输入 services.msc,然后按回车。 找到 MongoDB 服务:

在服务列表中找到 MongoDB 或者 MongoDB Server。 重启服务:

右键点击 MongoDB 服务,选择 重启。或者选择 停止,然后再点击 启动。

手动启动和停止(适用于手动启动的 MongoDB)

停止 MongoDB:

打开命令提示符,执行以下命令找到并终止 MongoDB 进程:

taskkill /f /im mongod.exe

启动 MongoDB:

通过命令提示符进入 MongoDB 安装目录,然后执行以下命令:

mongod --config "C:\Program Files\MongoDB\Server\5.0\bin\mongod.cfg"

配置文件路径根据你的安装路径调整。

通过这些步骤,你可以在 Windows 上重启 MongoDB 服务。

远程连接

失败信息:mongodb远程连接出现connect ECONNREFUSED(连接被拒绝)错误的解决方法

阿里云ECS(Windows Server)

在 Windows 上安装,Mongodb 的配置文件中的 bind_ip 默认为 127.0.0.1,默认只有本机 IP 可以连接,需要将 bind_ip 配置为:0.0.0.0,标识接收任何 IP 的连接。默认只绑定了本机IP

配置文件地址: C:\Program Files\MongoDB\Server\7.0\bin\mongod.cfg

修改为如下图

# network interfaces
net:
  port: 27017
  bindIp: 0.0.0.0

先停止然后启动

net stop MongoDBnet start MongoDB

Windows账户A访问Windows账户B安装的数据库

使用 windwos 账户A 安装 MongoDB,切换到 windows 账户B 通过localhost:27017 来访问可能失败

这通常与文件和服务的权限设置有关。以下是详细的解决方法:

  1. 为账户B授予MongoDB安装目录的访问权限
    • 打开 MongoDB 安装目录(通常是 C:\Program Files\MongoDB\Server<version>\)。
    • 右键点击安装目录,选择“属性”。
    • 转到“安全”选项卡,点击“编辑”按钮。
    • 点击“添加”按钮,输入账户B的用户名,然后点击“检查名称”确认。
    • 为账户B授予“读取和运行”、“列出文件夹内容”、“读取”和其他必要的权限。
    • 点击“应用”并保存更改。
  2. 配置MongoDB服务的权限(重启即可)
    • 打开“服务”窗口(按 Win + R,输入 services.msc,然后按 Enter)。
    • 找到 MongoDB 服务,右键点击选择“属性”。
    • 转到“登录”选项卡。
    • 选择“此账户”,然后输入账户A的用户名和密码。
    • 点击“应用”并重启MongoDB服务。

备注:MongoDBCompass 的安装是在账户目录下:C:\Users\XXX\AppData\Local\MongoDBCompass

MacOS

重启

macOS 上安装 MongoDB 后,使用以下命令来重启 MongoDB 服务:

  1. 停止 MongoDB 服务:
brew services stop mongodb
  1. 启动 MongoDB 服务:
brew services stop mongodb
  1. 重启 MongoDB 服务:
brew services stop mongodb

如果你是手动启动的 MongoDB(而不是通过 brew 服务管理),你可以使用以下命令:

  1. 停止 MongoDB:
sudo pkill -f mongod
  1. 启动 MongoDB: 进入 MongoDB 的安装目录,然后执行:
mongod --config /usr/local/etc/mongod.conf

这个命令假设你在安装时使用了默认的配置文件路径 /usr/local/etc/mongod.conf。如果你的配置文件位于其他路径,请替换为实际的路径。

通过这些步骤,你可以成功地在 macOS 上重启 MongoDB 服务。

安全性

注意: 当开放外网访问时,数据库默认没有启动访问控制,也就是只需要IP和端口就可以访问数据库,容易被攻击!!!

仅在内网访问

正常运营环境,应该放在内网访问

大多数情况下数据库应该仅由服务器访问,它们应该在同一个内网中

mongodb://localhost:27017/

外网访问

首先必须立刻,开启访问控制,否则别人使用IP和端口就能访问你的数据库,否则设置的账号密码不起作用

  1. 修改 C:\Program Files\MongoDB\Server\7.0\bin\mongod.cfg 配置文件中的
    1. net : 自定义端口,绑定所有的IP
    2. security: 启用访问控制,访问需要账号密码,且根据账户的权限才能执行相应的操作
# mongod.conf

net:
  #port: 27017
  port: 39363
  #bindIp: 127.0.0.1
  bindIp: 0.0.0.0


security:
    authorization: enabled

  1. 启用访问控制后,就不能使用简单的 mongodb://localhost:port 进行访问了
  2. 具有授权信息的访问:mongodb://admin:pwd@ip地址:端口/?authMechanism=SCRAM-SHA-1
  3. 启用访问控制后,备份和恢复数据库需要具有对应角色的账户才能操作,具体参照添加可以备份恢复的用户

用户管理

用户管理需要通过 Mongosh 操作,Mongosh 是嵌入到 MongoDB Compass 中的一个Javascript 运行环境,官方文档没怎么看懂

官方 用户管理方式

用户角色

内置角色

  • 创建用户时需要给用户指定角色
  • 一个用户可以有多个角色,不同的角色具有不同的权限,并且会享受角色的最高权限

常用角色

分类 角色 权限 使用频率
超级用户角色 root 最高权限
数据库用户角色 read 只能读链接3
readWrite 可读可写,服务器使用的数据库账户
数据库管理角色 dbAdmin 提供执行管理任务的能力,例如架构相关任务、索引和收集统计信息。该角色不授予用户和角色管理特权. 一般
dbOwner 数据库所有者可以对数据库执行任何管理操作。此角色结合了 readWrite、dbAdmin 和 userAdmin 角色授予的权限。
userAdmin 提供在当前数据库上创建和修改角色和用户的能力。由于 userAdmin 角色允许用户向任何用户(包括自己)授予任何特权,因此该角色还间接提供对数据库或(如果作用域为 admin 数据库)集群的超级用户访问权限。
备份和恢复角色 backup 备份 一般
restore 恢复

添加Root用户

添加一个超级管理员账户,此账户可以对其他管理员进行操作(注意,若要启动访问控制,在启动之前需要先添加一个超级管理员,以方便管理数据库和用户)


//root权限的
use admin
db.createUser(
  {
    user: "root",
    pwd:  "123456",
    roles: [ { role: "root", db: "admin" } ]
  }
)

添加readWrite用户

此用户提供给服务器连接数据库


use admin

db.createUser({
    user:"admin",
    pwd:"tzj001",
    roles:[
    {role:"readWrite",db:"TSIM"}]
})

添加可以备份恢复的用户

在启用访问控制的情况下,此用户可实现对数据库进行备份和恢复


use admin
db.createUser({
  user: "br",
  pwd: "br123",
  roles: [
    { role: "backup", db: "admin" },
    { role: "restore", db: "admin" }
  ]
})

备份命令

mongodump --uri="mongodb://br:br123@IP地址:端口" --out=一个文件夹

恢复命令

mongorestore --uri="mongodb://br:br123@localhost:端口/数据库名称?authMechanism=SCRAM-SHA-1&authSource=admin" --nsInclude="数据库名称.*" D:\TSIM\mongodb_backup\backup\指定还原的文件路径\TSIM
  • 就按照这个格式,替换用户、密码、IP、端口、要恢复的数据库名称、恢复的文件夹路径,否则出现些奇奇怪怪的问题,如skip。。。。
  • --nsInclude="数据库名称.*" 表示数据库下的所有集合,若只还原某个集合如(Users)那么写 --nsInclude="数据库名称.Users",多个时:--nsInclude="数据库名称.Users",--nsInclude="数据库名称.集合2"

其他操作

删除 admin 用户

db.drop("admin")

查看当前用户

db.getUsers()

或者

db.system.users.find()

或者

show users

连接字符串

当在Web程序中配置MongoDB的连接字符串,有多种方式,简单的记录两种

第一种: 连接本机上的 MongoDB ,适用于服务程序和数据库在同一个主机

"ConnectionString": "mongodb://localhost:27017/数据库名称?authSource=admin",

第二种: 连接非本机上的 MongoDB ,使用密码连接

"ConnectionString": "mongodb://userName:password@domain:27017/数据库名称?authSource=admin",

authSource=admin 说明:认证源属于管理员级别

过滤器

当使用 MongoDB C# 驱动程序进行数据查询和操作时,可以通过 Builders <T>.Filter 构造过滤器(Filter),它们用于定义查询条件或者更新/删除操作的条件。以下是一些常用的过滤器方法及其用法解释:

Eq 方法

Eq 方法用于创建一个等于(equal)条件的过滤器。它适用于需要匹配指定字段与特定值相等的情况。

var filter = Builders<BsonDocument>.Filter.Eq("fieldName", value);

例如,如果要查询字段 "name" 等于 "Alice" 的文档,可以使用:

var filter = Builders<BsonDocument>.Filter.Eq("name", "Alice");

In 方法

In 方法用于创建一个包含于(in)条件的过滤器。它用于匹配指定字段的值在一个给定的列表或集合中的情况。

var filter = Builders<BsonDocument>.Filter.In("fieldName", values);

例如,如果要查询字段 "department" 的值在 ["IT", "HR", "Finance"] 中的文档,可以使用:

var filter = Builders<BsonDocument>.Filter.In("department", new List<string> { "IT", "HR", "Finance" });

And 方法

And 方法用于创建一个与(and)条件的过滤器,即多个条件必须同时满足。

var filter = Builders<BsonDocument>.Filter.And(
    Builders<BsonDocument>.Filter.Eq("field1", value1),
    Builders<BsonDocument>.Filter.Eq("field2", value2)
);

例如,如果要查询同时满足 "name" 等于 "Alice" 和 "age" 大于等于 30 的文档,可以使用:

var filter = Builders<BsonDocument>.Filter.And(
    Builders<BsonDocument>.Filter.Eq("name", "Alice"),
    Builders<BsonDocument>.Filter.Gte("age", 30)
);

Gte, Gt, Lte, Lt 等方法

这些方法用于创建比较条件的过滤器:

  • Gte(greater than or equal):大于等于
  • Gt(greater than):大于
  • Lte(less than or equal):小于等于
  • Lt(less than):小于
var filter = Builders<BsonDocument>.Filter.Gte("fieldName", value);

例如,如果要查询 "score" 大于等于 80 的文档,可以使用:

var filter = Builders<BsonDocument>.Filter.Gte("score", 80);

组合多种过滤器

你可以通过组合多个过滤器来构建复杂的查询条件。例如,如果要查询 "department" 是 "IT" 或者 "HR" 且 "age" 大于等于 25 的文档,可以这样组合:

var filter = Builders<BsonDocument>.Filter.And(
    Builders<BsonDocument>.Filter.In("department", new List<string> { "IT", "HR" }),
);

AnyIn 方法

在 MongoDB 的 C# 驱动程序中,AnyIn 方法用于创建一个过滤器,用于检查一个数组字段中是否至少包含一个指定的值。这在需要查询数组字段中是否存在特定元素的情况下非常有用。

用法示例

假设你有一个文档结构如下:

{
  "_id": ObjectId("60a9e54d95b06b84cf6e832f"),
  "name": "Alice",
  "interests": ["Reading", "Music", "Sports"]
}

其中,interests 是一个数组字段,包含了 Alice 的兴趣爱好。

如果你想要查询具有特定兴趣爱好的文档,例如查询兴趣包括 "Music" 或 "Sports" 的文档,可以使用 AnyIn 方法来构建过滤器:

var filter = Builders<BsonDocument>.Filter.AnyIn("interests", new BsonArray { "Music", "Sports" });

解释

Builders <BsonDocument>.Filter.AnyIn 方法用于创建一个数组字段的过滤器,该方法接受两个参数:

第一个参数是字段名 "interests",这是你要进行过滤的数组字段。

第二个参数是一个 BsonArray,其中包含了要匹配的值的列表。在上面的例子中,我们使用了 new BsonArray { "Music", "Sports" } 来表示我们要查询包含 "Music" 或 "Sports" 的兴趣爱好的文档。

注意事项

AnyIn 方法是针对数组字段进行查询的一种特定方式。它只匹配数组中至少包含一个指定值的文档。如果数组字段中同时包含了多个指定值,文档也会被匹配。 在实际应用中,可以根据具体的业务需求和数据结构来灵活使用 AnyIn 方法,从而实现复杂的查询操作。 以上是关于 AnyIn 方法在 MongoDB C# 驱动程序中的基本用法和示例解释。

总结

以上是一些常见的 MongoDB C# 驱动程序中过滤器方法的用法。它们可以帮助你构建灵活且强大的查询条件,以满足各种数据查询、更新和删除的需求。在实际应用中,根据具体的业务需求和数据结构,你可以灵活地组合这些方法来实现复杂的数据操作。

事务

官方文档

// For a replica set, include the replica set name and a seedlist of the members in the URI string; e.g.
// string uri = "mongodb://mongodb0.example.com:27017,mongodb1.example.com:27017/?replicaSet=myRepl";
// For a sharded cluster, connect to the mongos instances; e.g.
// string uri = "mongodb://mongos0.example.com:27017,mongos1.example.com:27017/";
var client = new MongoClient(connectionString);

// Prereq: Create collections.
var database1 = client.GetDatabase("mydb1");
var collection1 = database1.GetCollection<BsonDocument>("foo").WithWriteConcern(WriteConcern.WMajority);
collection1.InsertOne(new BsonDocument("abc", 0));

var database2 = client.GetDatabase("mydb2");
var collection2 = database2.GetCollection<BsonDocument>("bar").WithWriteConcern(WriteConcern.WMajority);
collection2.InsertOne(new BsonDocument("xyz", 0));

// Step 1: Start a client session.
using (var session = client.StartSession())
{
    // Step 2: Optional. Define options to use for the transaction.
    var transactionOptions = new TransactionOptions(
        writeConcern: WriteConcern.WMajority);

    // Step 3: Define the sequence of operations to perform inside the transactions
    var cancellationToken = CancellationToken.None; // normally a real token would be used
    result = session.WithTransaction(
        (s, ct) =>
        {
            collection1.InsertOne(s, new BsonDocument("abc", 1), cancellationToken: ct);
            collection2.InsertOne(s, new BsonDocument("xyz", 999), cancellationToken: ct);
            return "Inserted into collections in different databases";
        },
        transactionOptions,
        cancellationToken);
}

Bson序列化特性

BsonIgnoreExtraElements

类特性描述:反序列化时用来忽略多余的字段,一般版本兼容需要考虑,低版本的协议需要能够反序列化高版本的内容,否则新版本删除字段,旧版本结构反序列化会出错

使用情况

  1. 新版本中将某个属性或字段删除
  2. 新版本中给某个属性或字段添加 BsonIgnore 特性

注意,若在基类中定义的属性,也需要在子类中添加此特性才有效!

时区

原因1: MongoDB自带的Date是UTC的时间,中国是东八区,所以差了8个小时。

解决方法: 在mongodb可视化工具Robomongo中,我们可以通过"Options" - “Display Dates in…” - "Local Timezone"来设置显示本地时间。

原因2: MongoDB中存储的时间是标准时间 UTC +0:00

解决方法: C#的驱动支持一个特性,将实体的时间属性上添加上这个特性并指时区就可以了。

例如:

[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime CreateTime{get;set;}

数据库迁移

Windows 环境 工具:Cmd、PowerShell

将 MongoDB 数据库迁移到另一台服务器或集群通常涉及以下几个步骤:

  1. 安装 Mongodb 数据库工具
  2. 备份现有数据库
  3. 将备份文件传输到目标服务器
  4. 在目标服务器上恢复备份

具体步骤如下:

安装 MongoDB 数据库工具

注意:我们安装 Mongodb 数据库时,并没有附带安装 mongodb 的命令行工具,需要另外下载

1. 下载工具

2. 安装工具

下载完成后,解压缩安装包并将其内容放置在你希望安装的目录中,例如 C:\mongodb-database-tools。

3. 配置环境变量

为了在命令提示符中使用 mongodumpmongorestore 命令,需要将 MongoDB 数据库工具的安装路径添加到系统的 PATH 环境变量中:

  • 右键单击“此电脑”或“计算机”图标,然后选择“属性”。
  • 在左侧菜单中选择“高级系统设置”。
  • 在“系统属性”窗口中,点击“环境变量”按钮。
  • 在“系统变量”部分找到并选择 Path 变量,然后点击“编辑”。
  • 在“编辑环境变量”窗口中,点击“新建”,并输入 MongoDB 数据库工具的安装路径,例如 C:\mongodb-database-tools\bin
  • 点击“确定”保存更改。

4. 重启命令提示符:

关闭所有打开的命令提示符窗口,然后重新打开一个新的命令提示符窗口,以确保更改生效。

5. 验证安装

打开新的命令提示符窗口,输入以下命令以验证 mongodump 是否可用:

mongodump --version

如果命令输出了版本信息,则说明安装和配置成功,现在,可以使用 mongodump 和 mongorestore 命令来备份和恢复 MongoDB 数据库。

备份现有数据库

你可以使用 mongodump 工具来创建数据库的备份。以下是命令示例:

mongodump --uri="mongodb://username:password@source_host:port/dbname" --out=/path/to/backup

这个命令会把 dbname 数据库的所有数据导出到指定路径 /path/to/backup。

定时备份

☀️最好创建定时任务,定时备份数据,避免数据丢失。

一、创建定时任务脚本,示例:

@echo off

:: 处理时间的小时数为个位数时,出现空格问题。使用0补位
set t=%time:~0,2%%time:~3,2%%time:~6,2%
set t=%t: =0%


:: 定义备份目录(可以根据需要更改)
set backupDir=C:\TZJ\TSIM\mongodb_backup\backup\%date:~0,4%-%date:~5,2%-%date:~8,2%-%t%

:: 创建备份目录
if not exist %backupDir% (
    mkdir %backupDir%
)

:: 备份 MongoDB 数据库(修改为实际的数据库和路径)
mongodump --uri="mongodb://localhost:27017/TSIM" --out=%backupDir%

:: 输出备份完成信息
echo Backup completed at %date% %time%

⚠️ 脚本注意事项

  • 需要处理当小时数小于10时出现空格,需要补位0
  • 脚本中不要出现 pause,否则在计划中,当前实例会一直存在,导致后续无法创建新实例(在设置中默认配置“请勿创建新实例”),即使设置了可以并行运行,那也会无限制的创建新实例,资源浪费。

二、配置 Windows 任务计划程序

通过 Windows 任务计划程序来定时执行上面的备份脚本。

步骤:

  • 打开任务计划程序:按下 Win + S,搜索“任务计划程序”并打开。
  • 创建基本任务:
  • 在任务计划程序窗口中,点击右侧的“创建基本任务”。
  • 为任务命名,例如“MongoDB 定时备份”。
  • 选择触发器,例如每日、每周、每月等。比如选择“每日”。
  • 设置时间,选择希望执行任务的时间。
  • 设置操作:
  • 在“操作”步骤,选择“启动程序”。
  • 浏览并选择上一步中创建的批处理文件 (例如 backup_mongo.bat)。
  • 完成任务:点击完成,任务将自动按照设置的计划执行。

三、验证任务

完成后可以手动运行任务以验证是否正常工作。在任务计划程序中,右键点击任务,选择“运行”,查看备份文件是否生成。

将备份文件传输到目标服务器

你可以使用 scp 命令(对于 Linux)或其他文件传输工具(如 SFTP、FTP 等)将备份文件传输到目标服务器:

Windows

通过远程桌面直接拷贝即可。

Linux

scp -r /path/to/backup user@target_host:/path/to/destination

在目标服务器上恢复备份

在目标服务器上使用 mongorestore 工具来恢复备份的数据:

mongorestore --uri="mongodb://username:password@target_host:port/dbname" /path/to/destination/backup

启用了访问控制处理

参照添加可以备份恢复的用户

异常处理

SASL(Simple Authentication and Security Layer)错误通常与身份验证和安全相关,特别是在连接到 MongoDB 时使用用户名和密码进行身份验证时。为了排查和解决这个问题,请按照以下步骤操作:

1. 检查连接字符串

确保你的连接字符串格式正确,包括用户名、密码、主机和端口,例如:

mongodb://username:password@source_host:port/dbname

2. 使用 URI 编码特殊字符

如果用户名或密码中包含特殊字符(如 @, :, # 等),需要对这些字符进行 URI 编码。你可以使用 URL编码在线工具 来对特殊字符进行编码,常见的有:

  • @ 编码为 %40
  • : 编码为 %3A
  • # 编码为 %23
  • % 编码为 %25

例如,如果密码是 pa@ss:word#123,则连接字符串应为:

mongodb://username:pa%40ss%3Aword%23123@source_host:port/dbname

3. 使用 --authenticationDatabase 选项

如果用户名和密码属于 admin 数据库(或其他非目标数据库),需要指定 --authenticationDatabase 选项。例如:

备份数据库:

mongodump --uri="mongodb://username:password@source_host:27017/dbname" 
--authenticationDatabase="admin" --out="C:\path\to\backup"

恢复数据库:

mongorestore --uri="mongodb://username:password@target_host:27017/dbname" --authenticationDatabase="

4. 启用 SCRAM-SHA-1 或 SCRAM-SHA-256

MongoDB 3.0 及以上版本默认使用 SCRAM-SHA-1 进行身份验证,MongoDB 4.0 及以上版本默认使用 SCRAM-SHA-256。如果你的 MongoDB 服务器配置了不同的身份验证机制,需要确保客户端也支持该机制。

其他注意事项

确保版本兼容性:确保源 MongoDB 服务器和目标 MongoDB 服务器的版本兼容。如果版本差异过大,可能需要进行数据格式的转换。

网络带宽和速度: 考虑到数据传输的时间和速度,特别是对于大数据量的情况下,可能需要更长的时间。

安全性:在传输敏感数据时,请确保使用安全的传输协议和方法。

使用 MongoDB Atlas

如果你使用的是 MongoDB Atlas,MongoDB 官方提供了一些迁移工具,如 mongomirror,可以帮助你将自托管的 MongoDB 数据库迁移到 Atlas 上。这些工具可以实现实时数据同步和迁移。

在线迁移

如果你需要在迁移过程中保持数据库的高可用性,可以考虑使用 MongoDB 的副本集(replica set)来进行无缝迁移。这个方法更为复杂,但可以确保在迁移过程中数据的持续可用。

Synology (群晖)

Putty 连接

登录群晖

  1. 使用SSH,输入地址,端口(22) 打开命令窗口
  2. 输入群晖账户和密码
  3. 输入 sudo -i 切换到root 下,提供权限,此时,再次输入账号的密
  4. 输入: cd /volume1 切换到容器1,这里根据你的容器名称
  5. 然后使用 ls 查看内容列表

运行Python 程序

需要先安装Python,直接在套件中安装即可。

运行python,切换到对应文件夹下,python3 test.py

PowerShell 连接

群晖上启动 SSH 功能,并设置一个可以访问的端口默认22如果不能访问,设置其他端口

1721886156722

使用 PowerShell 登录,输入 ssh username@domain -p port,账号和地址真确,会提示 ECDSA host key for ip address 否则提示: not in list of known hosts ,然后提示输入密码,输入密码不可见,输入成功后显示账号和主机名称。

1721886427739

切换到root账户,可以看到当前是 $ 符号,表示非root账号,输入:sudo -i, 再次输入密码,此时变为 # ,表示 root 账户

1721886444618

切换到根目录 cd ..,将 ~# 变为 /#,并用 ls ,查看目录

1721886458497

/ : 表示根节点

~ : 表示当前登录用户的 home 目录

如果以 root 账号登陆,~ 代表 /root/ 目录,当你任何环境下输入: #cd ~ 会进入到 /root 目录。

Docker 操作

查询镜像

docker 安装在 lib64 里面 ,输入 cd lib64

列出容器docker ps命令手册

1721887061068

获取容器/镜像的元数据:docker inspect,如:查看WIKI这个容器的所有挂载信息:

1721887074140

命令说明:

  • grep: 表示全局正则表达式
  • Mounts: 匹配挂载开始
  • -A : 所有
  • 100 : 返回的行数
  • Source: 宿主的目录,也就是我们看得见的目录
  • Destination: 容器目录

拉取镜像

现在的群晖在 Docker hub 里面显示查询镜像失败,获取不到镜像,使用命令行获取

B站视频

参照命令行章节,连接群晖

sudo -i 获取到超级用户权限

搜索镜像,如: mongo,如数命令:

docker search mongo

搜索结果:

NAME                              DESCRIPTION                      STARS     OFFICIAL   AUTOMATED
mongo                             MongoDB document databases ...   1989      [OK]     
mongo-express                     Web-based MongoDB admin int...   22        [OK]     
mvertes/alpine-mongo              light MongoDB container          19                   [OK]
mongooseim/mongooseim-docker      MongooseIM server the lates...   9                    [OK]
torusware/speedus-mongo           Always updated official Mon...   9                    [OK]
jacksoncage/mongo                 Instant MongoDB sharded cluster  6                    [OK]
mongoclient/mongoclient           Official docker image for M...   4                    [OK]
jadsonlourenco/mongo-rocks        Percona Mongodb with Rocksd...   4                    [OK]
asteris/apache-php-mongo          Apache2.4 + PHP + Mongo + m...   2                    [OK]
19hz/mongo-container              Mongodb replicaset for coreos    1                    [OK]
nitra/mongo                       Mongo3 centos7                   1                    [OK]
ackee/mongo                       MongoDB with fixed Bluemix p...  1                    [OK]
kobotoolbox/mongo                 https://github.com/kobotoolb...  1                    [OK]
valtlfelipe/mongo                 Docker Image based on the la...  1                    [OK]

拉取镜像: docker pull mongo:latest,冒号后面需要跟版本号(latest:表示最新的版本号)

使用中转加速拉取,在群晖里面配置加速:容器套件-》注册表-》设置,添加可用的镜像(注:2024-12-12 测试所有的镜像都无法使用)

本期涉及的一些网站和链接

MySQL

<a name="mysql-install"></a>

安装

下载镜像后启动安装,仅需要设置下端口映射和环境变量,其他保持默认。

1721887472748

操作

浏览器登录 DSM,进入 Mysql 容器,选择终端机,点击新增,选择新增的 bash(不能使用默认的控制台,需要新增一个控制台)

#这里不知道为什么不能输入123,是复制的
mysql -uroot -ptzj123

image-20220307113952416

使用 help 命令查看

Wiki

安装

环境需求说明:2核 1G内存

打开Dcoker,在注册表搜索 wiki,找到 requarks/wiki ,选中后点击下载

1721888262468

下载后在映像如图

1721888271732

配置mysql数据库

Mysql容器的安装参考: MySQL 安装

在数据库里面创建好数据库,如:wiki

配置环境变量

官方手册

启动镜像:在镜像列表中选中 wiki ,点击启动

1721889057429

常规设置:填写容器名称,然后点击 高级设置

1721889072045

高级设置:启用自动重新启动,可勾选

1721889096301

端口配置http:3000https:3443(但是目前无法使用)

1721889115653

配置环境变量,根据官方手册如下

1721889177004

对应配置如下

1721889238900

配置文件启动

使用配置文件启动,需要存储空间进行挂载,让容器加载我们挂载的配置文件。

官网上对挂载配置文件说明,看到容器会读取 /wiki/config.yml配置文件,配置文件模板

1721889365564

群晖上的目录

1721889374780

编辑好配置文件后,按照下图挂载,忽略 content 的挂载

1721889389093

配置文件主要内容:

1721889420293

启动容器,使用 http://你的IP地址或者域名:3000访问

注意还不能使用https访问,这是个问题,后面会讲

HTTPS访问

停止容器

:官网上说可以简单配置 LETSENCRPT 实现 Https,但是没成功,还是使用自定义密钥的方式来做

1721890749526

创建证书,参看 创建证书

挂载证书目录,还是看下图,我们会将证书拷贝到 docker/wiki/data/content中

1721890845501

为什么是这个目录? 首先,通过 docker inspect wiki 查看这个目录是容器本身存在的,并且可以挂载的,我们也可以挂载其他的目录,但是必须是/wiki/...这样的。

!!!不能直接挂载/wiki/data !!!

下载证书,创建后下载下来,解压拷贝到群晖的文件夹下

1721890855919

修改配置文件

1721890867533

启动容器,使用 https://你的ip或者域名:3443 访问

证书

新增证书

打开 控制面板 -> 安全性 -> 证书

1721890353286

点击 新增,打开 创建证书 面板

1721890369541

点击 下一步

1721890416094

选择 创建自我签署证书,点击 下一步

后面的依次填写信息创建即可。

Mysql容器的安装参考: MySQL 安装

Drive

证书过期

选中 Nas 主机,出现 编辑连接 按钮,点击按钮输入密码即可。

1721889904289

反向代理服务器

应用场景:解决容器中的服务只能使用 http 访问问题。

以下描述 通过 https 访问Nas,通过反向代理访问容器中的 http 服务器。

  1. 点击DSM控制面板->登录门户->高级->反向代理服务器
  2. 添加反向代理服务器按钮,弹出窗口,点击新增
  3. 填写名称
  4. 来源协议:选择 https
  5. 来源主机名:*,代表说有来源
  6. 来源端口:填写一个Nas未使用的端口
  7. 目的地协议:http
  8. 目的地主机名:这里填写容器所在的网关,一般为 172.17.0.1

alt text

  1. 目的地端口:容器的端口,注意这里不是容器绑定的本机端口

配置完整截图

alt text

  1. 自定义标题和高级设置默认,点击保存
  2. 控制面板->外部访问->路由器配置中,新增->内置配置,选择反向代理(端口为8007)添加,即可访问

Windows

安装

官网下载

官网 可以更新当前系统,以及通过下载工具下载系统制作成 U盘 或者 下载IOS文件

当下载工具后运行,可选择(win10和win11相同操作):

  • U盘:制作U盘
  • IOS文件:保存文件

其他下载

- 最好使用原版的安装文件,不要使用各种PE自带的系统,如韩博士的在线安装会自带很多垃圾软件、垃圾游戏、垃圾广告

设置共享

选择要共享的文件夹,打开属性,共享标签,设置为共享

设置可以访问的账户,如果希望无密码共享,选择Everyone,并设置权限为完全控制

设置网络控制中心,启用文件共享,设置所有网络无密码

磁盘管理

进行分区等操作

右键 WIN 图标或者按下 win+x -> 磁盘管理

删除时找不到该项目

使用 bat 文件处理

  • 新建 bat 文件,内容:
:: 将要处理的文件或者文件夹拖到此命令文件上
DEL /F /A /Q \\?\%1
RD /S /Q \\?\%1

参考:知乎文档

WinSW

Windows Service Wrapper in a permissive license

在 Windows 中将普通程序作为服务安装,这样开机可自动运行,避免去添加任务计划

github-仓库地址

实例:安装Nginx服务

普通使用模式:运行 Nginx 后,直接闪过一个控制台窗口,在任务管理器中查看 Nginx 是否运行,电脑重启后需要手动去启动,有点不方便

使用WinSW将Nginx安装为服务

  1. 下载 Nginx并解压,假设解压后的路径为 D:\nginx-1.25.4
  2. 下载 WinSW,下载后是一个 WinSW-x64.exe 文件,将其放到 D:\nginx-1.25.4 下,也就是和 nginx.exe 同层级
  3. WinSW-x64.exe 重命名为 nginx-service.exe
  4. 创建文件 nginx-service.xml 文件
<!-- nginx-service.xml -->
<service>
    <id>nginx</id>
    <name>nginx</name>
    <description>nginx</description>
    <logpath>logs</logpath>
    <logmode>roll</logmode>
    <depend></depend>
    <executable>nginx.exe</executable>
	<arguments>--service</arguments>
</service>
- id: 服务id,假设启动了多个 nginx ,这里需要填一个唯一的
- name: 服务名称
- description:服务描述
- logpath:日志保存的文件夹,当前目录的logs文件夹下,这里不要填绝对路径,否则文件移动后就不对了
- logmode:日志模式,滚动
- executable:服务安装的二进制文件,这里不要填绝对路径,否则文件移动后就不对了
- arguments:服务启动时,命令行参数:如果有需要就填,这里没有需要可以不填
  1. 以管理员身份运行 PowerShell
  2. 将目录切换到 nginx.exe 相同的目录
    1. 执行语句 nginx-service.exe install 进行服务安装,名称为:nginx,可通过 logs/nginx-service.wrapper.log 查看日志信息
    2. 执行语句 nginx-service.exe start 启动服务
    3. 执行语句 nginx-service.exe stop 停止服务
    4. 执行语句 nginx-service.exe restart 重启服务
    5. 执行语句 nginx-service.exe uninstall 卸载服务
  3. 打开服务窗口里面同样可以进行相关的操作

服务模式下不要操作窗口句柄

控制台程序中不要使用和界面相关的接口,如下面这段代码将会出现获取句柄为空的错误

 [DllImport("user32.dll ", EntryPoint = "RemoveMenu")]
 extern static int RemoveMenu(IntPtr hMenu, int nPos, int flags);

 private static void SetWinStyle()
 {

    /* 设置窗口样式 */

    Console.WindowWidth = 120;
    Console.BufferWidth = 220;//240 为最大值 保证可以换行
    Console.BufferHeight = 1000;

    Console.Title = title;
    //与控制台标题名一样的路径
    //  string fullPath = System.Environment.CurrentDirectory + "\\" + title + ".exe";

#if !DEBUG
    //根据控制台标题找控制台
    int WINDOW_HANDLER = FindWindow(null, Console.Title);
    Thread.Sleep(100);
    //找关闭按钮
    IntPtr CLOSE_MENU = GetSystemMenu((IntPtr)WINDOW_HANDLER, IntPtr.Zero);
    int SC_CLOSE = 0xF060;
    //关闭按钮禁用
    RemoveMenu(CLOSE_MENU, SC_CLOSE, 0x0);
#endif
    Console.CancelKeyPress += new ConsoleCancelEventHandler(CloseConsole);
}

解决方案

这里就要用到命令行参数,在 xxx-service.xml 文件中添加一个参数 <arguments>--service</arguments> ,如果多个参数空格隔开

当使用服务启动时,命令行参数中将包含: --service 参数

  //这里根据命令行参数来判断是否为服务模式启动,服务模式下不能设置窗口样式

  bool isServiceMode = args.Contains("--service");
  if (!isServiceMode)//非服务下,设置窗口样式
  {
      SetWinStyle(); 
  }

Windows 命令行

cd

更改当前目录。

官网

命令行语法项

下表描述用于指示命令行语法的表示法。

表示法 说明
不含方括号或大括号的文本 必须按所显示键入的项。
必须为其提供值的占位符。
[Text inside square brackets] 可选项。
{Text inside braces} 一组必需的项。 你必须选择一个。
竖线 (|) 互斥项的分隔符。 你必须选择一个。
省略号 (…) 可重复使用多次的项。

创建批处理文件

在Windows中,批处理文件以 .bat为扩展名。

创建一个名为 hello.bat的文件,并输入以下内容:

echo Hello, World!
pause

保存文件后,双击该文件即可运行。

执行批处理文件

执行一个批处理文件,可以通过双击运行,也可以右键以管理员身份运行,但他们有区别。

区别项目 双击运行 管理员运行
执行目录 工作目录是该.bat文件所在的目录 工作目录是 C:\Windows\System32,这容易导致某些文件路径错误,解决办法,添加以下代码:
@echo off
cd /d %~dp0
权限 以当前用户的权限运行,如果当前用户没有管理员权限,某些操作(如修改系统文件、注册表、安装软件等)可能会失败 以管理员权限运行。能够执行需要提升权限的操作
环境变量 使用当前用户的环境变量,有些环境变量仅加入到了系统环境变量中,可能无法访问 使用系统管理员的环境变量,这可能会导致一些变量(如用户目录等)不同
用户账户控制(UAC) 不会触发 UAC 提示 会触发 UAC 提示,要求用户确认提升权限。这是为了确保用户有意图执行管理员级别的操作。
安装路径和访问权限 对用户目录(如 C:\Users\Username)有完全访问权限,但对系统目录(如 C:\Windows、C:\Program Files)的访问可能受限。 对所有目录都有完全访问权限,包括系统目录。
操作系统保护机制 受操作系统保护机制的约束较多,不能执行某些系统级操作。 如果脚本涉及到计划任务,可能需要管理员权限来创建或修改计划任务。

双击 hello.bat文件后,会弹出一个命令行窗口,显示 Hello, World!,然后等待用户按下任意键后关闭窗口。

中文编码

在文件中加入

chcp 65001 > nul

备注:>nul :表示将输入重定向到空设备,也就是不输出这条命令的操作信息,此处如果不加会输出一条消息:Active code page:6 5001

参考

call(调用批处理程序)

从另一个批处理程序调用一个批处理程序,而不停止父批处理程序。 call 命令接受标签作为调用的目标。

若要从另一个批处理程序运行 checknew.bat 程序,请在父批处理程序中键入以下命令:

call checknew

如果父批处理程序接受两个批处理参数,并且你希望它将这些参数传递给 checknew.bat,请在父批处理程序中键入以下命令:

call checknew %1 %2

cd(同chdir:显示或更改当前目录)

显示当前目录的名称或更改当前目录。 如果仅与驱动器号一起使用(例如,cd C:),cd 将显示指定驱动器中当前目录的名称。 如果使用时不带参数,cd 将显示当前驱动器和目录。

语法:

cd [/d] [<drive>:][<path>]
cd [..]
chdir [/d] [<drive>:][<path>]
chdir [..]

参数

参数 说明
/d 更改当前驱动器以及驱动器的当前目录。
: 指定要显示或更改的驱动器(如果与当前驱动器不同)。
<> 指定要显示或更改的目录的路径。
[..] 指定要更改为父文件夹。
/? 在命令提示符下显示帮助。

注解

如果启用了命令扩展,则以下条件适用于 cd 命令:

  • 当前目录字符串将转换为使用与磁盘上的名称相同的大小写。 例如,cd c:\temp 会将当前目录设置为 C:\Temp(如果磁盘上是这种大小写)。

  • 系统不会将空格视为分隔符,因此 <path> 可以包含不带引号的空格。 例如: 复制

    cd username\programs\start menu
    

    相当于:

    复制

    cd "username\programs\start menu"
    

    如果禁用扩展,则需要引号。

  • 若要禁用命令扩展,请键入: 复制

    cmd /e:off
    

示例

要返回到根目录,即驱动器的目录层次结构顶层:

cd\

要更改与你当前所在驱动器不同的驱动器上的默认目录,请输入以下命令:

cd [<drive>:[<directory>]]

若要验证对目录的更改,请键入:

cd [<drive>:]

chcp(设置语言)

设置代码页和语言

代码页国家/地区或语言
437美国
850多语言 (拉丁文我)
852西里尔语 (俄语)
855西里尔语 (俄语)
857土耳其语
860葡萄牙语
861冰岛语
863加拿大法语
865北欧
866俄语
869现代希腊语
936中文(gbk)
65001utf-8

若要查看活动的代码页设置,请键入︰

chcp

将显示类似于以下内容的消息:Active code page: 437

若要将活动代码页更改为 936(中文),请键入:

chcp 936

如果指定的代码页无效,将显示以下错误消息:Invalid code page

clean(删除分区)

从获得焦点的磁盘中删除所有分区或卷格式设置。

不要轻易使用

cls(清除命令窗口)

清除命令提示符窗口。

若要清除命令提示符窗口中显示的所有信息并返回到空白窗口,请键入:

cls

cmd

启动命令解释器的新实例 Cmd.exe。 使用时如果没有参数,cmd 将显示操作系统的版本和版权信息。

copy(复制)

将一个或多个文件从一个位置复制到另一个位置。

copy 文档

若要将名为 memo.doc 的文件复制到当前驱动器中的 letter.doc,并确保文件结束字符 (CTRL+Z) 位于复制的文件的末尾,请键入:

copy memo.doc letter.doc /a

若要将名为 robin.typ 的文件从当前驱动器和目录复制到驱动器 C 上名为 Birds 的现有目录,请键入:

copy robin.typ c:\birds

备注

如果 Birds 目录不存在,则会将文件 robin.typ 复制到名为 Birds 的文件中,该文件位于磁盘驱动器 C 上的根目录中。

若要合并当前目录中的 Mar89.rpt、Apr89.rpt 和 May89.rpt,并将其放入名为 Report 的文件(也在当前目录中),请键入:

copy mar89.rpt + apr89.rpt + may89.rpt Report

备注

如果合并文件,copy 命令会使用当前日期和时间标记目标文件。 如果省略 destination,将使用列表中第一个文件的名称合并和存储文件。

若要合并 Report 中的所有文件,当名为 Report 的文件已存在时,请键入:

copy report + mar89.rpt + apr89.rpt + may89.rpt

若要将当前目录中所有文件扩展名为 .txt 的文件合并到名为 Combined.doc 的单个文件,请键入:

copy *.txt Combined.doc

若要使用通配符将多个二进制文件合并为一个文件,请包含 /b。 这可以防止 Windows 将 CTRL+Z 视为文件结束字符。 例如,键入:

copy /b *.exe Combined.exe

注意

如果合并二进制文件,生成的文件可能因内部格式问题而无法使用。

  • 将扩展名为 .txt 的每个文件与其相应 .ref 文件合并会创建文件名相同,但扩展名为 .doc 的文件。 Copy 命令将 file1.txt 与 file1.ref 合并成 file1.doc,然后该命令将 file2.txt 与 file2.ref 合并成 file2.doc,依此类推。 例如,键入:
copy *.txt + *.ref *.doc

若要合并扩展名为 .txt 的所有文件,然后将扩展名为 .ref 的所有文件合并为一个名为 Combined.doc 的文件,请键入:

copy *.txt + *.ref Combined.doc

create(创建分区)

创建分区命令

del(删除文件)

删除一个或多个文件。 此命令执行与 erase 命令相同的操作。

del 命令还可以使用不同参数从 Windows 恢复控制台运行。 有关详细信息,请参阅 Windows 恢复环境 (WinRE)

警告

如果使用 del 从磁盘中删除某个文件,则无法检索该文件。

要删除驱动器 C 上名为 test 的文件夹中的所有文件,请键入以下任一命令:

del c:\test
del c:\test\*.*

若要删除文件夹中名称中包含空格的所有文件,需要用双引号将完整路径括起来。 键入以下任一命令:

del "c:\test folder\"
del "c:\test folder\*.*"

要从当前目录中删除文件扩展名为 .bat 的所有文件,请键入:

del *.bat

要删除当前目录中的所有只读文件,请键入:

del /a:r *.*

delete(删除分区)

删除分区或卷。 此命令还会从磁盘列表中删除动态磁盘。

dir(显示目录)

显示目录的文件和子目录的列表。 如果使用此命令时不带参数,则将显示磁盘的卷标和序列号,后跟磁盘上的目录和文件列表(包括其名称以及上次修改每个目录和文件的日期和时间)。 对于文件,此命令显示扩展名和大小(以字节为单位)。 此命令还显示列出的文件和目录总数、其累积大小以及磁盘上剩余的可用空间(以字节为单位)。

dir 命令还可以使用不同参数从 Windows 恢复控制台运行。 有关详细信息,请参阅 Windows 恢复环境 (WinRE)

语法

dir [<drive>:][<path>][<filename>] [...] [/p] [/q] [/w] [/d] [/a[[:]<attributes>]][/o[[:]<sortorder>]] [/t[[:]<timefield>]] [/s] [/b] [/l] [/n] [/x] [/c] [/4] [/r]

参数

参数说明
[<drive>:][<path>]指定要查看其列表的驱动器和目录。
[<filename>]指定要查看其列表的特定文件或文件组。
/p一次显示一个列表屏幕。 要查看下一个屏幕,请按任意键。
/q显示文件所有权信息。
/w以宽格式显示列表,每行最多包含五个文件名或目录名称。
/d以与 /w 相同的格式显示列表,但文件按列排序。
/a[[:]<attributes>]仅显示具有指定属性的目录和文件的名称。 如果不使用此参数,此命令将显示除隐藏文件和系统文件之外的所有文件的名称。 如果在未指定任何属性的情况下使用此参数,则此命令将显示所有文件的名称,包括隐藏文件和系统文件。 可能的属性值列表包括:* d - 目录* h - 隐藏文件* s - 系统文件* l - 重新分析点* r - 只读文件* a - 可用于存档的文件* i - 非内容索引文件你可以使用这些值的任意组合,但不要使用空格分隔值。 (可选)可以使用冒号 (:) 分隔符,或者你可以使用连字符 (-) 作为前缀来表示“非”。 例如,使用 -s 属性不会显示系统文件。
/o[[:]<sortorder>]根据 sortorder 对输出进行排序,可以是以下值的任意组合:* n - 按名称字母顺序排列* e - 按扩展名字母顺序排列* g - 首先列出组目录* s - 按大小,最先列出最小的文件* d - 按日期/时间,最先列出最早的文件* 使用**-** 前缀可反转排序顺序多个值将按照你列出它们的顺序进行处理。 不要用空格分隔多个值,但可以选择使用冒号 (:)。如果未指定 sortorder,则 dir /o 将按字母顺序列出目录,后跟文件,这些文件也按字母顺序排序。
/t[[:]<timefield>]指定要显示或用于排序的时间字段。 timefield 的可用值包括:* c - 创建时间* a - 上次访问时间* w - 最后写入时间
/s列出指定目录和所有子目录中出现的指定文件名的每个匹配项。
/b显示纯粹的目录和文件列表,不含其他信息。 /b 参数将覆盖 /w。
/l使用小写显示未排序的目录名称和文件名。
/n在屏幕最右侧显示带有文件名的长列表格式。
/x显示为非 8dot3 文件名生成的短名称。 显示内容与 /n 的显示内容相同,但短名称插入到长名称之前。
/c以千位分隔符显示文件大小。 此选项为默认行为。 使用 /-c 隐藏分隔符。
/4以四位数格式显示年份。
/r显示文件的备用数据流。
/?在命令提示符下显示帮助。

注解

  • 若要使用多个 filename 参数,请用空格、逗号或分号分隔每个文件名。

  • 可以使用通配符(***** 或 ?)来表示文件名的一个或多个字符,并显示文件或子目录的子集。

  • 可以使用通配符 ***** 来替换任何字符串,例如:

    • dir *.txt 将列出当前目录中扩展名以 .txt 开头的所有文件,例如 .txt、.txt1、.txt_old。
    • dir read *.txt 将列出当前目录中以 read 开头并且扩展名以 .txt 开头的所有文件,例如 .txt、.txt1 或 .txt_old。
    • dir read *.* 将列出当前目录中以 read 开头的所有文件(无论任何扩展名)。

    星号通配符始终使用短文件名映射,因此你可能会得到意外的结果。 例如,以下目录包含两个文件(t.txt2 和 t97.txt): 复制

    C:\test>dir /x
    Volume in drive C has no label.
    Volume Serial Number is B86A-EF32
    
    Directory of C:\test
    
    11/30/2004  01:40 PM <DIR>  .
    11/30/2004  01:40 PM <DIR> ..
    11/30/2004  11:05 AM 0 T97B4~1.TXT t.txt2
    11/30/2004  01:16 PM 0 t97.txt
    

    你可能期望键入 dir t97\* 后将返回文件 t97.txt。 但是,键入 dir t97\* 会返回这两个文件,因为星号通配符使用其短名称映射 T97B4~1.TXT,因此会认为文件 t.txt2 与 t97.txt 匹配。 同样,键入 del t97\* 会删除这两个文件。

  • 可以使用问号 (?) 代替名称中的单个字符。 例如,键入 dir read???.txt 会列出当前目录中扩展名为 .txt 的任何文件,这些文件以 read 开头,后跟最多三个字符。 这包括 Read.txt、Read1.txt、Read12.txt、Read123.txt 和 Readme1.txt,但不包括 Readme12.txt。

  • 如果在 attributes 中使用 /a 和多个值,则此命令仅显示具有所有指定属性的文件的名称。 例如,如果将 /a 与 r 和 -h 一起使用作为属性(使用 /a:r-h/ar-h),则此命令将仅显示未隐藏的只读文件的名称。

  • 如果指定多个 sortorder 值,则此命令将首先按第一个条件对文件名进行排序,然后按第二个条件排序,依此类推。 例如,如果将 /o 与 sortorder 的 e 和 -s 参数一起使用(使用 /o:e-s/oe-s),则此命令按扩展名对目录和文件的名称进行排序,首先显示最大的文件,然后显示最终结果。 按扩展名按字母顺序排序会导致首先显示没有扩展名的文件名,然后显示目录名称,然后显示带有扩展名的文件名。

  • 如果使用重定向符号 (>) 将此命令的输出发送到文件,或者使用管道符号 (|) 将此命令的输出发送到另一个命令,则必须使用 /a:-d 和 /b,以仅列出文件名。 可以将 filename 与 /b 和 /s 一起使用,以指定此命令要在当前目录及其子目录中搜索与 filename 参数匹配的所有文件名。 此命令仅列出它找到的每个文件名的驱动器号、目录名、文件名和文件扩展名(每行一个路径)。 在使用管道符号将此命令的输出发送到另一个命令之前,应首先在 Autoexec.nt 文件中设置 TEMP 环境变量。

示例

若要按字母顺序、宽格式逐个显示所有目录,并在每个屏幕后暂停,请确保根目录是当前目录,然后键入:

dir /s/w/o/p

输出列出了根目录、子目录和根目录中的文件,包括扩展名。 此命令还会列出树中每个子目录中的子目录名称和文件名。

若要更改前面的示例,以便 dir 显示文件名和扩展名,但省略目录名称,请键入:

dir /s/w/o/p/a:-d

要打印目录列表,请键入:

dir > prn

如果指定 prn,则目录列表将发送到连接到 LPT1 端口的打印机。 如果打印机连接到其他端口,则必须将 prn 替换为正确端口的名称。

还可以将 prn 替换为文件名,从而将 dir 命令的输出重定向到文件。 也可以键入路径。 例如,若要将 dir 命令的输出定向到 Records 目录中的 dir.doc 文件,请键入:

dir > \records\dir.doc

如果 dir.doc 不存在,dir 将创建此文件,除非 Records 目录不存在。 如果 Records 目录不存在,将显示以下消息:

File creation error

若要显示驱动器 C 上所有目录中扩展名为.txt 的所有文件名的列表,请键入:

dir c:\*.txt /w/o/s/p

dir 命令以宽格式显示每个目录中按字母顺序排列的匹配文件名列表,并且每次屏幕填满时都会暂停,按任意键可继续显示。

echo(打印消息和回显功能)

显示消息或者打开或关闭命令回显功能。 如果不结合任何参数使用,echo 会显示当前回显设置。

若要显示当前的回显设置,请键入:

echo

若要在屏幕上回显空白行,请键入:

echo.

备注

不要在句点前面包含空格。 否则,将显示句点而不是空白行。

若要防止在命令提示符处回显命令,请键入:

echo off

备注

关闭回显时,命令提示符不会出现在“命令提示符”窗口中。 若要再次显示命令提示符,请键入 echo on。

若要防止批处理文件中的所有命令(包括 echo off 命令)显示在屏幕上,请在批处理文件的第一行中键入:

@echo off

可以将 echo 命令用作 if 语句的一部分。 例如,若要在当前目录中搜索文件扩展名为 .rpt 的任何文件,并在找到此类文件时回显消息,请键入:

if exist *.rpt echo The report has arrived.

以下批处理文件在当前目录中搜索文件扩展名为 .txt 的文件,并显示一条消息来指示搜索结果:

@echo off
if not exist *.txt (
echo This directory contains no text files.
) else (
   echo This directory contains the following text file^(s^):
   echo.
   dir /b *.txt
   )

如果运行该批处理文件时未找到 .txt 文件,则会显示以下消息:

This directory contains no text files.

如果运行该批处理文件时找到 .txt 文件,则会显示以下输出(此示例假设文件 File1.txt、File2.txt 和 File3.txt 存在):

This directory contains the following text file(s):

File1.txt
File2.txt
File3.txt

find(搜索文本字符)

在一个或多个文件中搜索文本字符串,并显示包含指定字符串的文本行。

语法

find [/v] [/c] [/n] [/i] [/off[line]] <"string"> [[<drive>:][<path>]<filename>[...]]

参数

参数说明
/v显示不包含指定 <string> 的所有行。
/c对包含指定 <string> 的行进行计数,并显示总计。
/n每行前面都有文件的行号。
/i指定搜索不区分大小写。
/off[line]如果文件设置了脱机属性,则不会被跳过。
<"string">必需。 指定想要搜索的字符组(用引号括起来)。
[<drive>:][<path>]<filename>指定要在其中搜索指定字符串的文件的位置和名称。
/?在命令提示符下显示帮助。

若要显示包含字符串“pencil sharpener”的 pencil.md 中的所有行,请键入:

find "pencil sharpener" pencil.txt

为了在 report.txt 文件中找到文本“科学家标明他们的论文仅供讨论,并不是最终报告。”(含引号),请键入:

find """The scientists labeled their paper for discussion only. It is not a final report.""" < report.txt

若要搜索一组文件,可以使用通配符。 若要在当前目录中搜索扩展名为 .bat 且包含忽略大小写的字符串 PROMPT 的文件,请键入

find /i "PROMPT" *.bat

若要在包含字符串 CPU 的目录中查找文件名,请使用管道 (|) 将 dir 命令的输出定向到 find 命令,如下所示:

dir c:\temp /s /b | find "CPU"

查找不包含 “agent” 的所有正在运行的进程:

tasklist | find /v /i "agent"

检查服务是否正在运行:

sc query  Winmgmt | find "RUNNING" >nul 2>&1 && (echo service is started) || (echo service is stopped)

for(遍历文件)

为一组文件中的每个文件运行指定命令。

语法

for {%% | %}<variable> in (<set>) do <command> [<commandlineoptions>]

参数

参数说明
{%% | %}<variable>必填。 表示可替换参数。 使用单百分号 (%) 可在命令提示符下执行 for 命令。 使用双百分号 (%%) 可在批处理文件中执行 for 命令。 变量区分大小写,并且必须用字母值表示,例如 %a、%b 或 %c。
(<set>)必需。 指定要对其运行命令的一个或多个文件、目录或文本字符串或值范围。 需要使用括号。
<command>必需。 指定要对每个文件、目录或文本字符串执行的命令,或针对 set 中包含的值范围执行的命令。
<commandlineoptions>指定要与指定命令一起使用的任何命令行选项。
/?在命令提示符下显示帮助。

备注

  • 可以在批处理文件中使用此命令,也可以直接从命令提示符使用此命令。
  • 以下属性适用于 for 命令:
    • 此命令将 % variable%% variable 替换为指定集中的每个文本字符串,直到指定的命令处理完所有文件。
    • 变量名称区分大小写并且是全局性的,一次最多不能超过 52 个处于活动状态。
    • 可以对变量使用任何字符,但为了避免与批处理参数 %0%9 混淆,请避免使用数字 0 到 9。 对于简单的批处理文件,单个字符(如 %%f)将起作用。
    • 可以在复杂的批处理文件中对变量使用多个值,以区分不同的可替换变量。
  • set 参数可以表示单个文件组或多组文件。 可以使用通配符(***** 和 ?)指定文件集。 以下是有效的文件集: 复制
  (*.doc)
  (*.doc *.txt *.me)
  (jan*.doc jan*.rpt feb*.doc feb*.rpt)
  (ar??1991.* ap??1991.*)
  • 使用此命令时,set 参数中的第一个值将替换 % variable%% variable,然后指定的命令将处理此值。 这一直持续到与 set 值相对应的所有文件(或文件组)都被处理为止。
  • In 和 do 不是参数,但必须将它们与此命令一起使用。 如果省略其中任何一个关键字,则会出现错误消息。
  • 如果启用了命令扩展(默认状态),则支持下列形式的 for:
    • 仅限目录:如果 set 参数中包含通配符(***** 或 ?),则将针对与 set 参数匹配的每个目录(而不是指定目录中的一组文件)执行指定的命令。 语法为:

      for /d {%%|%}<variable> in (<set>) do <command> [<commandlineoptions>]
      
    • 递归:遍历以 drive:path 为根的目录树,并在树的每个目录中执行 for 语句。 如果在 /r 之后没有指定目录,则使用当前目录作为根目录。 如果 set 参数只是一个句点 (.),则仅枚举目录树。 语法为:

      for /r [[<drive>:]<path>] {%%|%}<variable> in (<set>) do <command> [<commandlineoptions>]
      
    • 迭代值范围:使用迭代变量设置起始值 (start#),然后逐步通过设置的值范围,直到该值超过设置的结束值 (end#)。 /l 将通过比较 start # 和 end # 来执行迭代。 如果 start # 小于 end #,则执行该命令。 当迭代变量超过 end# 时,命令 shell 退出循环。 你还可以使用 step# 来逐步减少值的范围。 例如,(1,1,5) 生成序列 1 2 3 4 5,(5,-1,1) 生成序列 5 4 3 2 1。 语法为:

      for /l {%%|%}<variable> in (<start#>,<step#>,<end#>) do <command> [<commandlineoptions>]
      
    • 迭代和文件解析:使用文件解析来处理命令输出、字符串和文件内容。 使用迭代变量定义要检查的内容或字符串,并使用各种 parsingkeywords 选项进一步修改解析。 使用 parsingkeywords 标记选项来指定哪些标记应作为迭代变量传递。 如果使用时不带标记选项,/f 将只检查第一个标记。 文件解析包括读取输出、字符串或文件内容,然后将其拆分为单独的文本行,并将每一行解析为零个或多个标记。 然后调用 for 循环,并将迭代变量值设置为标记。 默认情况下,/f 传递每个文件的每一行中的第一个空白分隔标记。 空行将被跳过。 语法为:

      for /f [<parsingkeywords>] {%%|%}<variable> in (<set>) do <command> [<commandlineoptions>]
      for /f [<parsingkeywords>] {%%|%}<variable> in (<literalstring>) do <command> [<commandlineoptions>]
      for /f [<parsingkeywords>] {%%|%}<variable> in ('<command>') do <command> [<commandlineoptions>]
      

      set 参数指定一个或多个文件名。 将依次打开、读取和处理每个文件,然后才会移动到 set 参数中指定的下一个文件。 要覆盖默认分析行为,请指定 parsingkeywords。 这是一个带引号的字符串,其中包含一个或多个用于指定不同分析选项的关键字。

      如果使用 usebackq 选项,请使用以下语法之一:

      for /f [usebackq <parsingkeywords>] {%%|%}<variable> in (<set>) do <command> [<commandlineoptions>]
      for /f [usebackq <parsingkeywords>] {%%|%}<variable> in ('<literalstring>') do <command> [<commandlineoptions>]
      for /f [usebackq <parsingkeywords>] {%%|%}<variable> in (`<command>`) do <command> [<commandlineoptions>]
      

      下表列出了可用于 parsingkeywords 的分析关键字。 展开表

      关键字说明
      eol=<c>指定行尾字符(只有一个字符)。
      skip=<n>指定文件开头要跳过的行数。
      delims=<xxx>指定分隔符集。 这将替换默认分隔符集,即空格和制表符。
      tokens=<x,y,m–n>指定每次迭代时要将每行中的哪些标记传递给 for 循环。 因此,会分配额外的变量名。 m-n 指定从第 m 个到第 n 个标记的范围。 如果 tokens= 字符串中的最后一个字符是星号 ( ***** ),则会分配一个额外的变量,并且会接收解析的最后一个标记之后的行上的剩余文本。
      usebackq指定将双引号字符串作为命令运行,使用单引号字符串作为文字字符串,或者对于包含空格的长文件名,允许将 <set> 中的每个文件名都括在双引号中。
    • 变量替换:下表列出了可选语法(适用于任何变量 I): 展开表

      带修饰符的变量说明
      %~I扩展 %I,这将移除所有周围的引号。
      %~fI%I 扩展为完全限定的路径名。
      %~dI%I 扩展为仅包含驱动器号。
      %~pI%I 扩展为仅包含路径。
      %~nI%I 扩展为仅包含文件名。
      %~xI%I 扩展为仅包含文件扩展名。
      %~sI将路径扩展为仅包含短名称。
      %~aI%I 扩展为文件属性。
      %~tI%I 扩展为文件的日期和时间。
      %~zI%I 扩展为文件的大小。
      %~$PATH:I搜索 PATH 环境变量中列出的目录,然后将 %I 扩展为找到的第一个目录的完全限定名称。 如果未定义环境变量名称或搜索找不到该文件,则此修饰符将扩展为空字符串。

      下表列出了可用于获取复合结果的修饰符组合。 展开表

      带组合修饰符的变量说明
      %~dpI%I 扩展为仅包含驱动器号和路径。
      %~nxI%I 扩展为仅包含文件名和扩展名。
      %~fsI%I 扩展为仅包含短名称的完整路径名。
      %~dp$PATH:I在 PATH 环境变量中列出的目录中搜索 %I,然后扩展为找到的第一个目录的驱动器号和路径。
      %~ftzaI%I 扩展为类似于 dir 命令的输出行。

      在上面的示例中,可以将 %I 和 PATH 替换为其他有效值。 有效的 for 变量名称将结束 %~ 语法。 通过使用大写的变量名(如 %I),可以提高代码的易读性,并避免与不区分大小写的修饰符混淆。

  • 解析字符串:你可以在直接字符串上使用解析逻辑,方法是:使用双引号(不带 usebackq)或单引号(带usebackq)--例如,(MyString) 或 ('MyString')。for /f``<literalstring> <literalstring> 被视为来自文件的单行输入。 解析用双引号括起的 <literalstring> 时,命令符号(如 \ & | > < ^)被视为普通字符。
  • 解析输出:可以使用 for /f 命令来解析命令的输出,方法是在括号之间加上反引号 <command>。 它将被视为命令行,随后传递给子 cmd.exe。 输出会捕获到内存中,并像文件一样进行解析。

示例

要在批处理文件中使用 for,请使用以下语法:

for {%%|%}<variable> in (<set>) do <command> [<commandlineoptions>]

要使用可替换变量 %f 显示当前目录中扩展名为.doc 或.txt 的所有文件的内容,请键入:

for %f in (*.doc *.txt) do type %f

在前面的示例中,当前目录中扩展名为 .doc 或 .txt 的每个文件都将被替换为 %f 变量,直到显示每个文件的内容。 要在批处理文件中使用此命令,请将所有出现的 %f 替换为 %%f。 否则,将忽略该变量并显示错误消息。

要分析文件并忽略注释行,请键入:

for /f eol=; tokens=2,3* delims=, %i in (myfile.txt) do @echo %i %j %k

此命令将解析 myfile.txt 中的每一行。 此命令将忽略以分号开头的行,并将每行的第二个和第三个标记传递给 for 主体(标记由逗号或空格分隔)。 for 语句的主体引用 %i 以获取第二个标记,引用 %j 以获取第三个标记,引用 %k 以获取所有剩余标记。 如果你提供的文件名包含空格,请在文本两边使用引号(例如,File Name)。 要使用引号,必须使用 usebackq。 否则,引号将被解释为定义要解析的文字字符串。

%i 在 for 语句中显式声明。 %j 和 %k 是使用 tokens= 隐式声明的。 可以使用 tokens= 来指定最多 26 个标记,前提是不会导致尝试声明超过字母 z 或 Z 的变量。

要通过将 set 放在括号中来解析命令的输出,请键入:

for /f "usebackq delims==" %i in (`set`) do @echo %i

要对目录中的所有文件执行递归循环(包括子目录)并回显其完整路径、上次修改时间及其文件大小,请键入:

for /r "C:\My Dir\" %A in (*.*) do echo %~ftzA

if(条件)

在批处理程序中执行条件处理。

语法

if [not] ERRORLEVEL <number> <command> [else <expression>]
if [not] <string1>==<string2> <command> [else <expression>]
if [not] exist <filename> <command> [else <expression>]

如果启用了命令扩展,请使用以下语法:

if [/i] <string1> <compareop> <string2> <command> [else <expression>]
if cmdextversion <number> <command> [else <expression>]
if defined <variable> <command> [else <expression>]

参数

参数说明
not指定仅当条件为 false 时,才应执行该命令。
errorlevel <number>仅当由 Cmd.exe 运行的上一个程序返回等于或大于 number 的退出代码时,才指定 true 条件。
<command>指定在满足上述条件时应执行的命令。
<string1>==<string2>仅当 string1 和 string2 相同时,才指定 true 条件。 这些值可以是字符串或批处理变量(例如,%1)。 无需将字符串用引号括起来。
存在 <filename>如果指定的文件名存在,则指定 true 条件。
<compareop>指定三字母比较运算符,包括:* EQU - 等于* NEQ - 不等于* LSS - 小于* LEQ - 小于或等于* GTR - 大于* GEQ - 大于或等于
/i强制字符串比较忽略大小写。 可以采用 if 的 string1==string2 形式使用 /i。 这些比较是通用的,因为如果 string1 和 string2 仅由数字组成,则字符串将转换为数值并执行数值比较。
cmdextversion <number>仅当与 Cmd.exe 的命令扩展功能关联的内部版本号等于或大于指定的数值时,才指定 true 条件。 第一个版本为 1。 向命令扩展添加大量增强功能时,它会以 1 为增量增加。 禁用命令扩展时(默认情况下,启用命令扩展),cmdextversion 条件永远不会为 true。
defined <variable>如果定义了变量,则指定 true 条件。
<expression>指定命令行命令以及要在 else 子句中传递给该命令的任何参数。
/?在命令提示符下显示帮助。

备注

  • 如果 if 子句中指定的条件为 true,则执行条件后面的命令。如果条件为 false,则忽略 if 子句中的命令,并且该命令执行 else 子句中指定的任何命令。

  • 程序停止时,它将返回退出代码。 若要使用退出代码作为条件,请使用 errorlevel 参数。

  • 如果使用 defined,则将以下三个变量添加到环境中:%errorlevel%、%cmdcmdline% 和 %cmdextversion%。

    • %errorlevel%:扩展为 ERRORLEVEL 环境变量的当前值的字符串表示形式。 此变量假定尚不存在名为 ERRORLEVEL 的现有环境变量。 如果存在,将获得该 ERRORLEVEL 值。
  • %cmdcmdline%:扩展到在 Cmd.exe 进行任何处理之前传递给 Cmd.exe 的原始命令行。 这假定尚不存在名为 CMDCMDLINE 的现有环境变量。 如果存在,将获得该 CMDCMDLINE 值。

  • %cmdextversion%:扩展为 cmdextversion 的当前值的字符串表示形式。 这假定不存在名为 CMDEXTVERSION 的现有环境变量。 如果存在,将获取该 CMDEXTVERSION 值。

  • 必须在 if 后面的命令所在的同一行上使用 else 子句。

### 示例

若要显示消息“如果找不到文件 Product.dat ,则找不到数据文件”,请键入:

if not exist product.dat echo Cannot find data file

若要在驱动器 A 中格式化磁盘并在格式化过程中发生错误时显示错误消息,请在批处理文件中键入以下行:

:begin
@echo off
format a: /s
if not errorlevel 1 goto end
echo An error occurred during formatting.
:end
echo End of batch program.

若要从当前目录中删除文件 Product.dat 或在找不到 Product.dat 时显示消息,请在批处理文件中键入以下行:

IF EXIST Product.dat (
del Product.dat
) ELSE (
echo The Product.dat file is missing.
)

备注

这些行可以组合成一行,如下所示:

IF EXIST Product.dat (del Product.dat) ELSE (echo The Product.dat file is missing.)

若要在运行批处理文件后回显 ERRORLEVEL 环境变量的值,请在批处理文件中键入以下行:

goto answer%errorlevel%
:answer1
echo The program returned error level 1
goto end
:answer0
echo The program returned error level 0
goto end
:end
echo Done!

若要在 ERRORLEVEL 环境变量的值小于或等于 1 时转到 ok 标签,请键入:

if %errorlevel% LEQ 1 goto okay

ipconfig

显示所有当前的 TCP/IP 网络配置值,并刷新动态主机配置协议 (DHCP) 和域名系统 (DNS) 设置。 在没有参数的情况下使用时,ipconfig 会显示 Internet 协议版本 4 (IPv4) 和 IPv6 地址、子网掩码以及所有适配器的默认网关。

mkdir(同md:创建目录)

创建目录或子目录。 命令扩展(默认情况下启用)允许使用单个 mkdir 命令在指定路径中创建中间目录。

若要在当前目录中创建名为 Directory1 的目录,请键入:

mkdir Directory1

若要在启用命令扩展的情况下在根目录中创建目录树 Taxes\Property\Current,请键入:

mkdir \Taxes\Property\Current

若要与上一示例中一样在根目录中创建目录树 Taxes\Property\Current,但禁用命令扩展,请键入以下命令序列:

mkdir \Taxes
mkdir \Taxes\Property
mkdir \Taxes\Property\Current

move(移动文件)

将一个或多个文件从一个目录移动到另一个目录。

语法

move [{/y|-y}] [<source>] [<target>]

参数

参数说明
/y停止确认是否要覆盖现有目标文件的提示。 COPYCMD 环境变量中可能预设了此参数。 可以使用 -y 参数替代此预设。 除非命令是从批处理脚本中运行,否则默认会在覆盖文件之前进行提示。
-y启动确认是否要覆盖现有目标文件的提示。
<source>指定要移动的文件的路径和名称。 若要移动或重命名目录,source 应为当前目录的路径和名称。
<target>指定要将文件移动到的路径和名称。 若要移动或重命名目录,target 应为目标目录的路径和名称。
/?在命令提示符下显示帮助。

示例

若要将扩展名为 .xls 的所有文件从 \Data 目录移动到 \Second_Q\Reports 目录,请键入:

move \data\*.xls \second_q\reports\

ren(rename)

重命名文件(文件夹)。

修改文件名示例:

ren a.txt b.txt

若要将当前目录中的所有 .txt 文件扩展名更改为 .doc 扩展名,请键入:

ren *.txt *.doc

若要将目录的名称从 Chap10 更改为 Part10,请键入:

ren chap10 part1

备注

  • 重命名文件时不能指定新的驱动器或路径。 你也不能使用此命令跨驱动器重命名文件或将文件移动到不同的目录(可以使用 mkdir 创建文件夹,move 移动文件路径 )。

  • filename2 中通配符表示的字符与 filename1 中对应的字符相同。

  • filename2 必须是唯一的文件名。 如果 filename2 与现有文件名匹配,则会显示以下消息:Duplicate file name or file not found。

在mac/linux/unix系统中,重命名文件使用mv命令。

注意 mv可以修改文件路径

setx(设置环境变量)

若 key 或者 value 中有空格,使用双引号括起来。

设置当前用户的环境变量

setx KEY VALUE

设系统环境变量,后面追加 /m

setx KEY VALUE /m

start

启动单独的命令提示符窗口以运行指定的程序或命令。

样例

复制文件示例

本示例描述,将指定目录下的三个文件夹复制到目标文件夹下,要求:

  • 复制所有子目录
  • 覆盖现有文件而不提示
@echo off
rem 将执行目录切换到当前目录,因为以管理员运行默认为 c://Windwows/System32 目录
cd /d %~dp0
rem 中文编码,避免一些中文乱码
chcp 65001 > nul
rem 设置源文件夹路径和目标文件夹路径变量
set "source_folder=TSIM.Shared"  
set "destination_folder=TSIM.Unity/Packages/TSIM.Shared"  
  
:: 确保目标文件夹存在,如果不存在则创建它  
if not exist "%destination_folder%" mkdir "%destination_folder%"  
  
:: 复制文件夹  
xcopy /E /I /Y "%source_folder%\Models" "%destination_folder%\Models"  
xcopy /E /I /Y "%source_folder%\Models_Common" "%destination_folder%\Models_Common"  
xcopy /E /I /Y "%source_folder%\Services" "%destination_folder%\Services" 


echo "复制完成!"
pause

选项说明

  • /E:复制所有子目录,包括空的子目录。
  • /I:如果目标不存在且复制多个文件,此选项将假定目标必须是目录。这在目标目录尚未创建时尤其有用,可以避免提示确认。
  • /Y:覆盖现有的目标文件而不提示。

获取日期和时间作为文件名

例子:创建一个用日期和时间命名的文件夹: C:\dddd-MM-dd-hhmmss

@echo off

:: 处理时间的小时数为个位数时,出现空格(会导致认为是命令分割)。使用0补位
set t=%time:~0,2%%time:~3,2%%time:~6,2%
set t=%t: =0%


:: 定义备份目录(可以根据需要更改)
set backupDir=C:\%date:~0,4%-%date:~5,2%-%date:~8,2%-%t%

分区MBR格式转GPT格式

本教程将指导您将MBR转换为GPT分区表。

步骤

  1. 打开DiskGenius,选择要转换的磁盘。
  2. 在左侧导航栏中,选择"分区管理"。
  3. 在右侧的"分区列表"中,右键单击要转换的分区,选择"转换为GPT"。
  4. 输入新分区的名称和大小,然后点击"确定"按钮。
  5. 等待转换完成。
  6. 重启计算机以使更改生效。

常见问题

  1. 在分区的前后没有足够的空闲空间?

    • 使用分区调整工具,前部分分区默认不变
    • 后部分分区设置 8MB,使用键盘上方的数字键输入,小键盘可能无法使用。
  2. 调整分区出现未使用簇的问题?

    • 首先,在开始菜单的运行中输入cmd,打开命令提示符。
    • 在命令行中输入“chkdsk /f /x c:”,其中“c:”应替换为实际提示有问题的盘符(有时候显示的是F盘)。这个命令会检查并修复磁盘上的文件系统错误。
    • 回车执行命令,然后根据提示进行操作。如果控制台提示在下次开机时处理,请重启电脑,并等待磁盘检查完成。
    • 完成磁盘检查后,通常就可以正常地在DiskGenius等工具中进行无损调整分区操作了,此时不会再出现“$Bitmap中有标记已使用的未用簇”的提示。
    • 请注意,执行这些操作前,请确保已备份重要数据,以防数据丢失。检查分区表是否已成功转换为GPT。
    • 检查分区是否已成功创建。
    • 尝试重新启动计算机并再次尝试使用分区。
  3. 将硬盘从MBR转GPT后无法进入系统?

    • 在CMD中执行命令:bcdboot F:\Windows /s F: /f uefi
    • 注意 F 是盘符号。
  4. 回到系统还是不能更新系统,出现错误

Mac

使用手册

MacOS 使用手册

在 Mac 上安装和卸载来自互联网或光盘的 App

安装 App

在 Mac 上,执行以下任一项操作:

对于从互联网下载的 App:在“下载”文件夹中,连按磁盘映像或软件包文件(外观像打开的盒子)。如果提供的安装器未自动打开,请打开安装器,然后遵循屏幕指示执行操作。

【注】如果看到一个警告对话框,提示正在安装无法识别的开发者开发的 App,请参阅打开来自身份不明开发者的 Mac App。

对于光盘上的 App:将光盘插入 Mac 上或连接到 Mac 的光盘驱动器。

若要重新安装从 App Store 下载的 App,请参阅安装从 App Store 购买的项目。

卸载 App

你可以移除从互联网或光盘所下载并安装的 App。

在 Mac 上,点按程序坞中的“访达”图标 ,然后点按“访达”边栏中的“应用程序”。

请执行以下一项操作:

如果 App 位于文件夹中:打开 App 的文件夹以检查卸载器。如果看到“卸载 [App]”或“[App] 卸载器”,请连按它,然后按照屏幕指示进行操作。

如果 App 并非位于文件夹中或没有卸载器:将 App 从“应用程序”文件夹拖移到废纸篓(位于程序坞末端)。

【警告】下次你或“访达”清倒废纸篓时,该 App 会从你的 Mac 永久移除。如果你有使用该 App 创建的文件,则可能无法再次打开它们。如果决定要保留 App,请在清倒废纸篓前将其放回原处。在废纸篓中选择该 App,然后选取“文件”>“放回原处”。

若要卸载从 App Store 下载的 App,请使用启动台。

命令

mv (重命名文件)

mv old_name new_name

cp (复制文件)

cp old_name new_name

rm (删除文件)

rm old_name

ls (列出文件)

ls

pwd (显示当前目录)

pwd

cd (切换目录)

cd /path/to/directory

mkdir (创建目录)

mkdir new_directory

rmdir (删除目录)

rmdir new_directory

cat (查看文件内容)

XCode

TestFlight

某个账号无法安装测试版本

情况描述:

上架了某个软件,进行过 Testflight 测试,中途开发者账号终止过订阅,该软件在 Testflight 中显示失效,后面上架此软件,但是之前的账号无法收到版本更新软件,新添加的测试账号可以收到。

过了几天后才收到。

下载Google商店中的APK

Nginx

说明

一个证书对应一个域名,即可以所有的代理端口都可以使用同一个证书。

配置UnityWebGL服务器

根据目录名称,访问unity 内容时,定位到 指定位置,配置和文件目录结构对比

1723531679363

1723531697200

访问001的Unity项目(443端口省略):https://www.11training.com/unisim/001/?参数

配置证书

  • 配置文件:这里的7130需要在路由器上配置映射到主机,并且要重启,然后更改防火墙设置,开发7130的进出规则

  • 证书的路径:相对于配置文件的,这里的cert文件夹就是和配置文件同目录的,当然可以放在外边去;
  • 重启生效。

命令行重启(勿用)

Windows:

❌不知道什么原因,使用命令行来进行重启,要么,就是启动了多个进程,要么就是关掉了进程没重启。

老老实实在任务管理器中 结束任务,再重启比较靠谱

chcp 65001 > nul
@echo off
cd /d %~dp0

nginx -s reload

echo Nginx 已重启
pause

关闭(有效)

@echo off
cd C:/TSIM/nginx-1.25.4
nginx -s stop
timeout /t 2 /nobreak >nul
pause

实现浏览器播放音视频

默认情况下,将音视频放在Nginx中,在浏览器上打开触发的是下载操作

修改配置文件(支持单格式)

server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
			# autoindex off; # 好像不能写这个
			expires 30d;
			add_header Content-Type audio/wav; # 这里配置支持wav文件,其他格式相应调整
			add_header Cache-Control "public";
            index  index.html index.htm;
        }
}

修改配置文件(支持多格式)

server {
  listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
			# autoindex off;
			expires 30d;
			# add_header Content-Type audio/wav; # 这里配置支持wav文件,其他格式相应调整
			# add_header Cache-Control "public";
			
			types {
				# 添加音频MIME类型
				audio/mpeg mp3;
				audio/ogg ogg;
				audio/wav wav;
				audio/x-aac aac;
				audio/x-ms-wma wma;
				audio/webm webm;
			}

			# 设置默认类型为octet-stream可作为通用后备
			default_type application/octet-stream;		
            index  index.html index.htm;
        }      
}

RabbitMQ

一般用于微服务架构的服务之间,但是也可以用在服务器-客户端之间。

概述

RabbitMQ 是一种消息队列中间件,通常用于服务之间的通信,而不是直接在服务器与客户端之间。它的作用是将消息从一个系统(生产者)传递到另一个系统(消费者),并在两个系统之间提供解耦。RabbitMQ的典型使用场景包括:

服务间通信:RabbitMQ常用于微服务架构中,作为服务之间的消息传递机制。各个微服务通过 RabbitMQ 发布和接收消息,这样它们可以异步地进行通信并解耦,避免直接的依赖。

异步任务处理:服务可以将任务投递到 RabbitMQ 队列中,任务被消费者异步处理。这样,处理者可以根据处理能力的需求灵活扩展。

事件驱动架构:在事件驱动架构中,RabbitMQ 可用作事件的传输通道,当某个事件发生时,相关服务会向 RabbitMQ 发布消息,其他服务可以消费并根据事件做出反应。

虽然 RabbitMQ 通常在服务间通信中使用,但它也可以在客户端和服务器之间使用,特别是在某些复杂的客户端-服务器应用程序中。例如,客户端可能会作为生产者发布消息到 RabbitMQ,服务器作为消费者接收并处理这些消息,或反过来。

总结来说,RabbitMQ 主要用于服务间的消息传递,帮助实现解耦和异步处理。

使用情景

  1. 简单的消息发送和接收。
  2. 一个业务只能一个消费者处理(WorkerQueues):当业务耗时,可以启动多个Work来处理,当某个工作进程处理业务中途意外终止时,可让MQ再次发送消息,交给下个工作进程处理,一个消息只能由由一个消费者消费。
  3. 一个业务多个消费者处理(PublishSubscirbe ):在一个交换器上,接收者随机,独占一个队列名称,生产者在交换器上发送消息,广播所有的队列,上个示例:在同一个队列(非独占)上发送消息,只要一个消费者消费了这个消息就没。

Git

退出代理导致连接失败

环境:macbook

现象:使用代理后,退出代理,出现无法连接仓库地址(浏览器正常),github 和 gitee都无法连接。

错误信息:

fatal: unable to access 'https://github.com/benjieqiang/programmer.git/': Failed to connect to 127.0.0.1 port 7890: Connection refused

解决方案:

查看代理 git config --global --get http.proxy git config --global --get https.proxy

取消代理 git config --global --unset http.proxy git config --global --unset https.proxy

推送失败

error: RPC failed; HTTP 400 curl 22 The requested URL returned error: 400

推送时出现以下错误提示


Enumerating objects: 22, done.
Counting objects: 100% (22/22), done.
Delta compression using up to 10 threads
Compressing objects: 100% (16/16), done.
Writing objects: 100% (16/16), 1019.20 KiB | 33.97 MiB/s, done.
Total 16 (delta 9), reused 0 (delta 0), pack-reused 0
error: RPC failed; HTTP 400 curl 22 The requested URL returned error: 400
send-pack: unexpected disconnect while reading sideband packet
fatal: the remote end hung up unexpectedly
Everything up-to-date

缓存区太小的原因:

设置为5大小

git config --global http.postBuffer 5242880000

在拉取中提示版本冲突

当在终端或者在VS中拉取时,提示版本冲突,此时都要回到终端来进行处理

当版本冲突时,会输出以下内容

Hint: You have divergent branches and need to specify how to reconcile them.
Hint: You can do so by running one of the following commands sometime before
Hint: your next pull:
Hint: 
Hint:   git config pull.rebase false  # merge
Hint:   git config pull.rebase true   # rebase
Hint:   git config pull.ff only       # fast-forward only
Hint: 
Hint: You can replace "git config" with "git config --global" to set a default
Hint: preference for all repositories. You can also pass --rebase, --no-rebase,
Hint: or --ff-only on the command line to override the configured default per
Hint: invocation.

输出的内容给出解决方案,以下作为一个中文说明

这个错误提示是因为你的本地分支和远程分支之间有分歧(divergent branches),Git 需要你指定如何处理这些分歧。在拉取(pull)代码时,Git 既可以通过合并(merge)来解决分歧,也可以通过重置(rebase)来解决分歧。

快速解决

使用终端执行命令:

git config pull.rebase false

再执行拉取命令

git config pull

如果进入 Vim 编辑模式,如提示我们编写 commit,修改合并后的冲突文件等,请参照 Vim 操作

SVN

检出指定目录

checkout 窗口,点击 Choose items...,选择要检出的目录

更新检出的目录

在已检出的项目文件夹上,右键->SVN->Update to Revision,在打开的窗口中点击 Choose items... ,选择要检出的目录

Vim

安装

Windows 安装

通过下载执行文件 安装 下载地址

下载后添加环境变量,即可使用 vim

MacOS

brew install vim

快速使用

Vim 的基本概念

Vim 是一种强大的文本编辑器,以其高效的编辑功能而著名。它有三种主要模式:

  • 命令模式:这是默认模式,用于导航和执行命令。
  • 插入模式:用于输入文本。
  • 末行模式:用于执行复杂命令。

1720764317950

操作指令

在终端中输入 vim,然后回车。你会看到一个新的 Vim 窗口。

模式 类型 命令 说明
命令模式 移动光标 h 向左移动光标
j 向下移动光标
k 向上移动光标
l 向右移动光标
删除文本 x 删除光标所在的字符
dd 删除整行
撤销和重做 u 撤销上一个操作
Ctrl + r 重做上一个操作
进入插入模式 i 在光标前插入,进入插入模式
a 在光标后插入,进入插入模式
o 在当前行下方打开新行并插入,进入插入模式
进入末行模式 : 进入末行模式,并且底线输入里面显示 :
插入模式 返回命令模式 Esc 返回命令模式。
插入模式下和普通的文本编辑一样,但是不能使用Ctr + z 等进行撤销,可退回到命令模式使用相关命令
末行模式 返回命令模式 Esc 返回命令模式,注意不是退出Vim
保存文件 :w 按下 enter 键,保存当前更改并返回命令模式
另存为 :w 文件名 按下 enter 键,另存为指定文件名,并返回命令模式
退出Vim :q 按下 enter 键,退出Vim,注意如果更改了会提示执行保存操作
强制退出Vim :q! 按下 enter 键,强制退出
保存并退出Vim :wq 按下 enter 键,保存并退出Vim 常用

什么是 vim?

Vim是从 vi 发展出来的一个文本编辑器。代码补完、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。

简单的来说, vi 是老式的字处理器,不过功能已经很齐全了,但是还是有可以进步的地方。 vim 则可以说是程序开发者的一项很好用的工具。

连 vim 的官方网站 (http://www.vim.org) 自己也说 vim 是一个程序开发工具而不是文字处理软件。

vim 键盘图:

1720748336783

vim 的使用

基本上 vi/vim 共分为三种模式,分别是命令模式(Command mode)输入模式(Insert mode)底线命令模式(Last line mode)。 这三种模式的作用分别是:

命令模式:

用户刚刚启动 vi/vim,便进入了命令模式。

此状态下敲击键盘动作会被Vim识别为命令,而非输入字符。比如我们此时按下i,并不会输入一个字符,i被当作了一个命令。

以下是常用的几个命令:

  • i 切换到输入模式,以输入字符。
  • x 删除当前光标所在处的字符。
  • : 切换到底线命令模式,以在最底一行输入命令。

若想要编辑文本:启动Vim,进入了命令模式,按下i,切换到输入模式。

命令模式只有一些最基本的命令,因此仍要依靠底线命令模式输入更多命令。

输入模式

在命令模式下按下i就进入了输入模式。

在输入模式中,可以使用以下按键:

  • 字符按键以及Shift组合,输入字符
  • ENTER,回车键,换行
  • BACK SPACE,退格键,删除光标前一个字符
  • DEL,删除键,删除光标后一个字符
  • 方向键,在文本中移动光标
  • HOME/END,移动光标到行首/行尾
  • Page Up/Page Down,上/下翻页
  • Insert,切换光标为输入/替换模式,光标将变成竖线/下划线
  • ESC,退出输入模式,切换到命令模式

底线命令模式

在命令模式下按下:(英文冒号)就进入了底线命令模式。

底线命令模式可以输入单个或多个字符的命令,可用的命令非常多。

在底线命令模式中,基本的命令有(已经省略了冒号):

  • q 退出程序
  • w 保存文件

按ESC键可随时退出底线命令模式。

简单的说,我们可以将这三个模式想成底下的图标来表示:

1720748363917

vim 使用实例

使用 vi/vim 进入一般模式

如果你想要使用 vi 来建立一个名为 runoob.txt 的文件时,你可以这样做:

vim runoob.txt

直接输入 vi 文件名 就能够进入 vi 的一般模式了。请注意,记得 vi 后面一定要加文件名,不管该文件存在与否!

1720748407988

按下 i 进入输入模式(也称为编辑模式),开始编辑文字

在一般模式之中,只要按下 i, o, a 等字符就可以进入输入模式了!

在编辑模式当中,你可以发现在左下角状态栏中会出现 –INSERT- 的字样,那就是可以输入任意字符的提示。

这个时候,键盘上除了 Esc 这个按键之外,其他的按键都可以视作为一般的输入按钮了,所以你可以进行任何的编辑。

1720748419445

按下 ESC 按钮回到一般模式

好了,假设我已经按照上面的样式给他编辑完毕了,那么应该要如何退出呢?是的!没错!就是给他按下 Esc 这个按钮即可!马上你就会发现画面左下角的 – INSERT – 不见了!

在一般模式中按下 :wq 储存后离开 vi

OK,我们要存档了,存盘并离开的指令很简单,输入 :wq 即可保存离开!

1720748431371

OK! 这样我们就成功创建了一个 runoob.txt 的文件。

Npm

安装

通过安装 Nodejs 来一起安装

Windows环境安装:下载msi文件安装

设置镜像源

国内使用 npm 非常慢,甚至就用不了,要么使用淘宝镜像,或者使用 cnpm

博客文章

(一)使用 淘宝 镜像

npm config set registry https://registry.npm.taobao.org/

(二)使用 cnpm,‌这是淘宝提供的 npm 镜像,‌使用 cnpm 安装依赖通常会比使用 npm 快很多。‌安装cnpm命令

npm install -g cnpm --registry=https://registry.npm.taobao.org/

使用 cnpm install 来安装包,如:

cnpm install xxxx

错误解决

code CERT_HAS_EXPIRED

Npm 的证书过期,需要安装证书,可以创建自定义证书,但是比较麻烦,可以设置忽略证书

npm config set strict-ssl false

恢复证书

npm config set strict-ssl true

代理导致无法访问

错误信息:

 ECONNREFUSED
npm ERR! syscall connect
npm ERR! errno ECONNREFUSED
npm ERR! FetchError: request to https://registry.npmjs.org/create-tauri-app/-/create-tauri-app-3.10.0.tgz failed, reason: connect ECONNREFUSED 127.0.0.1:31181
npm ERR!     at ClientRequest.<anonymous> (C:\Program Files\nodejs\node_modules\npm\node_modules\minipass-fetch\lib\index.js:130:14)
npm ERR!     at ClientRequest.emit (node:events:512:28)
npm ERR!     at onerror (C:\Program Files\nodejs\node_modules\npm\node_modules\agent-base\dist\src\index.js:117:21)
npm ERR!     at callbackError (C:\Program Files\nodejs\node_modules\npm\node_modules\agent-base\dist\src\index.js:136:17)
npm ERR!     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
npm ERR!  FetchError: request to https://registry.npmjs.org/create-tauri-app/-/create-tauri-app-3.10.0.tgz failed, reason: connect ECONNREFUSED 127.0.0.1:31181
npm ERR!     at ClientRequest.<anonymous> (C:\Program Files\nodejs\node_modules\npm\node_modules\minipass-fetch\lib\index.js:130:14)
npm ERR!     at ClientRequest.emit (node:events:512:28)
npm ERR!     at onerror (C:\Program Files\nodejs\node_modules\npm\node_modules\agent-base\dist\src\index.js:117:21)
npm ERR!     at callbackError (C:\Program Files\nodejs\node_modules\npm\node_modules\agent-base\dist\src\index.js:136:17)
npm ERR!     at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
npm ERR!   code: 'ECONNREFUSED',
npm ERR!   errno: 'ECONNREFUSED',
npm ERR!   syscall: 'connect',
npm ERR!   address: '127.0.0.1',
npm ERR!   port: 31181,
npm ERR!   type: 'system'
npm ERR! }
npm ERR!
npm ERR! If you are behind a proxy, please make sure that the
npm ERR! 'proxy' config is set properly.  See: 'npm help config'

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\ABYTE\AppData\Local\npm-cache\_logs\2024-10-08T01_59_58_888Z-debug-0.log

解决方案:

该错误信息表明 NPM 在尝试下载 create-tauri-app 包时无法连接到 127.0.0.1:31181,导致 ECONNREFUSED 错误。这意味着你的机器拒绝了连接请求。常见的原因包括代理配置问题或网络连接问题。

以下是一些可能的解决方案:

  1. 检查代理设置:如果你在公司网络或其他需要代理服务器的网络环境中,请确保 NPM 的代理配置正确。使用以下命令查看代理配置:
npm config get proxy
npm config get https-proxy

如果配置了代理,但网络环境不需要代理,可以通过以下命令禁用代理:

npm config delete proxy
npm config delete https-proxy
  1. 清除 NPM 缓存:有时缓存可能会导致下载问题,可以尝试清除缓存:
npm cache clean --force
  1. 检查是否运行了本地 NPM Registry

错误信息中的地址 127.0.0.1 是本地回环地址,通常表示 NPM 正在尝试连接到你本机上运行的服务。如果你之前配置了本地的 NPM Registry 服务,但该服务没有启动或者端口号不正确,你可能会遇到此问题。

检查本地配置:

npm config get registry

如果 registry 配置为 http://127.0.0.1:31181,但你并没有运行本地的 NPM Registry,改为默认的 NPM Registry:

npm config set registry https://registry.npmjs.org/
  1. 检查网络连接

确保你可以正常访问外网,并且没有防火墙或其他网络安全设置阻止对 registry.npmjs.org 的访问。

通过命令行测试连接:

ping registry.npmjs.org

或者使用 curl 工具测试:

curl -I https://registry.npmjs.org/
  1. 更新 NPM 版本

如果以上步骤无法解决问题,尝试更新 NPM 版本以修复潜在的 bug:

npm install -g npm

如果问题仍然存在,请告知你所处的网络环境和其他相关配置,我可以进一步帮助分析。

PIP

安装

参考

通过 ensurepip 安装

Mac/Linux

python3 -m ensurepip --upgrade

Windows

C:> py -m ensurepip --upgrade

SSH

连接步骤

Windows 上,首先安装 OpenSSH,然后使用 PowerShell 连接

假设服务器名称为:SVRNAME,域名为:domain.com,账户为:zhangsan,默认端口 22 开放

一、连接服务器

ssh zhangsan@domain.com -p 22

若端口为 22 ,可不写,否则要指定端口

二、若连接成功提示如下提示输入密码,输入的密码不会显示,输入完成后按回车键(若提示连接失败,检查域名和端口是否正确,可使用IP地址等尝试)

zhangsan@domain.com's password:

三、成功后进入当前账号的家目录( ~: 表示家目录,$:表示非 root 账号),登陆时都是非 root 账号类型

zhangsan@SVRNAME:~$ 

此时可以查看当前家目录下的目录结构

zhangsan@SVRNAME:~$ ls

四、切换到 root 账号 (只有具有管理员权限的才可以切换成功)

zhangsan@SVRNAME:~$ sudo -i

五、再次输入密码

zhangsan@SVRNAME:~$ sudo -i
password:

六、输入成功后,zhangsan 变为 root, $ 变为 #

root@SVRNAME:~# 

使用 ls,提示 FOR SYSTEM USE ONLY. DO NOT UPLOAD FILES HERE.,后面再详细讲解。

root@SVRNAME:~#  ls

七、切换到根目录,任何时候都可以使用 cd / 切换到根目录

root@SVRNAME:~# cd /
root@SVRNAME:/# 

八、切换到家目录,任何时候都可以使用 cd ~ 切换到家目录

root@SVRNAME:/# cd /
root@SVRNAME:~# 

九、退出登陆,如果是 root 登陆状态,那么将退出到 非root账号登陆状态,再次退出才能完全退出登陆

exit

拓展

符号认知

  • ~ : 表示当前登录用户的 home 目录,实际就是代表一个文件夹的路径

  • / : 表示根节点

  • $ : 非 root 账户

  • # : root 账户

根目录(/)

根目录就是整个设备目录的最开始处,注意并不是某个存储盘的根,任何账户使用 cd /,都可进入根目录:

bin  boot  config  dev  etc  etc.defaults  initrd  lib  lib32  lib64  lost+found  mnt  opt  proc  root  run  sbin  sys  tmp  tmpRoot  usr  var  var.defaults  volume1  volume2
  • root 是 root 账号的家目录,也就是 ~# 对应的文件夹,此文件夹拒绝访问,可以看到它也是属于根目录下的一个文件夹而已

  • volume1 是 存储盘1

  • volume2 是 存储盘2

  • /$ : 非 root 账户在根目录下,注意 /$ 并不是连在一起的,如 /lib$ 表示在根目录的 lib 文件夹下

  • /# : root 账户在根目录下。

路径以 "/" 开头代表从根目录开始的绝对路径

家目录(~)

每个账户都有自己的 home 目录,它实际上也是对应了一个以根开始的目录,只是用 ~ 来代替根目录到家文件夹这段路径,使用 cd ~,都可进入根目录.

简要而言:~ 代表一个以用户的名称命名的文件夹路径,root 账号代表 /root 文件夹,非 root 账号代表 /某个盘/homes/账号名称 文件夹路径

root 的家目录

~# 等价于 /root

zhangsan 的家目录

~$ 等价于 /volume1/homes/zhangsan

注意:这里的家目录是设置在 volume1 存储盘上,其他可能不同

常用命令

  • sudo -i 切换到 root 登陆
  • cd / 进入根目录
  • cd ~ 进入家目录
  • cd .. 进入上级目录
  • cd ../.. 进入上级的上级目录,依次...
  • exit 退出登陆,如果是 root 登陆状态,那么将退出到 非 root 账号登陆状态,再次退出才能完全退出登陆

Docker

B站博主的网站-Docker 快速入门

连接服务器

连接Docker所在的服务器:前往,连接成功后即可使用 docker 命令

命令

Docker-菜鸟教程

文件路径

当使用某个文件时,除非使用绝对路径(以 根目录/开头的路径),都是以执行 docker 命令的目录开始查找。

以配置 bagetter 为例,使用了一个环境变量配置文件 bagetter.env,和文件夹的挂载 /vulome2/docker/bagetter/8008/data

目录结构如下:

/vulome2/docker/bagetter/8008/
    + data
    - bagetter.env

使用绝对路径

docker run -d --rm --name nuget-server -p 8008:8008 --env-file /vulome2/docker/bagetter/8008/bagetter.env -v "/vulome2/docker/bagetter/8008/data:/data" bagetter/bagetter:latest

使用相对路径,此时假设在 /vulome2/docker/bagetter/8008 下执行的 docker

docker run -d --rm --name nuget-server -p 8008:8008 --env-file bagetter.env -v "data:/data" bagetter/bagetter:latest
  • 注意 docker-compose,在官网上写的是 docker compose 无法执行。

实例-BaGetter

搭建包管理器

搭建私有 Nuget 仓库,BaGetter-github

以下流程基于群晖的 Docker 进行部署。

一、连接Docker所在的服务器

二、创建外部挂载的文件夹 /volume2/docker/bagetter/8008/data,如果不创建会报错找不到文件夹

备注:这里根据自己设定文件夹的位置

切换到根目录

cd /

创建目录(使用 -p 可创建多层)

mkdir -p /volume2/docker/bagetter/8008/data

切换到8008目录,这样比较方便

cd volume2/docker/bagetter/8008

三、在 /volume2/docker/bagetter/8008下创建环境变量配置文件 bagetter.env,并添加如下内容:

# The following config is the API Key used to publish packages.
# You should change this to a secret value to secure your server.
ApiKey=NUGET-SERVER-API-KEY

Storage__Type=FileSystem
Storage__Path=/data
Database__Type=Sqlite
Database__ConnectionString=Data Source=/data/db/bagetter.db
Search__Type=Database

# 这个需要在环境变量中进行配置,否则无效
ASPNETCORE_HTTP_PORTS=8008

主要点:

  • 自定义一个ApiKey,方便后期发布包时使用,若未配置会出现警告,但是也能上传包,若配置了必须携带key才能发布
  • 设置 ASPNETCORE_HTTP_PORTS否则端口映射失败

四、使用配置文件运行并创建容器,

docker run -d --rm --name nuget-server -p 8008:8008 --env-file bagetter.env -v "data:/data" bagetter/bagetter:latest

注意使用 -d 设置为后台运行,否则终端退出后,容器会移除。

此命令会检查本地是否存在镜像,若不处存在会先拉去,再创建。也可以先拉取。

搜索镜像

docker search bagetter

拉取镜像

docker pull bagetter/bagetter:latest

五、浏览包

如果在主机的windows或者linux系统上的docker环境,通过访问 http://你的域名:8008

如果在群晖上,还需要将开放 8008 端口,使用群晖的域名和 8008 端口访问.

六、通过反向代理实现https访问

无论是在浏览器中还是在 vs 中使用 http,都会显示很多警告,甚至出现无法访问的问题。所以最好配置 https

群晖中配置反向代理

发布包

一、设置包属性

包ID:在部署的仓库中必须是唯一的,设置好后,就不要更改了

标题:与ID相同即可

包版本:每次打包必须更新版本号,不能存在相同的版本号,版本规范,特定版本号的格式为 Major.Minor.Patch[-Suffix],其中的组件具有以下含义:

  • 主要:重大更改
  • 次要:新增功能,但可向后兼容
  • 补丁:仅可向后兼容的 bug 修复
  • -Suffix(可选):连字符后跟字符串,表示预发布版本(遵循语义化版本控制或 SemVer 约定)。

排序:

1.0.1
1.0.1-zzz
1.0.1-rc.10
1.0.1-rc.2
1.0.1-open
1.0.1-beta
1.0.1-alpha2
1.0.1-alpha10
1.0.1-aaa

作者、公司:自定义

产品:默认

说明(Description):包的简要说明

发行说明(PackageReleaseNotes):包的版本更新日志,可以直接填写文本,也可以从一个文件中去读取,在项目文件中进行配置

<PropertyGroup>
  <ChangeLogFile>$(MSBuildProjectDirectory)\CHANGELOG.md</ChangeLogFile>
  <PackageReleaseNotes>$([System.IO.File]::ReadAllText($(ChangeLogFile)))</PackageReleaseNotes>
</PropertyGroup>

二、设置许可证

许可证:可选择许可证表达式、填写MIT

二、打包

右键项目->打包,输出窗口查看打包信息,打包文件默认在: bin/Release/xxx.nuget

三、发布包

vs 中打开 powershell 终端,使用命令发布,将 NUGET-SERVER-API-KEY 替换为自己的Key(若搭建的库中没配置key,不需要key),package.1.0.0.nupkg 替换为实际的包名(注意路径)

dotnet nuget push -s http://xxxx:8008/v3/index.json -k NUGET-SERVER-API-KEY package.1.0.0.nupkg

四、删除包

注意:使用命令删除包,仅仅是将版本标记为 未列出 状态,也就是包的信息还在服务器的数据库中,这就导致无法再上传同名的包,由于bagetter没有提供管理界面,所以目前能想到的办法就是直接操作数据库文件进行修改。

打开 powershell 终端(注意:使用vs中的powershell会出现无法输入 y/n 的情况,因为删除的时候要求确认)。

假设打包的文件为:test-pkg.1.0.0.nupkg, 那么删除此版本的命令为:

dotnet nuget delete text-pkg 1.0.0 -s http://xxxx:8008/v3/index.json -k NUGET-SERVER-API-KEY 

先备份数据文件夹

将docker共享文件夹设置为可以在网上邻居上显示

使用sqlitestudio 连接数据库文件 bagetter.db

可以将 Packages 表中对应版本号的数据的版本号 改为一个测试版本号,也就是不删除,这样可以保持数据完整性,同时又可以重复上传同名的版本号

使用包

  • 在VS工具栏:工具->Nuget 包管理器->程序包管理器设置,打开设置窗口
  • 选择:程序包源标签
  • 点击右上角的 +
    • 名称:自定义
    • 源:http://xxxx:8008/v3/index.json (警告要求使用 https 很烦,可通过 反向代理服务器 实现https访问)
    • 点击更新保存
  • 使用时切换程序包源即可。

注意事项

  • 需要在环境变量中配置 ASPNETCORE_HTTP_PORT=8008 或者在群晖的界面设置中更改,否则在运行命令中设置的端口无效

  • 很多容器中的服务都不支持 https,通过群晖的反向代理来实现

  • 如果配置了 ApiKey, 发布包时要携带 Key

正则表达式

使用正则替换

使用正则表达式匹配字符串 [MemoryPackOrder(0)],其中的 0 可能是其他的数字

\[MemoryPackOrder\(\d+\)\]

这个正则表达式的各部分解释如下:

  • \[ 和 \]:匹配方括号(因为方括号在正则表达式中是特殊字符,所以需要用反斜杠转义)。
  • MemoryPackOrder:匹配这个具体的字符串。
  • \( 和 \):匹配圆括号,同样需要转义。
  • \d+:匹配一个或多个数字。

如果你需要支持负数或其他形式的数字,可以根据需要调整表达式。

Vscode 中使用

使用Pandoc转换world为md时,携带了宽高信息,需要删除

  1. 使用下面的匹配,只能匹配部分情况(可能是换行的原因)
\{width="([\d.]+)in"\s+height="([\d.]+)in"\}
  1. 通过分段来匹配其他情况

分段匹配

\{width="(\d+\.\d+in)"
height="(\d+\.\d+in)"\}

文件转换

MarkitDown

Markitdown

介绍

MarkItDown 是一款将各种文件转换为 Markdown 的实用程序(例如,用于索引、文本分析等)。它支持:

PDF 微软幻灯片软件 单词 Excel 图像(EXIF 元数据和 OCR) 音频(EXIF 元数据和语音转录) HTML 基于文本的格式(CSV、JSON、XML) ZIP 文件(迭代内容)

要安装 MarkItDown,请使用 pip: pip install markitdown。或者,您可以从源代码安装它:pip install -e .

使用总结

world 转 md

  • 能够很好的保留原文件的表格格式
  • 图片转为base64占位符,也就是丢失了,不晓得是不是需要使用大模型来转

在Python中使用

Python中的基本用法:

from markitdown import MarkItDown

md = MarkItDown()
result = md.convert("test.xlsx")
print(result.text_content)
# 输出文件
with open("output.md", "w") as file:
    file.write(result.text_content)
from markitdown import MarkItDown
from openai import OpenAI

client = OpenAI(api_key= "你的api_key")
md = MarkItDown(llm_client=client, llm_model="gpt-4o-mini") # 你拥有的模型
result = md.convert("11.png")

# 打印
print(result.text_content)

# 输出文件
with open("output.md", "w") as file:
    file.write(result.text_content)

命令行使用

根据官网的说法命令行使用失败,提示 markitdown 不是命令

通过使用 Python 来调用解决

python.exe -m markitdown test.docx > test.md

Pandoc

介绍

各种格式的相互转换

使用总结

world 转 md

  • 表格的格式混乱

  • 图片能够很好的提取出来

一般用法

安装好后,重新打开终端输入下面命令

pandoc .\test.docx -o test.md --extract-media=./

注意:如果不加上 --extract-media=./ ,不提取图片

合并转换

powershell

合并当前目录下的所有文档

 pandoc (Get-ChildItem -Filter *.docx).FullName -s -o output.docx
  • -s :单文件
  • -o: 输出路径

注意:如果输出为md,加上 --extract-media=./

合并指定文件

powershell

合并当前目录下的所有文档

 pandoc 1.docx 2.docx -s -o output.md --extract-media=./

注意:如果输出为md,加上 --extract-media=./

Imagemagick

图片格式转换

官网

安装

  • 按下图下载 alt text

  • 安装过程中,确保勾选了 “Install legacy utilities (e.g., convert)” 选项。这个选项确保你可以使用命令行工具(如 convert)进行操作。

  • 在安装时,还可以选择将 ImageMagick 添加到系统的 PATH 环境变量,这样你就可以在任何地方直接运行命令。

  • 如果在安装时没有选择将 ImageMagick 添加到 PATH 环境变量,或者你不确定是否已经配置,可以手动添加它:路径一般为: C:\Program Files\ImageMagick-x.x.x-Q16,其中 x.x.x 是你安装的版本号

使用示例说明

  • 注意:7版本和之前的版本有很大的不同,下面以最新7版本为例

  • 使用示例系统环境 Windows

  • 工具:PowerShell

  • 使用CMD失败

单张转换

转换webp图片为JPG图片

magick input.webp output.jpg

批量转换

批量转换当前目录webp图片为JPG图片(PowerShell):

magick mogrify -format JPEG *.webp

批量转换并删除源文件

foreach ($file in Get-ChildItem -Filter *.emf) {
    # 定义输出文件路径
    $outputPath = "$($file.DirectoryName)\$($file.BaseName).png"

    # 转换文件为 PNG 格式
    magick $file.FullName $outputPath

    # 删除原始 EMF 文件
    Remove-Item $file.FullName -Force
}

递归批量转换并删除源文件

指定源文件路径批量转换:

# 获取当前执行命令的目录
$sourceDirectory = Get-Location

# 递归查找所有 .emf 文件
Get-ChildItem -Path $sourceDirectory -Recurse -Filter "*.emf" | ForEach-Object {
    # 定义输出文件路径
    $outputFile = $_.FullName.Replace($_.Extension, ".png")

    # 使用 ImageMagick 转换文件
    magick  $_.FullName $outputFile

    # 如果转换成功,删除源文件
    if (Test-Path $outputFile) {
        Remove-Item $_.FullName
    }
}

在cmd中使用for批量转换(使用失败)

for /r %f in (*.emf) do magick "%f" "%~dpnf.png"

阿里云

域名和证书

  • 购买域名:根据官网来操作,可以个人/企业方式购买,购买域名后还需要认证(可能之前没认证过需要验证)
  • 云解析:将域名和云主机绑定,添加记录时,把 @(主域名,如 baidu.com)wwww(www前缀的域名,如www.baidu.com) 都添加上。

购买免费证书:

  • 先购买免费证书(一年20个免费的)
  • 创建证书
  • 填写域名,并根据提示设置,自动Dns验证等
  • 提交审核,注意这里点击下验证,是否成功,可以看到 需要填写 TXT 记录(注意这里有可能是CNAME记录),在我们的云解析里面看下是否有相关的一条 记录,没有就加上,我第一次操作的时候没自动加上,审核失败,然后第二次看这里自动多了条记录。如下图:

  • 审核通过 后出现了下载按钮,可根据选项下载

  • 证书的更新 :免费证书仅3个月的有效期,在将要过期前,我们可以再申请一个证书,替换将要过期的证书。

云服务器(ECS)

配置Nginx

如何下载并在服务器上安装SSL证书_数字证书管理服务(原SSL证书)(SSL Certificate)-阿里云帮助中心 通过数字证书管理服务购买并签发SSL证书后,您需要将已签发的SSL证书安装至服务器,才能使SSL证书生效。本文介绍如何下载及安装SSL证书。 https://help.aliyun.com/zh/ssl-certificate/user-guide/installation-overview?spm=a2c4g.11186623.0.0.7e0a3359kkfStu

详细看文档,注意以下几点:

  • 设置安全组,注意这里一定要选择地域,之前找不到这个地域在哪里,看半天才看到。

  • 主机的防护墙可以关闭

访问失败

部署环境

  • Nginx 代理
  • 阿里云ECS
  • 阿里云上的域名
  • 阿里云云解析
  • 阿里云上申请的免费证书

问题现象:

有的浏览器能访问,有的不能,偶尔能,偶尔不能。

售后回复

由于网站未备案导致,访问被拦截,详细内容:

网站托管在中国内地(大陆)的服务器上,您需根据所在省市的管局规则进行备案申请。当您使用阿里云中国内地(大陆)节点服务器时,您可以在PC端或移动端的阿里云ICP代备案系统中提交ICP备案申请,审核通过便可开通网站访问服务。

ICP备案流程

云存储(OSS)

配置访问凭证

官方文档

官网中存在多种访问凭证,此处介绍两种:使用AK(开发调试时使用)和使用ECSRAMRole(部署到阿里云ECS上后使用)

使用AK

⚠️ 警告 阿里云账号拥有资源的全部权限,AK一旦泄露,会给系统带来巨大风险,不建议使用。推荐使用最小化授权的RAM用户的AK。

如果您的应用程序部署运行在安全、稳定且不易受外部攻击的环境中,需要长期访问您的OSS,且不能频繁轮转凭证时,您可以使用阿里云主账号或RAM用户的AK(Access Key ID、Access Key Secret)初始化凭证提供者。需要注意的是,该方式需要您手动维护一个AK,存在安全性风险和维护复杂度增加的风险。如何获取AK,请参见CreateAccessKey - 创建主账号或RAM用户访问密钥

步骤:

  1. 创建用户:进入RAM 访问控制

  2. 添加创建用户,填写登录名称,访问方式必须勾选 “OpenAPI调用访问”

  3. 若勾选了“控制台访问”,会出现“设置密码”等多个选项(此处可不勾选)

  4. 确定有弹出安全验证,要求输入手机验证码

  5. 验证通过后,显示账号,记录ID和Sercret(后面将不会再显示)

  6. 如果使用失败,给账户添加访问OSS的权限

SDK使用示例:

using Aliyun.OSS;
using Aliyun.OSS.Common;
using Aliyun.OSS.Common.Authentication;

// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
var accessKeyId = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID");
var accessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET");

var credentialsProvider = new DefaultCredentialsProvider(new DefaultCredentials(accessKeyId, accessKeySecret, ""));

//  填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
const string endpoint = "https://oss-cn-hangzhou.aliyuncs.com";

// 填写Bucket所在地域对应的Region。以华东1(杭州)为例,Region填写为cn-hangzhou。
const string region = "cn-hangzhou";

var conf = new ClientConfiguration();

var client = new OssClient(endpoint, credentialsProvider, conf);
client.SetRegion(region);

使用使用ECSRAMRole

如果您的应用程序运行在ECS实例、ECI实例、容器服务Kubernetes版的Worker节点中,建议您使用ECSRAMRole初始化凭证提供者。该方式底层实现是STS Token。ECSRAMRole允许您将一个角色关联到ECS实例、ECI实例或容器服务 Kubernetes 版的Worker节点,实现在实例内部自动刷新STS Token。该方式无需您提供一个AK或STS Token,消除了手动维护AK或STS Token的风险。如何获取ECSRAMRole,请参见CreateRole - 创建角色。

创建角色步骤:

  1. 创建角色,打开RAM 访问控制-角色

  2. 点击“创建角色”,选择“阿里云服务”,下一步

  3. 选择“普通服务角色”,填写角色名称、备注(可选)、选择受信服务为:“云服务器”,点击完成

  4. 在角色列表,点击新增的角色名称,在授权管理中,点击新增授权,添加“AliyunOSSFullAccess”和“ AliyunECSFullAccess”的授权,创建角色完成。

授予实例RAM角色给ECS实例:

  1. 登录ECS管理控制台

  2. 在左侧导航栏,选择实例与镜像 > 实例。

  3. 在页面左侧顶部,选择目标资源所在的资源组和地域。地域

    alt text

  4. 找到要操作的ECS实例,选择图标 > 实例设置 > 授予/收回RAM角色。

    alt text

  5. 在对话框中,选择创建好的实例RAM角色,单击确定。

    alt text

SDK 使用示例


using Aliyun.OSS;
using Aliyun.OSS.Common;
using Aliyun.OSS.Common.Authentication;

class CredentialsProviderWrapper : ICredentialsProvider
{
    private Aliyun.Credentials.Client client;
    public CredentialsProviderWrapper(Aliyun.Credentials.Client client)
    {
        this.client = client;
    }

    public ICredentials GetCredentials()
    {
        var accessKeyId = client.GetAccessKeyId();
        var accessKeySecret = client.GetAccessKeySecret();
        var token = client.GetSecurityToken();
        return new DefaultCredentials(accessKeyId, accessKeySecret, token);
    }

    public void SetCredentials(ICredentials creds)
    {
    }
};

var config = new Aliyun.Credentials.Models.Config()
{
    Type = "ecs_ram_role",
    // 选填,该ECS角色的角色名称,不填会自动获取,但是建议加上以减少请求次数
    RoleName = "<RoleName>"     
};
var credentialsClient = new Aliyun.Credentials.Client(config);

var credentialsProvider = new CredentialsProviderWrapper(credentialsClient);

//  填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
const string endpoint = "https://oss-cn-hangzhou.aliyuncs.com";

// 填写Bucket所在地域对应的Region。以华东1(杭州)为例,Region填写为cn-hangzhou。
const string region = "cn-hangzhou";

var conf = new ClientConfiguration();

var client = new OssClient(endpoint, credentialsProvider, conf);
client.SetRegion(region);

总参考:通过ECS实例RAM角色授权ECS访问其他云资源

OSS绑定自定义域名

绑定自己的域名

默认域名仅能下载,无法预览,也就是是无法在线查看图片,播放视频(原因是默认的链接头部强制性的定义了触发下载操作)。通过绑定自定义域名来实现预览图片或者播放视频。

创建域名并绑定

  1. 说明域名必须备案,一个域名只能绑定一个 Bucket,假设为 名为 “mybucket” 的Bucket绑定域名

  2. 如果使用了阿里云的 ECS 云服务器,那么可以使用 ECS 云服务器的域名,来解析一个二级域名,比如当前注册的域名为 yuming.com,目前可以通过 yuming.com 或者 www.yuming.com 访问 ECS 云服务器上部署的网站,同时可以添加解析 abc.yuming.com 来绑定 oss 的 bucket.

  3. 在域名控制台添加解析,这里选择 CNAME,并添加一个二级域名 abc(这步优先在OSS绑定域名)

    alt text

  4. OSS 绑定域名 abc.yuming.com,注意不是绑定 yuming.com(现在域名控制台添加解析后,再操作这步)

    alt text

  5. 为资源设定 自有域名,然后通过URL地址可预览,注意只有在公共读的权限下才可以使用这个地址预览,否则要使用签名

    alt text

  6. 关于签名获取地址,通过 官网文档 获取各语言的接口

设置Https证书

现在的网站都是需要 https 来访问,如果不配置,将无法获取到资源,并提示: net::ERR_CERT_COMMON_NAME_INVALID

  1. 先在“证书管理页面”,选择“个人测试证书”

  2. 点击 “创建证书”,填写域名名称,这里填写 OSS 下 指定 Bucket 的域名,如:abc.domain.com,勾选“快捷签发”,选择所在地,选择你的服务所在的区域,提交审核,一般1分钟左右就通过了

  3. 回到OSS的“域名管理页面”,点击“证书托管”,点击“证书名称”下拉框选择证书,选择当前创建的证书即可。

  4. 注意证书有效期3个月,到期后,再来申请更新

设置防盗链(禁止使用链接下载)

使用场景

  • 禁止未经授权的使用:保护图片、视频、音频等敏感或有版权的文件不被未经授权的网站下载或访问。

  • 节省带宽和资源消耗:通过防止盗链,避免其他网站直接链接到自己的资源文件,减少了非法访问带来的带宽消耗和服务器资源压力。

  • 付费内容保护:在提供付费内容的场景中,防盗链可以确保只有购买者才能访问和使用付费内容,防止非法传播和盗版。

  • 合作伙伴授权访问:某些情况下,需要与合作伙伴网站共享资源或数据,通过防盗链可以限制只有特定合作伙伴可以访问和使用共享资源。

  • aliyun-oss-防盗链操作指南

  • 备注:若配置了防盗链,在 “Referer” 中添加本地 localhost:port 来调试

自建服务器

本文介绍自己搭建服务器步骤。

服务器主机

准备一台主机作为服务器,开发的程序在这个主机上运行,然后通过公网IP访问,如果在访问量不大的情况下,可将服务程序、数据库等都运行到此主机上。

域名

可以在网上找一些免费域名来使用,也可以在阿里云等网站上购买域名,购买域名后,需要将域名解析到公网IP,这样就可以通过域名来访问服务器了。此处介绍通过公云获取免费域名,并配置域名动态解析的过程。

获取公云上的免费域名

管理页面-公云pubyun

  • 提供免费域名
  • 提供动态域名解析
  • 在华为路由器中可使用3322的动态域名解析功能
  • 我们可以根据这个来创建自签名证书(开发),这个证书在访问时会提示警告,甚至禁止访问

缺点

不能使用它来创建 生产证书,因为需要 在域名的控制台添加 TXT 记录解析,这个免费的无法访问控制台。因此在访问的时候有警告提示。

在路由器上配置公云的DNS

根据使用的解析服务商,使用注册它们的用户,可以使用它们的免费域名,但是无法通过它们的域名生产证书,因为无法进入域名控制台配置解析记录

新增

列表

路由器配置虚拟服务器

配置端口映射,配置完成后需要 重启

NAT-DMZ

设置哪些主机(IP)对外开放

UPnP

这个要打开,群晖Nas要使用到。

Nginx 作为代理服务器

详细见: Nginx

Let's Encrypt 申请免费证书

入门指南 - Let's Encrypt - 免费的SSL/TLS证书 (letsencrypt.org)

安装

https://certbot.eff.org/

  1. 选择运行环境和操作系统,如下图,运行在Windows的Nginx 上,往下拉找到相关的下载链接

  1. 安装说明:Windows 只能获取证书,而不能直接安装到服务器上,配置还需要自己来操作,安装和运行都需要管理员权限,如果开始没使用管理员权限,再使用管理员权限安装一遍。
  2. cmd 和 power shell都支持运行
  3. 可以在非运行我的服务器的其他电脑上 运行 Certbot,获取证书后,再拷贝过去即可

运行

  • 管理员打开PowerShell 复制命令 ,将域名改为自己的,回车运行,这个命令是手动验证DNS,自动验证的方式经常失败,就按照这个来就好了
 certbot certonly -d "域名例子.com" --manual --preferred-challenges dns-01 --server https://acme-v02.api.letsencrypt.org/directory
  • 执行上面的,出现以下图示内容,需要添加 DNS TXT 记录,然后 回车继续,注意先要添加了才回车

  • 阿里云给域名解析 添加记录,注意这里我们在网上找的那些 免费域名 (如:公云或者花生壳这些)无法添加记录,因为我们无法控制它门,需要进入控制台的

https://dns.console.aliyun.com/#/dns/setting/

  • 点击添加记录

  • 填写记录配置,这里要根据上面的信息来配置,下面两个图对应好即可,点击确定,然后再shell上点回车继续即可。当我们获取到证书后,这个记录值好像是可以删除的。

  • 回车后应该准确的获取到证书了

证书位置:

c:\Certbot\live\abytesh.cn\下的对应两个文件。

待解决的问题

自动更新问题,因为上述的方式是手动的,所以不会自动更新,后续再研究。

Typora

Mac版本激活

csdn博客

  • 首先去官网选择mac版本下载安装 typora下载

  • 然后打开typora包内容找到 /Applications/Typora.app/Contents/Resources/TypeMark/

  • 编辑器打开上面文件夹,这里我拉到vscode,找到 page-dist/static/js/Licen.. 如下图

1717637565532

  • 输入 hasActivated="true"==e.hasActivated 搜索,将它改为 hasActivated="true"=="true"

  • 重新打开typora,看到成功激活:

    1717637552044

Xournal++

数位板手写程序,适合网课等讲解。

官网

alt text

Bulk Rename Utility

批量文件重命名工具

官网

完全免费!!!

命令行版本的 Bulk Rename Utiltiy (例如用于脚本),请到我们的网站下载 Bulk Rename Command

64 位版本的 WIndows (XP/2003/Vista),那么你将受益于 64 位版本的 Bulk Rename Utility

如果你扫描一个很大的文件夹遇到错误时想取消,请按 escape 键。

批量重命名实用程序允许您根据极其灵活的标准轻松重命名文件和整个文件夹,添加日期/时间戳、替换数字、插入文本、转换大小写、添加自动编号、处理文件夹和子文件夹......还有更多!

alt text

案例

文件名: 1.漫步人生路-邓丽君(Av268167852,P1).mp3 ,

  1. 去除后面的括号和括号里面的

在正则替换选项下,输入下面的表达式,替换保留空,注意,检查表达式是否有空格之类的,在网上复制的时候可能带有空格,导致匹配失败

\([^\)]*\)
  1. 去除前面的编号

正则表达式:

^\d+\.

有时候是有点扯拐,总是匹配不上

ExifTool

官网

照片元数据编辑命令行工具,比较冷门

ExifTool 是一个独立于平台的Perl 库,外加一个命令行应用程序,用于读取、写入和编辑各种 文件中的元信息。ExifTool 支持许多不同的元数据格式,包括 EXIF、 GPS、 IPTC、 XMP、 JFIF、 GeoTIFF、 ICC Profile、 Photoshop IRB、 FlashPix、 AFCP和 ID3、 Lyrics3,以及佳能、 卡西欧、 大疆 、 FLIR 、富士、 通用电气、 GoPro 、惠普 、 JVC /Victor、 柯达、 Leaf、 美能达/柯尼卡美能达、 摩托罗拉、 尼康、 任天堂、奥林巴斯/爱普生、 松下/徕卡、 宾得/朝日、 飞思、 Reconyx 、理 光、 三星、 三洋、 适马/Foveon和 索尼等品牌 的许多数码相机的制造 商说明。

Telegram

在Mac上无法登录问题

电脑端的应用在登录时二维码一直刷新,且代理已经开启了全局模式,仍然无法连接,点Log in by phone Number也显示连接失败。

alt text

打开蓝猫软件的控制台,在设置中找到端口号(一般是7890,若是Verge,端口为:7897)。

Clash X

Clash X

Clash Verge Clash Verge

打开TG,点击扫码登录界面的右上角的盾牌标签

add proxy

点击 Done后,等待连接成功,若代理一直再转圈,调整下Clash的相关设置看看

最后返回登录界面进行登录。

参考文章

Excel

定义选项列表

需要配置一个用户表,并导入数据库,在用户表中,“性别列” 希望通过下拉组合框来选取,当选择 “男” 和 “女” 选项,导入数据库时将其转换为对应的ID(1或2)。

使用隐藏列(推荐)

当前两个工作表 Sheet1Sheet2,在 Sheet2 中定义序列,在 Sheet1中使用。

Sheet2 中定义序列

1722309632314

选中 A1A2 (不要选中 B 列),工具栏:选择 公式->定义名称,输入 GenderList,然后点击确定

1722309864171

回到 Sheet1 ,选中整个 A 列,工具栏:数据->数据验证->在 “允许” 下拉菜单中选择 “序列”,在 “来源” 框中输入 =GenderList,点击 “确定”。

1722310224441

为了将选项转换为ID,可以使用 VLOOKUP 公式。假设你在 Sheet1 中使用下拉选项在 A1:A10,你可以在 B1 输入以下公式:

=VLOOKUP(A1, Sheet2!$A$1:$B$2, 2, FALSE) //注意这里因为只有“男”、“女”两个选项,如果有5个选项,应该为 =VLOOKUP(A1, Sheet2!$A$1:$B$5, 2, FALSE)

B 列的其他项通过复制 B1 单元格(鼠标按住下拉),然后将 B 列隐藏,在配置时比较简洁

1722310717241

导入数据库,导入数据库时使用 B 列数据,不要使用 A 列数据。

使用宏进行转换

你可以使用VBA宏在导入数据库前将“男”和“女”转换为对应的ID。

打开VBA编辑器

Alt + F11 打开VBA编辑器,在VBA编辑器中,点击 "插入" > "模块"。

输入宏代码

Sub ConvertGenderToID()
    Dim ws As Worksheet
    Dim lastRow As Long
    Dim i As Long
  
    Set ws = ThisWorkbook.Sheets("Sheet1") ' 替换为你的工作表名称
    lastRow = ws.Cells(ws.Rows.Count, "B").End(xlUp).Row
  
    For i = 1 To lastRow
        Select Case ws.Cells(i, "B").Value
            Case "男"
                ws.Cells(i, "C").Value = 1
            Case "女"
                ws.Cells(i, "C").Value = 2
        End Select
    Next i
End Sub

运行宏,按 F5 运行宏,它会将B列中的性别选项转换为C列中的ID。

导入数据库,导入 C 列中的数据到数据库。这样,你可以在Excel中显示“男”和“女”,但在导入数据库时将其转换为对应的ID。

Visual Studio

移动文件时启用命名空间更新

这个功能默认是启用的,但是某些情况下,不希望这个功能。

设置:菜单->工具->选项->项目和解决方案:移动文件时启用命名空间更新

包管理器

中文乱码

CSDN:https://blog.csdn.net/Michael_fchou/article/details/104349977

第一种情况:

VS的输出编码更改为UTF-8,按照图示安装UTF-8插件,若未解决问题,进行第二种操作

1722582982618


第二种情况:

如果已经装了UTF-8插件但是控制台输出的中文仍然是乱码。

  1. 打开电脑的控制面板

1717636919260

  1. 选择“时钟和区域”

1722583533082

  1. 选择“区域”

1722583560162

  1. 选择“管理”标签

1722583584907

  1. 选择“更改系统区域设置”

1722583601644

  1. 勾选“Beta版:使用 Unicode UTF-8 提供全球语言支持(U)”
  2. 最后重启电脑就ok了

Entity Framework

在VS包控制台执行 dotnet ef错误

dotnet : 无法执行,因为找不到指定的命令或文件。
所在位置 行:1 字符: 1
+ dotnet ef dbcontext scaffold "server=192.168.1.236;database=JzErp;uid ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (无法执行,因为找不到指定的命令或文件。:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
可能的原因包括:
  *内置的 dotnet 命令拼写错误。
  *你打算执行 .NET 程序,但 dotnet-ef 不存在。
  *你打算运行全局工具,但在路径上找不到具有此名称且前缀为 dotnet 的可执行文件。

原因:

从 3.0 起,EF Core 命令列工具 (dotnet ef) 不在 .NET Core SDK 里面,需另装。命令如下:

dotnet tool install --global dotnet-ef

安装成功如下

PM> dotnet tool install --global dotnet-ef
可使用以下命令调用工具: dotnet-ef
已成功安装工具“dotnet-ef”(版本“6.0.2”)

参考:http://www.manongjc.com/detail/28-rmfcuflorcnbrte.html

迁移

先创建迁移文件

dotnet ef migrations add xxxx

然后更新数据库

dotnet ef database update

项目文件加载异常

当出现以下问题时,通过删除 .vs 文件处理:

  • 解决方案加载异常
  • 项目文件加载异常
  • Asp.net.core 中,@page 名称正确,无法路由

查找替换功能失效

删除文件:

C:\Program Files\Microsoft Visual Studio\2022[Enterprise/Pro/Community]\Common7\IDE\CommonExtensions\Microsoft\Editor\ServiceHub\Indexing.servicehub.service.json

并重新启动Visual Studio

VS CODE

VS CODE 各语言的使用支持

下载

  • 下载地址:https://code.visualstudio.com/Download
  • 选择下载的版本,最好下载system版本
  • 国内加速下载方法,在浏览器中打开下载内容页面,选中下载的VsCode,右键选择复制下载地址,将地址中的 vscode.cdn.azure.cn 替换 az764295.vo.msecnd.net,使用新地址下载。

插件

GBKtoUTF8

自动将 GBK格式转化为UTF-8格式。进行Unity开发,开始用的VS,VS强大,但是太臃肿,用起来自身就占用了7-8个G,每次编译Uniyt 很耗时,换成 vscode,但是出现中文乱码,此插件解决此问题。

Markdown

边界和预览切换

ctrl+shift+v

设置 Markdown 中粘贴图片的位置

  • VS Code 中,按下 Ctrl + , ,打开设置界面。

  • 在搜索框中输入 markdown.copy , 找到 Markdown> Copy Files:Destination

  • 新增配置项 key 为 **/*.md , value 为 你的目标路径,比如我想将图片放在 assets 目录下markdown文件同名的目录下,那么我就可以设置为 assets/${documentBaseName}/${fileName}, 其中 ${documentBaseName}代表markdown文件的文件名,${fileName} 代表图片的文件名。

  • 可选择要为 当前用户 设置还是为 当前工作空间 设置 保存设置即可

创建指向另一个文件中的标题的链接

需要链接到另一个 Markdown 文档中的标题,但不记得或不想输入完整的文件路径?尝试使用工作区标题补全!首先,只需输入##Markdown 链接即可查看当前工作区中所有 Markdown 标题的列表:

针对当前工作区中所有 Markdown 标题的建议

接受以下某个补全操作即可插入该标题的完整链接,即使该链接位于另一个文件中:

alt text

版本:2023.1.3

使用教程

  1. 官网下载软件,安装
  2. 激活

插件

安装插件

  1. 打开Rider IDE。
  2. 转到菜单栏中的 “File” -> “Settings”(或者使用快捷键 Ctrl + Alt + S)。
  3. 在弹出的窗口中,选择 “Plugins” 选项。
  4. 在 “Plugins” 选项下,您会看到一个齿轮图标,点击它并选择 “Install Plugin from Disk”。
  5. 在弹出的对话框中,浏览并选择您下载到本地的插件文件(通常是以 .zip 或 .jar格式存储的)。
  6. 点击 “OK” 安装插件。
  7. 安装完成后,Rider会提示您重启IDE以应用插件的更改。选择 "Restart"以重新启动Rider。

插件推荐

播放器

编号 软件 说明
1 视频播放器:potplayer 外网访问
2 录屏软件:OBS Studio 开源,免费,可选择录屏来源,如某个程序,下载时有点慢,可以在群晖上下载

Markdown

设置字体风格

代码:

> ⚠️ <span style="color:red;font-weight:bold;">这是一条红色粗体的警告信息</span>

显示:

⚠️ 这是一条红色粗体的警告信息

表格

嵌入 Html 自定义表格(横向拉伸,固定每列的比列,设置为excel样式等)

代码

<style>
.table-container {
    display: flex;
    justify-content: center;
    width: 100%;
}

.excel-table {
    width: 100%;
    border-collapse: collapse;
    font-family: Arial, sans-serif;
    font-size: 12px; /* 设置字体大小 */
    table-layout: fixed; /* 固定表格布局  这样可实现表格第一列固定宽度,剩下的按比例自动拉伸*/
}

.excel-table th, .excel-table td {
    border: 1px solid #d0d7de;
    padding: 8px;
    text-align: left;
}

.excel-table th {
    background-color: #f0f3f5;
    font-weight: bold;
}

.excel-table tr:nth-child(even) {
    background-color: #f9f9f9;
}

.excel-table tr:hover {
    background-color: #e9e9e9;
}

.excel-table th:nth-child(1), .excel-table td:nth-child(1) {
    width: 30%;
    /*  width: 100px; 固定第一列宽度 */
}

.excel-table th:nth-child(2), .excel-table td:nth-child(2) {
    width: 20%;
}

.excel-table th:nth-child(3), .excel-table td:nth-child(3) {
    width: 50%;
}
</style>

<div class="table-container">
    <table class="excel-table">
        <thead>
            <tr>
                <th>列1</th>
                <th>列2</th>
                <th>列3</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td><a href="https://example.com">链接1</a></td>
                <td><a href="https://example.com">链接2</a></td>
                <td><a href="https://example.com">链接3</a></td>
            </tr>
            <tr>
                <td><a href="https://example.com">链接4</a></td>
                <td><a href="https://example.com">链接5</a></td>
                <td><a href="https://example.com">链接6</a></td>
            </tr>
        </tbody>
    </table>
</div>

效果

列1(宽100) 列2(宽40%) 列3(宽60%)
1 链接1
  1. 有序
  2. 有序
2 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容
  • 无序
  • 无序

图标

通过代码转换

图标网站 上查找要使用的图标,如 🍎 的 Unicode 编码为 U+1F34E,则与此表情对应的 code 为 1F34E,在 Markdown 文档中对应的代码为 &#x{code}; 既在 Markdown 文档中输入 &#x1F34E; 即可显示为 🍎。

常用图标

复制即可使用

⚠️❗❓❕❔❎❌ ❗ ‼️ ⁉️ ⭕✔️✅

❤️ ✨⭐ ❗❓❕❔ ✊✌️ ✋✋ ☝️

☀️ ☔ ☁️ ❄️ ⛄ ⚡ ⛅

☎️ ☎️ ⌛ ⏳ ⏰ ⌚ ➿ ✉️ ✉️ ✂️ ✒️ ✏️ ⚽ ⚾️ ⛳ ♠️ ♥️ ♣️ ♦️ ☕

⛪ ⛺ ⛲ ⛵ ⛵ ⚓ ✈️ ⛽ ⚠️ ♨️

1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 0️⃣ #️⃣ ◀️ ⬇️ ▶️ ⬅️ ↙️ ↘️ ➡️ ⬆️ ↖️ ↗️ ⏬ ⏫ ⤵️ ⤴️ ↩️ ↪️ ↔️ ↕️ ⏪ ⏩ ℹ️ ️ ️ ️ ♿ ㊙️ ㊗️ Ⓜ️ ⛔ ✳️ ❇️ ✴️ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ⛎ ❎ ️ ️ ️ ♻️ ©️ ®️ ™️ ❌ ❗ ‼️ ⁉️ ⭕ ✖️ ➕ ➖ ➗ ✔️ ☑️ ➰ 〰️ 〽️ ▪️ ▫️ ◾ ◽ ◼️ ◻️ ⬛ ⬜ ✅ ⚫ ⚪

目录树

案例

Models/
    ├── User.cs
    ├── Product.cs
    └── 文件夹
        ├── User.cs
        └── Order.cs
  • 每个层级缩进一个 tab
  • 非最后一个使用:├── 名称
  • 最后一个使用:└── 最后一个