/* * @Author: dvlproad dvlproad@163.com * @Date: 2023-02-11 14:56:20 * @LastEditors: dvlproad dvlproad@163.com * @LastEditTime: 2023-02-12 15:18:22 * @FilePath: /undefined/Users/lichaoqian/Project/CQBook/dvlproadHexo/source/_posts/Architecture架构/h5js/common_js/url_util.js * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ /// 获取指定地址的主地址 function getRequestUrlMain(browserUrl) { if (browserUrl == null || browserUrl == "" || typeof browserUrl == "undefined") { var url = decodeURI(location.search); //获取url中"?"符后的字串 } else { var url = browserUrl; } var url_main; var paramStartIndex = url.indexOf("?"); if (paramStartIndex != -1) { // [js中截取字符串的三个方法 substring()、substr()、slice()](https://segmentfault.com/a/1190000016387899) url_main = url.substring(0, paramStartIndex); } else { url_main = url; } return url_main; } /// 获取指定地址的所有参数 function getRequestUrlParams(browserUrl) { if (browserUrl == null || browserUrl == "" || typeof browserUrl == "undefined") { var url = decodeURI(location.search); //获取url中"?"符后的字串 } else { var url = browserUrl; } var theRequest = new Map(); var paramStartIndex = url.indexOf("?"); if (paramStartIndex != -1) { var str = url.substr(paramStartIndex + 1); strs = str.split("&"); for (var i = 0; i < strs.length; i++) { var keyValueComponent = strs[i].split("="); var key = keyValueComponent[0]; var value = keyValueComponent[1]; theRequest[key] = unescape(value); } } return theRequest; } // [js获取url中的中文参数出现乱码解决](https://www.codeleading.com/article/87533774933/) // "http://localhost:4000/Architecture%E6%9E%B6%E6%9E%84/h5js/dvlp_h5js_demo/dvlp_h5js_demo.json"; // url含中文示例 // "http://localhost/test/test.html?p=广东&c=珠海" function getValueByQueryKey(browserUrl, key) { // 获取参数 if (browserUrl == null || browserUrl == "" || typeof browserUrl == "undefined") { var url = decodeURI(location.search); //获取url中"?"符后的字串 // var url = window.location.search; // 在本地用file打开,而不是http打开的话,此值会为空 } else { var url = browserUrl; } // 正则筛选地址栏 var reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)"); // 匹配目标参数 var result = url.substr(1).match(reg); //返回参数值 // return result ? unescape(result[2]) : null; // url中含中文时候,取值会出错 return result ? decodeURIComponent(result[2]) : null; } /// 获取本页面指定参数key的值 function getQueryString(key) { var url = window.location.search; // 在本地用file打开,而不是http打开的话,此值会为空 return getValueByQueryKey(url, key); }

第2章:JQuery高级技巧

[TOC]

JQuery高级技巧

1、jq –arg传递的变量select()没有硬编码值就不能工作吗?

1
2
3
Product_Personnel_Array=$(cat ${Product_Personnel_FILE_PATH} | ${JQ_EXEC} -r '.[]') # -r 去除字符串引号
echo ${Product_Personnel_Array} | ${JQ_EXEC} -r --arg branchLastCommitUserName "$branchLastCommitUserName" 'select(.git_name=="qian")'
echo ${Product_Personnel_Array} | ${JQ_EXEC} -r --arg branchLastCommitUserName "$branchLastCommitUserName" 'select(.git_name==$branchLastCommitUserName)'

2、shell - jq 处理json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 0、基础用法(验证通过✅)
FILE_PATH=~/Project/Bojue/mobile_flutter_wish/bulidScript/app_info.json
jq '.package_notification_argument_current.wechat.error.mentioned_list=2' $FILE_PATH

# 1.1、先测试修改数值时候(字符串),能否正确对应到key上(验证通过✅)
UpdateJsonKeyValue="1314"
jq --arg UpdateJsonKeyValue "$UpdateJsonKeyValue" '.package_notification_argument_current.wechat.error.mentioned_list=$UpdateJsonKeyValue' $FILE_PATH

# 1.2、先测试修改数值时候(数组),能否正确对应到key上
UpdateJsonKeyValue_Array=("a" "b" "c")
#UpdateJsonKeyValue="${UpdateJsonKeyValue_Array[*]}" # 这个是错误的❌
UpdateJsonKeyValue='["a", "b", "c", "d"]' # 必须转成这个才能通过✅
jq --arg UpdateJsonKeyValue "$UpdateJsonKeyValue" '.package_notification_argument_current.wechat.error.mentioned_list=$UpdateJsonKeyValue' $FILE_PATH

# 2、继续测试同时修改数值和key时候,能否正确对应到key上(验证失败❌)
UpdateJsonKey="package_notification_argument_current.wechat.error.mentioned_list"
UpdateJsonKeyValue=("a" "b" "c")
jq --arg UpdateJsonKey "$UpdateJsonKey" --arg UpdateJsonKeyValue "$UpdateJsonKeyValue" '.[$UpdateJsonKey]=$UpdateJsonKeyValue' $FILE_PATH


key要加[],值不用
1
jq '.test_group_1[] | select(.name == test_1-1).checksum = 1' $FILE_PATH

页面跳转相关

优雅的自定义返回

一、背景

从页面进入弹窗(全局WebView)。弹窗和页面不在同一路由栈上,从而使得当页面返回的时候,需要自己判断返回到哪。

二、场景示例

1
2
3
页面APage --> 页面BPage --> 弹窗CView --> 页面DPage

页面APage <-- 页面BPage <-- 弹窗CView <--

三、自定义返回

1、判断的核心

  • 判断的核心:在进入的时候标记下从哪来,点击返回的时候即回哪去。

2、必选处理的事及其初始主要代码

  • 必选处理的事:

    ①自定义返回按钮事件。

    ②传递进入时候的标记。

  • 处理事项的主要代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 进入页面 APage.swift
    func goPage() {
    let currentRoutePath = "xxx" // Replace with the actual value of currentRoutePath
    let viewController = BPage(fromRoutePath: currentRoutePath)
    self.navigationController?.pushViewController(viewController, animated: true)
    }

    // 返回页面 BPage.swift
    func goBack() {
    if fromRoutePath == "ShareWebView" {
    ShareWebViewRouteUtil.hide() // 全局webView视图
    } else {
    if isPresent {
    self.dismiss(animated: true, completion: nil)
    } else {
    self.navigationController?.popViewController(animated: true)
    }
    }
    }

3、所需值fromRoutePath的传入与使用

进入时候的标记赋值方案,主要有以下两种。

3.1、方案1(土)

进入页面的代码处标记。缺点:每个需要的地方的都需要写相应的代码。

3.2、方案2(精)

页面进行路由跳转的时候,将前一页的页面命名路由名传递给下一页。

1、添加页面跳转监听:进行对原本参数的添加,达到新值的传入目的。

2、跳转的目的页面将所有参数接收,待后续内部独立处理。

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
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
// 页面跳转的监听
class AppNavigatorObserver {
// 在进入的时候,自动将当前路由路径添加上到传递给下个页面的所有参数中
@override
void didPush(Route route, Route? previousRoute) {
final Object? arguments = route?.settings.arguments;
if (arguments != null && arguments is Map<String, dynamic>) {
arguments['fromRoutePath'] = previousRoute?.settings.name ?? '';
}
}
}

// route_handle.dart
class RouteUtil {
var minePageHandler = Handler(handlerFunc: (BuildContext? context, Map<String, List<String>> parameters) {
return MinePage.fromArgs(arguments: getArguments(context));
});
}

// base_page.dart
abstract class BJHBasePage extends LifeCyclePage {
final Sting? fromRoutePath;
const BasePage({
Key? key,
this.fromRoutePath, // 基类增加 可空的fromRoutePath 参数
}) : super(key: key);


// mine_page.dart
class MinePage extends BasePage {
final String userId;
MinePage({
Key? key,
String? fromRoutePath,
required this.userId,
}) : super(key: key, fromRoutePath: fromRoutePath);

MinePage fromArgs({Key? key, required Map<String, dynamic> arguments}) {
return WriteInvoicePage(
fromRoutePath: arguments["fromRoutePath"],
userId: arguments["userId"] ?? "",
);
}

void goBack() {
if fromRoutePath == "ShareWebView" {
ShareWebViewRouteUtil.hide() // 全局webView视图 的关闭
} else {
SystemRouteUtil.pop();
}
}
}

End

页面跳转相关

页面跳转相关(路由)

一、随时获取上一个页面的路径

背景:埋点为避免去从所有埋点里查找进入此页面前的上一个进入页面,希望直接知道此页面的上页面路径。

路由规范

字段 描述 示例
1 sourceFrom 从哪个环境过来
(如h5跳转到app)
appType/h5Type
2

1、无整屏显示的弹窗,即从任何弹窗进入记为从页面进入

二、优雅的自定义返回

背景:从页面进入弹窗(全局WebView)。弹窗和页面不在同一路由栈上,从而使得当页面返回的时候,需要自己判断返回到哪。

详见:优雅的自定义返回

三、一个不用重加载的WebView

背景:一个基于WebView的Cocos2d游戏,因为启动加载引擎会有3-5秒的加载时长。为了更好的游戏体验,希望启动后重新进入不用重新加载。

详见:一个不用重加载的WebView

四、游戏与app跳转交互优化调研

常见:web游戏,极小,游戏页独立存在

特殊:web游戏可以调到app页时候

关键策略:

1、调研游戏内容多网页共享方案
2、建立游戏测试页,提供后续方案验证基础
3、开发方案demo,进行游戏内容多网页实验
4、如果方案有效,后续进行含业务的完整方案梳理;则如果无效则进行其他方案调研

参考文档:《全局WebView》中的【多页面网页内容共享】

调研过程:

1、完成Flutter游戏测试页的建立,并通过尝试共享网页视图或控制器两种方式,进行游戏内容在多页面的共享测试。得出共享时候视图树会重绘,即网页会重新加载,从而无法达到整个跳转过程在同一路由栈下的正常跳转交互。

2、完成实现原生游戏测试页,并在原生上通过共享单例视图方式,进行游戏内容在多页面的共享测试。仍会出现在共享时候视图重绘操作。

3、后续预想方案

方案①游戏页面与其他网页一样常规的正常重新开辟。

优点:能使得整个跳转过程在同一路由栈下的正常跳转交互,享有系统的各种进出交互。

优化点:页面开辟时候,游戏加载慢。

方案②游戏使用唯一独立窗口,形如支付宝蚂蚁森林。需要从游戏内去app的其他页面时候,使用先缩小游戏窗口,到一个悬浮按钮,再弹出目标页面。

存在现象:从目标页再进游戏的时候需要从悬浮按钮进入。

路由解析详见 路由规范(含点击属性规范))

使用WebUrl解析的方法

1
2
3
4
5
6
7
8
// 解析方法如下:
/// 获取指定web地址的所有参数
static Map<String, dynamic>? getAllParamsFromWebUrl(String webUrl, {bool paramToObjectIfOK = false});

/// 将字符串value按需求转成 string 或者 object(如果可以转的情况下)
static dynamic getValueFromWebParamValueString(String value, {bool paramToObjectIfOK = false});

// 构造方法如下:

解析方法详细内容如下:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import 'dart:convert';

import 'package:flutter/foundation.dart';

class WebUrlUtil {
/// 获取指定web地址的所有参数
///
/// [webUrl]:要获取参数的地址。
///
/// [paramToObjectIfOK]:一个布尔值,指示是否将参数值转换为对象(如果可能)。默认为false。
///
/// 返回包含地址参数的Map对象,其中参数名作为键,参数值作为值。
///
/// 例如:https://www.baidu.com/?a=1&b=2
/// 返回:{a: 1, b: 2}
static Map<String, dynamic>? getAllParamsFromWebUrl(
String webUrl, {
bool paramToObjectIfOK = false,
}) {
var paramStartIndex = webUrl.indexOf('?');
if (paramStartIndex == -1) {
return null;
}

Map<String, dynamic> paramMap = {};
var str = webUrl.substring(paramStartIndex + 1);
var strs = str.split('&');

for (var i = 0; i < strs.length; i++) {
var keyValueComponent = strs[i].split('=');
var key = keyValueComponent[0];
String value = keyValueComponent[1];
dynamic element = getValueFromWebParamValueString(
value,
paramToObjectIfOK: paramToObjectIfOK,
);

paramMap[key] = element;
}

return paramMap;
}

/// 将字符串value按需求转成 string 或者 object(如果可以转的情况下)
///
/// [value]:要处理的参数的值。
///
/// [paramToObjectIfOK]:一个布尔值,指示是否将参数值转换为对象(如果可能)。默认为false。
///
/// 返回参数值的处理结果。
static dynamic getValueFromWebParamValueString(
String value, {
required bool paramToObjectIfOK,
}) {
try {
value = Uri.decodeComponent(value);
} catch (error) {
// value = value;
debugPrint("不用解码");
}

if (paramToObjectIfOK != true) {
return value;
}

dynamic element; // 如果 json.decode 成功,返回类型会变化,所以需另声明变量
try {
element = json.decode(value);
} catch (error) {
element = value;
}
return element;
}
}

构造方法详细内容如下:

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
import 'dart:convert';

import 'package:flutter/foundation.dart';

class WebUrlUtil {
static String addH5CustomParams(
String newString,
Map<String, dynamic> h5Params,
) {
for (String h5ParamKey in h5Params.keys) {
var h5ParamValue = h5Params[h5ParamKey]; // 类型不一定是String
if (h5ParamValue == null) {
continue;
}
late String h5ParamEncodeValue;
if (h5ParamValue is String) {
// h5ParamEncodeValue = h5ParamValue;
h5ParamEncodeValue =
Uri.encodeComponent(h5ParamValue); // 要编码,否则即使是字符串,但是含中文时候,也会出错
} else {
// String h5ParamParamString = h5ParamValue.toString();
// String h5ParamParamString =
// FormatterUtil.convert(h5ParamValue, 0); // 使用此行来修复json字符串没有引号的问题
String h5ParamParamString = jsonEncode(
h5ParamValue); // 使用此行来修复json字符串没有引号的问题,且避免使用FormatterUtil.convert时候的换行问题
h5ParamEncodeValue = Uri.encodeComponent(
h5ParamParamString); // 要编码,否则url,在app中的webView无法识别(虽然在goole chrome或safari上可以识别)
}

String extraString = "$h5ParamKey=$h5ParamEncodeValue";
if (newString.contains('?')) {
newString += "&$extraString";
} else {
newString += "?$extraString";
}
}
return newString;
}
}

End

路由规范(含点击属性规范)

[toc]

路由规范

一、路由地址规范

1、路由/点击操作类型

序号 操作类型标识 操作类型描述 场景举例
1 openPage 页面跳转 发布内容按钮
2 popupWindow 弹出弹窗 查看物流按钮
3 popupToast 弹出Toast 催一催按钮

路由操作,常与按钮点击结合在一起,所以我们顺便约定【点击按钮的属性规范】。

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
// 页面跳转
{
"action_type": "route",
"action_name": "发布内容",
"action_value": "appScheme://openPage?pageName=PublishPage&title=123"
}

// 弹出弹窗
{
"action_type": "popup",
"action_name": "查看物流",
"action_value": "appScheme://popupWindow?windowName=LookLogisticsWindow&uid=123&goodId=456
}

// 弹出Toast
{
"action_type": "toast",
"action_name": "催一催",
"action_value": "appScheme://popupToast?toastName=common&title=Error&message=something_wrong
}


// 其他(点击发起其他请求)
{
"action_type": "custom",
"action_name": "删除订单"
}

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
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
56
57
58
59
60
61
62
63
64
65
66
67
68
// 基类
enum TapType {
route,
popup,
toast,
custom,
}

abstract class BaseTapModel {
final String? id; // 可用来作为埋点的id
final TapType? actionType;
final String? actionValue;
BaseTapModel({this.id, this.actionType, this.actionValue});
}

// 有 文字/图片
class OnTapModel extends BaseTapModel {
final String? actionName;
final String? actionImageUrl; // 用于支持图片
final String? selectedActionName;
final String? selectedActionImageUrl;

OnTapModel({
String? id,
TapType? actionType,
String? actionValue,
this.actionName,
this.actionImageUrl,
this.selectedActionName,
this.selectedActionImageUrl,
}) : assert(actionName != null || actionImageUrl != null),
super(
id: id,
actionType: actionType,
actionValue: actionValue,
);
}

// 有 badge
enum BadgeType {
dot, // 原点
number, // 数字
}

class BadgeButtonModel extends OnTapModel {
final BadgeType badgeType;
final int? badgeCount;

BadgeButtonModel({
String? id,
TapType? actionType,
String? actionValue,
String? actionName,
String? actionImageUrl,
String? selectedActionName,
String? selectedActionImageUrl,
this.badgeType = BadgeType.dot,
this.badgeCount,
}) : super(
id: id,
actionType: actionType,
actionValue: actionValue,
actionName: actionName,
actionImageUrl: actionImageUrl,
selectedActionName: selectedActionName,
selectedActionImageUrl: selectedActionImageUrl,
);
}

路由解析

三、WebUrl 的解析与构造

使用WebUrl解析的方法

1
2
3
4
5
6
7
8
// 解析方法如下:
/// 获取指定web地址的所有参数
static Map<String, dynamic>? getAllParamsFromWebUrl(String webUrl, {bool paramToObjectIfOK = false});

/// 将字符串value按需求转成 string 或者 object(如果可以转的情况下)
static dynamic getValueFromWebParamValueString(String value, {bool paramToObjectIfOK = false});

// 构造方法如下:

解析方法详细内容如下:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import 'dart:convert';

import 'package:flutter/foundation.dart';

class WebUrlUtil {
/// 获取指定web地址的所有参数
///
/// [webUrl]:要获取参数的地址。
///
/// [paramToObjectIfOK]:一个布尔值,指示是否将参数值转换为对象(如果可能)。默认为false。
///
/// 返回包含地址参数的Map对象,其中参数名作为键,参数值作为值。
///
/// 例如:https://www.baidu.com/?a=1&b=2
/// 返回:{a: 1, b: 2}
static Map<String, dynamic>? getAllParamsFromWebUrl(
String webUrl, {
bool paramToObjectIfOK = false,
}) {
var paramStartIndex = webUrl.indexOf('?');
if (paramStartIndex == -1) {
return null;
}

Map<String, dynamic> paramMap = {};
var str = webUrl.substring(paramStartIndex + 1);
var strs = str.split('&');

for (var i = 0; i < strs.length; i++) {
var keyValueComponent = strs[i].split('=');
var key = keyValueComponent[0];
String value = keyValueComponent[1];
dynamic element = getValueFromWebParamValueString(
value,
paramToObjectIfOK: paramToObjectIfOK,
);

paramMap[key] = element;
}

return paramMap;
}

/// 将字符串value按需求转成 string 或者 object(如果可以转的情况下)
///
/// [value]:要处理的参数的值。
///
/// [paramToObjectIfOK]:一个布尔值,指示是否将参数值转换为对象(如果可能)。默认为false。
///
/// 返回参数值的处理结果。
static dynamic getValueFromWebParamValueString(
String value, {
required bool paramToObjectIfOK,
}) {
try {
value = Uri.decodeComponent(value);
} catch (error) {
// value = value;
debugPrint("不用解码");
}

if (paramToObjectIfOK != true) {
return value;
}

dynamic element; // 如果 json.decode 成功,返回类型会变化,所以需另声明变量
try {
element = json.decode(value);
} catch (error) {
element = value;
}
return element;
}
}

构造方法详细内容如下:

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
import 'dart:convert';

import 'package:flutter/foundation.dart';

class WebUrlUtil {
static String addH5CustomParams(
String newString,
Map<String, dynamic> h5Params,
) {
for (String h5ParamKey in h5Params.keys) {
var h5ParamValue = h5Params[h5ParamKey]; // 类型不一定是String
if (h5ParamValue == null) {
continue;
}
late String h5ParamEncodeValue;
if (h5ParamValue is String) {
// h5ParamEncodeValue = h5ParamValue;
h5ParamEncodeValue =
Uri.encodeComponent(h5ParamValue); // 要编码,否则即使是字符串,但是含中文时候,也会出错
} else {
// String h5ParamParamString = h5ParamValue.toString();
// String h5ParamParamString =
// FormatterUtil.convert(h5ParamValue, 0); // 使用此行来修复json字符串没有引号的问题
String h5ParamParamString = jsonEncode(
h5ParamValue); // 使用此行来修复json字符串没有引号的问题,且避免使用FormatterUtil.convert时候的换行问题
h5ParamEncodeValue = Uri.encodeComponent(
h5ParamParamString); // 要编码,否则url,在app中的webView无法识别(虽然在goole chrome或safari上可以识别)
}

String extraString = "$h5ParamKey=$h5ParamEncodeValue";
if (newString.contains('?')) {
newString += "&$extraString";
} else {
newString += "?$extraString";
}
}
return newString;
}
}

End

MTC Client Cache Best Practice

Server/Client orcharstrated Cache Machenism

Invalidate cache and naming is two most difficult things

两种场景:浏览器请求与应用请求

浏览器控制

<img src="...."/>

参考 HTTP Caching 文档

  • 彻底不缓存
1
Cache-Control: no-store
  • 缓存,但每次都去服务器检查是否有新数据
1
Cache-Control: no-cache
  • 缓存,但在一定时间内不去服务器检查是否有新数据
1
Cache-Control: max-age=3600
  • 使用 If-Modified-Since 来判断

    服务器端下发:

1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

浏览器端后续请求:

1
2
3
4
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT
  • 使用 If-None-Match 来判断 服务器端下发:
1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "deadbeef"
Cache-Control: max-age=3600

浏览器端后续请求:

1
2
3
4
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "deadbeef"

MTC 实践中,使用的是 ETag/If-None-Match, 数据有变化时,只在 Redis 中生成一个新的 ETag,后续处理判断 ETag 是否一致

例一:MTC Avatar

  • 服务器端:
1
2
3
4
5
6
7
8
9
return (
h
.response(fs.createReadStream(avatarinfo.path))
.header("Content-Type", avatarinfo.media)
.header("X-Content-Type-Options", "nosniff")
.header("Cache-Control", "max-age=600, private")
//.header("Cache-Control", "no-cache, private")
.header("ETag", avatarinfo.etag)
);
  • 客户端浏览器网络效果:

    avtar_image

  • 结果:

    1. 浏览器在 10 分钟内,不再重新请求,而是使用缓存中的图片
    2. 请求时,只有在图片变化时,服务器返回新图片,否则返回 304 头,浏览器使用已有缓存

例二:MTC Template Cover

  • 服务器端:
1
2
3
4
5
6
7
8
9
10
    return (
h
.response(fs.createReadStream(coverInfo.path))
.header("Content-Type", "image/png")
.header("X-Content-Type-Options", "nosniff")
.header("Cache-Control", "no-cache, private")
//.header("Cache-Control", "no-cache, private")
.header("ETag", coverInfo.etag)
);
}
  • 客户端浏览器网络效果:

TPL Cover Image

  • 结果:
    1. 浏览器每次都请求新数据
    2. 请求时,只有在图片变化时,服务器返回新图片,否则返回 304 头,浏览器使用已有换

应用控制

  • 如使用 Fetch, Axios, HttpClient 等请求数据,包括移动 APP 客户端从服务器获取数据
  • 需要自行设计实现
  • 两个问题:
    • 是否有更合理的方案?
    • APP 中如何实现?

业务层刷新机制思考:

  • 须考虑用户本地操作后,需要刷新
    • 当前用户操作,及时刷新
    • 其它用户,可定时刷新
  • 须考虑页面切换,使用本地缓存数据,无须访问服务器重新拉取
  • 须考虑自动刷新
    • 设置合理的间隔
    • 设置总刷新次数,防止用户不活动
      • 或检测用户鼠标动作,上次鼠标移动在多长时间内,断定为用户活跃,继续自动刷新,否则停止。
        • 提供手动刷新按钮(APP 移动端为下拉)

技术层刷新机制:

  • 服务端对每种业务对象设置 ETag Key, 每次返回数据,带上一个 ETag
    • 这个 ETag,用于表示某种数据是否刷新,仅在有变化时,修改这个 ETag
  • 客户端对每次请求的结果,进行缓存,同时记录 ETag;
  • 客户端每次请求,通过 If-None-Match 带入 ETag
  • 服务端相应请求时,判断服务端带来的 ETag 是否与服务端的 ETag 一致
    • 如一致,返回 304
    • 如不一致,返回 200,及最新数据
  • 客户端根据 Header 是否是 304,决定使用已缓存数据,还是更新缓存为服务端返回数据。

具体实现方式:

服务端 CORS 设置中,允许 ETag

1
2
3
4
5
6
7
routes: {
cors: {
origin: ["*"],
credentials: true,
additionalExposedHeaders: ["ETag", "X-Content-Type-Options"],
},
},

客户端请求,将请求内容组装为 cacheKey,并 MD5 位 cacheID

1
2
const cacheKey = { method, path, body, token };
const cacheID = MD5(JSON.stringify(cacheKey));

请求可以带 cache 控制 cacheFlag,用户控制:

  • preDelete: 在请求前,删除已有缓存
  • useIfExists: 如存在缓存,直接返回缓存,不执行服务器请求
  • 否则,在请求头中添加 If-None-Match
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (cacheFlag === CACHE_FLAG.preDelete) {
console.log(path, 'Preclear cache');
delete theCache[cacheID];
}
if (theCache[cacheID]) {
foundCache = true;
//console.log(path, 'Found cacheID in cache, should add If-None-Match header');
opts.headers['If-None-Match'] = theCache[cacheID].etag;
// } else {
// console.log(path, 'no local cache');
if (cacheFlag === CACHE_FLAG.useIfExists) {
console.log(path, 'Return cached');
return JSON.parse(theCache[cacheID].data);
}
}
  • 服务器端检查到 If-None-Match 后,判断业务数据是否有改变,如没有改变,直接返回 304,不用做数据库查询等操作
1
2
3
4
5
6
7
8
9
10
11
12
13
let ifNoneMatch = req.headers["if-none-match"];
let latestETag = await Cache.getETag(`ETAG:TODOS:${doer}`);
if (ifNoneMatch && latestETag && ifNoneMatch === latestETag) {
return h
.response({})
.code(304)
.header("Content-Type", "application/json; charset=utf-8;")
.header("Cache-Control", "no-cache, private")
.header("X-Content-Type-Options", "nosniff")
.header("ETag", latestETag);
}
//后续的操作无须执行
...

以上代码中,服务器端只需要按照业务逻辑,刷新相应的的 ETag Redis 记录,数据有变化,就修改 Redis 中相应 eTag key 的值。 如没有修改,则直接返回 304

  • 客户端请求后接收到 304, 则直接使用缓存中的数据
1
2
3
4
5
if (response.status === 304) {
returnCode304 = true;
responseETag = response.headers.get('etag');
return theCache[cacheID].data;
}
  • 客户端从服务器端接收到的不是 304,则说明,有新数据,则写入客户端缓存(包括收到的 etag, 用于下次请求时,通过 If-None-Match 带到服务器上对比)
1
2
3
4
5
6
7
8
9
if (responseETag) {
theCache[cacheID] = {
path: path,
data: jsonText,
etag: responseETag ? responseETag : '',
};
//Write to local cache
fetchCache.set(theCache);
}

缓存数据的记录方法

  • 浏览器自身: 不用管它
  • Cookie: 数据量非常小
  • localStorage: 跨浏览器 , 跟配置相关的数据建议用 LS
  • Session,但是使用 K-V
  • indexedDB: 数据库,SQL 差不多,比 LS 复杂, 也是跨浏览器
  • Sveltekit 的 Session 浏览器关掉,就消失。只要在 session 内部,没有问题

收益

  • 用户体验提升, 快速展示,不存在刷新
    • 直接使用缓存,不请求
    • 请求,但得到的是 304, 数据量非常小。直接是缓存
  • 网络和服务器的压力
    • 直接使用缓存,不请求时, 请求变少
    • 返回 304 时, 304 的判断是在 Redis 里做,后续的所有数据库操作都没有必要。大部分的请求,是可以直接 304 的。
  • 整体平台的相应能力提升
  • 整体的费用得以下降

Future

等待异步

有时候执行某个动作,需要等到另一个事件结束,可以用Completer类。

常见的有:

执行某个动作,需要弹窗等待用户选择某条件后再执行后续代码,

//事例

var c = Completer<bool>();

Dialogs.tipsCard(title, tip,
  failMsg: '',
  actions: [i18n.ok],
  callback: (index) => c.complete(true),
  dismissCallBack: () {
    if (!c.isCompleted)
      c.complete(false);
  },
);

return c.future;

其他:

Completer

Completer允许你做某个异步事情的时候,调用c.complete(value)方法来传入最后要返回的值。最后通过c.future的返回值来得到结果,(注意:宣告完成的complete和completeError方法只能调用一次,不然会报错)。看下面的例子更容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test() async {
Completer c = new Completer();
for (var i = 0; i < 1000; i++) {
if (i == 900 && c.isCompleted == false) {
c.completeError('error in $i');
}
if (i == 800 && c.isCompleted == false) {
c.complete('complete in $i');
}
}

try {
String res = await c.future;
print(res); //得到complete传入的返回值 'complete in 800'
} catch (e) {
print(e);//捕获completeError返回的错误
}
}

怎么将一个Callback回调转化成Future同步方法(Callback to Future),可以配套async / await去使用呢?

使用场景:dio请求使用call,但外部基本还是习惯使用future方式,为了减少接口的改动,使用callback转future方式。

参考文章:Flutter&Dart Callback转Future

compute

在一个页面中做耗时比较大的运算时,就算用了async / await异步处理, ui页面的动画还是会卡顿,因为还是在这个UI线程中做运算,异步只是说我可以先运行其他的,等我这边有结果再返回,但是,记住,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。

要解决这个卡顿问题,可以把运算移到另一个线程中,在dart中,这里不是称呼线程,是Isolate,直译叫做隔离,是因为隔离不共享数据,每个隔离中的变量都是不同的,不能相互共享。

Isolate的操作比较复杂,dart中封装了一层简单的实现

1
2
/// package:flutter/foundation.dart
Future<R> Function<Q, R>(FutureOr<R> Function(Q), Q, {debugLabel: String})compute

使用方法

1
2
3
4
5
6
7
8
import 'package:flutter/foundation.dart';

function callback( val ){
...
return res
}
/// `callback` 必须是顶级方法或者是类的静态方法
var res = await compute( callback , val );

简单来讲就是运行var res = await compute( callback , val )函数。callback的传入参数是val, return的数据就是callback的res;

使用场景

  • 方法执行在几毫秒或十几毫秒左右的,应使用Future
  • 如果一个任务需要几百毫秒或之上的,则建议compute(只有一次返回)或Isolate(用于订阅或有多次返回的)

Flutter/Dart :如何在app启动前等待异步任务?

我在一个dart应用程序上工作,我想获取缓存(SharedPreferences)中存在的数据,然后在应用程序的UI (主屏幕)上显示它。

问题:由于SharedPreferences是一个等待调用,我的主页加载,试图读取数据,应用程序崩溃,因为还没有从SharedPreferences中获取数据,并且应用程序在此之前加载。

其他参考文章:

多任务

dart笔记13:用future实现等待多个任务完成后,再得到所有的执行结果

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
import 'dart:async';

void main() {
print('start');

Future task1 = Future(() {
print('task1');
return 1;
});

Future task2 = Future(() {
print('task2');
return 2;
});

Future task3 = Future(() {
print('task3');
return 3;
});

Future future = Future.wait([task1, task2, task3]);

future.then((value) {
print(value);
}).catchError((e){

});


print('end');

//执行结果:
//start
//end
//task1
//task2
//task3
//[1, 2, 3]
}

FutureBuilder

Flutter:使用 Completer 实现自定义任务队列

环境变量

[TOC]

Mac 配置环境变量

环境变量的设置文件:

非M1类型的Mac:~/.bash_profile

是M1类型的Mac:~/.zshrc

image-20220321012950969

修改方法:

非M1类型的Mac:终端输入open -n ~/.bash_profile

是M1类型的Mac:终端输入open -n ~/.zshrc

编辑完保存并退出

非M1类型的Mac:输入 source ~/.bash_profile 使环境变量生效。

是M1类型的Mac:输入 source ~/.zshrc 使环境变量生效。

高质量的项目

一、管理

1、规范代码管理

2、搭建项目框架,制定代码规范

3、制订接口文档规范

4、统一封装满足多样化网络请求的网络库基础接口

5、组件的解耦封装

6、在当前工程中导入另一个工程文件

可扩展性:构建可扩展的iOS应用架构

稳定性治理:监控和分析崩溃日志来识别和修复潜在的问题。内存泄露。

iOS应用的稳定性可以通过多种方式来提升。首先,通过监控和分析崩溃日志来识别和修复潜在的问题。其次,通过优化代码和内存管理来减少卡死和崩溃的发生。例如,避免死锁、线程饥饿和内存泄漏等问题。此外,使用性能监控工具来检测应用的性能瓶颈,如卡顿和延迟,也是提高稳定性的重要手段。

灵活性:面向协议编程

二、风险预测

1、风险可能

1、不要过分相信服务器返回的数据会永远的正确。

2、在对数据处理上,要进行容错处理,进行相应判断之后再处理数据,这是一个良好的编程习惯。

2、思考:如何防止存在潜在崩溃方法的崩溃

  • 众所周知,Foundation框架里有非常多常用的方法有导致崩溃的潜在危险。对于一个已经将近竣工的项目,若起初没做容错处理又该怎么办?你总不会一行行代码去排查有没有做容错处理吧!——– 别逗逼了,老板催你明天就要上线了!
  • 那有没有一种一劳永逸的方法?无需动原本的代码就可以解决潜在崩溃的问题呢?

3、解决方案

解决方案:拦截存在潜在崩溃危险的方法,在拦截的方法里进行相应的处理,就可以防止方法的崩溃

步骤:

1
2
3
1、通过category给类添加方法用来替换掉原本存在潜在崩溃的方法。
2、利用runtime方法交换技术,将系统方法替换成我们给类添加的新方法。
3、利用异常的捕获来防止程序的崩溃,并且进行相应的处理。

4、消息转发机制

iOS的消息转发机制详解

5、消息转发机制的运用

1、简单问题引导

用Runtime解决服务器返回NSNull问题

思路:重写NSNull的消息转发方法, 让他能处理这些异常的方法.

防止Crash 组件

2、系统性解决:

iOS runtime实用篇 —避免常见崩溃

其中AvoidCrash的代码如下:AvoidCrash源代码

附:NSException

iOS被开发者遗忘在角落的NSException-其实它很强大

利用OC的消息转发机制实现多重代理

三、资源

app瘦身

四、代码

1、单元测试

单元测试分为3种:

1
2
3
逻辑测试:测试逻辑方法
异步测试:测试耗时方法(用来测试包含多线程的方法)
性能测试:测试某一方法运行所消耗的时间

为什么要使用单元测试:

1
2
3
经济上的问题:

假设要开发的是对接获取验证码接口的方法,难道运行一次就真的请求一个短信验证码?短信下发平台可是会¥扣钱¥的。更何况,万一第三方平台没有响应或超时,我们的测试就失败了,这种异步的、不确定的测试,无论从金钱还是时间上衡量,都不够经济,因此很难实现。

什么情况下时序使用单元测试(单元测试使用的注意事项):

1
2
3
4
5
6
1、不是所有的方法都需要测试。
例如:私有方法不需要测试!只有暴露在 .h 中的方法需要测试!面向对象有一个原则:开闭原则!
2、所有跟 UI 有关的都不需要测试,也不好测试。
把 业务逻辑 代码封装出来!变成可以测试的代码,让程序更加健壮!
3、一般而言,代码的覆盖度大概在 50% ~ 70%
从github上得知:YYModel测试覆盖度为83%,AFNetworking测试覆盖度为77%,两者都是比较高的。

2、优化多线程处理,改善多线程嵌套严重,请求耗时的问题

解决:优化多线程处理,改善多线程嵌套严重,请求耗时的问题。

详细:原本项目,采用多线程嵌套的同步方式处理多个线程请求到数据后,再执行最后操作。经优化多线程处理为异步执行时,改善了多线程嵌套严重,请求耗时的问题。

3、定时器使用的优化

4、UITableView等的性能优化

5、优化代码逻辑处理(耗电量、耗流量)

性能优化