请求规范

[toc]

一、各种示例

1、request的HEADER公共入参(固定+可变的)示例

1
2
3
4
5
6
7
8
9
10
11
12
13
- HEADER(REQUEST):
{
"version":"1.16.30",
"buildNumber":"16301610",
"platform":"iOS",
"appType":"monkey",
"appFeatureType":"dev",
"packageChannel":"general",
"trace_id":"1683158692451101/2023-05-04 08:04:52/_sy_]onw^w^",
"Authorization":"bearer clientApp247deea3-da67-47bc-94f7-04b83ca0fb07",
"content-type":"application/json",
"content-length":"28"
}

参数详情介绍见下文《公共入参介绍》。

2、request的BODY公共入参(固定+可变的)示例

示例可详见 《埋点规范》–【埋点入参】–固定的公共入参 。附:其实改部分也可考虑放在HEADER中

3、response的返回结构示例

1
2
3
4
5
6
7
8
{
"status": 200,
"message": "请求成功,但是业务处理失败",
"errorCode": 1001,
"errorMessage": "参数name不能为空",
"data": null,
"timestamp": "2023-05-04T09:00:00Z"
}

二、设置request的HEADER公共入参(固定不变7个+可变N个)

网络库初始化所需的header参数

header形参 header形参名 描述 示例
必填 Map headerCommonFixParams header 中公共但【不变】的参数 如上
可选 block headerCommonChangeParamsGetBlock header 中公共但【会变】的参数 { ‘trace_id’: TraceUtil.traceId() }
可空 String headerAuthorization header 中公共但会变的参数
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
// 1、初始化网络库的时候定义
required Map<String, dynamic> headerCommonFixParams, // header 中公共但不变的参数
String? headerAuthorization, // header 中公共但【基本不会变】的参数--Authorization
Map<String, dynamic> Function()? headerCommonChangeParamsGetBlock, // header 中公共但会变的参数
// 示例:
headerCommonChangeParamsGetBlock: () {
return {
'trace_id': TraceUtil.traceId(),
};
},
// 1.2、初始化网络库的时候使用 headerCommonFixParams
// headers 的所有参数处理
Map<String, dynamic> headers = {};
// ① header 中公共但【固定不变】的参数
headers.addAll(headerCommonFixParams);
// ② header 中公共但【基本不会变】的参数--Authorization--策略:因为这只是一次性取值,后面的没法自动变化,需要调用update接口
if (headerAuthorization != null && headerAuthorization.isNotEmpty) {
headers.addAll({'Authorization': headerAuthorization});
}
// 将 header 参数设置到网络单例中
dio!.options = dio!.options.copyWith(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
contentType: contentType,
headers: headers,
);

// 2、具体发起网络请求里进行: headers 的所有参数处理
if (options.headers != null) {
if (optionsHeaderCommonChangeParamsGetBlock != null) {
Map<String, dynamic> customHeaders =
optionsHeaderCommonChangeParamsGetBlock();
options.headers!.addAll(customHeaders);
}
}

1、【固定的】公共请求参数 headerCommonFixParams(7个)

获取方法形如:

1
2
3
4
headerCommonFixParams: await CommonParamsHelper.commonHeaderParams(
appFeatureType: appFeatureType,
channel: channelName,
);

参数介绍如下:

序号 类型 字段 值示例 用途
1 平台系统 platform iOS/Android
2 app的唯一标识 appType 区分出同一公司的多个app
3 版本号(1.月.日) version 1.04.30
4 版本编译号(月日时分)
安卓隔年需加+12
buildNumber 04301610
5 功能类型 appFeatureType dev、inner、formal 区分出内测、正式功能
6 渠道 packageChannel appstore
7 唯一设备码 deviceId 未登录时的关闭检查更新

1.1、平台(iOS/Android)

1.2、app类型(同一公司多个app)/App的唯一标识

1.3、版本号(1.月.日)

1.4、版本编译号(月日时分)

1.5、功能类型(蒲公英dev、内测inner、正式formal)

1.6、渠道

平台 应用商店 渠道名
iOS App Store general
Android 华为应用市场 huawei
Android 小米应用商店 Xiaomi
Android Vivo应用商店 vivo
Android ColorOS应用商店 oppo
Android 应用宝 yingyongbao
Android 360手机助手 360
Android 百度手机助手 baidu
Android 酷安应用市场 kuan
Android 阿里应用市场 alibaba
Android 官网包 general

1.7、DeviceId 唯一设备码

Android 端主要取 Android ID

iOS 端先尝试获取 IDFA,如果获取不到,则取 IDFV

2、【变化的】公共请求参数 headerCommonChangeParamsGetBlock

获取方法形如:

1
2
3
4
5
6
headerCommonChangeParamsGetBlock: () {
return {
'trace_id': TraceUtil.traceId(),
'Authorization': headerAuthorization,
};
},

参数介绍如下:

序号 类型 字段 值示例 其他
1 用户token Authorization bearer clientApp247deea3-da67-47bc-94f7-04b83ca0fb07
2 跟踪id trace_id 1683158692451101/2023-05-04 08:04:52/_sy_]onw^w^

2.1、用户 Authorization

该值为登录时候,后台接口返回。且需要在用户状态变更的时候同步更新。

2.2、跟踪 trace_id

组成部分:时间戳/时间字符串/10位随机字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:math';
import 'package:flutter/foundation.dart';

class TraceUtil {
static String traceId() {
String timeStamp = DateTime.now().microsecondsSinceEpoch.toString();
String timeString = DateTime.now().toString().substring(0, 19);
String randomString = TraceUtil.generateRandomString(10);
String traceId = "$timeStamp/$timeString/_$randomString";
return traceId;
}

// Declare a fucntion for reusable purpose
static String generateRandomString(int len) {
final _random = Random();
final result = String.fromCharCodes(
List.generate(len, (index) => _random.nextInt(33) + 89));
return result;
}
}

三、设置request的BODY公共入参(当不放header时候)

详见 《埋点规范》–【埋点入参】–固定的公共入参 其实可考虑放在HEADER中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1、初始化网络库的时候定义
required Map<String, dynamic> bodyCommonFixParams, // body 中公共但不变的参数
Map<String, dynamic> Function()? bodyCommonChangeParamsGetBlock, // body 中公共但会变的参数(一般可考虑放在header中)

// 2、具体发起网络请求里进行: body 的所有参数处理
// ① body 中公共但不变的参数
Map<String, dynamic> allParams = Map.from(_bodyCommonFixParams);
// ② body 中公共但会变的参数
if (_bodyCommonChangeParamsGetBlock != null) {
allParams.addAll(_bodyCommonChangeParamsGetBlock!());
}
// ③ body 中自定义的其他参数
if (customParams != null && customParams.isNotEmpty) {
allParams.addAll(customParams);
}

四、response返回结构介绍

为了准确判断业务是否成功,通常需要同时判断status和errorCode两个字段。

1
2
3
4
5
6
7
8
{
"status": 200,
"message": "请求成功,但是业务处理失败",
"errorCode": 1001,
"errorMessage": "参数name不能为空",
"data": null,
"timestamp": "2023-05-04T09:00:00Z"
}
  1. 状态码(status code):表示请求的处理结果,常见的状态码包括200(请求成功)、400(请求参数错误)、401(未授权)、403(禁止访问)、404(请求的资源不存在)等。
  2. 消息(message):对请求处理结果的简要描述,可以用于前端展示错误信息或者提示。
  3. 数据(data):请求处理成功后返回的数据,例如json格式的数据、图片、文件等。
  4. 时间戳(timestamp):表示服务器处理请求的时间,可以用于前端调试和日志记录。
1
2
3
4
5
6
7
8
9
10
if (response.status >= 200 && response.status < 300) {
// 请求成功,继续处理业务逻辑
if (response.errorCode) {
// 业务逻辑处理失败,根据errorCode和errorMessage进行相应的处理
} else {
// 业务逻辑处理成功,根据data字段获取返回的数据
}
} else {
// 请求失败,根据错误码和错误信息进行相应的处理
}

附1:不建议把status和errorCode用同一个字段表示

将状态码(status code)和业务错误码(error code)合并成一个字段可能会带来一些问题,建议不要采用这种方法。

首先,将状态码和业务错误码合并在一个字段中会导致客户端处理逻辑复杂化。客户端需要在处理请求结果时先判断状态码,然后再解析业务错误码,这会增加客户端代码的复杂度,降低代码的可维护性。

其次,将状态码和业务错误码合并在一个字段中还会带来一些歧义。状态码是HTTP协议定义的标准字段,具有固定的含义,例如200表示请求成功,400表示请求参数错误,500表示服务器内部错误等。而业务错误码是根据具体业务定义的,具有一定的灵活性。将这两种含义合并在一起会导致状态码的含义变得模糊,不利于代码的可读性和维护性。

因此,建议在response结构中分别使用状态码和业务错误码两个字段来表示请求的处理结果。这样可以使客户端处理逻辑更加清晰,并且可以使状态码和业务错误码的含义保持清晰和统一。

五、埋点的入参

详见埋点规范

五、请求接口的整理规范

背景:为了能够将请求接口的用途和页面关联起来。

目的:①便于省去后端频繁询问该页面该位置调用的是什么接口(虽然已有日志,但是仍有部分后端人员懒到不愿去自己排查)。

②完成前端依赖服务关系的梳理,并一直到后台各级服务-数据层。

序号 参数 必填 描述与其目的 purpose 策略
1.1 caller_page 必填 接口调用者(页面、管理器等)
①多页面调用接口:告知后台,以对业务进行逻辑区分处理
②日志使用:方便查看与统计每个页面调用的接口
可全局记录获当前页面
eg: goods_detail_page
1.2 caller_view 可空 接口调用者(视图)
同上(有时候一个page下有多个tab)
必须请求单独设置
eg: sku_choose_window
2 caller_times 必填 调用场景
①页面打开:open_page
②页面输入:input
②页面点击:click
open_page\ input\ click
3 purpose 必填 接口用途中文
①日志使用:便于快速/直接筛选出问题根源接口
eg: 商品搜索
4 byApper 必填 前台具体开发者的id eg:byQian
5 byApier 必填 后端具体开发者的id eg:byApier2

方案:

将 purpose 添加到 body 的参数中

1
_params.addPurpose(caller_page: "商城页", caller_time:open_page|click, purpose: "商品搜索").byQian.byApier2;

请求接口的整理产出效果示例如下:

请求接口整理

该图来源于脑图: 请求接口的整理规范.xmind

附:多处页面调用的公共接口提取出,描述在页面中调用的公共接口的部分添加颜色或者其他标记来标识使用的是哪个部分的公共接口。

六、网络库的加强

1、网络日志加强

目的:查看日志的时候能清晰的知道接口的中文含义,以及调用其的页面或者其他类或实例。

方法:每个接口请求时候添加对应页面。

2、网络时长统计

目的:统计加载每个页面的过程中,所消耗的网络时长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NetworkUtil.requestApi1(caller:aPage, belongLoadPage:true)

requestUrl(caller, belongLoadPage) {
if (belongLoadPage != true) {
return;
}

PageRequestManager.
map[caller]["apiStartCount"] = map[caller]["apiStartCount"] + 1
dio.request({
url: url,
completeCallback: () {
int callerApiStartCount = map[caller]["apiStartCount"];
int callerApiEndCount = map[caller]["apiEndCount"];
callerApiEndCount++;
map[caller]["apiEndCount"] = callerApiEndCount;
if (callerApiEndCount == callerApiStartCount) {
_pageLoadingEnd();
}
},
);
}

End

妙用注解

[toc]

妙用注解

前言

一、 为什么要使用注解开发

在日常开发过程中,我们经常会面临以下挑战:大量重复的模板代码、臃肿的代码降低可读性,以及JSON序列化和反序列化问题。为应对这些问题,各种编程语言都有自己的解决方案。在Dart语言中,采用了注解这一高效手段来解决这些问题。

注解就像是一把神奇的钥匙,为我们解锁了许多高效开发的大门。以下是使用注解的一些原因:

  1. 自动生成代码:注解就像会魔法的小精灵,能够在编译时自动生成代码,减轻了我们手动编写重复代码的负担,让开发变得更加轻松。
  2. 简化代码:注解可以减少重复代码,提高代码质量。
  3. 提高可读性:注解可以将元数据与实际代码分离,使代码更易于理解。
  4. 扩展功能:通过注解和注解处理器,我们可以在编译时执行一些额外操作,例如代码检查、代码优化等

二、什么是注解?

注解(Annotation)是一种编程语言特性,它不属于特定的架构或框架,而是可以在多种编程语言和开发框架中使用的一种工具。注解提供了一种方式,允许开发者在代码中添加元数据,这些元数据可以由编译器、运行时环境或其他工具在不同阶段使用。

在Dart语言中,注解是一种特殊的语法,用于在编译时或运行时向代码添加额外的元数据。注解以@符号开头,后跟一个编译时常量表达式。这种元数据可以用于指导工具(如静态分析器、编译器和构建器)执行特定操作,例如代码生成、静态检查和优化。注解不会影响程序的执行过程,但可以在编译时或运行时被工具和库访问,以实现各种目的。

三、注解实践:使用注解为每个页面类添加页面描述信息,埋点中经常需要

1、iOS:使用宏定义实现注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义宏,用于创建页面信息注解
#define CreatePageInfoAnnotation(pageClass, pageKey, pageDes) \
@interface pageClass (PageInfo) \
@property (nonatomic, readonly) NSString *pageKey; \
@property (nonatomic, readonly) NSString *pageDescription; \
@end \
@implementation pageClass (PageInfo) \
- (NSString *)pageKey { \
return pageKey; \
} \
- (NSString *)pageDescription { \
return pageDes; \
} \
@end


// LoginViewController.m
@PageInfoAnnotation(LoginViewController, @"login_page", @"登录页面") // 调用宏,为类添加注解
@implementation LoginViewController
@end

2、Flutter:使用source_gen库生成文件实现注解

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
// page_info_generator.dart
import 'package:source_gen/source_gen.dart';
class TestGenerator extends GeneratorForAnnotation<CJPageInfoMetadata> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
String className = element.name;
String? pageKey = annotation.read("pageKey");
String? pageDesc = annotation.read("pageDesc");
return """
extension ${className}APTExtension on ${className} {
String? apt_pageKey() {
return ${pageKey};
}
String? apt_pageDesc() {
return ${pageDesc};
}
}
""";
}
}

// TestModel.dart
@CJPageInfoMetadata("我是页面描述", 'home_page')
class TestModel {
// 属性
int age;
int bookNum;

// 方法
void fun1() {}
void fun2(int a) {}
}

结果就会生成一堆dart文件。

四、进入页面前的登录判断

其他参考文章:自定义iOS注解

1、最差做法

1
2
3
4
5
6
7
8
9
10
11
12
// HomeViewController.m
- (void)goMineHomePage {
// 进入个人主页前,需要先判断是否登录
if (!UserManager.isLogin) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
return;
}

MineHomeViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
}

2、统一到路由中优化:在路由中拦截+拦截器(可选)

所有跳转使用 RouterManger,在RouterManager中限制未登录允许的页面。

2.1、常规路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// HomeViewController.m
- (void)goMineHomePage {
[RouterManger push:RouterNames.mine_home_page argument:nil];
}

// RouterManger.m
- (void)push:(String *)pageName argument:(NSDictionary *)argument {
if (!UserManager.isLogin) { // 进入个人主页前,需要先判断是否登录
if ([RouterNames.mine_home_page].contains(pageName)) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return;
}
}

getVCHandle = [HandleManager.handleMap objectForKey:pageName];
UIViewController *viewController = getVCHandle(argument:argument);
[topVC pushViewController:viewController animated:YES];
}

2.2、优化路由,引进拦截器:对路由中的跳转进行改进,使用拦截器

改进后的代码如下:

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
// HomeViewController.m
- (void)goMineHomePage {
[RouterManger push:RouterNames.mine_home_page argument:nil];
}

// RouterManger.m 优化为拦截器
- (void)push:(String *)pageName argument:(NSDictionary *)argument {
for (var interceptor in interceptors) {
bool canPush = [interceptor canPushPageName:pageName arguments: arguments];
if (!canPush) return;
}

getVCHandle = [HandleManager.handleMap objectForKey:pageName];
UIViewController *viewController = getVCHandle(argument:argument);
[topVC pushViewController:viewController animated:YES];
}

// LoginInterceptor.m
- (BOOL)canPushPageName:(NSString *)pageName arguments:(NSDictionary *)arguments {
if (!UserManager.isLogin) { // 进入个人主页前,需要先判断是否登录
if ([RouterNames.mine_home_page].contains(pageName)) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return NO;
}
}
return YES;
}

3、在调用原始方法之前插入额外的执行逻辑(不推荐,不使用)

《在调用原始方法之前插入额外的执行逻辑》

三、常见的注解示例

以下内容摘自:Flutter 注解开发

  1. @override:这个注解表示一个方法覆盖了父类的方法。它可以帮助我们检查是否正确地实现了方法覆盖,如果没有正确实现,编译器会给出警告。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Animal {
    String makeSound() {
    return "makeSound";
    }
    }

    class Dog extends Animal {
    @override
    void makeSound() {
    print('makeSound');
    }
    }
  2. @JsonSerializable():这个注解就是我们项目中使用到的json_annotation库的@JsonSerializable()注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @JsonSerializable()
    class ConversationListEntity {
    String? nextSeq;
    late List<ConversationListResList> resList;

    ConversationListEntity();

    factory ConversationListEntity.fromJson(Map<String, dynamic> json) => $ConversationListEntityFromJson(json);

    Map<String, dynamic> toJson() => $ConversationListEntityToJson(this);

    @override
    String toString() {
    return jsonEncode(this);
    }
    }
  3. 我们项目中的@RootView(jumpId: "1005"):用来为订单详情页面生成路由代码。

    1
    2
    3
    4
    5
    6
    7
    @RootView(jumpId: "1005")
    class OrderDetail extends BaseWidget {
    OrderDetail({Key? key}) : super();

    @override
    OrderDetailState createState() => OrderDetailState();
    }

附1、iOS注解原理介绍

1、在oc中实现注解一样的东西肯定是通过宏来实现的。

2、在oc中实现注解主要有4个方向:代码中注解,类注解,属性注解,方法注解。

其中代码中注解的形式应该是最常见的。你说没见过?libextobjc中那一套@weakify和@strongify你们总用得很爽吧。拆开看看这就是做代码中注解的标准形式了。而类注解,考虑了很久暂时还是误解状态,因此也没法讲了。因此实现属性注解和方法注解是关键。

3、既然是宏实现的,因此注解宏展开后应该是实际能够在对应的段落实际有效的语法才对。另外为了和面向对象的oc类型进行关联,因此在oc中可以随便乱写的c代码当然很难办了。因此我们可以做的宏很快就限定下来了,在属性中宏展开新的属性,在方法中宏展开新的方法。

在oc中实现注解一样的东西

实现

首先实现一下方法注解。由于我们知道我们需要展开方法,因此我们很快就能写出这样的宏:

1
2
3
4
5
6
7
8
// __COUNTER__ 这个宏每次使用都会自动+1
// ## 是宏的直接串接
#define path(x) \
- (id)__klmurl_path_##__COUNTER__() { \
return x ;\
}

所以结果是 __klmurl_path_1

@是哪儿来的,毕竟宏里面本来是不允许有这样的符号的。原来强项在展开的内容前加了无用的带@的表达式:参考 @weakify 其也只是宏定义weakify

1
2
3
4
5
6
#define weakify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

#define rac_keywordify autoreleasepool {}
#define rac_keywordify try {} @catch (...) {}

这样才能很自然的串一个@。

优化考虑有什么带@的而且没有什么卵用的标示呢。@compatibility_alias

这一段展开后的代码是:

1
2
3
4
5
6
7
8
9
// __COUNTER__ 这个宏每次使用都会自动+1
// ## 是宏的直接串接
#define path(x) \
compatibility_alias _KLMURL_0 KLMURL;
- (id)__klmurl_path_##__COUNTER__() { \
return x ;\
}

所以结果是 __klmurl_path_1

附1、Flutter 注解

参考文章:Flutter 注解处理及代码生成

1、注解的调用和执行结果

对 TestModel 类进行注解

1
2
3
4
5
6
7
8
9
10
@CJPageInfoMetadata("我是页面描述", 'home_page')
class TestModel {
// 属性
int age;
int bookNum;

// 方法
void fun1() {}
void fun2(int a) {}
}

以要生成如下 TestModel.g.dart 文件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// CJPageInfoMetadata
// **************************************************************************

extension TestModelAPTExtension on TestModel {
String? apt_pageKey() {
return "home_page";
}
String? apt_pageDesc() {
return "我是页面描述";
}
}

2、注解内部的实现

2.1、注解及生成器

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
// 注解
class CJPageInfoMetadata {
final String pageDesc;
final String pageKey;

const CJPageInfoMetadata(this.pageDesc, this.pageKey);
}

// 生成器
import 'package:source_gen/source_gen.dart';
class TestGenerator extends GeneratorForAnnotation<CJPageInfoMetadata> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
String className = element.name;
String? pageKey = annotation.read("pageKey");
String? pageDesc = annotation.read("pageDesc");
return """
extension ${className}APTExtension on ${className} {
String? apt_pageKey() {
return ${pageKey};
}
String? apt_pageDesc() {
return ${pageDesc};
}
}
""";
}
}

2.2、使用注解生成代码的运行结果

命令执行成功后将会生成一个新的文件:TestModel.g.dart 其内容:

最后生成的文件为

2.2、生成器中 Element element, ConstantReader annotation, BuildStep buildStep 各值

2.2.1、Element element:要注解的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
element.toString: class TestModel
element.name: TestModel // 要注解的对象的【名】:TestModel
element.metadata: [@CJPageInfoMetadata("我是页面描述", 'home_page')]
element.kind: CLASS // 元素类型有 CLASSFIELDFUNCTION 等,
element.displayName: TestModel
element.fields: [int age, int bookNum] // 要注解的对象的【属性】:[int age, int bookNum]
element.methods: [fun1() → void, fun2(int a) → void] // 要注解的对象的【方法名】:[fun1() → void, ...]
element.documentationComment: null
element.enclosingElement: flutter_annotation|lib/demo_class.dart
element.hasAlwaysThrows: false
element.hasDeprecated: false
element.hasFactory: false
element.hasIsTest: false
element.hasLiteral: false
element.hasOverride: false
element.hasProtected: false
element.hasRequired: false
element.isPrivate: false
element.isPublic: true
element.isSynthetic: false
element.nameLength: 9
element.runtimeType: ClassElementImpl
...
2.2.2、ConstantReader annotation:对注解对象添加的注解信息
1
2
3
4
annotation.runtimeType: _DartObjectConstant
annotation.read("pageDesc"): '我是页面描述'
annotation.read("pageKey"): 'home_page'
annotation.objectValue: CJPageInfoMetadata (pageDesc = String ('我是页面描述'); pageKey = String ('home_page'))
2.2.3、BuildStep buildStep :提供的是该次构建的输入输出信息:
1
2
3
4
5
6
7
8
9
buildStep.runtimeType: BuildStepImpl
buildStep.inputId.path: lib/demo_class.dart
buildStep.inputId.extension: .dart
buildStep.inputId.package: flutter_annotation
buildStep.inputId.uri: package:flutter_annotation/demo_class.dart
buildStep.inputId.pathSegments: [lib, demo_class.dart]
buildStep.expectedOutputs.path: lib/demo_class.g.dart
buildStep.expectedOutputs.extension: .dart
buildStep.expectedOutputs.package: flutter_annotation

3、使用注解生成代码前的准备

注解的生成器 Generator 的执行需要 Builder 来触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1、使用Generator 创建一个Builder
Builder testBuilder(BuilderOptions options) => LibraryBuilder(TestGenerator());

// 2、创建 build.yaml 文件,将我们要执行的 builder 配置进去
builders:
testBuilder:
import: "package:flutter_annotation/test.dart"
builder_factories: ["testBuilder"]
build_extensions: {".dart": [".g.part"]}
auto_apply: root_package
build_to: source

// 3、运行 Builder,命令执行成功后将会生成一个新的文件:TestModel.g.dart 其内容:
flutter packages pub run build_runner build

Flutter注解的Builder的原理很像使用shell或者python脚本遍历文件的方式。

已知@Description是注解,其作用为提取 @Description 的方法描述到一个文件。

请说说@Description注解的内部实现(使用shell或者python脚本遍历文件的方式,不在本次讨论中)

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
// my_class.dart
class MyClass {
@Description('This method does something important.') // 注解
void myImportantMethod() {
print('Doing something important.');
}
}

// description_generator.dart
import 'package:source_gen/source_gen.dart';
import 'package:code_builder/code_builder.dart';
import 'package:annotations/annotations.dart'; // 引入上面创建的注解库

class DescriptionGenerator extends GeneratorForAnnotation<Description> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { {
// 检查注解是否应用在方法上
if (element.isMethod) {
final description = annotation.read('value').stringValue;
final method = element.asMethod();

// 创建一个方法调用的描述文本
final code = Block.of([
Directive.code("print('Method ${method.name} description: $description');")
]);

// 返回生成的代码
return code.toString();
}

// 如果注解没有应用在方法上,返回空字符串
return '';
}
}

其他参考文章

https://pub.dev/packages/mustache_template

mustache_template 是一个用于Flutter的Dart模板库,它支持dart2jsdart2native

End

必备知识架构-算法

[toc]

稳定与不稳定:如果a原本在b的前面并且a=b,排序之后a仍然在b的前面那就是稳定排序,如果排序之后a可能会出现在b的后面则是不稳定排序。所以冒泡排序是稳定的。选择排序可能不稳定。

空间复杂度:是指算法在计算机内执行时所需的存储空间与n的规模之间的关系。

时间复杂度:排序的的总操作次数,与n的规模之间的关系。

以下是我们在面试中要问的主要问题:

1.数据结构:
a、 链表
b、 堆叠
c、 排队
d、 二叉树
e、 哈希表

2.算法:
a、 排序(像快速排序和归并排序),二分搜索法,贪婪算法。
b、 二叉树(比如遍历、构造、操作…)

iOS 开发中常用的排序(冒泡、选择、快速、插入、希尔、归并、基数)算法

排序算法 平均时间复杂度 其他时间复杂度 平均空间复杂度 是否是稳定性的
冒泡排序 O(n^2) 最好:O(n),已是有序的
最坏:O(n^2),是逆序的
O(1) 稳定
选择排序 O(n^2) —— O(1) 不稳定
快速排序 O(nlogn) 最好:O(nlogn),每次划分都非常平衡时
最坏:O(n^2),每次划分非常不均匀时
O(nlogn) 不稳定
不稳定发生在交换的时刻;
归并排序 O(nlogn) —— O(n) 稳定

因为冒泡和选择排序都是原地排序算法,不需要额外的存储空间。所以其空间复杂度都是O(1)。

归并排序需要额外的空间来存储合并过程中的临时数组,其空间复杂度O(n)。

11.5.3 快速排序为什么快

在Object-C中学习数据结构与算法之排序算法

二分搜索法,是一种【在有序数组中】查找某一特定元素的搜索算法。

iOS查找算法之二分查找

漫画:五分钟学会贪心算法

有一个背包,最多能承载150斤的重量,现在有7个物品,
重量分别为[35, 30, 60, 50, 40, 10, 25],
价值分别为[10, 40, 30, 50, 35, 40, 30],
应该如何选择才能使得我们的背包背走最多价值的物品?

局部最优解

1、每次都尽量选择当前【价值最高】的物品
重量分别为[35, 30🚗, 60, 50🚗, 40🚗, 10🚗, 25], 130
价值分别为[10, 40②✅, 30, 50①✅, 35④✅, 40③✅, 30], 165

2、每次都尽量选择当前【重量最小】的物品
重量分别为[35④, 30③, 60, 50, 40⑤, 10①, 25②], 140
价值分别为[10✅,40✅, 30, 50, 35✅, 40✅, 30✅], 155

3、每次都尽量选择当前【价值密度最高】的物品
重量分别为[35🚗, 30🚗, 60, 50🚗, 40, 10🚗, 25🚗], 150
价值分别为[10✅, 40✅, 30, 50✅, 35, 40✅, 30✅], 170
价值密度 [0.285⑤, 1.333②, 0.5, 1.0④, 0.875, 4.0①, 1.2③]

想象一下下列场景:

  1. 从通讯录中寻找某个联系人
  2. 从一大堆文件中寻找某个文件
  3. 到了影厅之后,寻找电影票上指定的座位

如果以上情况中,联系人、文件、影厅座位这些“数据”没有按照需要的顺序组织,如何找到想要的特定“数据”呢?会非常麻烦!所以说,对于需要搜索的数据,往往应该先排个序!

排序比较 & 对比归并排序与快速排序

image-20240902163103782

前言

1
2
3
4
5
6
// 交换数组中的两个元素
void swap(NSMutableArray *array, NSInteger i, NSInteger j) {
NSNumber *temp = array[i];
array[i] = array[j];
array[j] = temp;
}

算法图解

034-Data-AlgorithmCollect-iOS

Data-AlgorithmCollect-1

一、冒泡排序

实现一个冒泡排序或者快速排序

从小到大排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
假设45132,按从小到大排序
i = 0 的时候,j的相邻两个位置都要比较排一下位置:
j = 0 的时候:arr_M = 45132(a[0]和a[1]比得到的结果)
j = 1 的时候:arr_M = 41532(a[1]和a[2]比得到的结果)
j = 2 的时候:arr_M = 41352(a[2]和a[3]比得到的结果)
j = 3 的时候:arr_M = 41325(a[3]和a[4]比得到的结果)
i=0已经将第一个最大的值放到最后了。

i = 1 的时候,j的相邻两个位置都要比较排一下位置:
j = 0 的时候:arr_M = 14325
j = 1 的时候:arr_M = 13425
j = 2 的时候:arr_M = 13245
i=1已经将第二个最大的值放到最后了。

冒泡排序代码实现

二、选择排序

从数组的第a[i+1]个元素开始到第n个元素,寻找最小的元素(的位置)。(具体过程为:先设最小位置为i=0,从该位置往后逐一比较,若遇到比之小的则记下该最小值的位置,结束后交换);

先为a[0]取到最小值,再为a[1]取到最小值,再继续…..

从小到大排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
假设45132,按从小到大排序
i = 0 的时候,j的相邻两个位置都要比较排一下位置:
j = i+1=1 的时候:arr_M = 45132(此时a[0]的4和a[1]的5比较:得到的结果)
j = 2 的时候:arr_M = 15432(此时a[0]的4和a[2]的1比较:得到的结果)
j = 3 的时候:arr_M = 15432(此时a[0]的1和a[3]的3比较:得到的结果)
j = 4 的时候:arr_M = 15432(此时a[0]的1和a[4]的2比较:得到的结果)
i=0已经将第一个最小的值放到第一位了。

i = 1 的时候,j的相邻两个位置都要比较排一下位置:
j = 2 的时候:arr_M = 14532(a[1]和a[2]比得到的结果)
j = 3 的时候:arr_M = 13542(a[1]和a[3]比得到的结果)
j = 4 的时候:arr_M = 12543(a[1]和a[4]比得到的结果)
i=1已经将第二个最小的值放到最第二位了。

选择排序代码实现

三、快速排序

漫画:什么是快速排序?(完整版)

快速排序(Quicksort)是对冒泡排序的一种改进。

同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。

不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。这种思路就叫做分治法

1
2
3
4
5
6
7
8
// 快速排序函数 quickSort(array, 0, [array count] - 1);
void quickSort(NSMutableArray *array, NSInteger left, NSInteger right) {
if (left < right) {
NSInteger pivotIndex = partition(array, left, right);
quickSort(array, left, pivotIndex - 1);
quickSort(array, pivotIndex + 1, right);
}
}

快速排序代码实现

1、排序原理:

设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,

快排图快排图

然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序。

2、排序流程

快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。

(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。

(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

四、归并排序

Hello 算法 11.6 归并排序

图解排序算法(四)之归并排序

1、划分阶段:通过递归不断地将数组从中点处分开(以把序列分成元素尽可能相等的两半),将长数组的排序问题转换为短数组的排序问题。当子数组长度为 1 时终止划分,开始合并。

归并排序

归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。

2、合并阶段:持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。

归并排序的合并过程如下:

  1. 初始化:创建两个指针,分别指向两个已排序数组的起始位置。
  2. 比较元素:比较两个指针所指向的元素。
  3. 选择较小元素将较小的元素放入临时数组,并移动该元素所在数组的指针。
  4. 重复比较:重复步骤2和3,直到其中一个数组的所有元素都被比较过。
  5. 复制剩余元素:如果一个数组的所有元素都已经被比较并复制到临时数组中,将另一个数组中剩余的元素直接复制到临时数组的末尾。
  6. 返回临时数组:临时数组现在是一个合并后的有序数组。

示例:要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

img

附:深度优先遍历(DFS)和广度优先遍历(BFS)

深度优先遍历(Depth First Search, 简称 DFS)

img

广度优先遍历(Breath First Search, 简称 BFS) 也叫层序遍历,指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。

BFS 一般是解决最短路径问题。

img

iOS算法篇-leetcode题目记录

附:排序算法代码

1、冒泡排序代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)methodMaopao {
int array[5] = {4, 5, 1, 3, 2};

for (int i = 0; i < 5-1; i++) { //需要比较几遍
for(int j = 0; j < 5-1-i; j++) { //每一遍都从0开始到倒数第-i个
if (array[j] > array [j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}

for(int i = 0; i < 5; i++) {
printf("%d\n",array[i]);
}
}

2、选择排序代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sort(items: Array<Int>) -> Array<Int> {
var list = items
for i in 0..<list.count {
//记录当前最小的数,比较i+1后更大的数进行记录
var minIndex = i
for j in i+1..<list.count {
if list[j] < list[minIndex] {
minIndex = j
}
}
// 交换
let temp = list[minIndex]
list[minIndex] = list[i]
list[i] = temp
}
return list
}

3、快速排序代码实现

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
//单趟 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
if (left >= right) return left;

int begin = left;
int end = right;
int key = a[left]; //保存要排序的值
while (begin < end) //当左右指针相遇时结束
{
// begin是坑,从后往前找比key小的值填到坑里
while (begin < end && a[end] >= key) {
end--;
}
a[begin] = a[end];

// 此时end位置是坑,从前往后比key大的值填到坑中
while (begin < end && a[begin] <= key) {
begin++;
}
a[end] = a[begin];
}

//begin和end相遇的地方是key对应的位置
a[end] = key;
return end; //返回排好位置的元素的下标
}

END

< 返回目录

Git获取信息

[toc]

一、获取commit信息:

1、获取指定分支最后一次提交的信息

1
2
3
Git获取信息
# 获取指定分支最后一次提交的作者
git show --format=%aN -b origin/dev_all -s

2、获取指定分支最后一次提交的信息–本地

我想可以获取到远程origin/shop_report分支的最后一次提交作者,但是我又不想说必须得去拉取到本地,请问我该怎么处理

如果您不想将 origin/shop_report 分支拉取到本地仓库中,您可以尝试使用以下命令来获取该分支的最后一次提交的作者名字:

1
git ls-remote --heads origin dev_all | cut -f 1 | xargs -I {} git show --format=%aN {} -s | head -n 1

该命令将会首先使用 git ls-remote 命令获取 origin/shop_report 分支的引用头(ref),然后使用 cut 命令提取该引用头的哈希值。接着,使用 xargs 命令将该哈希值作为参数传递给 git show 命令,并使用 --format=%aN 参数指定输出作者名字。最后,使用 head 命令提取第一行输出,即为该分支的最后一次提交的作者名字。

请注意,这种方式虽然可以不将 origin/shop_report 分支拉取到本地仓库中,但是每次执行该命令都会从远程仓库中获取该分支的引用头和最后一次提交的作者信息,因此速度可能较慢,特别是在网络条件不好的情况下。如果您需要频繁地获取 origin/shop_report 分支的最后一次提交的作者信息,建议还是将该分支拉取到本地仓库中。

1、git 查看远程分支

要查看远程分支,可以使用以下命令:

1
git branch -r

该命令会列出所有远程分支的列表,以remotes/为前缀。如果你想查看特定的远程分支,可以使用以下命令:

1
git branch -r | grep <remote-branch-name>

其中,<remote-branch-name>是指你要查看的远程分支名称,这个命令会查询匹配远程分支名称的所有分支,然后在终端输出它们的名称。

2、远程删掉了,但本地执行git branch -r 还是显示出来

2.1、git remote prune origin

如果你在远程删除了一个分支,但是本地执行git branch -r命令仍然显示该分支名,这可能是因为本地的远程分支列表缓存了该分支名。为了解决这个问题,你可以执行以下命令来更新本地的远程分支列表:

1
git remote prune origin

该命令会从远程 Git 仓库中删除已经不存在的分支,并更新你本地的远程分支列表。执行完这个命令后,再次执行git branch -r,就应该不会再看到已经被删除的分支名了。

2.2、git remote prune origin 执行出错了

如果git remote prune origin命令执行出错了,很可能是由于你的本地缓存信息有问题,请执行以下操作修复问题:

  1. 首先,运行以下命令来更新远程分支缓存:
1
2
# 这个命令会强制更新缓存,在更新过程中删除远程不存在的分支,然后更新你本地的远程分支列表
git remote update origin --prune

如果上述命令没有解决问题,你需要手动清除本地缓存。

1
2
3
4
5
6
7
8
9
# 这个命令会从远程 Git 仓库中下载所有最新的分支,同时删除远程不存在的分支,然后更新本地分支列表。
git fetch --all --prune

# 如果上述命令仍然无法解决问题,你可以尝试处理该git工程下的 .git/refs/remotes 文件夹
# 尝试方式1:手动删除指定分支

# 尝试方式2:删除所有本地缓存目录并重新克隆仓库。
rm -rf .git/refs/remotes
git clone <repository-url>

End

Git使用

详细而全的文档:Pro Git(中文版)

Git回滚代码到某个commit

回退命令:

1
2
3
4
5
6
7
8
在git push的时候,有时候我们会想办法撤销git commit的内容 
1、找到之前提交的git commit的id
git log
找到想要撤销的id
2、git reset –hard id
完成撤销,同时将代码恢复到前一commit_id 对应的版本
3、git reset id
完成Commit命令的撤销,但是不对代码修改进行撤销,可以直接通过git commit 重新提交对本地代码的修改
1
2
3
4
5
6
$ git reset HEAD^         			回退到上个版本,代码还在

注意:如果加上使用--hard会导致这里虽然回到了上个版本,但是commit上去,未push出去的就没法找回了
$ git reset --hard HEAD^ 回退到上个版本
$ git reset --hard HEAD~3 回退到前3次提交之前,以此类推,回退到n次提交之前
$ git reset --hard commit_id 退到/进到 指定commit的sha码

当你有多处备份的时候,你可以强制性的执行

git reset –hard HEAD^

但是你刚commit上去,未push出去的就没法找回了。

pod install报错:ArgumentError - Malformed version number string

网上的参考:pod install报错:ArgumentError - Malformed version number string

实际:通过执行sudo gem install cocoapods命令重装cocoapods即解决了。

其他CocoaPods那些错

GitHub 出现 POST git-receive-pack (chunked) 解决方案详解

出现 POST git-receive-pack (chunked) 的原因就是 当使用 HTTPS 提交到 Git 上时使用不检查加密要是东西过多将导致提交停止。

解决方法:

1
2
3
4
5
6
7
方案1>  使用 Git 提交代码
进入到要提交的代码的目录,里面包含 .git 文件夹,输入指令 git config http.postBuffer 524288000

方案2> 使用 SourceTreee 提交代码
如图按照顺序依次点击在最后一步增加
[http]
postBuffer = 524288000

POST git-receive-pack (chunked).png)

再次提交将会成功。

相关参考:GitHub 出现 POST git-receive-pack (chunked) 解决方案详解

Github Push 方式

[toc]

Git的其他参考文章:

  • Git 面試題

  • 問:用一般方式的合併,跟使用 rebase 方式合併,有什麼不同?各有何優缺點?

    一般的合併方式,有些情況(非快轉合併)會產生一個額外的 commit 來接合兩邊分支,而 rebase 合併分支跟一般的合併分支的明顯差別,就是使用 rebase 方式合併分支不會有這個合併的 Commit。

    如果就以最後的的結果來說,檔案內容來說是沒什麼差別,但在 Git 的歷史紀錄上來說就有一些差別,誰 rebase 誰,會造成歷史紀錄上先後順序不同的差別。例如 cat 分支 rebase 到 dog 分支的話,表示 cat 分支會被接到 dog 分支的後面;反之如果是 dog 分支 rebase 到 cat 上的話,表示 dog分支 會被接到 cat 分支的後面。

    使用 rebase 的好處,是整理出來的歷史紀錄不會有合併用的 commit,看起來比較乾淨(也是有些人不覺得這乾淨多少),另外歷史紀錄的順序可以依照誰 rebase 誰而決定先後關係(不過這點不一定是優點或缺點,端看整理的人而定);缺點就是它相對的比一般的合併來得沒那麼直覺,一個不小心可能會弄壞掉而且還不知道怎麼 reset 回來,或是發生衝突的時候就會停在一半,對不熟悉 rebase 的人來說也許是個困擾。

    通常在還沒有推出去但感覺得有點亂(或太瑣碎)的 commit,我會先使用 rebase 來整理分支後再推出去。rebase 等於是在修改歷史,這個行為會做出平行時空,修改已經推出去的歷史可能會對其它人帶來困擾,所以對於已經推出去的內容,請不要任意使用 rebase。

【狀況題】怎麼有時候推不上去…

偶爾在執行 Push 指令的時候會出現這個錯誤訊息:

1
2
3
4
5
6
7
8
9
$ git push
To https://github.com/eddiekao/dummy-git.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://github.com/eddiekao/dummy-git.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

這段訊息的意思是線上版本的內容比你電腦裡這份還要新,所以 Git 不讓你推上去。

怎麼造成的?

通常這個狀況會發生在多人一起開發的時候,想像一下這個情境:

new repository

  1. Sherly 跟 Eddie 兩個人在差不多的時間都從 Git Server 上拉了一份資料下來準備進行開發。
  2. Sherly 手腳比較快,先完成了,於是先把做好的成果推一份上去。
  3. Eddie 不久後也完成了,但當他要推上去的時候發現推不上去了…

怎麼解決?

解決方法算是有兩招

第一招:先拉再推

因為你電腦裡的內容是比較舊的,所以你應該先拉一份線上版本的回來更新,然後再推一次:

1
2
3
4
5
6
7
8
9
$ git pull --rebase
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/eddiekao/dummy-git
37aaef6..bab4d89 master -> origin/master
First, rewinding head to replay your work on top of it...
Applying: update index

這裡加了 --rebase 參數是表示「內容抓下來之後請使用 Rebase 方式合併」,當然你想用一般的合併方式也沒問題。合併如果沒發生衝突,接著應該就可以順利往上推了。

第二招:無視規則,總之就是聽我的!(誤)

凡事總有先來後到,在上面的例子中,Sherly 先推上去的內容,後推的人就是應該拉一份下來更新,不然照規定是推不上去的。不過這規則也是有例外,只要加上了 --force 或是 -f 參數,它就會強迫硬推上去,把 Sherly 之前的內容蓋掉:

1
2
3
4
5
6
7
8
9
$ git push -f
Counting objects: 19, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (17/17), done.
Writing objects: 100% (19/19), 2.16 KiB | 738.00 KiB/s, done.
Total 19 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), done.
To https://github.com/eddiekao/dummy-git.git
+ 6bf3967...c4ea775 master -> master (forced update)

雖然這樣一定會成功,但接下來你就要去面對 Sherly,跟她解釋為什麼你把她的進度蓋掉了。更多關於 Force Push 的說明,可參考「【狀況題】聽說 git push -f 這個指令很可怕,什麼情況可以用它呢?」章節介紹。

Github Push 失败问题

[toc]

参考文章:

github push代码不成功

问题一:Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.

image-20220423220008550

remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.

remote: Please see https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/ for more information.

意思就是你原先的密码凭证从2021年8月13日开始就不能用了,要使用个人访问令牌(personal access token),就是把你的密码替换成token。

解决:github push更新token验证方式。

1、Please use a personal access token instead.(密码方式不适用了,需改使用token方式登录)

1、登录github官网进入setting

img

2.页面拉到底 找到 Developer Setting

3.右侧菜单栏找到Personal access tokens 点击后 找到右上角的Generate new token

img

4、token的使用

在终端上cd到要项目中,然后执行git pull。

image-20220423223148829

此时弹出

Username for ‘https://github.com': 此处输入你github 用户名

Password for ‘https://用户名@github.com’:把复制的token粘贴到此处(这里原先是输入密码的现在改成token)回车就好了

2、Failed to connect to github.com 443(登录方式是token了,但连接失败。)

image-20220423221025009

image-20220423221809343

解决方式,终端输入如下命令:

git config –global http.proxy

3、remote: Write access to repository not granted.(connect上了,但其他操作失败,需指定token的权限)

image-20220423222009245

解决:

image-20220423222457565

创建的时候下面的权限和是否设置过期时间(我是吧所有权限都勾选了) 根据自己情况选择 最后创建完后 吧token复制下来

4、LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

image-20220423224215251

fatal: unable to access ‘https://github.com/dvlproad/001-UIKit-CQDemo-Flutter.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

image-20220423223640807

解决:终端输入

git config http.sslVerify “false”

git config –global http.sslVerify “false”

三、访问GitHub遇到SSL_ERROR_SYSCALL错误解决方法

其他参考文章:访问GitHub遇到SSL_ERROR_SYSCALL错误解决方法

检查1:电脑上是否有curl-openssl(一般都有)

openssl version

image-20220423232921517

如果没有,则安装curl-openssl。安装方法如下:

1
brew install curl-openssl

image-20220423233608415

2、安装xxx时候失败,Error: No such file or directory @ rb_sysopen

1、原因

原来是一个依赖包下载不成功(harfbuzz-3.1.1.arm64_monterey)

解决:使用brew单独安装即可,然后再就是再次下载之前的包

用brew单独下载依赖包,就避免了找不到依赖版本的错误

1
brew install zstd

image-20220423234308334

之后再返回继续执行之前的

1
brew install curl-openssl

image-20220423234350492

Git

目录

  • 一、https 和 SSH 的区别
  • 二、在 github 上添加 SSH key 的步骤
  • 三、git撤销

前言

很多朋友在用github管理项目的时候,都是直接使用https url克隆到本地,当然也有有些人使用 SSH url 克隆到本地。然而,为什么绝大多数人会使用https url克隆呢?

这是因为,使用https url克隆对初学者来说会比较方便,复制https url 然后到 git Bash 里面直接用clone命令克隆到本地就好了。而使用 SSH url 克隆却需要在克隆之前先配置和添加好 SSH key 。

因此,如果你想要使用 SSH url 克隆的话,你必须是这个项目的拥有者。否则你是无法添加 SSH key 的。

一、https 和 SSH 的区别:

1、前者可以随意克隆github上的项目,而不管是谁的;而后者则是你必须是你要克隆的项目的拥有者或管理员,且需要先添加 SSH key ,否则无法克隆。

2、https url 在push的时候是需要验证用户名和密码的;而 SSH 在push的时候,是不需要输入用户名的,如果配置SSH key的时候设置了密码,则需要输入密码的,否则直接是不需要输入密码的。

二、在 github 上添加 SSH key 的步骤:

1、首先需要检查你电脑是否已经有 SSH key

进入~/.ssh目录,检查是否已经存在 id_rsa.pubid_dsa.pub 文件,如果文件已经存在,那么你可以跳过步骤2,直接进入步骤3。

2、创建一个 SSH key

$ ssh-keygen -t rsa -C "your_email@example.com"

1
2
3
4
代码参数含义:
-t 指定密钥类型,默认是 rsa ,可以省略。
-C 设置注释文字,比如邮箱。
-f 指定密钥文件存储文件名。

以上代码省略了 -f 参数,因此,运行上面那条命令后会让你输入一个文件名,用于保存刚才生成的 SSH key 代码,如:

1
2
Generating public/private rsa key pair.
# Enter file in which to save the key (/c/Users/you/.ssh/id_rsa): [Press enter]

当然,你也可以不输入文件名,使用默认文件名(推荐),那么就会生成 id_rsa 和 id_rsa.pub 两个秘钥文件。

接着又会提示你输入两次密码(该密码是你push文件的时候要输入的密码,而不是github管理者的密码),

当然,你也可以不输入密码,直接按回车。那么push的时候就不需要输入密码,直接提交到github上了,如:

1
2
Enter passphrase (empty for no passphrase): 
# Enter same passphrase again:

接下来,就会显示如下代码提示,如:

1
2
3
4
Your identification has been saved in /c/Users/you/.ssh/id_rsa.
# Your public key has been saved in /c/Users/you/.ssh/id_rsa.pub.
# The key fingerprint is:
# 01:0f:f4:3b:ca:85:d6:17:a1:7d:f0:68:9d:f0:a2:db your_email@example.com

当你看到上面这段代码的收,那就说明,你的 SSH key 已经创建成功,你只需要添加到github的SSH key上就可以了。

3、添加你的 SSH key 到 github上面去

进入账号的SSH key添加处,将id_rsa.pub 文件的内容复制上去添加即可。(记得 SSH key 代码的前后不要留有空格或者回车。)

4、测试一下该SSH key

在git Bash 中输入以下代码

$ ssh -T git@github.com
当你输入以上代码时,会有一段警告代码,如:

1
2
3
The authenticity of host 'github.com (207.97.227.239)' can't be established.
# RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
# Are you sure you want to continue connecting (yes/no)?

这是正常的,你输入 yes 回车既可。如果你创建 SSH key 的时候设置了密码,接下来就会提示你输入密码,如:

Enter passphrase for key ‘/c/Users/Administrator/.ssh/id_rsa’:
当然如果你密码输错了,会再要求你输入,知道对了为止。

注意:输入密码时如果输错一个字就会不正确,使用删除键是无法更正的。

密码正确后你会看到下面这段话,如:

1
2
Hi username! You've successfully authenticated, but GitHub does not
# provide shell access.

如果用户名是正确的,你已经成功设置SSH密钥。如果你看到 “access denied” ,者表示拒绝访问,那么你就需要使用 https 去访问,而不是 SSH 。

github上如何修改别人的开源项目并提交

github上的开源项目是人人都可download下来修改,并贡献源码的,但不是无原则的,如果开源作者提交的代码,被任何人都download下来修改并且随意提交的话,那会有很多恶意修改源码的行为出现,也就没有github良好的开源生态, 那么github上是怎么修改别人的源码并且提交,让自己成为同一项目的源码贡献者呢。有一下三种机制:

1. 开源作者把你加入项目合作者里面

首先他会进入自己项目的settings里,如下:

Collaborators
然后在Collaborators里面搜索你的github用户名,add进去,之后你会在github上受到一个invitation提示,确认一下之后,你就可以在任何IDE里面用git clone到本地,URI选择这个github的开源地址就行,相当于这个github地址作为一个git的远程代码服务器。

2. 第二种方法是fork到本地,然后pull request

找到这个开源项目,点击右上角fork按钮,然后在你自己的github里面就会出现这个项目,修改完之后,在你的github里点击pull request, 之后原作者会收到这个请求,通过之后你就贡献了自己的源码

Fork&pull request

3. 建立organization

登录自己github账号后,进入github.com, 点击右上角,创建一个开源项目组织,人人都可贡献源码

Organization

三、git撤销

git reset HEAD^
拉取最近一次提交到版本库的文件到暂存区 改操作不影响工作区

四、git删除远程提交

这是一个之后需要删除掉的远程commit

在需要删除掉的远程commit后,我又提交了一个commit

在需要删除掉的远程commit后,我又提交了第二个commit

git 删除远程仓库的某次提交

git删除远程提交

1
2
3
4
5
6
7
8
9
10
11
删除上一次远程仓库的提交,修改上次提交的代码,做一次更完美的commit。
>
git reset commitId (注:不要带–hard)到上个版本
>
git stash 暂存修改
>
git push --force 强制push,远程的最新的一次commit被删除
>
git stash pop 释放暂存的修改,开始修改代码
>
git add . -> git commit -m "massage" -> git push

git删除远程提交

End