开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

射线与三角形的交点

发表于 2024-04-27

前言

射线与三角形的求交是一道经典面试题,它是模型选择的必备基础。

这道题要是说的话,并不难,但要往深了说,也不太容易。

而你若想脱颖而出,那就得往深了说。

我接下来会整体的说一下这个解题的思路和方法,有些公式我不会说太细,大家哪里不懂可以点击后面的链接学习。

前期准备

  • 点积:www.bilibili.com/video/BV13s…
  • 叉乘:www.bilibili.com/video/BV1vs…
  • 直线和平面的交点公式:wuli.wiki/online/LPin…
  • 矩阵行列式:www.bilibili.com/video/BV1Qs…
  • 克莱姆法则:www.bilibili.com/video/BV1Pb…

两种解法

image-20240425162453279

射线与三角形求交的常见解法有两种:

  • 先求交,后过滤:先求射线与三角形所在平面的交点,然后判断此交点是否在三角形中。
  • 先过滤,后求交:逐步过滤射线与三角形相交的条件,最后推导出交点。

第一种方法比较简单,很容易推导和理解,但计算效率会比较低,需要具备的知识是叉乘、点积、射线与平面的求交公式。

第二种方法相对难一些,需要具备扎实的线性变换基础,计算效率会高于前者,需要具备的知识是叉乘、点积、矩阵行列式、克莱姆法则(Cramer’s rule)。

咱们依次说一下这两种方法。

解法一

我们用”先求交,后过滤”的方法求射线与三角形交点。

image-20240425172553070

已知:

  • 三角形ABC
  • 射线,原点为O,方向为d

求:射线与三角形的交点P

解:

1.通过AB和AC的叉乘,求出三角形ABC所在平面的垂线n。

image-20240427145451292

若n等于0,则三角形的三点共线,甚至共点,需要过滤掉。

注:这里的n 不需要归一化。

2.通过射线与平面的求交公式求出交点。

根据射线与平面的求交公式可得:

image-20240425172046691

  • t:射线上的时间或距离标量
  • O:射线原点
  • A:平面上一点

射线的方程是:

image-20240425171022590

  • r(t) :在射线上到射线原点的距离为t的点位。

将之前求出的t值代入r(t)便可求出交点,设此交点为P。

3.判断点P是否在三角形ABC中。

在右手坐标系中,若AP^AC,BP^BA,CP^CB分别点积AB^AC都大于0,则点P在三角形ABC中,此时的点P就是射线与三角形的交点,如下图所示:

image-20240426071905554

否则,点P在三角形ABC之外,如下图所示:

image-20240426073101605

解法二

我们用”先过滤,后求交”的方法求射线与三角形交点。

以具象化的方式把问题和问题的解法画出来,要比数字计算更容易理解。所以我接下来会把这个问题的解法画出来。

这个解法的核心是:基于射线的方向和三角形的两条边把射线的源点投影到一个新的空间中。

image-20240426095727618

已知:

  • 三角形ABC
  • 射线,原点为O,方向为d

求:射线与三角形的交点P

解:

根据已知条件,我们可以得到一个线性变换矩阵[-d,AB,AC],矩阵也可以理解为坐标系。

在这个矩阵中,A是原点,-d是射线的反方向,AB,AC是三角形的两条边。

向量AO在坐标系[-d,AB,AC] 中的本地坐标位是(t,u,v)。

由上面的条件可知,(t,u,v) 就是向量AO在[-d,AB,AC] 中的本地坐标位。

image-20240426102942350

所以(t,u,v) 就是[-d,AB,AC]的逆矩阵乘以AO。

image-20240426103625778

因为-d是一个单位向量,所以t值就是世界坐标系中射线与三角形ABC相交的距离,由此距离可以得到交点P:

image-20240426102004413

现在我们已经用一个很快的方式把射线与三角形的交点P给算出来了,那说好的过滤呢?

过滤就是要在刚才的计算过程中进行逐步拦截,判断射线有没有可能和三角形存在交点,若不存在,就不用再往后走了。

一边过滤一边求交

1.先判断矩阵[-d,AB,AC]的有效性。

[-d,AB,AC]的有效性可以通过其行列式来判断。

三维矩阵的行列式可以理解为由以矩阵基向量为临边的平行六面体的有向体积。

[-d,AB,AC] 的行列式的计算方式如下:

image-20240426143734207

  • det 是determinant 的缩写,即行列式
  • AB叉乘AC的结果是垂直于AB、AC的向量,此向量的长度是以AB、AC为临边的平行四边形的面积
  • -d点积上面的向量,就是把-d投影到AB和AC的垂线上,从而得到平行六面体的高,然后乘以上面的向量的向量的长度,也就是平行四边形的面积,从而就得到了以-d,AB,AC为临边的平行六面体的有向体积,效果如下:

image-20240426160804630

det([-d,AB,AC]) 的值有三种情况:

  • 为正时,射线从三角形所在平面的正面穿过;
  • 为负时,射线从三角形所在平面的背面穿过,若三角形背面不可选,则无交点;
  • 为零时,矩阵[-d,AB,AC]发生空间降维,射线可能与三角形平行或者出现零向量,射线与三角形所在的平面没有交点或者有无数个交点。此情况一般不考虑,默认无交点。

2.把[-d,AB,AC]的逆矩阵乘以AO的过程分解、分别计算u,v,t,并过滤。

这时候就需要用到克莱姆法则,其规则如下:

image-20240426163259352

上面的结构和咱们之前求(t,u,v) 时的结构是一样的:

image-20240426102942350

image-20240426173104253

顺便回顾一下我们在解法一里说过的,射线与平面的求交公式:

image-20240425172046691

大家有没有觉得这两种解法里的t 有点相似?对,它们不仅相似,而且一样。

对于为什么用克莱姆法则就能算出t,u,v 来,我会在最后简单解释一下。

当然大家也可以看我在前期准备里提供的链接。

接下来咱们先往后走,说一下过滤条件。

当t 满足以下条件时不会有交点:

  • t<0

t 是从射线的原点O 向着射线的方向d 推进的距离,当此值小于0时,是向射线反方向推进的,这肯定是不对的。

当u和v 满足以下任一条件都不会有交点:

  • u<0
  • v<0
  • u+v>1

(t,u,v)可以理解为在[-d,AB,AC] 中的本地坐标。

当u或v小于0 时,交点会跑到三角形外面,这个比较好理解,因为点A就是源点,[-d,AB,AC] 中的基向量AB、AC 是三角形的边。

那u+v≤1 是什么概念呢?

这就是一个简单的向量加法。

想象你在直角边为1的等腰直角三角形内跑步,三角形中的任一点都可以理解为你先沿着AB轴跑了一段,然后又沿着AC轴跑了一段,你能跑出的最远距离只能是直角边的长度

3.把t 代入射线方程求出交点P。

image-20240426102004413

代码实现

这是我用three.js写的一个计算过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
php复制代码/* 三角形ABC */
const A=new Vector3(0, 0, 0,)
const B=new Vector3(0, 0, -1)
const C=new Vector3(1, 0, -1)
// 背面是否可选
let backfaceCulling=false

/* 射线 */
// 射线原点
const O=new Vector3(0,1,0)
//射线方向
const d=new Vector3(0.2,-1,-0.8)

/* 射线与三角形的求交 */
// 射线的反方向
const _d=new Vector3(-d.x,-d.y,-d.z)
// 向量AB
const AB=new Vector3().subVectors( B, A )
// 向量AC
const AC=new Vector3().subVectors( C, A )
// AB和AC垂线,其长度是以AB,AC为临边的平行四边形的面积
const n=new Vector3().crossVectors( AB, AC )
// 矩阵[-d,AB,AC]的行列式,即以-d,AB,AC为临边的平行六面体的有向体积
let det = _d.dot( n )
if ( det < 0 ) {
if ( backfaceCulling ){
return null
}
} else if ( det === 0 ) {
return null;
}
// 点A到射线源点的向量
const AO=new Vector3().subVectors( O, A )
// 从射线的原点O 向着射线的方向d 推进的距离
const t=AO.dot( n )/det
// 当t<0的时候,向射线反方向推进,与实际不符
if ( t < 0 ) {
return null
}
// AO在AB上的投影坐标
const u = _d.dot(new Vector3().crossVectors( AO, AC ))/det;
if ( u < 0 ) {
return null
}
// AO在AC上的投影坐标
const v = _d.dot(new Vector3().crossVectors( AB, AO ) )/det;
if ( v < 0 ) {
return null
}
// 当u + v >1时会超出三角形的范围
if ( u + v >1 ) {
return null
}
// r ( t ) = O + t d
const p=O.clone().add(d.clone().multiplyScalar(t))

在three.js的Ray对象里有一个intersectTriangle() 方法。

这个方法略有冗余,比如其中的sign 代表了行列式的取向,但这是不需要的,因为我们可以在两个行列式相除的时候,得到行列式取向负负得正的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
kotlin复制代码intersectTriangle( a, b, c, backfaceCulling, target ) {
// Compute the offset origin, edges, and normal.
// from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h
_edge1.subVectors( b, a );
_edge2.subVectors( c, a );
_normal.crossVectors( _edge1, _edge2 );

// Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,
// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
// |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
// |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
// |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
let DdN = this.direction.dot( _normal );
let sign;
if ( DdN > 0 ) {
if ( backfaceCulling ) return null;
sign = 1;
} else if ( DdN < 0 ) {
sign = - 1;
DdN = - DdN;
} else {
return null;
}
_diff.subVectors( this.origin, a );
const DdQxE2 = sign * this.direction.dot( _edge2.crossVectors( _diff, _edge2 ) );

// b1 < 0, no intersection
if ( DdQxE2 < 0 ) {
return null;
}

const DdE1xQ = sign * this.direction.dot( _edge1.cross( _diff ) );
// b2 < 0, no intersection
if ( DdE1xQ < 0 ) {
return null;
}

// b1+b2 > 1, no intersection
if ( DdQxE2 + DdE1xQ > DdN ) {
return null;
}

// Line intersects triangle, check if ray does.
const QdN = - sign * _diff.dot( _normal );

// t < 0, no intersection
if ( QdN < 0 ) {
return null;
}

// Ray intersects triangle.
return this.at( QdN / DdN, target );
}

扩展-克莱姆法则

image-20240427064402673

当我们说向量p 的x 位置是2的时候,我们可以这么理解这个2 的几何概念:

  • x 轴上的刻度
  • 用来缩放基向量x 的标量
  • 以向量p、基向量y 和基向量z为临边的平行六面体的有向体积,即矩阵[p,y,z] 的行列式det([p,y,z] )

image.png

最后一种理解就是理解克莱姆法则的关键。

y叉乘z的结果是一条长度为1的垂直于y和z的向量,而p与这个向量的叉乘,就是p在这个向量上的正射影乘以这个向量的长度。

根据当前的情况可知,y叉乘z的结果就是基向量z,z的长度是1。

p 在z 上的正射影就是2。

所以,det([p,y,z] ) 的值就是2*1=2

大家理解完了这个原理后,咱们再做个假设。

假设这个向量p 是通过一个矩阵M乘以向量a(ax,ay,az) 得到的。

image-20240427152509869

矩阵M 的基向量x是原来的3倍,其余的基向量不变:

image-20240427072850248

那a 应该在哪里?

理解矩阵变换关系的同学肯定可以看出,这是一个3*ax=2,ax=2/3 的问题。

因为矩阵M中其它的基向量没变,所以向量a的位置是(2/3,0,0)。

其矩阵关系是这样的:

image-20240427071410175

可是如果我只想知道ax 的值,不想知道其它值,应该怎么办呢?

那我们就得把这个矩阵变换的过程分解开来。

因为我们当前的变换都是线性变换,所以矩阵M的张成空间缩放了多少,就是ax 缩放了多少,即:

image-20240427152928894

det([p,y,z])代表了点P在ax所在的轴向上位置,也就是ax 被矩阵M缩放后的位置。

我基于各个原理,继续往后推。

当矩阵M的其它基向量方式改变的时候,[p,y,z]中的基向量也得做相应改变。

所以,当矩阵M是这样的时候:

image-20240427074029063

那ax 就应该这么算:

image-20240427075010216

其结果还是2/3,这是因为我们没有改变向量p和3x,而矩阵中的4y和5z代表的只是以其为临边的平行四边形的面积,上下一除,就被约掉了,我们就需要它被约掉。

向量a中的其它分量也可以按照同样的原来计算。

image-20240427075107742

注:0的出现是由于向量p与其它的两个向量共面导致的。

这就是克莱姆法则。

总结

在计算机图形学里,数学即代码。

坚实的数学基础,会让你写出的代码简洁优雅。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot 业务开发中的算法应用:递归+回溯算法

发表于 2024-04-27

前言

最近在做一个权限管理类的项目,在做首页菜单功能时,遇到一个问题:

当点击某个菜单时,页面顶部需要显示当前菜单的面包屑信息。

研究了一下,最后使用递归+回溯算法,完成了需求。感觉这个功能还挺常见的,在这里记录一下具体的方案,希望可以帮到大家,谢谢!

需求

面包屑导航其实就是这样的

image.png

这个功能还是挺常见的,具体效果就是:当打开某个菜单时,在页面的顶部 显示当前菜单的层级信息。一般是从当前菜单的根菜单开始。

例如:系统中菜单层级为:系统管理->权限管理->角色管理,那么当在点击角色管理菜单时,面包屑部分就会变成系统管理/权限管理/角色管理这样。

下面我们分步来实现:

  1. 封装树形菜单数据
  2. 封装菜单的面包屑层级数据

准备工作

表设计

菜单表设计如下:

image.png

这应该算是最简单的菜单表的设计了,同时也插入了一些测试数据(这里规范了根菜单的父菜单id为0):

image.png

代码

项目依赖为 SpringBoot v2.6.13,引入了mybatis-plus,如下,创建了菜单相关的实体类、dao层接口、service层接口,此外,还封装了一个菜单Vo。

1
2
3
4
5
6
7
8
java复制代码@Data
@TableName("t_menu")
public class MenuEntity {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private Integer parentId;
}
1
2
3
java复制代码@Mapper
public interface MenuMapper extends BaseMapper<MenuEntity> {
}
1
2
3
java复制代码public interface MenuService extends IService<MenuEntity> {
List<MenuVo> listTree();
}
1
2
3
java复制代码@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, MenuEntity> implements MenuService {
}

菜单vo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Data
public class MenuVo {
/**
* ID
*/
private Integer id;
/**
* 菜单名称
*/
private String name;
/**
* 子菜单集合
*/
private List<MenuVo> subMenus;
}

代码实现

递归封装树形菜单

service层,定义了一个方法listTree,用来返回树形菜单数据

1
2
3
java复制代码public interface MenuService extends IService<MenuEntity> {
List<MenuVo> listTree();
}

实现类中,重写了该方法,通过递归的方式,完成了功能。

封装了一个方法getSubMenus,根据传入的parentId,获取它的子菜单的树形数据。实际就是递归调用本方法,这里就不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, MenuEntity> implements MenuService {
/**
* 返回树形菜单数据
*
* @return
*/
@Override
public List<MenuVo> listTree() {
List<MenuEntity> list = list();
return getSubMenus(list, 0);
}

/**
* 根据传入的菜单id 封装子菜单树形数据
*
* @param list
* @param parentId
* @return
*/
private List<MenuVo> getSubMenus(List<MenuEntity> list, Integer parentId) {
return list.stream()
//筛选出当前菜单id的直接子菜单
.filter(menu -> parentId.equals(menu.getParentId()))
.map(menu -> {
MenuVo vo = new MenuVo();
//封装vo
BeanUtils.copyProperties(menu, vo);
//通过递归的方式,封装当前菜单的子菜单
vo.setSubMenus(getSubMenus(list, menu.getId()));
return vo;
}).collect(Collectors.toList());
}
}

递归+回溯 封装面包屑信息

下面我们需要获取每一个菜单的面包屑信息,首先在MenuVo中新增一个属性,是一个List<String>集合,用来封装每一层菜单的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Data
public class MenuVo {
/**
* ID
*/
private Integer id;
/**
* 菜单名称
*/
private String name;
/**
* 子菜单集合
*/
private List<MenuVo> subMenus;
/**
* 当前菜单层级信息:即面包屑数据
*/
private List<String> titles;
}

对应MenuService中的方法也要进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, MenuEntity> implements MenuService {
/**
* 返回树形菜单数据
*
* @return
*/
@Override
public List<MenuVo> listTree() {
List<MenuEntity> list = list();
return getSubMenus(list, 0, new ArrayList<>());
}

/**
* 根据传入的菜单id 封装子菜单树形数据
*
* @param list
* @param parentId
* @return
*/
private List<MenuVo> getSubMenus(List<MenuEntity> list, Integer parentId, List<String> titles) {
return list.stream()
//筛选出当前菜单id的直接子菜单
.filter(menu -> parentId.equals(menu.getParentId()))
.map(menu -> {
MenuVo vo = new MenuVo();
//封装vo
BeanUtils.copyProperties(menu, vo);
//封装面包屑信息
int size = titles.size();
titles.add(menu.getName());
vo.setTitles(new ArrayList<>(titles));
//通过递归的方式,封装当前菜单的子菜单
vo.setSubMenus(getSubMenus(list, menu.getId(), titles));
//进行回溯操作,将截至到本层菜单的标题信息都删除
//这里直接使用 titles.indexOf(title),也是因为菜单名称一般都不会重复。如果会重复,可以采取其他处理方式
titles.removeIf((title) -> titles.indexOf(title) >= size);
return vo;
}).collect(Collectors.toList());
}
}

上面的代码中:

  • getSubMenus方法新增了一个参数List<String> titles,这个List集合的作用就是:将每一层的菜单名称封装到里面,方便它的每一个子菜单去获取。
  • 第31行:将当前层的菜单名称,添加到titles中。
  • 第32行:创建一个新的List集合,将titles中的数据复制到里面,然后赋值给vo里面的titles属性。(这里是因为如果直接将titles赋值到vo中,因为这是一个共享变量,所以最后会导致所有菜单的titles值都一样了。所以这里需要新建一个List集合。)
  • 第34行:递归调用时,将共享变量titles,继续进行了传入。
  • 第37行:这里是进行了回溯操作,下面简单介绍一下回溯操作。

回溯操作

在菜单数据的封装过程中,菜单的数据结构,其实就是一个树,比方说:如果每个菜单都只有两个子菜单,那它就是一个二叉树。

而树的深度优先搜索,就是通过递归来完成的。

而在递归过程中,当遍历到某个节点时,如果想对于接下来的两个分支,分别进行计算。那么就可以用到回溯操作。因为我们当前titles是一个共享变量,所以肯定左右两个分支需要分别计算,不可能把左右分支的数据都封装到titles里边,那就乱了。

所以代码中,当进行了左分支的计算之后,就将之前添加进titles的元素,再删除掉。再进行右分支的计算,这样 左右分支的数据不会互相影响。

其实,如果不需要封装面包屑数据,根本用不到回溯,递归就可以了。

回溯算法的原理,一句两句说不清楚,大家可以去看力扣 第39题,能看懂官方题解中的搜索回溯解法的话,回溯算法也能明白了。我也是做这道题,才了解了回溯算法的原理的。

总结

总的来说,还是很兴奋的。之前刷过一段时间的算法题,一直觉得对于我这种CRUD Boy 用处不大。结果今天在业务开发中应用到了回溯算法,虽然可能没有多高级,但还是很高兴,感觉自己学到了很多。

感谢各位的阅读,文章中有不对的地方,感谢各位指正,谢谢~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

axios 跨端架构是如何实现的?

发表于 2024-04-27

本文是“axios源码系列”第三篇,你可以查看以下链接了解过去的内容。

  1. axios 是如何实现取消请求的?
  2. 你知道吗?axios 请求是 JSON 响应优先的

我们都知道,axios 是是一个跨平台请求方案,在浏览器端采用 XMLHttpRequest API 进行封装,而在 Node.js 端则采用 http/https 模块进行封装。axios 内部采用适配器模式将二者合二为一,在隐藏了底层的实现的同时,又对外开放了一套统一的开放接口。

那么本文,我们将来探讨这个话题:axios 的跨端架构是如何实现的?

从 axios 发送请求说起

我们先来看看 axios 是如何发送请求的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码// 发送一个 GET 请求
axios({
method: 'get',
url: 'https://jsonplaceholder.typicode.com/comments'
params: { postId: 1 }
})

// 发送一个 POST 请求
axios({
method: 'post'
url: 'https://jsonplaceholder.typicode.com/posts',
data: {
title: 'foo',
body: 'bar',
userId: 1,
}
})

dispatchRequest() 方法

当使用 axios 请求时,实际上内部是由 Axios 实例的 .request() 方法处理的。

1
2
3
4
5
6
javascript复制代码// /v1.6.8/lib/core/Axios.js#L38
async request(configOrUrl, config) {
try {
return await this._request(configOrUrl, config);
} catch (err) {}
}

而 ._request() 方法内部会先将 configOrUrl, config 2 个参数处理成 config 参数。

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// /v1.6.8/lib/core/Axios.js#L62
_request(configOrUrl, config) {
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}

// ...
}

这里是为了同时兼容下面 2 种调用方法。

1
2
3
4
5
6
7
javascript复制代码// 调用方式一
axios('https://jsonplaceholder.typicode.com/posts/1')
// 调用方式二
axios({
method: 'get',
url: 'https://jsonplaceholder.typicode.com/posts/1'
})

当然,这不是重点。在 ._request() 方法内部请求最终会交由 dispatchRequest() 处理。

1
2
3
4
5
6
javascript复制代码// /v1.6.8/lib/core/Axios.js#L169-L173
try {
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}

dispatchRequest() 是实际调用请求的地方,而实际调用是采用 XMLHttpRequest API(浏览器)还是http/https 模块(Node.js),则需要进一步查看。

1
2
javascript复制代码// /v1.6.8/lib/core/dispatchRequest.js#L34
export default function dispatchRequest(config) { /* ... */ }

dispatchRequest() 接收的是上一步合并之后的 config 参数,有了这个参数我们就可以发送请求了。

跨端适配实现

1
2
javascript复制代码// /v1.6.8/lib/core/dispatchRequest.js#L49
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

这里就是我们所说的 axios 内部所使用的适配器模式了。

axios 支持从外出传入 adapter 参数支持自定义请求能力的实现,不过很少使用。大部分请求下,我们都是使用内置的适配器实现。

defaults.adapter

defaults.adapter 的值如下:

1
2
javascript复制代码// /v1.6.8/lib/defaults/index.js#L40
adapter: ['xhr', 'http'],

adapters.getAdapter(['xhr', 'http']) 又是在做什么事情呢?

适配器实现

首先,adapters 位于 lib/adapters/adapters.js。

所属的目录结构如下:

image.png

可以看到针对浏览器和 Node.js 2 个环境的适配支持:http.js、xhr.js。

adapters 的实现如下。

首先,将内置的 2 个适配文件引入。

1
2
3
4
5
6
7
8
javascript复制代码// /v1.6.8/lib/adapters/adapters.js#L2-L9
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';

const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter
}

knownAdapters 的属性名正好是和 defaults.adapter 的值 [‘xhr’, ‘http’] 是一一对应的。

而 adapters.getAdapter([‘xhr’, ‘http’]) 的实现是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
javascript复制代码// /v1.6.8/lib/adapters/adapters.js#L27-L75
export default {
getAdapter: (adapters) => {
// 1)
adapters = Array.isArray(adapters) ? adapters : [adapters];

let nameOrAdapter;
let adapter;

// 2)
for (let i = 0; i < adapters.length; i++) {
nameOrAdapter = adapters[i];
adapter = nameOrAdapter;

// 3)
if (!isResolvedHandle(nameOrAdapter)) {
adapter = knownAdapters[String(nameOrAdapter).toLowerCase()];
}

if (adapter) {
break;
}
}

// 4)
if (!adapter) {
throw new AxiosError(
`There is no suitable adapter to dispatch the request `,
'ERR_NOT_SUPPORT'
);
}

return adapter;
}
}

内容比较长,我们会按照代码标准的序号分 4 个部分来讲。

1)、这里是为了兼容调用 axios() 时传入 adapter 参数的情况。

1
2
3
4
5
javascript复制代码// `adapter` allows custom handling of requests which makes testing easier.
// Return a promise and supply a valid response (see lib/adapters/README.md).
adapter: function (config) {
/* ... */
},

因为接下来 adapters 是作为数组处理,所以这种场景下,我们将 adapter 封装成数组 [adapters]。

1
2
javascript复制代码// /v1.6.8/lib/adapters/adapters.js#L28
adapters = Array.isArray(adapters) ? adapters : [adapters];

2)、接下来,就是遍历 adapters 找到要用的那个适配器。

到目前为止,adapters[i](也就是下面的 nameOrAdapter)既可能是字符串('xhr'、'http'),也可能是函数(function (config) {})。

1
2
3
javascript复制代码// /v1.6.8/lib/adapters/adapters.js#L37
let nameOrAdapter = adapters[i];
adapter = nameOrAdapter;

3)、那么,我们还要检查 nameOrAdapter 的类型。

1
2
3
4
javascript复制代码// /v1.6.8/lib/adapters/adapters.js#L42-L48
if (!isResolvedHandle(nameOrAdapter)) {
adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
}

isResolvedHandle() 是一个工具函数,其目的是为了判断是否要从 knownAdapters 获取适配器。

1
2
javascript复制代码// /v1.6.8/lib/adapters/adapters.js#L24
const isResolvedHandle = (adapter) => typeof adapter === 'function' || adapter === null || adapter === false;

简单理解,只有 adapter 是字符串的情况('xhr' 或 'http'),isResolvedHandle(nameOrAdapter) 才返回 false,才从 knownAdapters 获得适配器。

typeof adapter === ‘function’ || adapter === null 这个判断条件我们容易理解,这是为了排除自定义 adapter 参数(传入函数或 null)的情况。

而 adapter === false 又是对应什么情况呢?

那是因为我们的代码只可能是在浏览器或 Node.js 环境下运行。这个时候 httpAdapter 和 xhrAdapter 具体返回是有差异的。

1
2
3
4
5
6
7
javascript复制代码// /v1.6.8/lib/adapters/xhr.js#L48
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {/* ...*/}

// /v1.6.8/lib/adapters/http.js#L160
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
export default isHttpAdapterSupported && function httpAdapter(config) {/* ... */}

也就是说:在浏览器环境 httpAdapter 返回 false,xhrAdapter 返回函数;在 Node.js 环境 xhrAdapter 返回 false,httpAdapter 返回函数。

因此,一旦 isResolvedHandle() 逻辑执行完成后。

1
javascript复制代码if (!isResolvedHandle(nameOrAdapter)) {/* ... */}

会检查 adapter 变量的值,一旦有值(非 false)就说明找到适配器了,结束遍历。

1
2
3
javascript复制代码if (adapter) {
break;
}

4)、最终在返回适配器前做空检查

1
2
3
4
5
6
7
8
9
javascript复制代码// 4)
if (!adapter) {
throw new AxiosError(
`There is no suitable adapter to dispatch the request `,
'ERR_NOT_SUPPORT'
);
}

return adapter;

如此,就完成了跨端架构的实现。

总结

本文我们讲述了 axios 的跨端架构原理。axios 内部实际发出请求是通过 dispatchRequest() 方法处理的,再往里看则是通过适配器模式取得适应于当前环境的适配器函数。

axios 内置了 2 个适配器支持:httpAdapter 和 xhrAdapter。httpAdapter 是 Node.js 环境实现,通过 http/https 模块;xhrAdapter 这是浏览器环境实现,通过 XMLHttpRequest API 实现。Node.js 环境 xhrAdapter 返回 false,浏览器环境 httpAdapter 返回 false——这样总是能返回正确的适配器。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

敏感信息前端加密的意义和实现方案

发表于 2024-04-27

引言

在传统安全模型中,数据传输往往依赖于HTTPS(SSL/TLS)协议来保证数据的安全性,但在网络通信并不总是安全的。为了保护数据的安全性,前端加密尤为重要,因为前端是用户与应用程序之间的桥梁,承担着用户输入的传输和展示任务。

如果前端未对敏感信息进行加密传输,则可能会被恶意第三方截取、篡改或窃取;而且未加密的明文信息也会被记录在日志或日志数据库中,甚至在某些系统中用户密码都是明文存储,这种将用户信息直接暴露在外行为是十分危险的,给别有用心之人带来很多可乘之机。

本篇文章将介绍前端加密的意义、加密的原理、常用的加密方式以及如何在Java中实现这些加密方式。

原理

加密的核心原理是通过一定的数学算法对原始数据进行变换,生成不易被破解的密文。常见的加密算法包括对称加密和非对称加密:

  • 对称加密: 对称加密使用相同的密钥对数据进行加解密,加解密的过程是可逆的。常见的对称加密算法有DES、AES等。对称加密的优点是加解密速度快,但需要保证密钥的安全传输,否则容易被破解
  • 非对称加密: 非对称加密使用一对密钥使用公钥加密,使用私钥解密。加密数据只能通过与公钥相对应的私钥解密,公钥或非对应私钥无法解密。常见的非对称加密算法有RSA等。非对称加密的优点是安全性高,但加解密速度相对较慢

加密算法的安全性取决于密钥的长度和算法的复杂性,越复杂的算法和越长的密钥越难以被破解,加密的过程通常包括以下几个步骤:

  • 密钥生成:对称加密需要生成密钥,非对称加密需要生成一对公钥和私钥
  • 加密:使用密钥对原始数据进行加密,生成密文
  • 解密:使用密钥对密文进行解密,恢复原始数据

常用方式

在前端开发中,常用的加密方式有以下几种:

  • AES对称加密:Java中javax.crypto包提供的Cipher类实现AES(高级加密标准:Advanced Encryption Standard)对称加密,在前端加密中被广泛使用
  • RSA非对称加密:Java中java.security包提供的KeyPairGenerator类和javax.crypto包提供的Cipher类实现RSA非对称加密,适用于加密小量数据和数字签名
  • 哈希算法:将任意长度数据应设为固定长度数据,但加密后数据不可逆,一般用于验证数据的完整性和可靠性,常用有MD5和SHA-256

实现

接下来,给出Java中使用AES和RSA加密算法对系统必备登录功能密码明文进行加解密处理示例

AES加密

后端在前端每次请求都生成一份一次性密钥(key)及初始向量(iv),密钥长度可以是16字节、24字节或32字节,初始向量长度必须是16字节

将key和iv进行存储,并使用安全技术(如Https)传给前端,前端根据本次获取到的key及iv对密码进行加密处理,并将用户名和加密后密码传给后端接口

1
2
3
4
5
6
7
php复制代码function encryptWithAes(key, iv, plainText){
return CryptoJS.AES.encrypt(plainText, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
}

后端接口获取到前端登录账号和密码信息,使用Java中javax.crypto包提供的Cipher类,根据已存储的key和iv对密码进行解密操作,并将明文返回,再根据后端密码规则对账号和密码进行验证

1
2
3
4
5
6
7
8
9
ini复制代码public static String decrypt(String encryptedData) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
// 解密数据
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes, StandardCharsets.UTF_8);
}

RSA加密

首先,在后端Java中使用java.security包提供的KeyPairGenerator类生成公钥和私钥,将私钥进行存储,并将公钥返回给前端

1
2
3
4
5
ini复制代码KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(KEY_SIZE);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();

前端拿到公钥后,使用JSEncrypt和公钥对密码进行加密,并将加密后的密码传到后端进行密码校验

1
2
3
4
5
scss复制代码function encryptWithPublicKey(publicKey, password) {
var encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
return encryptor.encrypt(password);
}

后端登录接口获取前端账号和密码信息,使用Java中javax.crypto包提供的Cipher类,根据已存储的私钥对加密后密码进行解密操作,并返回明文数据,再根据后端密码规则对账号和密码进行验证

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码public static String decryptWithPrivate(String encryptText, String privateKey) throws Exception {
if (StringUtils.isBlank(encryptText)) {
return null;
}
Provider provider = new BouncyCastleProvider();
Security.addProvider(provider);
Cipher ci = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
ci.init(Cipher.DECRYPT_MODE, keyFactory.generatePrivate(keySpec));
return new String(ci.doFinal(Base64.getDecoder().decode(encryptText.getBytes())));
}

下图展示出登录及修改密码操作数据库日志,圈出来部分未进行加密处理,可以很清楚的看到登录密码明文信息,而加密后信息就很难看出密码内容
数据库日志

对比

使用AES加密任然需要将key和iv通过接口传给前端,若在传输过程中窃取到key和iv同样可以直接将加密后数据解密出来;而RSA只需要将公钥传给前端,即使公钥被窃取也无法对加密后数据进行解密处理,相比AES安全性更高

总结

加密是一种保护数据安全的手段,将原始数据转换为密文,增加数据在传输和存储过程中被窃取、篡改或破解的难度,保护用户数据的安全性,提升系统的安全性和可信度。在互联网应用中,加密可以提供以下几方面的保护:

  • 保护数据隐私:用户个人信息、账号密码等敏感数据在网络上传输过程中,可能被黑客截获而导致隐私泄露。加密可以有效保护这些数据,即使被截获也无法被解读
  • 防止数据篡改:数据在传输过程中,可能会被第三方篡改,从而导致数据的真实性受到破坏。可以通过数字签名等加密手段验证数据的完整性,防止数据被篡改
  • 抵御中间人攻击:中间人攻击是一种常见的网络攻击方式,黑客通过伪装成通信双方之一来窃取数据。加密可以防止中间人窃取敏感信息,保障通信安全
  • 减少日志风险:使用密文数据对敏感信息进行操作,即使后端日志泄露也不会直接暴露用户敏感信息,能一定程度保障用户隐私

点击查看更多文章

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Phi-2:小型语言模型令人惊人的能力 导语 概览 Phi-

发表于 2024-04-27

导语

phi-系列模型是微软研究团队推出的轻量级人工智能模型,旨在实现“小而精”的目标,能够实现在低功耗设备上例如智能手机和平板电脑上部署运行。截止目前,已经发布到了phi-3模型,本系列博客将沿着最初的phi-1到phi-1.5,再到phi-2和phi-3模型展开介绍,本文介绍phi-2模型。

  • 标题:Phi-2: The surprising power of small language models
  • 链接:www.microsoft.com/en-us/resea…

image.png

概览

过去的几个月中,微软研究院的机器学习基础团队发布了一套名为“Phi”的小语言模型(Small Language Models,SLMs),在各种基准测试中取得了显著的性能。包括1.3B参数的Phi-1,在现有的SLMs中实现了Python编码的最先进性能(具体来说是在HumanEval和MBPP基准测试中)。和扩展到常识推理和语言理解的1.3B参数模型Phi-1.5,其性能可与大5倍的模型相媲美。

本文介绍Phi-2,一个2.7B参数的语言模型,展示了出色的推理和语言理解能力,在少于13B参数的基础语言模型中表现出最先进的性能。在复杂的基准测试中,Phi-2匹配或超过了大25倍的模型,这要归功于模型扩展和训练数据策划方面的新创新。Phi-2以其紧凑的尺寸成为研究人员的理想试验模型,包括对机械性可解释性、安全性改进或对各种任务进行微调实验。在Azure AI Studio模型目录中提供了Phi-2,以促进语言模型的研究和开发。

image.png

Phi-2的关键亮点

语言模型规模的大幅增加至数千亿参数已经解锁了一系列新兴能力,重新定义了自然语言处理的格局。一个问题仍然存在,即是否可以通过战略选择训练数据,例如数据选择,在较小的规模上实现这种新兴能力。

Phi系列模型的工作旨在通过训练实现与规模更大的模型相当的SLMs来回答这个问题(尽管仍远未达到前沿模型)。本文通过Phi-2打破传统语言模型扩展规律的关键见解有两个:

首先,训练数据质量对模型性能至关重要。几十年来这一点已经为人所知,但本文将这一见解发挥到了极致,着重于“教科书质量”的数据,延续了之前的工作“Textbook is all you need”。本文的训练数据混合包含了专门创建的合成数据集,用于教导模型常识推理和一般知识,包括科学、日常活动和心理理论等内容。进一步通过精心筛选的网络数据增强训练语料库,这些数据根据教育价值和内容质量进行了过滤。其次,使用创新技术进行扩展,从1.3B参数模型Phi-1.5开始,并将其知识嵌入到2.7B参数的Phi-2中。这种规模化的知识传递不仅加速了训练收敛,而且在Phi-2的基准分数中显示出明显提升。

image.png

训练细节

Phi-2是基于Transformer的模型,使用下一个单词预测目标,通过对多次经过的合成和网络数据集进行了NLP和编码的混合训练,共计1.4T词元(token)。训练耗时14天,使用96个A100 GPU。Phi-2是一个基础模型,尚未通过人类反馈的强化学习对齐(RLHF),也没有进行指令微调。尽管如此,与经过对齐的现有开源模型相比,本文观察到在毒性和偏见方面表现更好(参见图3)。这与在Phi-1.5中观察到的情况一致,这归功于作者量身定制的数据策划技术,请查阅之前的技术报告以了解更多详情。

image.png

Phi-2 评估

下面总结了Phi-2在学术基准测试中与流行语言模型的性能对比情况。基准测试涵盖了几个类别,包括大型基准测试(BBH)(3-shot与CoT)、常识推理(PIQA、WinoGrande、ARC、SIQA)、语言理解(HellaSwag、OpenBookQA、MMLU(5-shot)、SQuADv2(2-shot)、BoolQ)、数学(GSM8k(8-shot))和编码(HumanEval、MBPP(3-shot))。

仅有2.7B参数的Phi-2在各种综合基准测试中超越了Mistral和Llama-2模型(其参数分别为7B和13B)。值得注意的是,与体积大25倍的Llama-2-70B模型相比,Phi-2在多步推理任务上(即编码和数学)表现更佳。此外,尽管体积较小,Phi-2在性能上与最近宣布的Google Gemini Nano 2相匹配或表现更好。

当然,模型评估面临一些挑战,许多公开基准测试可能泄漏到训练数据中。对于Phi-1,作者进行了广泛的净化研究以排除这种可能性,可以在第一篇报告中找到。作者相信,评判语言模型的最佳方式是在具体用例上进行测试。遵循这一精神,本文还使用了几个微软内部专有数据集和任务对Phi-2进行了评估,再次将其与Mistral和Llama-2进行了比较。可以观察到类似的趋势,即在平均水平上,Phi-2优于Mistral-7B,而后者优于Llama-2模型(7B、13B和70B)。

image.png

image.png

除了这些基准测试之外,本文还对研究界常用的提示进行了广泛测试。观察到的行为符合在基准测试结果的预期。例如,本文测试了用于探究模型解决物理问题能力的提示,最近用于评估Gemini Ultra模型的能力,并获得了以下结果:

image.png

image.png

扩大规模的最佳实践(Best Practices to Scale up)

在扩大规模时,作者首先给出了phi-1模型上的一些实验结果:

image.png

可以看到,训练的次数越长,模型性能越好,但这样就越花费时间。为此作者尝试在大模型中重用已经训练好的小模型权重,但因此也会面临一个挑战:如何把小模型的权重扩展到大模型的维度上?

作者尝试了一个之前研究提出的方式,即层数上使用下图的公式将层数进行映射:

image.png

在维度上,则保留那些已有的权重作为新的大权重参数矩阵的一部分,剩下的部分进行随机初始化即可,如下图所示:

image.png

经过这样的重用实验后,得到实验结果如下:

image.png

另一种有效的参数继承方式是平铺(Tiling),示意如下:

image.png

得到的最终性能表现如下:

image.png

image.png

总结

一个良好的、通用的SLM可以通过以下方式实现:

  • 与传统的网络数据相对比,生成和利用具有“教科书质量”的数据;
  • 吸收扩展规模的最佳实践,以增强整体性能。

参考

  1. The Surprising Power of Small Language Models, Mojan Javaheripi, Microsoft Research, nips.cc/media/neuri…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kotlin的成员扩展函数和implicit receive

发表于 2024-04-27

概念

implicit receiver: 隐式的接收器或者接收者,接受函数的调用和属性的访问。

在java中这个隐式的接收者其实就是this。而kotlin对它进行了一些关键性的拓展,在拓展的同时,为了方便描述和沟通,给它起了专属的名字:implicit receiver。

比如在Java中,可以隐式的调用内部类和外部类的成员变量,kotlin同理。

kotlin特殊的点在于,他可以直接通过函数来嵌套新的this。

例如kotlin协程的CoroutineScope就利用了这种概念,可以理解成提供了一个CoroutineScope的环境。

image.png

成员扩展函数

现在定义这样一个类IntMultiplier,它的内部有个成员变量,同时这个成员变量又是Int的扩展函数

1
2
3
kotlin复制代码class IntMultiplier(val time: Int = 2) {
fun Int.multiply() = this * time
}

我们是无法直接调用它的。所以我们要想办法创造一个IntMultiplier的环境,才能去使用这个扩展函数。

这里写了一个runAsOuter的函数,传进去一个函数类型的参数block,又设置block的receiver类型是IntMultiplier,这样就强行插入了一个IntMultiplier的this。

1
2
3
4
5
6
7
kotlin复制代码class IntMultiplier(val time: Int = 2) {
fun Int.multiply() = this * time

fun runAsOuter(block: IntMultiplier.() -> Unit) {
block()
}
}

这样就可以通过这个函数来进行调用了。

1
2
3
4
5
6
7
kotlin复制代码fun main() {
val multiplier = IntMultiplier()

multiplier.runAsOuter {
3.multiply()
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

介绍riverpod中的refinvalidate、ref

发表于 2024-04-27

解释riverpod中的ref.invalidate

在 Riverpod 中,ref.invalidate 是一个非常有用的方法,它允许你手动标记一个 provider 为无效(或 “dirty”),从而触发它的刷新或重新计算。这个特性在某些场景下特别有用,比如当你知道由于外部因素(例如,数据库更新、文件变化、网络请求等)导致 provider 的数据已经过时,需要重新获取或计算数据时。

使用 ref.invalidate

ref.invalidate 方法接受一个 provider 作为参数。当调用此方法时,Riverpod 会将指定的 provider 标记为无效,并在下次尝试读取该 provider 时重新执行它的创建函数或异步请求,从而获取最新的数据。

这里是一个简单的使用示例:

1
2
3
4
5
6
Dart复制代码final myProvider = Provider((ref) => 'Initial data');

void someFunction(WidgetRef ref) {
// 在某个事件或条件触发时,使 myProvider 无效
ref.invalidate(myProvider);
}

在上面的示例中,当 someFunction 被调用时,myProvider 被标记为无效。这意味着下次当你或你的应用尝试读取 myProvider 时,它的值将会被重新计算。

应用场景

ref.invalidate 方法在需要主动刷新或更新数据时非常有用。以下是一些常见的使用场景:

  • 外部数据变化:当你知道提供给 provider 的数据源(如数据库、文件系统或远程服务器)已经改变,而 provider 需要更新其数据以反映这些变化时。
  • 用户操作:用户执行了某个操作(如点击按钮)需要刷新数据或视图。
  • 定时更新:在特定时间间隔后需要更新数据,虽然这种情况可以使用其他方法实现,但在某些特定逻辑下 ref.invalidate 可以作为一种选择。

注意事项

  • 使用 ref.invalidate 时,需要谨慎考虑其对性能的影响,因为频繁地使 providers 无效并重新获取数据可能会导致性能问题,特别是当数据获取包含昂贵的操作(如网络请求)时。
  • 在使用 ref.invalidate 时,考虑其对应用状态管理的影响。确保应用的逻辑和用户体验不会因为数据的频繁更新而受到负面影响。

总之,ref.invalidate 是 Riverpod 提供的一个强大的工具,允许开发者在适当的时机手动触发 provider 的更新,以确保应用中的数据保持最新。

如果我们调用invalidate(provider),但是并没有执行read(provider),会发生什么呢

当你调用 ref.invalidate(provider) 标记一个 provider 为无效时,但之后并没有执行 ref.read(provider) 或其他会触发 provider 读取的操作(比如监听 provider 的状态),那么以下情况会发生:

  1. 延迟执行:provider 实际上并不会立即重新执行或刷新其数据。invalidate 方法仅仅标记了 provider 需要在下一次读取时刷新,所以如果没有后续的读取操作,provider 的创建方法或加载逻辑不会被立即触发。
  2. 等待触发:被标记为无效的 provider 会保持在这个状态,直到有操作尝试读取它。这意味着,只有当你的应用(或测试代码)再次请求这个 provider 的数据时,Riverpod 才会重新执行 provider 的构建函数或相关的异步操作来获取新的数据。
  3. 无立即影响:因为没有立即执行读取,所以应用的 UI 或状态也不会立即发生变化。任何依赖于该 provider 的 widgets 或逻辑都不会更新,直到 provider 被实际读取。
  4. 优化性能:这种延迟执行的行为有助于优化应用性能,避免不必要的数据加载和状态更新,特别是在数据变化频繁或更新成本较高的场景中。

示例

假设你有一个通过网络请求获取用户信息的 FutureProvider:

1
2
3
Dart复制代码final userProvider = FutureProvider<User>((ref) async {
return fetchUserData();
});

如果你执行了 ref.invalidate(userProvider) 但之后没有读取 userProvider,则 fetchUserData 方法不会被立即调用。只有在后续某个点通过 ref.watch(userProvider)、ref.read(userProvider) 或 widget 使用这个 provider 时,fetchUserData 才会被触发,从而加载最新的用户数据。

总的来说,ref.invalidate(provider) 是一个标记操作,真正的数据更新和逻辑执行需要等到下一次对该 provider 的有效读取发生时才会进行。这种设计有助于减少不必要的操作,让数据更新更加高效和可控。

请解释ref.refresh

ref.refresh 是 Riverpod 中的一个方法,用于立即重新执行一个 provider,并获取其最新值,而不管以前的状态如何。与 ref.invalidate 相比,ref.invalidate 只是标记一个 provider 在下次读取时需要刷新,而 ref.refresh 则会立即触发这个操作。这意味着 ref.refresh 会强制执行 provider 的创建函数或异步操作,无论当前 provider 是否被标记为无效,或它的数据是否被认为是最新的。

使用 ref.refresh

ref.refresh 通常用在需要立即更新数据的场景,例如用户触发了一个操作来刷新数据,或者你知道数据源已经变化,需要立即反映这个变化到应用中。通过调用 ref.refresh,你可以确保应用立即获取到最新的数据状态。

示例

假设你有一个用于获取用户信息的 FutureProvider:

1
2
3
Dart复制代码final userProvider = FutureProvider<User>((ref) async {
return fetchUserData();
});

如果你想在用户点击一个按钮时立即刷新用户信息,可以这样做:

1
2
3
4
5
6
Dart复制代码ElevatedButton(
onPressed: () {
ref.refresh(userProvider);
},
child: Text("Refresh User Data"),
);

当 ref.refresh(userProvider) 被调用时,Riverpod 会立即重新执行 fetchUserData() 函数,无论之前的异步操作是否已经完成或者数据是否被认为是最新的。这将导致与 userProvider 相关的 widgets 根据新获取的数据更新 UI。

注意事项

  • 使用 ref.refresh 会立即触发数据的重新加载,这可能会引起相关 UI 的快速重建。在某些情况下,如果不希望用户体验到界面的频繁重绘,需要谨慎使用。
  • 对于依赖其他 provider 数据的 provider,使用 ref.refresh 也会导致依赖的 provider 被重新计算。
  • 与 ref.invalidate 相比,ref.refresh 更为积极地确保数据的即时更新,但也可能带来更多的网络请求或计算,影响性能。合理安排刷新时机对于保持应用性能和用户体验是非常重要的。

总之,ref.refresh 是一个强大的工具,能够帮助你控制和管理应用的数据状态,确保用户总是看到最新的信息。正确使用这个方法可以显著提升应用的响应性和实用性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

具备什么样的技能才能成为架构师

发表于 2024-04-27

在过去的二十年里,技术界和软件开发方法经历了剧烈的变化和发展。新技术的出现和开发实践的进步使得软件项目变得越来越复杂,但同时也更加强大和灵活。在这样一个快速演变的环境中,软件架构师的作用日益显著,成为项目成功的关键因素。他们不仅是技术决策的制定者,也是团队之间协作的桥梁,确保技术解决方案既能满足当前需求,又具备长远的视野。

特别是在当今强调敏捷性和团队自治的文化背景下,架构师的角色并没有因为敏捷实践的推广而变得边缘化。相反,他们在协助团队快速适应变化、保持技术路线清晰和前瞻性方面扮演着至关重要的角色。在前几节中,我详细探讨了架构师的重要性,以及他们在适应敏捷开发模式中的关键作用。

本节的重点转向架构师本人,探讨了成为一名成功架构师所需的关键特质和技能(如图1所示)。尽管成为一名出色的架构师需要广泛的知识和技能,但基于个人经验,我将重点介绍几项被广泛认为最为关键的技能。这些技能希望能帮助读者深入了解架构师的核心能力,以及如何培养这些能力以适应不断变化的技术领域。

图1

1、简化能力

大多数架构图和规范都因设计而变得复杂。建筑师有目的地通过认为复杂性来展示他们的能力来做到这一点。然而,事实恰恰相反——复杂的架构无助于软件开发。架构必须逐步构建,以便新接触问题空间的人能够轻松理解。

我使用一种称为级别的技术,从级别 0 开始逐步构建系统。在大多数情况下,L0 是框图,L1 是解决方案图,L2 是技术和供应商产品。这种方法有助于解释复杂的问题并将其分解为模块。读或听的人确实理解,因为架构构建层层递进。

在讨论成为一个成功的架构师需要哪些技能时,有一个特别值得关注的能力——简化复杂性。我们常见的问题是,许多架构设计图和规范文档因为试图展现深奥的技术细节而变得异常复杂和难以理解。有时,这种复杂性似乎是有意为之,以显示架构师的专业能力。但实际上,这种复杂的架构设计并不利于软件项目的开发和理解。一个有效的架构,应该是可以被项目团队成员逐步理解的,尤其是那些刚刚接触到项目的新成员。

针对这一挑战,我采纳并推荐一种逐层递进的方法来构建和解释系统架构。这种方法从一个高度概括的视图开始,即级别0(L0),这一级别通常以框图的形式出现,旨在提供一个大致的概览。接下来,级别1(L1)进一步细化为解决方案图,展现了系统的关键组件及其相互作用。最后,级别2(L2)深入到具体的技术选择和供应商产品,详细说明了实现细节。

使用这种层次分明的方法,我们不仅能够有效地将复杂的架构问题拆解为更小、更易管理的单元,而且确保了信息的接收者能够通过逐层深入的方式,逐步建立对整个系统的理解。这种方法强调了架构师的简化能力,不是通过增加不必要的复杂性来展示技术深度,而是通过清晰、有条理的递进,使得复杂的系统变得简化和可理解。

1.2、视觉思维

在深入讨论架构师必备技能的过程中,我们不得不强调视觉思维的重要性。架构师天生倾向于利用视觉手段进行沟通和想法共享,这使得掌握如何通过图形模型表达思维成为了他们的基本技能之一。为了高效地使用这些视觉工具,架构师必须培养出一种能够将复杂概念直观化的思维模式。这种视觉化的方法体现在他们使用白板、记事本进行讨论,甚至在紧急情况下,在草稿纸上画出草图的习惯上。

我的个人经历恰好反映了视觉思维在职业生涯发展中的价值。从青少年时期开始绘制的七龙珠卡通,到在大学期间利用视觉笔记帮助记忆课程内容,这些经历早期就培养了我对视觉表达的喜爱和能力。当我作为一名软件专业人士进入工业界后,这种能力转化为了用草图来沟通复杂的软件概念。

这种视觉思维的能力允许架构师使用各种架构友好的符号和表示法,从基本的块图、流程图到UML和非标准的自定义图形,以清晰和引人入胜的方式传达复杂的架构理念。这不仅使我能够更有效地与同事和社区成员沟通,而且通过这种直观的方法,架构的复杂性变得更易于管理和理解。视觉思维不仅是架构师个人技能的重要组成部分,也是推动软件开发领域知识共享和创新的强大工具。

1.3、技术绘图

在讨论架构师所需技能的时,技术绘图能力不可或缺,架构师借助于技术图表来展示他们的视觉思维,而这些图表远非简单的美术作品,它们是技术信息的视觉传达。因此,架构师们需要不仅学会基本的技术制图技巧,而且还要掌握如何精确绘制几何图形、有效利用图表的空间布局以及提高图表的整体可读性。此外,为了确保所绘制图表的风格一致性和信息的真实性,搭建一个包含配色方案和图标库的媒体工具包也显得非常重要。

从个人经历来看,我们在高中期间就已经接触并使用流程图来描述业务流程,这是我早期接触技术绘图的经历之一。随着成为一名软件架构师,技术绘图成了我表达架构设计思想的关键工具。我的同事们经常对我制作的高质量图表表示好奇,询问我所使用的工具是什么。这里,我想强调的是,真正使图表突出的不仅是工具本身,而更多是背后的思考和绘图模式。我长期使用的processon,作为一款技术绘图工具,以其易用性和高效性赢得了我的青睐。

技术绘图技能的掌握使架构师能够将复杂的技术概念以一种清晰且直观的方式呈现出来,既保证了信息的传达既有逻辑性又有条理,又确保了不同背景的读者都能轻松理解和跟进。这一能力对于在团队内部促进有效沟通,以及与外部利益相关者进行有效交流,都是至关重要的,它让技术细节的讨论变得更加高效、明确,极大地提升了整个项目团队的工作效率和成果的质量。

1.4、系统思维

我们直奔主题:架构师的工作本质上是解决一系列复杂问题的过程。为了有效应对这些挑战,架构师必须装备自己以一种关键的能力——系统思维。这种思维方式要求架构师具备一个双重视角:一方面,需要有能力从宏观上把握和理解整个系统的结构和功能;另一方面,还要能够深入至系统的每一个构成部分,洞察每个组件的细节和作用。简言之,系统思维不仅为架构师在设计和实现预期的结果及目标提供了坚实的基础,它也是帮助他们在保持对大局的把握的同时,精细管理细节的关键。

通过将系统架构师的角色与帮助公司提升业务效率及降低运营成本的目标相结合,我深刻体会到了系统思维能力的重要性。视企业为一个复杂的自适应系统(CAS),超越了我对技术的传统理解,让我以一种更全面、更综合的视角来审视和构思架构。这种方法不仅加深了我对业务流程和需求的理解,而且促进了我与同事们的紧密合作。我们共同设计出的架构方案,旨在服务业务目标的同时,提升运营效率,从而为公司创造了更大的价值。

系统思维是架构师设计高效、有效系统架构不可或缺的基石。它不只是一个技术技能,更是一个跨学科、跨部门合作、优化业务流程的强大工具。通过不断地培养和应用这种思维方式,架构师能够为企业的发展贡献出自己的力量,带来持续的、长远的价值。

1.5、市场洞察

架构师需要了解的是架构师的职责不仅仅局限于设计和实施技术解决方案,更重要的是,他们需要具备敏锐的市场洞察力。这意味着架构师必须密切关注行业中的最新动态——包括技术的快速进步、新模式的出现以及最佳实践的采用。这样做可以使他们应用最前沿的方法来构建精确而高效的架构规划,不仅能够迅速产生价值,而且能够着眼未来,引领潮流。

科技行业的快速发展给架构师带来了不小的挑战,要紧跟这样的发展步伐,需要持续的学习和适应。就我个人而言,我采取了多种策略来保持与行业进步的步调一致。这包括在掘金、CSDN、今日头条、GitHub以及知乎等平台上关注那些在行业中具有影响力的人物,他们的见解和分享往往是洞察行业趋势的窗口。此外,我还订阅了一系列高质量的技术新闻资讯,并积极参与线上和线下的技术论坛,参加相关的技术活动,观看最新的教学视频。我还广泛阅读书籍和文章,以获得更深入的知识和理解。另一个我特别重视的是教学的力量——通过选择性地教授某些技术领域的课程,不仅加深了我对这些领域的理解,同时也为技术社区做出了贡献。

市场洞察力对于架构师而言,是一项至关重要的技能。它要求架构师不停地更新自己的知识库,以确保他们设计的架构方案既能满足当前的需求,又能预见未来的挑战并为之做好准备。通过利用各种信息资源和渠道,架构师可以有效地提升自己的市场洞察力,从而在快速变化的科技环境中保持领先地位。

1.6、沟通能力

架构师在业务和技术之间扮演的独特角色——他们是连接这两个世界的桥梁。借用前新加坡智能国家研究员Gregor Hohpe的比喻,架构师就像是在大厦的结构中,通过电梯连接顶层决策者和地下机房的技术人员,确保信息的自由流通和理解。这要求架构师不仅能够理解复杂的系统,还要能够以简洁明了的方式向非技术人员解释这些系统。有效沟通的方式多样,包括但不限于口头交流、书面文档以及视觉展示。

通过在架构领域近二十年的职业生涯,我深刻体会到沟通技能的重要性,它使我逐渐成为一位能够有效传达技术理念的技术传播者。无论是在指导团队成员、参与市场营销活动,还是在担任面向客户的技术解决方案架构师和业务分析师等角色中,我都在不断地锤炼自己的沟通能力。这些经历不仅加深了我的技术理解,也提升了我与团队成员、客户及合作伙伴之间的沟通效率,促进了彼此之间的理解和协作。

沟通能力对于架构师而言,不仅是一项基本技能,更是其成功不可或缺的关键因素。通过有效的沟通,架构师能够确保业务需求和技术实施之间的顺畅对接,促进项目的顺利进行。因此,不断地学习和提高沟通技能,对于每一位架构师来说都是一项持续的任务,它将在促进团队合作、推动项目成功方面发挥至关重要的作用。

1.7、编程能力

我们需要清晰地认识到一个事实:沟通能力固然对架构师来说至关重要,但如果仅仅专注于沟通而忽视了项目的具体实施,架构师的角色就可能沦为仅存在于理论或演示文稿中的“PPT架构师”。为了避免这种情况的发生,架构师必须投身于项目实施的具体细节之中,亲自参与到日常的工作流程里。借助敏捷开发和自组织团队的现代管理框架,架构师可以与开发团队紧密合作,直接参与到软件开发的过程中,贡献自己的力量。这不仅仅意味着架构师需要有能力进行架构设计,还意味着他们需要动手编码,直接面对各种技术配置挑战。

以我的经历为例,我深切地理解到编码对于架构师角色的极端重要性。在我长达十几年的职业生涯中,我积极参与了众多编码项目,其中包括国内某领先保险公司的首个智能客服和智能外呼系统的开发,一家主业电视企业的智能推荐系统建设,以及一个模块化中间件平台的创建等。即便后来我的工作角色更多地转向了作为对外的技术顾问,我仍未放弃编码工作。我通过为产品增加新的示例、扩展点以及开发演示中所需的用例,继续为项目贡献自己的技术力量。此外,我还利用晚间时间担任技术讲师,不仅以此完成编码任务,更向那些有志于成为程序员的非专业人士传授编码知识。

对于架构师而言,编程能力不仅是一项必备技能,更是他们成功的关键。这要求架构师深入到项目的实施层面,亲自参与到编码和技术配置中去。这种实际参与不仅能够确保所设计的架构得到有效实施,同时也能够促进架构师的技术能力和实战经验的提升。通过这样的实践,架构师能够更加深入地理解开发团队的需求和面临的挑战,进而设计出更加符合实际需要的解决方案,为项目的成功奠定坚实的基础。

2、总结

说了这么多,我们主要聚焦于大型项目或产品里的架构师角色,这些项目通常需求明确,多团队合作。但实际上,不是所有的项目都符合这种模式,也不是所有的团队都需要这样的角色分工。特别是在创业团队中,情况往往大不相同。对这些团队来说,最紧迫的任务是确保项目的生存和发展。因此,他们采纳的是一种更加灵活、快速的开发哲学——敏捷开发。这种方法强调快速迭代、灵活试错,37signals出版的《Getting Real》一书便是这种思维方式的经典之作。它特别适合于那些不依赖复杂底层架构、功能较为简单、可以迅速开发原型并通过连续小迭代进行改进的项目,如Web应用和移动应用程序。

进一步讲,架构师并非技术人员的唯一职业发展路径。实际上,成为架构师要求你在技术的广度与深度上都有所建树,同时还需要掌握大量的业务知识和组织管理能力。然而,很多技术人员更愿意深入挖掘技术本身的深度,他们对于投入大量时间去了解业务逻辑和提升沟通技巧可能并不感兴趣。这样的技术人员更适合走技术专家的路线,专注于深度探索特定的技术领域,如算法设计、编程语言的深度应用、运行环境(包括虚拟机、操作系统、应用服务器、中间件)以及复杂的通信机制等。他们解决的问题为软件技术的抽象化和模式化奠定了坚实的基础,显示出技术专家路径的重要性不亚于架构师。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我不想再翻代码找export了, 所以

发表于 2024-04-27

写前端, 还是要解决自己需求才好玩。

起因

当我快乐的CRUD的时候,我注意到了一个很没意义的操作:

  1. 当写src/view/xxxx文件时
  2. 总会跑到src/http/api/xxxx 或者 src/tools/xxx 文件, 去看看那个文件 export 的变量名
  3. 然后切回src/view/xxxx 来写 import xxx from ‘xxxx’.
  4. 重复 1-3 N次

我感到疲惫,所以我在想有没有更方便的方式。
比如文件目录中,展示这个文件的exports


然后还能直接拖拽, 拖拽时顺便带上import xxx from xxx


开整
–

于是我开始研究vscode插件的开发教程, 打开官方文档,霍,很简单嘛,也就两句命令.
code.visualstudio.com/api/get-sta…


接下来就是漫长的调研学习,不得不说vscode插件资料比较少,参考了下面的教程:

  • vscode 插件 官方教程中文翻译
  • vscode 插件 Treeview 开发教程
  • vscode Webview 完美集成 Webpack 热更新
  • 案例: 掘金一下
  • 案例: 代码提示
  • 案例: vscode+solidjs

学完发现,其实重要的就是两个逻辑:

  1. 遍历文件目录
  2. AST分析脚本

逻辑1就不说了,比较常规(常规到我直接用chatgpt生成), 大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
javascript复制代码async getDirectory(message) {
const directory = message.data.path
const entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(directory))
const resultPromise = entries.map(async ([name, type]) => {
const filePath = path.join(directory, name)

if (this.ignore.check(filePath, message.data.root))
return
const item: FileItem = {
id: filePath,
path: filePath,
title: name,
fileType: type === vscode.FileType.Directory ? 'Directory' : 'File',
leaf: false,
root: message.data.root,
}
if (type === vscode.FileType.File) {
const extname = path.extname(filePath).replace('.', '')
item.fileExt = extname
if (!['js', 'tsx', 'jsx', 'ts'].includes(extname))
return
}
if (type === vscode.FileType.Directory)
item.children = []
return item
})

const results = await Promise.all(resultPromise)
return [0, results.filter(item => item)]
}

核心还是在逻辑2上,想了想可能有两个办法:

  1. 通过动态import,进行运行时分析
  2. 通过Bable的AST进行静态语法分析

哪么选择哪一个逻辑更好呢? (小孩子才做选择,大人我全都要 !)

  • 对于动态 import,经朋友推荐,调研使用了 antfu 大佬开源的 pkg-exports 以及 local-pkg 库。
  • 对于BabelAST,找到了以下资源学习:
    • Babel官方文档
    • AST查看工具
    • 玩转AST

大致流程就是先babel.parser,再babel.traverse,最后分析AST就完事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
js复制代码export async function solveExports(code) {
const ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'decorators'],
errorRecovery: true,
})
const exports: any = []
traverse(ast, {
ExportDefaultDeclaration(astPath: any) {
const returnType = solveDetail(astPath)
exports.push({
name: 'default',
type: astPath.node.declaration.type,
returnType: returnType?.type,
children: returnType?.children,
})
},
ExportNamedDeclaration(astPath: any) {
const specifiers = astPath.node?.specifiers || []
specifiers?.forEach((specifier: any) => {
exports.push({ name: specifier.exported?.name, type: specifier.exported?.type })
})
const returnType = solveDetail(astPath)
exports.push({
name: astPath.node?.declaration?.id?.name,
type: astPath.node?.declaration?.type,
returnType: returnType?.type,
children: returnType?.children,
})
},
})
return exports
}
}

最后就是漫长的踩坑,写业务逻辑和业务调试,我后续再依次更新。

  • 比如如何配置工程化
  • 比如如何用solidjs开发webview
  • 比如支持vue,调研学习了vue-compiler
  • 比如分析出export的变量类型,以方便导入import
  • 等等等等

进度

目前感觉初步解决自己的需求,已发布体验版到VSCode商店.

VScode 插件商店搜索 XMouse 即可使用(也可以点击下方链接)

marketplace.visualstudio.com/items?itemN…

image.png

未来

  • 准备进一步分析文件export,以方便更好的使用。
  • 准备集成VSCode原生文件目录的核心功能,就不用来回切换面板。
  • 等等等等,期待建议

最后

我是尘码,曾鹅厂打工5年,现裸辞自由职业家里蹲 ing.

如对 Xmouse 有兴趣,欢迎私聊or提issue

XMouse github link

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

git从入门到入土 Git使用步骤

发表于 2024-04-27

Git使用步骤

1、Git客户端(入门咯)

官网

1
http复制代码https://git-scm.com

下载页面

1
http复制代码https://git-scm.com/download/win

建议下载 64-bit Git for Windows Portable 。

下载后得到 PortableGit-2.41.0-64-bit.7z.exe 自解压文件,

将其复制到 D:/ecuter/applications 目录下,双击启动自解压。

最后将 D:\ecuter\applications\PortableGit\bin 添加环境变量的 Path 变量中。
这里我多提一嘴,如果你默认安装在C盘,那就不用配置环境变量了,在我们环境变量中,已经默认有C盘下载的目录并指向那个文件夹下的bin,bin是什么?一般来说这个文件夹存放了大量的命令,比如你要执行git add . 命令,那你的终端是不是首先要认识git命令?因此才说要有环境变量,告诉你的电脑,git这个可执行命令存放在哪里,也就是配置环境变量的过程,比如你下载D盘的git目录,git底下就有一个用于存放命令的文件夹,那么你配置环境变量,地址指向这个文件夹,电脑就知道命令在哪里,也就认识这个命令了。

配置好环境变量后可以在 命令提示符 中执行 git -v 或 git --version 查看版本信息。

我们可以通过 git --help 或 git -h 来查看 git 帮助信息,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
sh复制代码C:\Users\Administrator>git -h
usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
[--super-prefix=<path>] [--config-env=<name>=<envvar>]
<command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
clone Clone a repository into a new directory
init Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
add Add file contents to the index
mv Move or rename a file, a directory, or a symlink
restore Restore working tree files
rm Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)
bisect Use binary search to find the commit that introduced a bug
diff Show changes between commits, commit and working tree, etc
grep Print lines matching a pattern
log Show commit logs
show Show various types of objects
status Show the working tree status

grow, mark and tweak your common history
branch List, create, or delete branches
commit Record changes to the repository
merge Join two or more development histories together
rebase Reapply commits on top of another base tip
reset Reset current HEAD to the specified state
switch Switch branches
tag Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
fetch Download objects and refs from another repository
pull Fetch from and integrate with another repository or a local branch
push Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.

2、SSH公钥

3.1、生成公钥

在 PowerShell 或 命令提示符 或 Git Bash 中执行以下命令即可生成公钥:

1
sh复制代码ssh-keygen -t ed25519 -C "Gitee SSH Key"

Windows 用户建议使用 Windows PowerShell 或者 Git Bash,

因为在 命令提示符 下无 cat 和 ls 等命令。

详细步骤可以查看由 gitee 官网提供的帮助文档: 生成、添加 SSH 公钥

3.2、添加公钥

这里的添加公钥是在 gitee.com 添加我们刚刚生成的 公钥。

首先登录 Gitee 官网,

随后鼠标悬浮到右上角用户头像处,在下拉菜单中选择设置,

在新开启的页面左侧找到 安全设置 ,点击 SSH公钥 打开添加公钥页面。

详细步骤可以查看由 gitee 官网提供的帮助文档: 生成、添加 SSH 公钥

3.3、可信主机

将 gitee.com 添加到本地的可信主机列表中。

1
sh复制代码ssh -T git@gitee.com

在等待用户输入时,选择输入 yes 后再回车。

该步骤会在用户主目录下的 .ssh 目录中产生 known_hosts 文件。

3、远程仓库

在登录 Gitee 之后,

鼠标悬浮到右上角的 + 号图标,

点击新建仓库即可开始新建仓库操作。

4、本地仓库

我们可以通过两种方式创建本地仓库:

  • 新建一个目录,并在其中执行 git init 初始化该目录为一个本地仓库
  • 通过克隆远程仓库得到一个本地仓库

因为在 第3部分 已经在 Gitee 创建一个远程仓库,所以我们可以将其克隆到本地。

1
sh复制代码git clone git@gitee.com:malajava/spring-tutorial.git

从远程仓库克隆成功后,在本地会生成一个 spring-tutorial 目录。

此时,我们就说这个 spring-tutorial 目录就是一个本地仓库,

在其内部一定包含一个 .git 目录,该目录就是本地仓库的所有配置。

5、代码管理

5.1、状态

首先进入到某个Git仓库内,比如 spring-tutorial 仓库中:

1
cmd复制代码D:\ecuter\codes\spring-tutorial>

通过 git status 来查看该仓库的状态:

1
2
3
4
5
cmd复制代码D:\ecuter\codes\spring-tutorial>git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

5.2、添加

将Git本地仓库中所有改动的文件添加到Git本地仓库的暂存区:

1
cmd复制代码D:\ecuter\codes\spring-tutorial> git add .

此处的 . 表示当前目录,而当前目录D:\ecuter\codes\spring-tutorial就是当前Git仓库spring-tutorial的根目录,所以该操作意味着将当前仓库下所有改动的文件添加到暂存区。

此处“改动的文件”包括:新增的文件、被修改的文件、被删除的文件

5.3、提交

将本地Git仓库中暂存区中的内容提交到本地Git仓库:

1
sh复制代码git commit -m "注释"

比如:

1
cmd复制代码D:\ecuter\codes\spring-tutorial> git commit -m "新增了一个Java源文件"

5.4、推送

可以通过 git push 命令将本地Git仓库中已经commit的操作推送到远程仓库:

1
sh复制代码git push origin master

当然你没有配钥匙的情况下,git可能不知道你要推到哪个仓库,因此你可以通过

1
csharp复制代码git remote add origin 加上你的仓库地址

然后再去做推送操作

5.5、拉取

可以通过 git pull 命令从远程仓库抓取最新代码:

1
sh复制代码git pull origin master

6.版本回退

有的时候我们修改了文件,然后把修改好的文件提交到我们的仓库后add. -> commit ->push 后,也许我们会修改很多次这个文件,然后提交很多次,那么在仓库这个文件就会出现很多个版本,但是在我们实际工作时,不可能记得每一次我们做了什么修改因此,git作用就出现了。版本控制系统肯定有某个命令可以告诉我们历史记录,在Git中,我们用git log命令查看。如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline参数:,那么这些一大串的数字是什么呢?其实就是你的提交版本号,他不直接指明1234,但是每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线,为什么commit id需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。

回退

那么我们修改了这么多个版本,如果我们发现现在这个版本不行,还是想要上一个版本怎么办呢?
首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD表示当前版本,也就是最新的提交1094adb...(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100。

现在,我们要把当前版本append GPL回退到上一个版本add distributed,就可以使用git reset命令:

1
2
csharp复制代码$ git reset --hard HEAD^
HEAD is now at e475afc add distributed

欸?此时已经还原到了上一个版本,注意!!!如果这个时候你后悔了,你还是想要新版本,但是此时,你用git log看此时已经不存在最新的那个版本了,你想后悔怎么办?只要你还没有把当前窗口关闭,那就还有机会,你往回拉,查看那个版本的版本号,版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。

工作区、暂存区

工作区:就是你在电脑里能看到的目录,就你的本地仓库,那个文件夹。

版本库:工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。

Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。

image.png
第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;

第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。

因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。

管理修改

git 管理的是修改而不是文件,你在本地修改了一个文件,然后你add 然后你又对这个文件进行修改,你再commit,那么此时第二次的修改不会提交到master上,因为你的暂存区存的是你第一次修改的,而工作区放的是你第二次修改的,commit是把暂存区的内容推送

撤销修改

人都会犯错,我们如果不小心在文件中添加了一些不该添加的内容,此时我们通过git status可以发现有一个文件修改了,并且git会告诉你git checkout -- file可以丢弃工作区的修改命令git checkout -- readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:

一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;

一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。

分支管理

分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
首先,我们创建dev分支,然后切换到dev分支:

1
2
css复制代码$ git checkout -b dev
Switched to a new branch 'dev'

git checkout命令加上-b参数表示创建并切换,相当于以下两条命令:

1
2
3
ruby复制代码$ git branch dev
$ git checkout dev
Switched to branch 'dev'

然后,用git branch命令查看当前分支:

1
2
3
markdown复制代码$ git branch
* dev
master

现在,我们把dev分支的工作成果合并到master分支上:

1
2
3
4
5
scss复制代码$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)

git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。`

注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。合并完成后,就可以放心地删除dev分支了:

1
ruby复制代码$ git branch -d dev

git rebase

有没有懂的同学?怎么用?什么时候用?欢迎补充

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…678…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%