tailwindcss
,而且在 Blazor
中集成 tailwindcss
的教程也很多本文使用一个更强大的 UnoCSS
,目前 Blazor
集成 UnoCSS
的教程却一个也找不到
这里记录一下如何在 Blazor
中集成 UnoCSS
,相信能给你带来更愉快的开发体验
本文对应源码:https://github.com/hal-wang/BlazorUnoCSS
本文均使用 pnpm,你也可以对应替换为 npm 或 yarn 等
UnoCSS
更强大好用在 Blazor
中,网上的教程都是使用 postcss
集成 tailwindcss
但 postcss
对 tailwindcss
支持度在 Blazor
中其实不太好
举个例子,比如 pt-30
、pt-30.2
,在 tailwindcss
中不可以使用,因为没有预设这个
但是根据本教程在集成 UnoCSS
后的 Blazor
却可以
因此按本教程集成 UnoCSS
后,不需要看着文档开发,自由度也更高
下面我们开始在 Blazor
集成 UnoCSS
在项目下执行语句,以创建 package.json
文件
1 | npm init -y |
在 package.json
文件中,增加 postcss
生成语句,用于生成 css
1 | "scripts": { |
安装依赖
1 | pnpm add @unocss/postcss |
此时 package.json
应该类似下面这样
1 | { |
增加文件 postcss.config.cjs
1 | // postcss.config.cjs |
增加文件 uno.config.ts
1 | // uno.config.ts |
增加或修改文件 wwwroot/app.css
1 | /* wwwroot/app.css */ |
现在如果执行语句 npm run buildcss
,会在 wwwroot/app.min.css
生成计算后的 css 文件
因此我们需要引用这个生成的文件
修改 App.razor 文件(有可能是其他文件,包含 html 头部基元信息)
增加
1 | <link rel="stylesheet" href="app.min.css" /> |
修改 Blazor 项目 .csproj 文件,增加 PreBuild
1 | <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> |
之后每次编译项目,都会自动执行语句 npm run buildcss
不需要再手动执行
GitHub: https://github.com/hal-wang/BlazorUnoCSS
]]>Span<T>
和 Memory<T>
能够以安全的方式使用指针访问内存,它们提供了一种类型安全的方法来访问任意内存的连续区域。他们表示连续的内存块,没有任何复制语义,类似于指针。
另外还有只读版本 ReadOnlySpan<T>
和 ReadOnlyMemory<T>
C#
允许以不安全的方式使用指针,类似 C/C++
。虽然效率高,但指针不被 GC
跟踪,容易造成内存泄漏
为此在 C# 7
中引入了新的类型
Span<T>
以类型安全的方式表示内存的连续部分Memory<T>
连续的内存区域ReadOnlySpan<T>
与 Span<T>
类似,但内存区域是只读的ReadOnlyMemory<T>
与 ReadOnlyMemory<T>
类似,但内存连续区域是只读的C# 7
对应的 .net
版本是 .NET Core 2.1
。在后续更新中逐渐增强完善
截至目前 C#
版本是 C# 12
,因此本文内容也是以 C# 12
为基础
Span<T>
是值类型的 ref struct
,定义类似于
1 | public readonly ref struct Span<T> |
ref struct
和 struct
相比有一些使用限制
ref struct
的字段的声明类型System.ValueType
或 System.Object
Lambda
表达式或本地函数捕获async
方法中使用。 但是,可以在同步方法中使用 ref struct
变量,例如,在返回 Task
或 Task<TResult>
的方法中。Span<T>
的索引器是使用 ref T
声明的,因此索引器返回的是实际存储位置的引用
1 | public ref T this[int index] |
Memory<T>
表示内存中一段连续的区域,对应的只读版本为 ReadOnlyMemory<T>
。
Memory<T>
很多用法与 Span<T>
相似
Memory<T>
实现原理类似如下
1 | public readonly struct Memory<T> |
Memory<T>
的很多操作与 Span<T>
类似,可以通过数组创建 Memory<T>
并进行切片。
Memory<T>
不是 ref struct
类型,因此 Memory<T>
可以在存储在堆,所以有不同于 Span<T>
的特性
Memory<T>
有个 Span
属性,可以获取 Span<T>
并处理
有了 Span<T>
为什么还要 Memory<T>
?
由于 Span<T>
无论何种情况,只能存在于栈中,因此 Span<T>
的使用限制比较多。
尤其是 Span<T>
无法在异步函数中使用,因此限制较少的 Memory<T>
更适用于这种场景。
1 | static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream) |
stackalloc
能在堆栈上分配内存块,分配的内存块不会被 GC
自动回收,生命周期仅限当前方法内
默认变量类型为指针,是不安全代码。但是可以将变量赋值给 Span<T>
或 ReadOnlySpan<T>
即为安全代码
1 | unsafe |
1 | Span<int> nums = stackalloc int[10]; // safe |
可以和定义数组一样赋初值
1 | Span<int> first = stackalloc int[3] { 1, 2, 3 }; |
Span<T>
的应用场景很广,这里只举例字符串,感受一下 Span<T>
的强大
Span<T>
对字符串的切片效率很高,在内存中不需要创建临时的字符串
比如对用户输入的表达式进行计算,这里仅简单的处理加法如 11+22
使用 string.SubString
,会在内存中生成临时字符串
如果在循环中执行次数很多,就会在堆中生成很多临时字符串
1 | var text = "11+22"; |
不会产生临时字符串,而且切片效率会高很多,没有给 GC 增加压力
1 | var text = "11+22"; |
List<T>
自增会造成 Span<T>
引用的位置错误,因此是这里是一个坑
先创建容量为 10 的 List<int>
,并赋初值,使用 CollectionsMarshal
创建一个 Span<int>
1 | var list = new List<int>(10); |
到这里,span 就是 list 中元素的所在的内存段,对 span[i]
的读写都和 list[i]
的读写都是操作同一段内存数据,一切正常
现在我们给 list 增加一个元素,list 长度超过容量 10,因此容量会自增到 20
1 | list.Add(10); |
此时,span 指向的仍然是原来的 list 内存段,但现在 list 已经通过自增变为了另一个新的内存段
那么现在 span
和 list
两个变量就不相干了,给 span[i]
和 list[i]
赋值都互不影响,span
就失去了作用和意义
1 | span[0] = 100; |
或
1 | list[1] = 100; |
1 | sudo add-apt-repository ppa:certbot/certbot |
这里是通过手动 dns 的方式验证域名
执行语句
1 | sudo certbot certonly --manual --preferred-challenges dns |
首次执行会让你输入安全邮箱
1 | Enter email address (used for urgent renewal and security notices) |
然后同意几个协议,都输入 Y
并回车
同意协议后,需要输入域名,用空格分隔
1 | Please enter the domain name(s) you would like on your certificate (comma and/or |
输入域名后,等待验证 dns
1 | Please deploy a DNS TXT record under the name: |
这时先按提示在云服务商添加 dns 的 TXT 记录,稍等片刻再回车
成功提示如下
1 | Successfully received certificate. |
证书位置在
1 | /etc/letsencrypt/live/domain.com |
Let’s Encrypt 创建的证书可能会出现证书链不完整的错误
需要修复一下证书链
浏览器打开 https://myssl.com/chain_download.html
选择 上传证书
编辑证书文件,将文件内容拷出来,粘贴到网页,再点击 获取证书链
,即得到修复后的证书内容
书中演示代码主要以 JavaScript 为主
书中提到一些代码的坏味道
命名是编程中最难的两件事之一。正因为如此,修改命名可能是最常用的重构方法。如果你发现改名很难,那就说明代码设计有问题。当我们不能给一个模块,一个对象,一个函数,甚至一个变量找到合适名称的时候,往往说明我们对问题的理解还不够透彻,需要重新去挖掘问题的本质,对问题域进行重新分析和抽象。
同一类的两个函数含有相同的表达式,就应该提炼。
活得最长,最好的程序,其中函数一般都很短。如果你觉得需要写注释,大部分情况就代表这个东西需要写进一个独立的函数里面,然后根据用途来命名比较好。条件表达式和循环往往也是提炼函数的信号。
使用类可以有效的缩短参数列表。如果多个函数有同样的几个参数,引入一个类就尤为有意义。
全局数据仍然是最刺鼻的坏味道之一。它的问题是,全局数据在任何地方都可以被修改。所以正确的做法是将全局数据封装起来,用函数将其包起来,这样就知道那些地方修改了它。有少量的全局数据或者无妨,但数量越多,处理难度就会指数上升。(良药与毒药的区别在于剂量)
核心是缩小作用域。可以通过封装变量来确保所有数据更新操作都通过很少几个函数来进行,使其更容易被监控。
发散式变化指某个模块因为不同的原因在不同的方向上发生变化。每次只关心一个上下文。找到引起发散式变化的原因,将它拆分出来。
在每次修改的时候,应该只修改一处,而不是到处的修改。因为一个需求,需要修改 3 处代码,那么这就需要思考,这 3 处代码是否应该抽离出来。一个常用的策略就是使用内联(inline)重构代码把本不该分散的逻辑拽回一处。
模块化,力求代码分出区域,最大化区域内部交互,最小化区域间交互。如果两个模块交互频繁,它们应该合并在一起。
如果在多个类中,出现了很多相同项的数据,你需要想想是否要通过将数据提炼成类,来抽离出一个独立对象。建议新建类而非简单的结构体。
很多程序员不愿意创建对自己的问题域有用的基本类型,如钱,坐标,范围等。比如有程序员用字符串来表示电话号码,实际上你应该抽象出来一个电话号码对象。
尽量使用多态而非 switch。
我们应该用管道操作(如 filter 和 map)来替代循环,这样能更快的看清被处理的元素和处理他们的动作。
能简单的代码,尽量简单。未来变复杂的时候,再去考虑它。
同上,能简单的代码,尽量简单。通用性?过早的优化是万恶之源
临时字段指内部某个字段仅为某种特定情况而设。临时的字段不应该存在。你需要给他们搬个新家,把所有和临时变量相关的代码搬至那里。
如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象,这就是消息链。消息链意味着客户端会耦合消息链的查找过程。应该将查找过程独立出一个函数。
委托函数过多时,减少委托,移除中间人,让调用者直接访问目标类进行操作。
减少模块之间频繁的数据交换,并把这种交换放到明面上。
当一个类代码行数太多或者功能职责太多的时候,拆掉它。两种拆分方法:提取新类,当大类的部分行为可以分解为一个单独的组件,则可以使用提取类的方式拆分。提取子类,当大类的部分行为可以以不同的方式实现或在极少数情况下使用,则可以使用提取子类方式拆分。
两个类有着相同的功能,但方法名称不同。重命名方法,并去除掉不必要的重复代码。
纯数据类常常意味着行为被放在了错误的地方。处理数据的行为应该从客户端移至纯数据类中。
如果子类复用了父类的实现,就应该支持父类的接口。
注释是提示你,这个地方该重构啦。如果你觉得需要写注释的时候,请先重构,试着让所有注释都变得多余。
将独立逻辑的一段代码,提炼为一个函数
1 | function printOwing(invoice) { |
提炼函数的动机:将意图与实现分开
将函数中的代码直接写在调用处
1 | function getRating(driver) { |
分解复杂表达式,用局部变量分解表达式
1 | return ( |
消除表现力不好的变量
1 | let basePrice = anOrder.basePrice; |
1 | return anOrder.basePrice > 1000; |
函数改名,函数名应表达其作用
1 | function circum(radius) {...} |
1 | function circumference(radius) {...} |
避免直接修改全局数据,以函数形式封装对该数据的访问
1 | let defaultOwner = { firstName: "Martin", lastName: "Fowler" }; |
1 | let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" }; |
将变量名改为易解释的名称
1 | let a = height * width; |
1 | let area = height * width; |
将一组参数,合并为一个对象进行传参
1 | function amountInvoiced(startDate, endDate) {...} |
1 | function amountInvoiced(aDateRange) {...} |
用面向对象的的方式,组合一组函数
1 | function base(aReading) {...} |
1 | class Reading { |
将一组函数组合为一个函数,返回结果组合成对象
1 | function base(aReading) {...} |
1 | function enrichReading(argReading) { |
将一段代码,根据处理的事不同,拆分为多个函数
1 | const orderData = orderString.split(/\s+/); |
1 | const orderRecord = parseOrder(order); |
随 Visual Studio 2012 发布
1 | async Task Func() { |
可以通过特性,在函数参数,取得调用方信息
随 Visual Studio 2015 发布
可以 using 引入类的静态成员
1 | using static namespace.Class; |
引入后可直接访问该类中的静态成员
可以给引入的命名空间取别名
1 | using name = namespace.subnamespace; |
支持异常的条件过滤
1 | try |
为属性赋初始值
1 | public static int Id { get; set; } = 123; |
用 Lambda 表达式定义类成员
只读属性
1 | public static DateTime Time => DateTime.Now; |
get/set 属性
1 | public string Name |
无返回值方法
1 | public void Func(int id) => this.Id = id; |
有返回值方法
1 | public int Func(int id) => id + 1; |
构造函数
1 | public class Location |
运算符 ?.
和 ?[]
1 | var title = user?.todos?[0]?.title; |
用 $
标识的字符串,其中 {}
内可写变量或表达式
1 | var name = "hal.wang"; |
获取变量的名称
1 | var user1 = new User(); |
随 Visual Studio 2017 发布
简化之前的 out 变量写法,可以不用单独定义变量
以前的写法为
1 | var intput = "123"; |
现在的写法可以是
1 | var intput = "123"; |
升级 Tuple 写法
1 | (double, int) t1 = (4.5, 3); |
1 | (double Sum, int Count) t2 = (4.5, 3); |
1 | var t3 = (Sum: 4.5, Count:3); |
类型相同可赋值
1 | (double, int) t1 = (4.5, 3); |
判断相等
1 | (int a, byte b) left = (5, 10); |
用 is
和 is not
判断变量类型,也可以判断 null
1 | var i = 1; |
比较离散值(switch)
1 | public string Func(string command) => command switch |
关系模式(switch)
1 | public string Func(int num) => num switch |
函数内部定义函数
用 _
命名的变量,可重复
默认参数可指定参数名传参
1 | public void Func(int arg1 = null, int arg2 = null) |
将值类型声明为引用类型
1 | int number = 6; |
是专门面向 .net core 的第一个主要 C# 版本
可以不止是约束,也可以实现完整的方法
增强 switch 的模式匹配
可以匹配对象中的属性
1 | public class Cls |
可以匹配元组
按属性位置取,组成元组,然后匹配
using 块如果范围为整个函数,可以省略大括号
本地函数可以声明为静态的
可以用 ref 声明 struct,表示此 struct 的实例对象都是引用类型
更灵活的可空特性
通过 ?
为字段、属性、方法参数、返回值等添加是否可为 null 的特性
可以返回异步版本的迭代器
1 | async IAsyncEnumerable<int> GetList() |
增加 IDisposable 的异步版本 IDisposableAsync
接口函数为 DisposeAsync()
1 | public class Cls : IAsyncDisposable |
可以指定数组范围
1 | var nums = new int[]{ |
声明范围
1 | int[] nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]; |
语法 ??=
如果左侧不为空,直接返回左侧
如果左侧为空,则将右侧赋值给左侧,然后返回左侧
1 | // WPF 常用 |
可以结合 $
和 @
声明字符串,结合二者功能,声明顺序不分先后
1 | var text = $@"abc\de{12}"; |
随 .NET5 一起发布,是面向 .NET5 版本的任何程序集的默认语言版本
用 record
声明的类型
是新的引用类型,相当于引用类型的 struct
与类的区别是,record
可以使用基于值的相等性
可以简单声明只读记录
1 | record Personal(string FirstName, string LastName); |
也可以用 init
声明可初始化的记录,只能在构造函数中或初始化类时赋值
1 | public record Personal |
也可以声明可读写记录
1 | public record Personal |
同类 record 的两个实例对象,只要属性值都相同,==
运算符就为 true
运行时类型必须相等,派生类型也不行
用 with
基于已有记录,新建一条记录
1 | Personal person1 = new("n1", "n2"); |
.ToString 会格式化属性名和属性值
1 | DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 } |
用 init 替换 set 声明属性,只能在构造函数或初始化时设置属性值
不用 Main 方法和 namespace,直接写函数体代码
改进模式匹配
and
模式要求两个模式都匹配or
模式要求任一模式匹配not
模式要求模式不匹配1 | public static bool IsLetter(this char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'; |
1 | public static bool IsLetterOrSeparator(this char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','; |
已知类型 new
表达式中可以省略类型
1 | private List<WeatherObservation> _observations = new(); |
允许对 lambda 和匿名方法,使用 static
修饰符
1 | Action act = static () => { |
允许 foreach 循环识别扩展方法 GetEnumerator
1 | public class PeopleExtensions{ |
类内部需要实现迭代器功能,即包含 Current
和 MoveNext
可以用 _
作为 lambda 和匿名方法的参数
(_, _) => 0
, (int _, int _) => 0
delegate(int _, int _) { return 0; }
对于 partial
类,可以在一处声明,在另一处实现
1 | partial class D |
.NET 6 的默认语言版本为 C# 10
扩展之前的 record 记录
record struct
声明值类型record class
声明引用类型readonly record struct
声明只读值类型1 | record struct Person |
1 | record struct Person(string name) |
一次引入,整个项目可用
1 | global using System.Math; |
用新方式声明 namespace 后,整个文件中的类都是属于这个命名空间
可以减少一组大括号,增加易读性
1 | namespace CustomNamespace; |
模式匹配可以用嵌套的属性或字段
1 | object people = new People |
1 | var fn1 = [CustomAttribute] () => { }; |
1 | var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b; |
1 | var inc = [return: NotNullifNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null; |
如果内插变量都是 const 修饰的,那么内插字符串也可以用 const 修饰
1 | const string str1 = "s1"; |
用 sealed
修饰 ToString
,子类不能再覆写 ToString
.NET 7 的默认语言版本为 C# 11
可创建泛型类并继承 Attribute
,实现泛型特性
1 | public class GenericAttribute<T> : Attribute { } |
1 | [ ] |
泛型类型必须完全构造,不能有任何参数
string?
/int?
等可空引用类型,应使用 string/int 等(int X, int Y)
,可使用 ValueTuple<int, int>
1 | public class GenericType<T> |
在字符串内插中 { }
, 允许换行
1 | var str = $"abc{ |
可以将数组或列表与模式的序列进行匹配
1 | int[] numbers = { 1, 2, 3 }; |
弃元可以匹配任何值
1 | int[] numbers = { 1, 2, 3 }; |
1 | List<int> numbers = new() { 1, 2, 3 }; |
在模式匹配中可使用切片模式
切片可匹配 0 个或多个元素,最多出现一个切片
1 | Console.WriteLine(new[] { 1, 2, 3, 4, 5 } is [> 0, > 0, ..]); // True |
用 3 个双引号 """
开头和结尾
1 | string longMessage = """ |
用 u8
后缀修饰字符串字面量,可指定 UTF-8 编码
1 | ReadOnlySpan<byte> AuthWithTrailingSpace = new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 }; |
用 required
修饰属性,要求必须初始化该属性
1 | public class Person |
如果用构造函数初始化,需要用 SetsRequiredMembers
特性修饰构造函数
1 | public class Person |
用 file
修饰类型,限制该类型的可见性为文件内
1 | file class HiddenWidget |
在其他代码文件的任何命名空间中,即使在部分类中,都无法访问该类型
.NET 8 的默认语言版本为 C# 12
不再局限于 record
类型,也可以在 class
和 struct
中使用主构造函数
1 | public class ExampleController(IService service) : ControllerBase |
可以使用展开运算符 ..
将集合中的元素与其他集合内敛
1 | int[] arr1 = [1, 2, 3]; |
类似 js
中对数组操作的展开运算符 ...
可以为 Lambda 表达式的参数定义默认值,与普通方法相同
可以用 using
指令为任意类型创建别名,类似 ts
中的 type
1 | using CustomType = (int X, int Y); |
1 | using CustomList = System.Collections.Generic.List<int>; |
内联数组是包含多个元素的连续块的结构,适用于高性能场景
System.Runtime.CompilerServices.InlineArrayAttribute
特性修饰 structSystem.Runtime.CompilerServices.InlineArrayAttribute
参数必须大于 01 | [ ] |
1 | var buffer = new Buffer(); |
能够根据输入坐标,创建飞线
示例代码均为 ts,效果如图
用于封装飞线相关内容,完成后可方便被调用,最后面有完成的完整代码
构造函数接收 Scene 对象,用于添加模型
包含两个 public 方法
1 | import * as THREE from 'three'; |
此部分根据输入坐标创建飞线模型,并使其动起来
使用 d3
转换
1 | import * as d3 from 'd3'; |
center()
函数传入地图中心经纬度
把这个方式封装一下
1 | export class LineRender { |
此后使用方式如 this.projection(lon, lat)
根据官方解释,由于 opengl 的限制,webgl 渲染器在大部分平台上会无视 lineWidth
Due to limitations of the OpenGL Core Profile with the WebGL renderer on most platforms linewidth will always be 1 regardless of the set value.
我们改为使用 Line2
首先导入相关包并创建模型,使用 Line2 的方式如下
1 | import { Line2 } from 'three/examples/jsm/lines/Line2'; |
按以上方式,我们要创建三维二次贝塞尔曲线
1 | export class LineRender { |
有两种容易实现的方式让线动起来
这里选的实现方式二
首先实现颜色的渐变,根据输入的颜色,生成一组渐变色
1 | export class LineRender { |
然后修改创建飞线的代码,让飞线填充渐变色
1 | export class LineRender { |
1 | import * as THREE from 'three'; |
通过改变渐变色的方式让线看起来在移动
每次将颜色数组的最后一个颜色(r + g + b 三个数字),提到最前面
这里由于动画较快,通过 _tick
降低了动画的速率
1 | export class LineRender { |
创建函数 addLines
并在 render
函数中调用
1 | export class LineRender { |
在飞线的开始和结尾处,有两个扩散动画的点,我们要增加点模型并让其动起来
创建函数 spotCircle
,根据坐标创建大小两个圆形
1 | export class LineRender { |
修改前面的 addLines
函数,每创建一根飞线就创建两个动画点
1 | export class LineRender { |
通过放缩点的大小,并改变其透明度,可实现动画效果
增加函数 pointAnimate
,并在动画函数中调用
1 | export class LineRender { |
按以上步骤,最终实现的 LineRender
类代码如下
1 | import * as THREE from 'three'; |
在调用处可以很方便的根据经纬度生成飞线
初始化时,执行如下代码
1 | const lineRender = new LineRender(scene); |
在动画函数中,执行如下代码
1 | lineRender.animation(); |
1 | <script lang="ts" setup> |
此示例的数据需按如下格式,将生成三条飞线
1 | export const data: [[number, number], [number, number]][] = [ |
Git LFS 是一个易于安装、易于配置,使用高效的 Git 拓展工具,它能有效的管理仓库中的大文件,避免仓库体积过大,影响项目管理效率
这段是废话,既然看到了这里,说明你应该已经需要用 LFS 了,跳过吧!
GitHub
, Gitee
, GitLab
, 云效等GitHub
目前限制文件不能超过 100MB
使用 LFS 不仅能解决大文件带来的仓库问题,同时托管服务也对大文件做了传输速度优化
按以下操作给已有仓库开启 LFS
1 | git lfs install |
.gitattributes
:1 | git lfs track "xxx/file.mp4" |
1 | git add -A |
之后就和普通仓库一样操作即可
可以重复执行以上命令
1 | git lfs track "xxx/file.mp4" |
也可以手动修改文件 .gitattributes
添加文件,可以和 .gitignore
一样使用通配符
如果之前的提交有大文件,在不回退提交的前提下,删除文件是不会在 git 中删除大文件的
可以用以下方式修改历史提交,彻底删除大文件
1 | du -ah .git/object |
如果在 Windows 系统无法执行上述语句,可以用 sh
命令切换到 git bash 中执行,后续命令同样如此
1 | git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -5 | awk '{print$1}')" |
1 | git filter-branch --force --index-filter 'git rm -rf --cached --ignore-unmatch 你的大文件名' --prune-empty --tag-name-filter cat -- --all |
1 | rm -rf .git/refs/original/ |
1 | git push origin main --force |
如果该分支被保护,需要在托管网站解除保护才能 force
提交
但是,如果设计框架或封装组件,可能就需要了解原型链
ES6 的类,可以认为是 ES5 的语法糖,因此本文主要以探究 ES5 为主
以 C# 为例,C++/JAVA 等类似
在 C# 中,类相当于一个模板,对象是模板创造的实例。代码编译后,类本身除了静态属性和静态函数,没有其他用处
在 JS 中,ES5 及之前是没有类的概念,对象是通过构造函数创建的,即构造函数承载了类的功能,在构造函数中可以使用 this 为实例对象增加字段和函数,以及赋值的操作。这点对于有其他语言基础的人来说,思想较难转变
构造函数其本身也是对象,也可以当成一个普通函数来用
1 | function Func(){} // 构造函数 |
JS 通过构造函数创建对象,会有以下对象参与其中
一般每个构造函数都有一个原型对象 Func.prototype
,简单函数的原型对象是 Object
对象
一般每个对象都有一个对象原型 obj.__proto__
,这个对象原型指向的是构造函数的原型对象,即
1 | function Func(){} // 构造函数 |
由于 JS 中调用对象方法时,先查找对象自身方法,再查找原型对象 __proto__
中的方法,因此对象可以使用对象原型中的方法
原型对象也可以置空,这样构造函数创建的对象将没有额外方法,如 toString
, hasOwnProperty
等
原型对象都有 constructor
字段,指向对应的构造函数
由前面得出结论,对象和对象原型 __proto__
形成了一个原型链,原型链的最顶端是 null
,形如
1 | obj.__proto__.__proto__.__proto__ == null // true |
上面可能有很多节点,也可能没有节点,取决于创建方法
类每多一次继承就会多一个节点,ES5 中的写法是给构造函数的 prototype
赋值并修改 prototype.constructor
1 | function Father() {} // 父类构造函数 |
下面写法会创建一个没有原型链的顶层对象,一般不会用到
1 | const obj = Object.create(null, {}) |
每个对象,都可以使用原型链中的任意方法,因为调用方法时会按原型链逐级向上查找
ES6 之前通过 构造函数 + 原型
实现面向对象编程
ES6 通过 类
实现面向对象编程
类的本质也是函数,也可以简单的认为,类就是 ES5 构造函数的简单写法
]]>惰性匹配是在 *
, +
, {m,}
后加上 ?
*
: 匹配前面 0 次或以上,尽可能多+
: 匹配前面 1 次或以上,尽可能多{m,}
: 匹配前面 m 次或以上,尽可能多*?
: 匹配前面 0 次或以上,尽可能少+?
: 匹配前面 1 次或以上,尽可能少{m,}?
: 匹配前面 m 次或以上,尽可能少字符串
1 | 1a-2b-3c-4d-e5-f6 |
除了 -
外,其他字符串都是不定长的,而且字符串也可以是其他除 -
外的字符甚至特殊符号
^.+\-
1a-2b-3c-4d-e5-
^.+?\-
1a-
前面的例子,如果想匹配后面的 -f6
,你可能会这样用
1 | \-.+?$ |
但匹配结果是 -2b-3c-4d-e5-f6
而不是 -f6
,和贪婪匹配结果一样
这是因为正则匹配是从前往后,当匹配到 -2
时发现匹配了一部分,就会继续向前查询 -2d
> -2d-
> -2d-3
> -2d-3c
等,直到查询 -2b-3c-4d-e5-f6
才找到满足条件的值
为了解决这个问题,可以用排除法,即排除前面的 -
1 | \-[^\-]+$ |
在代码中,如果想去除字符串前面一部分,或者字符串后面一部分,可以用 正则 + 替换
的方式
文件名
1 | image.png |
1 | \..+$ |
1 | const file = "image.png" |
.
1 | image.1.png |
按上面的写法只能取到 image
而不是 image.1
这样做保留的文件名更完整
1 | \.+[^\.]*$ |
1 | const file = "image.1.png" |
1 | ^.*\. |
1 | const file = "image.png" |
也称为零宽度断言,环视可以根据某个模式之前或之后的内容,要求匹配其他模式
匹配且要求紧随其后的内容为分组匹配的内容
1 | ?=分组 |
如 [a-zA-Z](?=\d)
,若字母后
是数字则匹配该字母 ,否则不
匹配,即 [a-z]
后
必须匹配 \d
对正前瞻含义取反,即匹配且要求紧随其后的内容不为分组匹配的内容
1 | ?!分组 |
如 [a-zA-Z](?!\d)
,若字母后
是数字则不
匹配该字母 ,否则匹配,即 [a-z]
后
必须不
匹配 \d
即对正前瞻方向取反,匹配且要求紧挨着之前的内容为分组匹配的内容
1 | ?<=分组 |
如 (?<=\d)[a-zA-Z]
,若字母前
是数字则匹配该字母 ,否则不
匹配,即 [a-z]
前
必须匹配 \d
即对正前瞻方向取反,匹配且要求紧挨着之前的内容为分组匹配的内容
1 | ?<!分组 |
如 (?<!\d)[a-zA-Z]
,若字母前
是数字则不
匹配该字母 ,否则匹配,即 [a-z]
前
必须不
匹配 \d
子匹配可以被引用,使用 \n
访问
如 abcd<custom-button>link</custom-button>efg
匹配 custom-button
标签和其中的内容
1 | <(custom-button)>.*</\1> |
Vue3
+ Vite
+ Pinia
+ TS
入门项目源码:https://github.com/hal-wang/vue3-vite-ts-template
1 | git clone https://github.com/hal-wang/vue3-vite-ts-template.git |
文中都是用 yarn,如果你使用 npm,可以相应替换
1 | yarn create @vitejs/app |
按提示,输入项目名
选择 vue -> vue-ts 模板
进入项目,在项目目录下执行
1 | yarn install |
1 | yarn dev |
如果你不需要区分多个环境,可以跳过这部分
Vite 使用 ESM
的方式访问环境变量,即不再使用 process.env
1 | import.meta.env.VITE_NAME |
你可以使用多个环境来用于 开发/生产 环境
在项目根目录下创建环境变量文件
命名格式为 .env.<name>
,如 .env.production
和 .env.development
环境变量文件内容
1 | NODE_ENV=development |
其中 NODE_ENV 值为 development
或 production
,对应 开发/生产 环境
在组件中,使用方式如下
1 | const url = import.meta.env.VITE_BASE_URL |
在 package.json
文件的 scripts 命令中,增加参数 --mode <name>
即可指定环境
如
1 | "dev": "vite", |
改为
1 | "dev": "vite --mode development", |
添加文件 src/types/global
1 | // 添加项目实际需要的内容 |
添加文件 src/build/env.ts
1 | export function wrapperEnv(envConf: Record<string, any>): ViteEnv { |
在使用的地方可以这样
1 | import { wrapperEnv } from '/@/build/env'; |
配置网络代理可以解决开发时的跨域问题,此配置仅开发环境有效,生产环境应配合 nginx 等实现转发
如果你的项目不需要与后端交互,或无需考虑跨域问题,可忽略此部分
添加文件 src/build/proxy.ts
创建 createProxy
函数用于创建代理
1 | import type { ProxyOptions } from 'vite'; |
修改 vite.config.ts
文件,增加
1 | const viteEnv = wrapperEnv(env); |
1 | server: { |
VITE_GLOB_API_PROXY_PREFIX
为代理的路由段
开发环境调用接口时,需要增加 /api
开头,如
1 | get('/api/user') |
发布环境不能加 /api
开头,因此你需要封装网络访问,以防止每次请求都判断运行环境
代码如
1 | if(dev){ |
配置路径别名后可以使用路径如 /@/views/index.ts
, /@/components/comp.ts
等
vite.config.ts
文件中,增加内容1 | import { resolve } from 'path'; |
/@/
用于模块,/#/
用于类型
tsconfig.json
中增加 compilerOptions.paths
和 compilerOptions.baseUrl
,用于支持 TS 语法检查1 | { |
配置好编辑器,能让开发更顺畅
创建 .vscode/settings.json
文件,用于存储 vscode 配置信息
1 | { |
.vscode/launch.json
文件1 | { |
edge
可换为 Chrome
或其他浏览器
vite.config.ts
文件,增加1 | build: { |
即 development 环境下启用 source map,开启后调试器才能正确找到执行语句所在代码位置
vite.config.ts
整体配置如
1 | import { defineConfig } from 'vite'; |
Pinia 是 Vue3 推荐的状态管理工具,对 TS 的支持很完善,用起来也比较舒服
1 | yarn add pinia |
在 src
下创建 store 文件夹,在 store 文件夹下创建 index.ts
文件,便于统一管理
index.ts
文件中添加代码
1 | import type { App } from 'vue'; |
main.ts
中修改代码如1 | import { createApp } from "vue"; |
app.ts
1 | import { defineStore } from 'pinia'; |
在需要使用的地方
1 | const appStore = useAppStore(); |
1 | const appStore = useAppStoreWithOut(); |
get
和 set
的 computed
1 | const appStore = useAppStore(); |
pinia 中的 getters 和 vuex 中的 getters 功能相同
如果你的网站不涉及多页面跳转,可以忽略此部分内容
1 | yarn add vue-router |
src
下创建 router 文件夹,在 router 文件夹下创建 index.ts
文件和 modules
文件夹,便于统一管理1 | import type { App } from "vue"; |
以上代码可以动态加载 router/modules
中的路由文件,此后各个模块的路由可以在此文件夹下创建
modules
文件夹中创建路由文件,如 home.ts
1 | import { RouteRecordRaw } from "vue-router"; |
上面示例同时需要创建 views/home/index.vue
文件
main.ts
中新增代码1 | setupRouter(app); |
如
1 | import { createApp } from "vue"; |
src/App.vue
中添加1 | <router-view></router-view> |
如
1 | <template> |
router-view
是用来渲染路由对应的页面组件
配置 nProgress 可以让页面顶部有个进度条
1 | yarn add nprogress |
src/router/guard.ts
1 | import { Router } from 'vue-router'; |
src/router/index.ts
中的 setupRouter
函数,增加以下代码1 | createProgressGuard(router); |
现在函数为
1 | // config router |
src/design/index.less
1 | #nprogress { |
@primary-color
是后面 增加 stylelint + postcss + less
部分增加的 less
变量
main.ts
中引入1 | import '/@/design/index.less'; |
Prettier 用于格式化代码
在项目目录下执行以下命令安装插件
1 | yarn add prettier --dev |
prettier.config.js
文件,代码如下1 | module.exports = { |
.prettierignore
文件,用于配置那些文件需要忽略检查1 | /dist/* |
在 package.json 中的 scripts 中新增
1 | "lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"", |
之后运行 npm run lint:prettier
即可格式化全部代码
ESlint 可以规范代码格式
1 | yarn add @typescript-eslint/eslint-plugin --dev |
1 | yarn add eslint-config-prettier --dev |
The engine "node" is incompatible with this module. Expected version ">= 16.9.0". Got "***"
,执行以下语句再重试1 | yarn config set ignore-engines true |
.eslintrc.js
文件1 | module.exports = { |
.eslintignore
文件,用于配置那些文件需要忽略检查1 | *.sh |
在 package.json 中的 scripts 中新增
1 | "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix", |
之后运行 npm run lint:eslint
即可检查全部代码是否有不规范的地方
Windi CSS 是一个功能类优先的 CSS 框架,与 Tailwind CSS 用法相同,但速度更快
1 | yarn add windicss --dev |
vite.config.ts
文件,增加如下代码1 | plugins: [WindiCSS()], |
整体代码如
1 | import { defineConfig } from 'vite'; |
main.ts
文件,增加如下代码1 | import 'virtual:windi.css' |
windi.config.ts
文件1 | import { defineConfig } from 'vite-plugin-windicss'; |
在项目目录下执行以下命令安装插件
1 | yarn add less --dev |
在 package.json 中的 scripts 中新增
1 | "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", |
之后运行 npm run lint:stylelint
即可检查全部代码是否有 CSS 不规范的地方
stylelint.config.js
文件存放 stylelint 的配置1 | module.exports = { |
.stylelintignore
文件,用于配置那些文件需要忽略检查1 | /dist/* |
vite.config.ts
文件中,增加下面预处理配置1 | css: { |
完整代码如:
1 | import { defineConfig } from 'vite'; |
使用 husky
+ lint-staged
,可以实现每次提交 git 前,自动检查代码的格式规范
在项目目录下执行以下命令安装插件
1 | yarn add lint-staged --dev |
在 package.json 中的 scripts 中新增
1 | "lint:staged": "lint-staged", |
之后运行 npm run lint:staged
即可手动检查
修改 package.json 文件,增加如下配置内容
1 | "lint-staged": { |
在 package.json 中的 scripts 中新增
1 | "prepare": "husky install", |
然后执行下面语句自动创建 .husky
文件夹
1 | yarn prepare |
在此之后,每次执行 yarn install
语句,会自动执行上面的语句
然后创建文件 .husky/pre-commit
,每次提交代码前会执行这个脚本
1 | !/bin/sh |
使用 husky
+ commitlint
,可以实现每次提交 git 前,自动检查格式规范
规划化提交格式,可用于自动更新 CHANGELOG.md
、自动生成 Release 内容等功能
husky 按前面的 增加 husky + lint-staged
部分安装和配置,此处仅介绍 commitlint
相关
在项目目录下执行以下命令安装插件,
1 | yarn add @commitlint/cli --dev |
增加文件 .commitlintrc.js
用于存放 commitlint
校验规则
1 | module.exports = { |
新增文件 .husky/commit-msg
存放提交代码前执行的脚本
1 |
|
配置后能够解析 svg 图标文件
1 | yarn add vite-plugin-svg-icons --dev |
vite.config.ts
文件,增加如下代码1 | plugins: [SvgIconsPlugin({ |
iconDirs 是配置图标文件目录,这里是 src/assets/icons
,也可以修改为其他目录
整体代码如
1 | import { defineConfig } from 'vite'; |
main.ts
文件,增加如下代码1 | import 'virtual:svg-icons-register'; |
封装组件用起来更方便,否则只能每次用到的地方都这样写
1 | <svg aria-hidden="true"> |
在 components/Icon
下创建 SvgIcon.vue
文件,内容为
1 | <template> |
使用时
1 | <SvgIcon name="name" color="red"/> |
通过文件的方式使用 svg 还不够方便,用上更强大的 iconify 吧
iconify 是功能丰富的图标框架,可以与任意图标库一起使用
1 | yarn add @iconify/iconify |
vite.config.ts
文件,增加如下代码1 | plugins: [PurgeIcons({})], |
整体代码如
1 | import { defineConfig } from 'vite'; |
在这里搜索图标即可使用,无需下载 https://icon-sets.iconify.design/
1 | <span class="iconify" data-icon="ic:baseline-add-reaction"></span> |
封装组件用起来更方便
在 src/components
下创建 Icon
文件夹和 Icon/index.vue
文件
1 | <template> |
使用时
1 | <Icon icon="ant-design:aliyun-outlined"/> |
Icon/index.vue
修改为
1 | <template> |
使用时
1 | <Icon icon="lavcode|svg" color="red" size="50"/> |
根据此教程,完整的 vite.config.ts
文件内容为
1 | import { ConfigEnv, loadEnv, UserConfig } from 'vite'; |
根据此教程,完整的 package.json
文件内容为
1 | { |
步骤较多,操作较繁琐,但成果很值得。
在 Windows 宿主机中成果截图
文中所出现的 WSL,如果没有特指,都是 WSL2。
VMware Tools
更是可以与宿主机互相复制文件,也能自适应窗口尺寸Docker 中运行 MacOS 较成熟的项目是 https://github.com/sickcodes/Docker-OSX
上手简单,安装快捷,一行命令就可以启动一个运行 MacOS 的容器
Docker 中的 MacOS 也是运行在 Linux 中的 KVM
此部分参考 https://docs.microsoft.com/zh-cn/windows/wsl/tutorials/gui-apps
此部分用于在 Windows 中以窗口形式操作 MacOS 虚拟机
完成后你也可以运行其他 Linux GUI 应用程序,你可以像本地应用一样运行 WSL Linux 中的应用程序,也可以将应用添加到开始菜单、固定到任务栏等
安装后可以得到无缝的 Linux + Windows 桌面体验
安装 vGPU 驱动后可以使用虚拟 GPU,可以使用硬件加速 OpenGL 渲染
根据你电脑显卡下载安装:
1 | wsl --update |
然后重启
1 | wsl --shutdown |
测试一下 GUI 能否正常使用
Nautilus
是 gnome 的文件管理器,这里用来安装测试
1 | sudo apt install nautilus -y |
安装完成后可以直接打开,像 windows 应用一样
1 | nautilus |
在 Windows 宿主机中 Nautilus 运行截图
以后在 windows 的 cmd 中执行 wsl nautilus
即可直接打开
默认 WSL 没有支持嵌套虚拟化,需要修改一下配置。
在 Windows 中,用户文件夹下编辑或新建文件 C:\Users\%User%\.wslconfig
。(User 是你的 Windows 系统用户名)
1 | [wsl2] |
此部分参考 https://docs.microsoft.com/zh-cn/windows/wsl/wsl-config
1 | wsl --shutdown |
使用 OSX-KVM 安装 MacOS 虚拟机
此部分参考 https://github.com/kholia/OSX-KVM
1 | sudo apt-get install qemu uml-utilities virt-manager git wget libguestfs-tools p7zip-full make -y |
1 | echo 1 > /sys/module/kvm/parameters/ignore_msrs |
1 | sudo usermod -aG kvm $(whoami) |
1 | git clone https://github.com/kholia/OSX-KVM.git |
1 | ./fetch-macOS-v2.py |
运行显示
1 | 1. High Sierra (10.13) |
实测 4 和 5 都可以,而且完成后 Big Sur 也能正常升级到 Monterey,其他没试。
下载完成后会有 BaseSystem.dmg 文件,需要转为 img 格式
1 | dmg2img BaseSystem.dmg BaseSystem.img |
1 | qemu-img create -f qcow2 mac_hdd_ng.img 128G |
其中 mac_hdd_ng.img
是文件名,可以任意修改
先修改 OpenCore-Boot.sh
文件
ALLOCATED_RAM
运行内存,建议最低改为 8GCPU_THREADS
CPU 线程CPU_CORES
CUP 核心数-drive id=MacHDD,if=none,file="$REPO_PATH/mac.img",format=qcow2
其中的 $REPO_PATH/mac_hdd_ng.img
为上一步创建的虚拟磁盘文件执行脚本
1 | ./OpenCore-Boot.sh |
执行完成后应该会弹出 QEMU 窗口。
如果没有,请确认前面的步骤 WSL2 是否正确开启 GUI,vGPU 驱动是否已正常安装。
再次启动也是运行这个脚本
安装流程和其他方式安装大致相同,只用注意一点,安装完成后,再次启动会多出个引导盘,选择多出来的那个。
显示要一个多小时,甚至二两多小时,实则不用那么久
正在安装 MacOS
选择如图第二个启动项
启动项
还需要安装一会,才能进入系统。
macOS-Simple-KVM
项目地址:https://github.com/foxlet/macOS-Simple-KVM
这个项目好像已经停滞不更新,截至目前已经一年多没更新了。
引导方式是四叶草,最高版本只支持 mojave,连 xcode 都不能用
故放弃
现在虽然你已经能成功使用 MacOS 了,但不方便管理,而且每次启动都需要运行脚本,如果有多个虚拟机就更难管理。
你可以放弃这一步,若放弃这一步:
./OpenCore-Boot.sh
wsl virt-manager
直接打开管理界面我们现在开始使用 virt-manager
管理这个虚拟机,提升使用体验。
如果不是 WSL,Linux 应该都能使用 systemctl
命令,但 WSL 的启动方式决定其不支持 systemctl
命令,因此也无法开启 libvirtd
,如果执行 service libvirtd start
会报找不到这个命令的错误,所以 virt-manager
就无法连接到 WSL 中的虚拟机。
但我们可以使用 genie
工具启用 systemctl
。
启用 systemctl
的部分参考 https://gist.github.com/djfdyuruiry/6720faa3f9fc59bfdf6284ee1f41f950
此部分是在 WSL Linux 系统中操作
1 | cd /tmp |
这一步是可选操作,为了安装新版 genie
genie Release 列表可以在这里查看 https://github.com/arkane-systems/genie/releases
1 | vim /tmp/install-sg.sh |
目前最新版是,2.2,因此修改 GENIE_VERSION
值为 2.2,如
1 | GENIE_VERSION="2.2" |
1 | chmod +x /tmp/install-sg.sh |
Errors were encountered while processing: systemd-genie
执行
1 | apt install systemd-genie |
Unmet dependencies. Try 'apt --fix-broken install' with no packages (or specify a solution). root@PC:/tmp# apt update
执行
1 | apt --fix-broken install |
在 WSL 中执行
1 | genie -c systemctl start libvirtd |
这个命令表示进入 genie 并执行 systemctl start libvirtd
命令,等同于:
1 | genie -s |
正常启动后就可以使用 virt-manager 了,但是我们还没有将刚才创建的虚拟机加入 virt-manager。
如果出现 Waiting for systemd....!
,用 Ctrl + C
取消即可
注意:这时你不能使用 vGPU,因为启动方式没有采用 WSL 默认的方式。你需要退出 WSL 并重新用 wsl
命令进入 WSL,才可以继续操作
此部分参考 https://github.com/kholia/OSX-KVM
macOS-libvirt-Catalina.xml
,把 CHANGEME
全部换为 OSX-KVM 所在路径,根据此教程就是你的 wsl 用户名1 | vim ./macOS-libvirt-Catalina.xml |
1 | sed "s/CHANGEME/$USER/g" macOS-libvirt-Catalina.xml > macOS.xml |
1 | virsh --connect qemu:///system define macOS.xml |
virt-manager
以打开 virt-manager
,你将能看到名称为 macOS
的虚拟机在 Windows 宿主机中 virt-manager 截图
现在你可以用 UI 的方式,编辑或启动名为 macOS 的虚拟机了
virt-manager
正确配置后,在 Windows 宿主机中,创建 .bat
脚本文件,内容为
1 | wsl genie -c systemctl start libvirtd |
每次运行这个脚本就可以快速打开 virt-manager
了
下面是可能会遇到的问题,有已解决的,也有未解决但有替代方案的
在 使用 virt-manager 管理
部分有说明
在 MacOS 中,关闭节能
设置位于 系统偏好设置 -> 节能
此时间段后关闭显示器
设为 永不
如果可能,使硬盘进入睡眠
运行 genie
命令可能出现这个问题,是由于 genie
在等待 systemd 回应
你可以 Control + C
取消并继续操作,也可以永久性的解决这个问题,参考
https://github.com/arkane-systems/genie/wiki/Systemd-units-known-to-be-problematic-under-WSL
可以用其他传输工具和剪切板共享工具
暂时没有直接复制粘贴的方法
即徽标键,在 MacOS 中是 Command 键,影响如 Command + C
, Command + V
等快捷键
可以临时改建,或用 VNC 连接 MacOS 使用
暂未没有更好的方法
可能是 CPU 性能不够,或者线程数给的不够,这种只能增加配置或者换电脑
也可能是 CPU 配置不正确,将 CPU 配置改为和 KVM 宿主机相同,即与 WSL CPU 配置相同,如图
CPU 配置
依次点击 KVM 中的菜单 View -> Resize to VM
,如图:
虚拟机显示大小与窗口大小不匹配
或勾选 Fullscreen
以全屏
docker ps
列出正在运行的容器-a
列出所有容器,包括未运行的-q
只列出容器 id -n=?
前几条docker container ls
等同于 docker ps
docker image ls
列出镜像-q
只列出镜像 iddocker iamges
等同于 docker image ls
docker volume ls
列出数据卷-q
只列出数据卷 iddocker pull [OPTIONS] NAME[:TAG|@DIGEST]
从 hub.docker.com 拉取镜像docker create [OPTIONS] IMAGE [COMMAND] [ARG...]
创建一个新的容器--name
容器名称-v
数据卷-e
环境变量-p
端口映射-it
使用交互方式进行docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
创建并运行一个容器-d
后台运行docker rm 容器id
删除指定容器,参数和 docker run
很像docker rm -f $(docker ps -aq)
删除所有容器docker --rm
用完后删除,一般用于测试docker rm iamge
删除镜像docker rmi -f $(docker iamges -aq)
删除所有镜像docker rmi
和 docker rm iamge
相同docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
进入容器后开启一个新的终端docker attach [OPTIONS] CONTAINER
进入容器正在执行的终端docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
拷贝文件docker 使用桥接模式连接网络,使用技术是 evth-pair
,evth-pair
是一对虚拟设备接口,他们都是成对出现的,一端连着协议,一端彼此相连。因此每启动一个容器,docker 会给容器分配一个 ip,并给宿主机分配一个 ip
宿主机和 docker 容器相互之间都能通过内网互联
不同集群使用不同的网络,可以保证集群安全
1 | docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 <name> |
1 | docker network connect <net1> <net2> |
执行后,就将 net1 的网络配置复制到了 net2 的网络下
1 | docker run --name mysql8 -v mysql8:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=<password> -d -p 3306:3306 mysql:8 --lower-case-table-names=1 --default-authentication-plugin=mysql_native_password |
1 | docker run --name=mssql2019 -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -v mssql2019:/var/opt/mssql -d mcr.microsoft.com/mssql/server:2019-latest |
1 | docker run -it --name mongo5 -p 27017:27017 -v mongo5:/etc/mongo -v mongo5db:/data/db -v mongo5configdb:/data/configdb -d mongo:5 --auth |
1 | docker run -itd --name redis6 -v redis6:/data -p 6379:6379 redis:6 |
指定配置
1 | docker run -itd --name redis -v redis:/data -v redis-cfg:/etc/redis -p 6379:6379 redis:6.0 redis |
https://github.com/dotnet/aspnetcore
https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor
https://github.com/BlazorExtensions
https://studyblazor.com
https://ant-design-blazor.gitee.io
Blazor 分为两种模式
服务端模式是在服务端运行并渲染,浏览器使用 SignalR 实时推送至服务器,服务器再返回给浏览器 DOM 差异部分。
也就是说,服务端模式是通过 SignalR 实现实时交互的
WebAssembly 模式所有的 .net 代码都是在浏览器中通过 JavaScript 运行的,因此首次加载页面会比较慢,需要下载一些文件。
WebAssembly 模式是基于 WebAssembly(缩写 WASM)实现的
WASM 已支持所有现代浏览器,并且优化速度很快,接近本地性能,得益于 WASM,传统软件有望使用 web 实现
]]>scp
却需要 sudo 权限,因此需要使用 root 账号 ssh 登录/etc/ssh/sshd_config
文件1 | vim /etc/ssh/sshd_config |
将以下内容
1 | #PermitRootLogin prohibit-password |
修改为
1 | PermitRootLogin yes |
注意删除前面 #
1 | systemctl restart sshd |
配置 远程端公钥 和 本地端私钥,可无需密码连接
这里仅记录远程端 Linux 如何配置公钥
/root/.ssh/authorized_keys
文件1 | vim /root/.ssh/authorized_keys |
添加公钥内容
1 | ssh root@<hostname> |
1 | docker pull mysql:8 |
1 | docker run --name mysql8 -v mysql8:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=<password> -d -p 3306:3306 mysql:8 --lower-case-table-names=1 --default-authentication-plugin=mysql_native_password |
在 docker 中修改 my.ini/my.cnf
不方便,而且每次创建容器都需要修改。但 docker 中的 mysql 支持加配置参数,相当于初始化即修改 my.ini/my.cnf
如:
在 windows 中,可能需要配置 lower-case-table-names
使用 mysql8.0,目前很多情况下需要配置 default-authentication-plugin
为 mysql_native_password
环境变量取值顺序:
先在构造函数依赖注入 IConfiguration configuration
然后以以下方式调用
1 | var value1 = configuration["key1"]; |
vs 右键项目,点击 Manage User secrets
可生成 secrets.json
文件,该文件与项目分离,但能够从中取值。
在项目 launchSettings.json
文件中设置的环境变量,如
1 | "IIS Express": { |
使用 dotnet
命令启动时输入的参数,如
1 | dotnet run key1="value1" key2="value2" |
如果使用静态文件,要使用 UseStaticFiles
中间件
如果定义默认文件,要使用 UseDefaultFiles
中间件
UseDefaultFiles
只能在 UseStaticFiles
前面
UseFileServer
结合了 UseStaticFiles
, UseDefaultFiles
, UseDirectoryBrowser
(不推荐使用) 中间件
Model = 模型类 + 仓储
处理传入的 http 请求并响应用户操作
渲染节点,传参 required:false
则非必选
可以使用 @if(IsSectionDefined(<name>))
进行条件渲染
1 | @section <name>{ |
可以在 Share 文件夹中,统一处理页面
支持文件分层,即也可以在试图文件夹中,统一处理同文件夹下页面
视图模板,默认命名为 _Layout.cshtml
,也可自定义命名。
可以在试图中设置 @Layout = "<LayoutName>"
,也可以在 _ViewStart.cshtml
中统一设置
用于统一引用命名空间
支持文件分层
1 | <environment include = "Development"> |
https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-5.0
]]>使用 Vue CLI 4
创建,可以用 vue create proj-name
或者 vue ui
,按提示,选择 TS
模板。
在 vue2 中,官方的 vuex 写法完全没有 TS 的味道,类型判断也没有作用,好在有更好的写法,那就是使用 vuex-module-decorators
。
运行下面命令安装 vuex-module-decorators
1 | npm i vuex-module-decorators |
OR
1 | yarn add vuex-module-decorators |
在项目中,一般都会模块化 store,每个模块都定义在 /store/modules/
文件夹下的 ts
文件
在一个模块中,写法如下:
1 | import { |
以上模块中包含控制 sidebarOpened
变量的内容。
内容直接以类成员变量方式定义
用于模块类,需要指定该模块的名称 name
和 vuex 实例 store
和传统的 mutation
一样,但可以直接以 this.
方式获取和修改 state
变量
和传统的 action
一样,可以以 this.
方式直接调用 mutation
,而不是 commit
可以设置 commit
值为 @Mutation
名称,并在函数结束时返回一个值,这将会自动调用对应 @Mutation
,如
1 | import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators' |
get
属性可以自动生成 getter
引用多个 state
模块,在 /store/index.ts
中代码如下:
1 | import Vue from "vue"; |
这里引入的都是模块中的 state 接口,便于调用处的类型提示。
在需要使用 vuex
的地方,使用方法如下:
1 | import { AppModule } from "@/store/modules/app"; |
上述代码如果用传统方式,使用方式如下:
1 | import store from '@/store' |
使用 vuex-module-decorators
不仅定义简单,调用简单,更不易出错
如果使用Vue-CLI
构建时,在Use class-style component syntax
这一步选择了Y
,那么就已经支持了类装饰器
否则需要单独安装:npm i vue-property-decorator vue-class-component
具体使用方式参考 vue-property-decorator
]]>于是很多人选择自建 NAT, 自建 NAT 有两种:
也有人想使用对象存储,但网页或命令行的操作方式,实在很不方便。
于是就引出了本文目标:让对象存储像本地文件一样简单操作。
下载方式:Rclone 官网 或 GitHub
下载后解压到任意目录,如 C:\Program Files\rclone
下载方式:GitHub
按提示默认安装
下载方式: Git 官网 或 GitHub
按提示默认安装
打开任意文件夹,并在左侧导航目录下找到【此电脑】,单击右键选择【属性】>【高级系统设置】>【环境变量】>【系统变量】>【Path】,单击【新建】。
在弹出的窗口中,填写 Rclone 解压后的路径(E:\AutoRclone),单击【确定】。
打开命令行,输入 rclone –version 命令,按 Enter,查看 Rclone 是否成功安装。(快捷键 win+r ,然后输入 cmd ,再回车确定可打开命令行)
命令行中输入 rclone config
并回车,出现配置列表
输入 n
并回车,然后输入磁盘名称,如 sync
,然后回车。
选择云服务列表,如果是阿里云/腾讯云等符合 s3 标准的云服务商,输入 4 并回车。然后选择具体云服务商,如果列表没有,则选择最后一个。
配置云服务商
env_auth>
直接按回车access_key_id>
输入云服务商 SecretId低频存储
,选择 STANDARD_IA
Edit advanced config
> 直接按回车
确认无误,按回车确定,再输入 q 退出配置
在命令行中输入
1 | rclone mount sync:/ S: --cache-dir D:\temp --vfs-cache-mode writes & |
上述命令执行后如果出现提示 The service rclone has been started
则挂载成功
此时在此电脑
已经能够看到挂载的盘符了。
如果不设置自动挂载,每次重启后挂载的磁盘都会消失,因此需要开机自动挂载磁盘。
D:/rclone.bat
,写入上述挂载本地磁盘的命令,命名以.bat 结尾1 | rclone mount sync:/ S: --cache-dir D:\temp --vfs-cache-mode writes & |
%USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
新建文件,写入1 | CreateObject("WScript.Shell").Run "cmd /c D:/rclone.bat",0 |
其中 D:/rclone.bat
改为前面 .bat
文件所在位置
]]>可以愉快的使用对象存储来同步文件了
常见问题,JS 语法糖
+true
+false
+"2"
+"0X3"
答:
常见问题,日常开发会遇到
答: 设置 el-table 高度 height 属性
el-dialog
内再定义 el-dialog
,即 el-dialog
嵌套,这样嵌套多层有没有问题?常见问题,日常开发会遇到
答:可以多级嵌套,但子级 el-dialog
必须要设置 append-to-body
属性为 true
,否则遮罩层显示会有问题。
el-form
的 rules
属性,能否根据 element-ui
的功能给某个 el-form-item
设置为必填项?不常见问题
答:两种方式:
el-form-item
也可以单独设置 rules
属性el-form-item
的 required
属性为 true
el-date-picker
的 显示格式 和 绑定值格式 是否相同?如何设置?常见问题,与后端交互必须会
答:可以不同也可以不同,:format
设置显示格式,:value-format
设置绑定值格式
vuex
,getter
是必须的吗?他有什么作用?常见问题
答:不是,getter
相当于 store
的计算属性,也可以在组件内使用计算属性来代替。
常见问题
答: 默认不开启严格模式的情况下可以,但是不提倡。
不常见问题,如果项目代码很规范才会用到
答:创建 store 的时候传入 strict: true
v-if
与 v-show
,值分别从 false
变为 true
, 是否会触发子组件的 created
函数?常见问题,封装组件必须要知道的
答:v-if 会,因为这个过程是销毁并重建组件;v-show 不会,只改变了 CSS 的 display 值
false
,父组件初始化时会不会触发子组件的 created
函数?常见问题,封装组件必须要知道的
答:v-show 会,v-show 的渲染是非惰性的;v-if 不会,v-if 的渲染是惰性的。
strs
,并且数组不为空,这种写法更新界面有没有问题:strs[0] = 'str'
,如果有问题,怎么修改?常见问题,vue 必须知道的坑
答: 不可以这样写,这样写界面不会刷新,这是 vue 底层的限制。修改为:
1 | strs.splic(0, 1, 'str') |
常见问题,vue 必须知道的坑
1 | <template> |
答: 赋值语句改为:
1 | this.$set(this.user,'userName','test') |
或
1 | Vue.set(this.user,'userName','test') |
v-model
绑定和 :value
取值的联系不常见问题,日常开发不涉及
答:v-model
本质是一种语法糖,v-model
的写法与 :value
+ @input
是一样的
ES6 常见写法,数组扩展
答:[1,2,3]
常见问题,考察数组扩展 和 面向对象
1 | const arr1=[1,2,3] |
答:false
,虽然数组内容相同,但 arr1 和 arr2 是两个对象
不常见问题,变量解构赋值
1 | let [x, y, ...z] = ['a']; |
答:
网页开发常见坑,学过 ES6 的
let
可避免。按 JS 开发规范,应避免使用var
声明变量,而是使用let
或const
1 | var name = 1 |
答:"11"
,因为 name
是浏览器保留字,值始终为字符串类型
name
命名,如何避免上面类型问题?常见问题
答:使用 let
声明变量
常见问题,考察面向对象。写公共代码会遇到
1 | function Point(x, y) { |
1 | class Point { |
答:都指向 Point 类的实例对象
then 回调在旧的写法中常见,async/await 在新写法中常见。日常简单使用不涉及,但如果要保证代码安全性,或者写公共代码要会
答: then 回调的方式,使用
1 | func().then(value=>{},reason=>{}) |
或
1 | func().then(value=>{}).catch(err=>{}) |
async/await 方式,使用
1 | try{ |
1 | await Promise().then(value=>{}) |
如果使用 async/await,才提问。考察 await 和 Promise 的关系
答:可以,因为 then
函数的返回值也是 Promise
。这就是 Promise
支持链式回调的原因
常见问题,提取公共代码会遇到,容易漏掉 return 造成潜在 bug
答: 会
不常见问题,日常如果出现这个问题,属于 bug
答: 先执行完 Promise 函数,执行完后再执行 then 回调,顺序与何时 resolve/reject 无关。
常见问题,主要考察 Promise 原理,但日常开发不会有这种写法
答: resolve 和 reject 都会改变 Promise 对象状态,但不可重复修改。因此先执行 resolve 进入 then 第一个回调,先执行 reject 进入 then 第二个回调。
]]>DevOps
,比如可以运行在各云服务商的容器服务内,如阿里云的“容器服务”和腾讯云的“云托管”等,让 C#后端开发也可以抛弃服务器了。使用 vs2019 创建ASP.NET Core Web Application
,勾选Enable Docker Support
,平台选择 Linux
也可以使用命令行创建:
dotnet new web
dotnet new mvc
dotnet new webapp
但需要另外创建 Dockerfile 文件
1 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base |
其中 Test 为项目名称,将其改为你自己的。
在项目文件夹下执行
1 | docker build . -t <image-name> -f Dockerfile |
如果操作成功,在控制台执行 docker image ls
应该能看到创建的新镜像。
之后,与其他镜像操作无异。
在控制台执行:
1 | docker create -p 80:80 --name <container-name> <image-name> |
如果操作成功,在控制台执行 docker ps -a
能看到新创建的容器,并且 STATUS
= Created
在控制台执行:
1 | docker start <container-name> |
如果操作成功,在控制台执行 docker ps -a
能看到新创建的容器,并且 STATUS
为启动时间
也可以执行以下语句启动所有容器:
1 | docker start $(docker ps -aq) |
docker ps
命令中, -a
为列出所有容器,-q
只显示 CONTAINER ID
,合在一起可以对所有容器做操作。
配合 GitHub 或 Gitee 等,可以让代码一经上传,就自动测试、打包、发布等。
WebHooks
,URL 为上一步的 API 访问地址脚本命令如下:
1 | git clone <repos-clone-url> |
https://github.com/hal-wang/DevOps
发布这个项目到你的服务器,按 README.md
配置好系统用户变量,设置 WebHooks,就可以实现更新代码自动发布项目。