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

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


  • 首页

  • 归档

  • 搜索

Linux平台Unity下RTMP RTSP低延迟播放器技术

发表于 2024-04-27

技术背景

国产操作系统对于确保信息安全、促进技术创新、满足特定需求以及推动经济发展等方面都具有重要意义,多以Linux为基础二次开发。2014年4月8日起,美国微软公司停止了对Windows XP SP3操作系统提供支持,这引起了社会和广大用户的广泛关注和对信息安全的担忧。

国产操作系统具有诸多好处:

  1. 信息安全与可控性提升:国产操作系统在设计和开发过程中,可以更加注重国内的信息安全标准和需求,通过自主研发,能够避免对外部系统的过度依赖,降低数据泄露和信息安全风险。此外,由于源代码掌握在自己手中,国家可以更好地控制操作系统的安全漏洞和后门问题,从而提高整个信息系统的安全可控性。
  2. 促进技术创新与产业发展:发展国产操作系统有助于推动国内软件技术的自主创新,提升整个软件产业的核心竞争力。通过自主研发,可以打破国外操作系统的技术垄断,掌握核心技术,为国内软件产业的发展提供有力支撑。同时,这也将促进相关产业链的发展,包括硬件、中间件、应用软件等,形成良性的产业生态。
  3. 满足特定需求与提升用户体验:国产操作系统可以根据国内用户的特定需求进行定制开发,提供更加符合国内使用习惯的服务和功能。这不仅可以提高用户的满意度和忠诚度,还可以为政府、企业等用户提供更加安全、高效、便捷的信息化解决方案。
  4. 培育新的经济增长点:随着数字经济的快速发展,操作系统作为数字基础设施的重要组成部分,具有巨大的市场潜力和商业价值。发展国产操作系统有助于培育新的经济增长点,推动国内软件产业的快速发展,为国家经济发展注入新的动力。
  5. 增强国家信息安全战略地位:在信息化时代,信息安全已成为国家安全的重要组成部分。发展国产操作系统有助于提升国家信息安全战略地位,增强国家在网络空间的话语权和影响力。

在发布国产操作系统|Linux平台的RTMP|RTSP直播播放SDK之前,大牛直播SDK(官方)在Windows、Android、iOS平台已经有了非常成熟的技术积累,功能齐全、稳定性高、超低延迟、超低资源占用,推进到Linux平台是顺理成章的。国产操作系统和Linux上的RTMP|RTSP直播播放模块,视频绘制使用XLib相关库实现, 音频输出使用PulseAudio和Alsa Lib,除了常规功能如实时静音、快照、buffer time设定、网络自动重连等,RTMP支持扩展H265播放, RTSP也支持H265播放。

Linux原生的RTSP、RTMP播放模块这里我们不做赘述,本文主要讲的是如何在Linux平台构建Unity下的RTSP和RTMP低延迟直播播放。

技术实现

国产操作系统和Linux平台下,Unity环境的播放器,和Windows、Android、iOS平台基础流程并无大的差异,简单来说,通过调用原生的播放模块,回调解码后的YUV或RGB数据,投递到Unity侧,在Unity下完成绘制,这里就需要原生的RTMP、RTSP播放模块,拉流解码延迟非常低,数据投递效率非常高,无图无真相:

Linux平台,我们是回调的YUV的数据,也就是 NT_SP_E_VIDEO_FRAME_FROMAT_I420:

1
2
3
4
5
6
7
csharp复制代码        /*定义视频帧图像格式*/
public enum NT_SP_E_VIDEO_FRAME_FORMAT : uint
{
NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 = 1, // 32位的rgb格式, r, g, b各占8, 另外一个字节保留, 内存字节格式为: bb gg rr xx, 主要是和windows位图匹配, 在小端模式下,按DWORD类型操作,最高位是xx, 依次是rr, gg, bb
NT_SP_E_VIDEO_FRAME_FORMAT_ARGB = 2, // 32位的argb格式,内存字节格式是: bb gg rr aa 这种类型,和windows位图匹配
NT_SP_E_VIDEO_FRAME_FROMAT_I420 = 3, // YUV420格式, 三个分量保存在三个面上
}

开始播放之前,把回调设置下去:

1
2
3
4
5
6
ini复制代码//video frame callback (YUV/RGB)
videoctrl[sel].sdk_video_frame_call_back_ = new VideoControl.SetVideoFrameCallBack(SDKVideoFrameCallBack);
videoctrl[sel].video_frame_call_back_ = new SP_SDKVideoFrameCallBack(NT_SP_SetVideoFrameCallBack);
NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(videoctrl[sel].player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, window_handle_, videoctrl[sel].video_frame_call_back_);

UInt32 flag = NTSmartPlayerSDK.NT_SP_StartPlay(videoctrl[sel].player_handle_);

视频帧结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
csharp复制代码    /*定义视频帧结构.*/
[StructLayoutAttribute(LayoutKind.Sequential)]
public struct NT_SP_VideoFrame
{
public Int32 format_; // 图像格式, 请参考NT_SP_E_VIDEO_FRAME_FORMAT
public Int32 width_; // 图像宽
public Int32 height_; // 图像高

public Int64 timestamp_; // 时间戳, 一般是0,不使用, 以ms为单位的

//具体的图像数据, argb和rgb32只用第一个, I420用前三个
public IntPtr plane0_;
public IntPtr plane1_;
public IntPtr plane2_;
public IntPtr plane3_;

// 每一个平面的每一行的字节数,对于argb和rgb32,为了保持和windows位图兼容,必须是width_*4
// 对于I420, stride0_ 是y的步长, stride1_ 是u的步长, stride2_ 是v的步长,
public Int32 stride0_;
public Int32 stride1_;
public Int32 stride2_;
public Int32 stride3_;
}

开始播放:

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
ini复制代码/*
* SmartPlayerLinuxMono.cs
* Author: daniusdk.com
*/
public void StartPlayer(int sel)
{
Debug.Log("StartPlayer++, sel: " + sel);

if (videoctrl[sel].is_playing_)
{
Debug.Log("StartPlayer, already started.. sel: " + sel);
return;
}

lock (videoctrl[sel].frame_lock_)
{
videoctrl[sel].cur_video_frame_ = null;
}

if (!OpenPlayerHandle(sel))
{
Debug.LogError("call OpenPlayerHandle failed, sel:" + sel);
return;
}

//video frame callback (YUV/RGB)
videoctrl[sel].sdk_video_frame_call_back_ = new VideoControl.SetVideoFrameCallBack(SDKVideoFrameCallBack);
videoctrl[sel].video_frame_call_back_ = new SP_SDKVideoFrameCallBack(NT_SP_SetVideoFrameCallBack);
NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(videoctrl[sel].player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, window_handle_, videoctrl[sel].video_frame_call_back_);

UInt32 flag = NTSmartPlayerSDK.NT_SP_StartPlay(videoctrl[sel].player_handle_);

if (flag == DANIULIVE_RETURN_OK)
{
videoctrl[sel].is_need_get_frame_ = true;
Debug.Log("NT_SP_StartPlay succeed, sel:" + sel);
}
else
{
NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
videoctrl[sel].player_handle_ = IntPtr.Zero;

videoctrl[sel].is_need_get_frame_ = false;
Debug.LogError("NT_SP_StartPlay failed, sel:" + sel);
}

videoctrl[sel].is_playing_ = true;
}

其中,调用的OpenPlayerHandle()实现如下:

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
ini复制代码/*
* SmartPlayerLinuxMono.cs
* Author: daniusdk.com
*/
private bool OpenPlayerHandle(int sel)
{
if (videoctrl[sel].player_handle_ != IntPtr.Zero)
return true;

window_handle_ = IntPtr.Zero;

videoctrl[sel].player_handle_ = new IntPtr();
UInt32 ret_open = NTSmartPlayerSDK.NT_SP_Open(out videoctrl[sel].player_handle_, window_handle_, 0, IntPtr.Zero);
if (ret_open != 0)
{
Debug.LogError("call NT_SP_Open failed, sel: " + sel);
return false;
}

if (IntPtr.Zero == videoctrl[sel].player_handle_)
return false;

videoctrl[sel].event_call_back_ = new SP_SDKEventCallBack(NT_SP_SDKEventCallBack);
NTSmartPlayerSDK.NT_SP_SetEventCallBack(videoctrl[sel].player_handle_, window_handle_, videoctrl[sel].event_call_back_);
videoctrl[sel].sdk_event_call_back_ = new VideoControl.SetEventCallBack(SDKEventCallBack);


/* ++ 播放前参数配置可加在此处 ++ */

int play_buffer_time_ = 0;
NTSmartPlayerSDK.NT_SP_SetBuffer(videoctrl[sel].player_handle_, play_buffer_time_); //设置buffer time

//int is_using_tcp = 1; //TCP模式
//NTSmartPlayerSDK.NT_SP_SetRTSPTcpMode(videoctrl[sel].player_handle_, is_using_tcp);

int timeout = 10;
NTSmartPlayerSDK.NT_SP_SetRtspTimeout(videoctrl[sel].player_handle_, timeout);

int is_auto_switch_tcp_udp = 1;
NTSmartPlayerSDK.NT_SP_SetRtspAutoSwitchTcpUdp(videoctrl[sel].player_handle_, is_auto_switch_tcp_udp);

Boolean is_mute_ = false;
NTSmartPlayerSDK.NT_SP_SetMute(videoctrl[sel].player_handle_, is_mute_ ? 1 : 0); //是否启动播放的时候静音

int is_fast_startup = 1;
NTSmartPlayerSDK.NT_SP_SetFastStartup(videoctrl[sel].player_handle_, is_fast_startup); //设置快速启动模式

Boolean is_low_latency_ = false;
NTSmartPlayerSDK.NT_SP_SetLowLatencyMode(videoctrl[sel].player_handle_, is_low_latency_ ? 1 : 0); //设置是否启用低延迟模式

//设置旋转角度(设置0, 90, 180, 270度有效,其他值无效)
int rotate_degrees = 0;
NTSmartPlayerSDK.NT_SP_SetRotation(videoctrl[sel].player_handle_, rotate_degrees);

int volume = 100;
NTSmartPlayerSDK.NT_SP_SetAudioVolume(videoctrl[sel].player_handle_, volume); //设置播放音量, 范围是[0, 100], 0是静音,100是最大音量, 默认是100

// 设置上传下载报速度
int is_report = 0;
int report_interval = 2;
NTSmartPlayerSDK.NT_SP_SetReportDownloadSpeed(videoctrl[sel].player_handle_, is_report, report_interval);

//设置播放URL
NTSmartPlayerSDK.NT_SP_SetURL(videoctrl[sel].player_handle_, videoctrl[sel].playback_url_);
/* -- 播放前参数配置可加在此处 -- */

return true;
}

停止播放:

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
ini复制代码/*
* SmartPlayerLinuxMono.cs
* Author: daniusdk.com
*/
private void StopPlayer(int sel)
{
Debug.Log("StopPlayer++, sel: " + sel);

videoctrl[sel].is_need_get_frame_ = false;
videoctrl[sel].is_need_init_texture_ = false;

if (videoctrl[sel].player_handle_ == IntPtr.Zero)
{
return;
}

UInt32 flag = NTSmartPlayerSDK.NT_SP_StopPlay(videoctrl[sel].player_handle_);
if (flag == DANIULIVE_RETURN_OK)
{
Debug.Log("call NT_SP_StopPlay succeed, sel: " + sel);
}
else
{
Debug.LogError("call NT_SP_StopPlay failed, sel: " + sel);
}

NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
videoctrl[sel].player_handle_ = IntPtr.Zero;

videoctrl[sel].is_playing_ = 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
ini复制代码    private void SDKVideoFrameCallBack(UInt32 status, IntPtr frame, int sel)
{
//这里拿到回调frame,进行相关操作
NT_SP_VideoFrame video_frame = (NT_SP_VideoFrame)Marshal.PtrToStructure(frame, typeof(NT_SP_VideoFrame));

VideoFrame u3d_frame = new VideoFrame();

u3d_frame.width_ = video_frame.width_;
u3d_frame.height_ = video_frame.height_;

u3d_frame.timestamp_ = (UInt64)video_frame.timestamp_;

int d_y_stride = video_frame.width_;
int d_u_stride = (video_frame.width_ + 1) / 2;
int d_v_stride = d_u_stride;

int d_y_size = d_y_stride * video_frame.height_;
int d_u_size = d_u_stride * ((video_frame.height_ + 1) / 2);
int d_v_size = d_u_size;

int u_v_height = ((u3d_frame.height_ + 1) / 2);

u3d_frame.y_stride_ = d_y_stride;
u3d_frame.u_stride_ = d_u_stride;
u3d_frame.v_stride_ = d_v_stride;

u3d_frame.y_data_ = new byte[d_y_size];
u3d_frame.u_data_ = new byte[d_u_size];
u3d_frame.v_data_ = new byte[d_v_size];


CopyFramePlane(u3d_frame.y_data_, d_y_stride,
video_frame.plane0_, video_frame.stride0_, u3d_frame.height_);

CopyFramePlane(u3d_frame.u_data_, d_u_stride,
video_frame.plane1_, video_frame.stride1_, u_v_height);

CopyFramePlane(u3d_frame.v_data_, d_v_stride,
video_frame.plane2_, video_frame.stride2_, u_v_height);

lock (videoctrl[sel].frame_lock_ )
{
videoctrl[sel].cur_video_frame_ = u3d_frame;
//Debug.LogError("sel: " + sel + " w:" + u3d_frame.width_ + "h:" + u3d_frame.height_);
}
}

Unity层拿到video frame后,刷新即可:

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
ini复制代码    private void UpdateProc(int sel)
{
VideoFrame video_frame = null;

lock (videoctrl[sel].frame_lock_)
{
video_frame = videoctrl[sel].cur_video_frame_;

videoctrl[sel].cur_video_frame_ = null;
}

if ( video_frame == null )
return;

if (!videoctrl[sel].is_need_get_frame_)
return;

if (videoctrl[sel].player_handle_ == IntPtr.Zero )
return;

if ( !videoctrl[sel].is_need_init_texture_)
{
if ( video_frame.width_ != videoctrl[sel].video_width_
|| video_frame.height_ != videoctrl[sel].video_height_
|| video_frame.y_stride_ != videoctrl[sel].y_row_bytes_
|| video_frame.u_stride_ != videoctrl[sel].u_row_bytes_
|| video_frame.v_stride_ != videoctrl[sel].v_row_bytes_ )
{
videoctrl[sel].is_need_init_texture_ = true;
}
}

if (videoctrl[sel].is_need_init_texture_)
{
if (InitYUVTexture(video_frame, sel))
{
videoctrl[sel].is_need_init_texture_ = false;
}
}

UpdateYUVTexture(video_frame, sel);
}

UpdateYUVTexture相关实现:

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
ini复制代码    private void UpdateYUVTexture(VideoFrame video_frame, int sel)
{
if (video_frame.y_data_ == null || video_frame.u_data_ == null || video_frame.v_data_ == null)
{
Debug.Log("video frame with null..");
return;
}

if (videoctrl[sel].yTexture_ != null)
{
videoctrl[sel].yTexture_.LoadRawTextureData(video_frame.y_data_);
videoctrl[sel].yTexture_.Apply();
}

if (videoctrl[sel].uTexture_ != null)
{
videoctrl[sel].uTexture_.LoadRawTextureData(video_frame.u_data_);
videoctrl[sel].uTexture_.Apply();
}

if (videoctrl[sel].vTexture_ != null)
{
videoctrl[sel].vTexture_.LoadRawTextureData(video_frame.v_data_);
videoctrl[sel].vTexture_.Apply();
}
}

技术总结

以上是Linux平台下Unity RTMP、RTSP直播播放器大概的实现参考,随着国产操作系统的推进,Linux下RTMP、RTSP高质量的播放器需求越来越大,Unity下,可以实现和Windows、Android等平台统一开发管理,非常方便。感兴趣的公司或开发者,可以单独跟我沟通探讨。

本文转载自: 掘金

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

CTF 数表 乘积

发表于 2024-04-27

截屏2024-04-27 00.54.26.png

截屏2024-04-27 00.54.46.png

你拿到了一个15*15的数表,请你求得数表中在一条线上(行、列、两个对角线)连续5个数的乘积的最大值,并将这5个数的10个数字按数字从小到大从左到右排列形成密码串,并分别提交。

file.jpg

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
python复制代码import pytesseract
from PIL import Image, ImageEnhance

# 打开图像文件
img = Image.open("file.jpg")

# 增强图像对比度,使文本更清晰
enhancer = ImageEnhance.Contrast(img)
img_enhanced = enhancer.enhance(1.5) # 调整对比度的级别

# 调整图像大小并应用阈值过滤器进行二值化处理
img_resized = img_enhanced.resize((1024, 768))
img_binarized = img_resized.convert("L").point(lambda x: 0 if x < 128 else 255, "1")

# 配置 Tesseract 以将图像视为单一块文本
custom_config = r"--oem 3 --psm 6"
text = pytesseract.image_to_string(img_binarized, config=custom_config)

# 替换识别结果中的度数符号为一个空格
text = text.replace("°", " ")
# print(text)


# 处理识别的文本,将其转换为二维数组
def convert_text_to_matrix(text):
# 根据换行符分割文本为多行
lines = text.strip().split("\n")

# 初始化二维数组
matrix = []
for line in lines:
# 过滤掉非数字字符,并以空格分割每一行中的数字
numbers = line.strip().split()
# 将字符串中的数字转换为整数列表
number_list = [int(num) for num in numbers if num.isdigit()]
# 如果列表非空,则添加到二维数组中
if number_list:
matrix.append(number_list)

return matrix


# 调用函数,将文本转换为二维数组
matrix = convert_text_to_matrix(text)

# 打印转换后的二维数组
print("矩阵:")
for row in matrix:
print(row)


# 辅助函数:计算列表中连续五个元素的乘积
def product_of_five(numbers):
from functools import reduce
from operator import mul

return reduce(mul, numbers)


# 定义函数计算最大乘积
def max_product(matrix):
n = len(matrix)
max_prod = 0 # 初始化最大乘积为 0
max_numbers = [] # 初始化最大乘积对应的数字序列

# 遍历矩阵的行和列
for i in range(n):
for j in range(n - 4):
# 计算每行的连续五个数字的乘积
row_prod = product_of_five(matrix[i][j : j + 5])
# 更新最大乘积和数字序列
if row_prod > max_prod:
max_prod = row_prod
max_numbers = matrix[i][j : j + 5]

# 计算每列的连续五个数字的乘积
col_prod = product_of_five([matrix[j + k][i] for k in range(5)])
if col_prod > max_prod:
max_prod = col_prod
max_numbers = [matrix[j + k][i] for k in range(5)]

# 遍历矩阵的对角线
for i in range(n - 4):
for j in range(n - 4):
# 计算主对角线上连续五个数字的乘积
diag1_prod = product_of_five([matrix[i + k][j + k] for k in range(5)])
if diag1_prod > max_prod:
max_prod = diag1_prod
max_numbers = [matrix[i + k][j + k] for k in range(5)]

# 计算副对角线上连续五个数字的乘积
diag2_prod = product_of_five([matrix[i + k][j + 4 - k] for k in range(5)])
if diag2_prod > max_prod:
max_prod = diag2_prod
max_numbers = [matrix[i + k][j + 4 - k] for k in range(5)]

# 对最大乘积对应的数字序列中的每一位数字进行排序
password = "".join(sorted("".join(map(str, max_numbers))))
return max_prod, password # 返回最大乘积和排序后的密码字符串


# 调用函数计算最大乘积和密码串
max_prod, password = max_product(matrix)
print("MAX SUM:", max_prod) # 打印最大乘积
print("PassWord:", password) # 打印排序后的密码串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码矩阵:
[71, 51, 67, 63, 89, 41, 92, 36, 54, 22, 40, 40, 28, 66, 33]
[60, 99, 13, 45, 22, 44, 75, 33, 53, 78, 36, 84, 20, 35, 17]
[28, 64, 23, 67, 10, 26, 38, 40, 67, 59, 54, 70, 66, 18, 38]
[68, 32, 62, 12, 20, 95, 63, 94, 39, 63, 68, 40, 91, 66, 49]
[25, 66, 73, 99, 26, 97, 17, 78, 78, 96, 83, 14, 88, 34, 89]
[89, 75, 10, 76, 44, 20, 45, 35, 14, 30, 61, 33, 97, 34, 31]
[28, 22, 75, 31, 67, 15, 94, 63, 80, 44, 62, 16, 14, 79, 53]
[42, 96, 35, 31, 47, 55, 58, 88, 24, 70, 17, 54, 24, 36, 29]
[48, 35, 71, 89, 67, 95, 54, 61, 37, 44, 60, 21, 58, 51, 54]
[68, 15, 64, 47, 69, 28, 73, 92, 13, 86, 52, 17, 77, 34, 89]
[83, 90, 35, 90, 16, 87, 97, 57, 32, 16, 26, 26, 79, 33, 27]
[87, 57, 62, 20, 72, 23, 46, 33, 67, 46, 55, 12, 32, 63, 93]
[73, 38, 25, 39, 11, 24, 94, 72, 18, 58, 46, 29, 32, 40, 62]
[41, 72, 30, 23, 88, 34, 62, 92, 69, 82, 67, 59, 85, 74, 84]
[29, 78, 31, 90, 31, 74, 31, 49, 71, 48, 86, 81, 16, 23, 57]
MAX SUM: 2171903490
PassWord: 1455677899

本文转载自: 掘金

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

Nodejs 第二十八章 邮件服务 Nodejs 第二十八章

发表于 2024-04-27

Nodejs 第二十八章 邮件服务

邮件服务在工作中扮演着多种关键角色,帮助提高团队效率和沟通流畅性。下面详细解释邮件服务在不同工作场景中的应用:

  1. 任务分配与跟踪:
* **用途**:邮件服务常用于分配具体任务、指派工作责任,并追踪项目的进展。
* **功能**:通过发送包含任务清单、工作指示和进度更新的邮件,可以确保每个团队成员都明确自己的职责和任务要求。这种通信方式能够有效帮助团队领导或项目管理者持续监控工作的完成情况。
  1. 错误报告和故障排除:
* **用途**:当软件或系统出现错误时,开发人员可以通过邮件向团队或相关人员报告问题。
* **功能**:邮件中可以包含详细的错误信息、重现问题的步骤以及问题发生的环境。团队成员就能够快速了解问题所在,并协助进行故障排除和修复。详细信息可能包括错误日志、堆栈跟踪以及可能的修复建议,使团队能更有效地响应和处理问题。
  1. 自动化构建和持续集成:
* **用途**:在持续集成(CI)和自动化构建的环境中,邮件服务用于通知团队成员关于构建状态的重要更新。
* **功能**:邮件通知可以包括构建成功或失败的信息、单元测试结果、代码覆盖率等。如果构建过程中出现错误或警告,系统会自动发送邮件给相关人员,他们就能及时了解并采取必要的措施。

第三方库使用

js-yaml

  • 作用:js-yaml 是一个用于解析和生成 YAML(一种常用于配置文件的人类友好的数据序列化标准)的 JavaScript 库。
  • 用途:它允许开发者在 Node.js 应用中轻松读取、写入和处理 YAML 文件。这对于配置管理特别有用,因为 YAML 格式的可读性比 JSON 更好,常用于存储配置设置。

nodemailer

  • 作用:nodemailer 是一个在 Node.js 中发送电子邮件的模块。
  • 用途:它支持各种邮件发送选项,包括但不限于文本内容、HTML内容、附件等。nodemailer可以与SMTP(简单邮件传输协议)服务配合使用,也支持直接传输和其他传输方法(如 Sendmail),使得它在应用程序中实现自动邮件通知、用户验证邮件和其他邮件发送功能变得非常简单。

yaml文件格式

  • 我们邮件的账号(密码| 授权码)不可能明文写到代码中,一般存放在yaml文件或者环境变量里面
+ pass就是password(密码),user则是用户邮箱账号
+ 这里需要注意在`pass:`或者`user:`后面继续填写内容的时候,要加上空格
1
2
3
yaml复制代码 # js-yaml 解析yaml文件
 pass: 授权码 | 密码
 user: xxxxx@qq.com 邮箱账号

代码实现

初始化邮件服务

  • 首先,是肯定需要先将两个需要用到的第三方库下载下来
+ mailer就是邮寄的意思,而nodemailer就是node-mailer=>使用node实现的邮件服务
1
2
复制代码 npm install js-yaml
 npm install nodemailer
  • 然后创建一个mail.http文件用来模拟发送请求(前面章节就有讲到了),一个mail.yaml文件用来存储内容,以及一个index.js用来写我们的代码啦
1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码 import nodemailer from "nodemailer"
 
 // 初始化邮件服务
 const transport = nodemailer.createTransport({
   service:'qq',
   host:"smtp.qq.com",
   port:465,//具体含义后面说
   secure:true,//是否要使用https
   auth:{
     user:"",//邮箱账号
     pass:""//邮箱密码/授权码
  }
 })
  • 这时候,我们可能就要疑惑了,这些参数要具体填写什么,我要去哪里看,比如说port端口参数,为什么要填465,有什么含义吗?
+ 很显然,这是有的,我们需要看**QQ邮件服务文档**,地址:[SMTP/IMAP服务 (qq.com)](https://wx.mail.qq.com/list/readtemplate?name=app_intro.html#/agreement/authorizationCode)
+ 那在这里面,我们就能够知道port为什么要填写465了,因为QQ的文档里面已经有写明如何使用了

image-20240426234830911

POP3/SMTP 设置方法

用户名/帐户: 你的QQ邮箱完整的地址

密码: 生成的授权码

电子邮件地址: 你的QQ邮箱的完整邮件地址

接收邮件服务器: pop.qq.com,使用SSL,端口号995

发送邮件服务器: smtp.qq.com,使用SSL,端口号465或587

授权码生成

  • 如何拿到授权码,就需要通过我们前面写出来的QQ开发文档链接跳转进去找到下图中的内容

image-20240426235044392

  • 然后选择生成授权码就OK了

image-20240426235428786

  • 成功生成界面如下:

image-20240426235606065

  • 从设备管理中,可以看到我们目前管理的授权码,然后将其存进mail.yaml文件里面
+ yaml存放的格式已经写在上面了,照着填进去就OK了
+ user填写QQ邮箱,pass填写我们刚刚拿到的授权码

image-20240426235921752

实现发送邮件功能

  • yaml文件设置好了之后,我们只需要在index.js中,将其读取出来,然后放入auth参数中。这里之所以绕一个弯路,不直接把内容存放进去的原因,就是为了防止隐私泄露(不用明文形式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
php复制代码 import nodemailer from "nodemailer"
 import yaml from "js-yaml"
 import fs from "node:fs"
 
 const mailConfig = yaml.load(fs.readFileSync('./mail.yaml', 'utf-8'))
 
 // 初始化邮件服务
 const transport = nodemailer.createTransport({
   service: 'qq',
   host: "smtp.qq.com",
   port: 465,
   secure: true,//是否要使用https
   auth: {
     user: mailConfig.user,//邮箱账号
     pass: mailConfig.pass//邮箱密码/授权码
  }
 })
  • 接下来就要起一个服务,然后实现发送邮件的功能。没错,现在才要开始实现,我们前面的内容是配置发送邮件的前置信息
+ 在下面这段代码中,我们在发送内容之前,必须要判断一下,是否是我们发送邮箱的地址,比如我们这里是以`/send/mail`进行发送的,这和我们区分动静分离的方式是有点像的,我们这里区分出来的是专门为了发送邮箱的接口
+ 其中to、subject、text都是不确定的参数,只有from是确定的。所以只有一个from可以写死(就是我们用来发送信息的那个邮箱账号)。而其他的三样内容都是要根据发送的请求携带参数来填入,且我们携带的这些参数作为邮件来说,必然是私密性质的,所以需要判断请求是否为POST请求才行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码 const server = http.createServer((req, res) => {
   const { pathname } = url.prase(req.url)
   const { method } = req
   if(method === 'POST' && pathname === '/send/mail'){
     // 发送邮件
     transport.sendMail( {
       to:'收件人',
       from:'发件人',
       subject:'主题',
       text:'内容'
    })
  }
 })
 
 server.listen(3000, () => {
   console.log("3000端口已经启动");
 })
  • 但上面的代码是没办法动态做到将请求的对应内容填入的,我们需要进行一定的修改
+ 我们先通过req.on中的data方法来监听到内容,再根据完整的数据进行JSON转化提取其中需要用到的数据传入我们的to、subject、text中
+ 其中这里发送邮件运用到ES6之后的写法,`to:to`类型的写法可以省略为`to`
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
javascript复制代码 import nodemailer from "nodemailer"
 import yaml from "js-yaml"
 import fs from "node:fs"
 import url from "node:url"
 import http from "node:http"
 
 const mailConfig = yaml.load(fs.readFileSync('./mail.yaml', 'utf-8'))
 
 // 初始化邮件服务
 const transport = nodemailer.createTransport({
   service: 'qq',
   host: "smtp.qq.com",
   port: 465,
   secure: true,//是否要使用https
   auth: {
     user: mailConfig.user,//邮箱账号
     pass: mailConfig.pass//邮箱密码/授权码
  }
 })
 
 const server = http.createServer((req, res) => {
   const { pathname } = url.parse(req.url)
   const { method } = req
   if (method === 'POST' && pathname === '/send/mail') {
     // 发送邮件
     let data = '';
     // 回调返回的是一个个片段,我们将其拼接起来为整体
     req.on("data", (chunk) => {
       data += chunk
    })
     req.on('end', () => {
       const { to, subject, text } = JSON.parse(data)
       transport.sendMail({
         to,
         from: mailConfig.user,
         subject,
         text
      })
       res.end("邮件发送成功")
    })
     // transport.sendMail( {
     //   to:'收件人',
     //   from:'发件人',
     //   subject:'主题',
     //   text:'内容'
     // })
  }
 })
 
 server.listen(3000, () => {
   console.log("3000端口已经启动");
 })

测试邮件服务

  • 那这样我们也是成功写完了,现在,就让我们来编写一个http请求吧,看下是否能够实现发送邮件的功能
+ 然后记得启动一下我们写在index.js中的服务
1
2
3
4
5
6
7
8
9
bash复制代码 //mail.http文件
 POST http://localhost:3000/send/mail HTTP/1.1
 Content-Type: application/json
 
 {
  "to":"1045098807@qq.com",//这里就发给我自己进行测试了,这里的注释记得删掉
  "subject":"小余测试邮件发送功能",
  "text":"我是小余,我为自己代言,我的微信为:XiaoYu2002-AI"
 }

image-20240427004209645

  • 能够看到成功发送内容到我们写的邮件服务接口上了
+ 通过打开QQ邮箱,我们也确实成功看到了内容成功发送

image-20240427004311335

本文转载自: 掘金

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

视频通话实时换脸:支持训练面部模型 开源日报 No23

发表于 2024-04-27

picture

iperov/DeepFaceLive

Stars: 19.7k License: GPL-3.0

picture

DeepFaceLive 是一个用于 PC 实时流媒体或视频通话的人脸换装工具。

  • 可以使用训练好的人脸模型从网络摄像头或视频中交换面部。
  • 提供多个公共面部模型,包括 Keanu Reeves、Mr. Bean 等。
  • 支持自己训练面部模型以获取更高质量和更好匹配度。
  • 包含 Face Animator 模块,可控制静态面部图片使用视频或摄像头实现动画效果。

seaweedfs/seaweedfs

Stars: 20.0k License: Apache-2.0

picture

SeaweedFS 是一个快速的分布式存储系统,适用于大量文件的 blobs、objects、files 和 data lake。其主要功能和核心优势包括:

  • Blob 存储具有 O(1) 磁盘查找和云层级。
  • Filer 支持 Cloud Drive、跨数据中心主动-主动复制、Kubernetes、POSIX FUSE 挂载点等多种功能。
  • 提供 S3 API 和 S3 Gateway,支持 Hadoop 和 WebDAV。
  • 支持加密和 Erasure Coding 技术。

opencontainers/runc

Stars: 11.1k License: Apache-2.0

runc 是一个根据 OCI 规范在 Linux 上生成和运行容器的 CLI 工具。

  • 根据 OCI 规范生成和运行容器
  • 支持安全审计
  • 仅支持 Linux 平台
  • 使用 Go Modules 进行依赖管理
  • 提供可选的构建标签,用于编译各种功能支持

stefanprodan/podinfo

Stars: 4.8k License: Apache-2.0

picture

podinfo 是一个用 Go 编写的微服务模板,展示了在 Kubernetes 中运行微服务的最佳实践。

  • 健康检查(可读性和活性)
  • 收到中断信号时优雅关闭
    • 用于监视秘密和配置映射的文件观察者
  • 使用 Prometheus 和 Open Telemetry 进行仪表化
  • 使用 zap 进行结构化日志记录
  • 遵循 12 要素应用程序设计原则,使用 viper 库
  • 故障注入(随机错误和延迟)
  • Swagger 文档支持

layerdiffusion/sd-forge-layerdiffuse

Stars: 2.2k License: Apache-2.0

picture

sd-forge-layerdiffuse 是一个通过 Forge 实现的 WebUI 的图层扩散项目。

  • 生成透明图像和图层
  • 支持基本的图像生成和图层功能
  • 提供多个模型用于转换 SDXL 为透明图片生成器
  • 高度动态且可能在未来一个月内发生大量变化

本文转载自: 掘金

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

2024-4-26 群讨论:PostgreSQL & MyS

发表于 2024-04-27

以下来自本人拉的一个关于 Java 技术的讨论群。关注公众号:hashcon,私信进群拉你

PostgresSQL 和 MySQL 各自适用的场景(仅考虑 OLTP)

假设都是默认的事务引擎,默认的编码压缩方式:

MySQL 与 PG 在 OLTP 的场景下,主要区别在于:两点:

  1. 对于二级索引处理的差异:
* MySQL 二级索引叶子节点是保存的主键的指针,PG 的二级索引叶子节点与主键索引一样直接是记录位置,行记录发生更新的时候,由于 MVCC 与可变长度字段与 null 字段,很可能导致行位置变化,对于 PG 需要更新所有二级索引,但是 MySQL 不需要
    + PG 如果是非索引字段更新,缓冲池够的情况下 Heap-Only Tuples (HOT) 生效,不一定用更新索引
    + 但是 Heap-Only Tuples (HOT) 其实在实际 OLTP 场景中,命中率不是特别理想。
* 所以 **MySQL 对于有二级索引的表高并发更新,以及涉及数据位置改变的更新**(比如更新 varchar 字段为更长的),**以及插入,会比 PG 表现好**。
* 但是,**这种设计下,MySQL 的二级索引读取性能肯定也不如 PG**。因此,需要好好考虑场景。
  1. 对于 MVCC 处理的差异:
* PostgreSQL 的 MVCC 基于 xmin, xmax 机制实现:当一行数据需要被更新或删除时,PostgreSQL 并不是直接更改原有的行记录。相反,它会:
    + 更新:插入一个新的行版本,其中 xmin 设置为当前事务的 ID,同时将旧版本行的 xmax 设置为当前事务的 ID。
    + 删除:简单地将行的 xmax 设置为当前事务的 ID。
* MySQL 的 MVCC 是基于行锁和 undo log实现的。每行记录都有两个隐藏的列,分别记录事务ID(`trx_id`)和回滚指针(`roll_pointer`)。读取数据时,InnoDB 会根据事务 ID 和回滚指针找到行数据的可见版本
* PG 的优势体现在读取,老版本也可以直接读取,同时读取这行不阻塞这行的更新。但是劣势也很明显,频繁更新,表膨胀过快,vacuum 有时候完全跟不上高速写操作,另外 vacuum 本身也有很多问题,autovacuum 本身在部分场景下会导致 dead tuple 不断积攒以至于一段时间后查询的 IO 开销将极为高昂,必须要 DBA 手动的进行释放(此处感谢:B站 滑稽\_1 [space.bilibili.com/38107834](https://space.bilibili.com/38107834) )。插入性能也会受这个多版本影响。
* MySQL 优势在写入,只为当前读写的行加锁,其他写入不受影响,并发写入更高。
* PostgreSQL 和 MySQL 在大表更新频率很高达到一定阈值的时候,不是那种订单表,交易表,而是类似于用户余额表那种,带来的查询与插入的性能严重下降。在这种场景下,PostgreSQL 本身由于 xmin 与 xmax 的回滚 MVCC 设计导致表膨胀过快,与 MySQL 类似 Oracle 的 Redolog 设计上,MySQL 需要分库分表的阈值相对于 PostgreSQL 高一些。PostgreSQL 之前推出过 zheap 想改用 Redolog,但是后来在 20 年之后就没有下文了,不知道为啥。参考:[wiki.postgresql.org/wiki/Zheap](https://wiki.postgresql.org/wiki/Zheap)

综合来看,其实 MySQL 更适合 OLTP 的场景。现在云服务商提供的数据库基本都实现了主从延迟很低,读取性能可以加从库解决。例如 Aurora,一个写入实例最多可以加 12 个读取实例,延迟在业务最高峰,也只有 300 ms,平常在 10 ms 左右。

PostgreSQL 目前的生态更丰富,并且 OLAP 的很多数据库,其实在协议层用的是 PostgreSQL。PostgreSQL 目前的发展方向,也主要在 OLAP 的生态场景不断完善。

另外,Uber 在 2015 年的时候,从分库分表的 PostgreSQL,转移到了分库分表的 MySQL 以应对他们的 OLTP 场景,原文:www.uber.com/en-HK/blog/…

个人简介:个人业余研究了 AI LLM 微调与 RAG,目前成果是微调了三个模型:

  1. 一个模型是基于 whisper 模型的微调,使用我原来做的精翻的视频按照语句段落切分的片段,并尝试按照方言类别,以及技术类别分别尝试微调的成果。用于视频字幕识别。
  2. 一个模型是基于 Mistral Large 的模型的微调,识别提取视频课件的片段,辅以实际的课件文字进行识别微调。用于识别课件的片段。
  3. 最后一个模型是基于 Claude 3 的模型微调,使用我之前制作的翻译字幕,与 AWS、Go 社区、CNCF 生态里面的官方英文文档以及中文文档作为语料,按照内容段交叉拆分,进行微调,用于字幕翻译。

目前,准确率已经非常高了。大家如果有想要我制作的视频,欢迎关注留言。

本人也是开源代码爱好者,贡献过很多项目的源码(Mycat 和 Java JFRUnit 的核心贡献者,贡献过 OpenJDK,Spring,Spring Cloud,Apache Bookkeeper,Apache RocketMQ,Ribbon,Lettuce、 SocketIO、Langchain4j 等项目 ),同时也是深度技术迷,编写过很多硬核的原理分析系列(JVM)。本人也有一个 Java 技术交流群,感兴趣的欢迎关注。

另外,一如即往的是,全网的所有收益,都会捐赠给希望工程,坚持靠爱与兴趣发电。

本文转载自: 掘金

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

Stencil 搭建 Web Component 组件库项目

发表于 2024-04-26

前言

在之前介绍了 Web Component 组件库有什么优势,这篇讲一下如何使用 Stencil 搭建 web component 组件库

Stencil 是一个开源的编译器,用于构建可复用、可扩展的 Web Component 组件库,是由 Ionic 团队开源的,方面我们以一种简单高效的方式来创建和管理 Web Component 组件库。

为什么选择Stencil

选择 Stencil 的原因在于它的跨框架兼容性、性能优化以及对标准 Web Components 的支持

Stencil的优势和特点

  • 性能优化:自动懒加载、异步渲染等特性使得组件加载更快。
  • 跨框架:生成的组件可以在多个框架中使用,如Vue、Angular、React等。
  • JSX 支持:可以使用类似于 React 的方式来编写组件。
  • TypeScript持:使用强类型语言来编写组件,提高了代码的可读性和可维护性
  • 测试工具:提供了一套完整的测试工具,可以方便地进行单元测试和端到端测试。
  • 社区支持:由Ionic团队维护,社区活跃,资源丰富。

Stencil 搭建开发环境

使用 Stencil 提供的脚手架安装项目,进入 packages 目录,执行创建命令

1
2
3
4
5
shell复制代码# npm
npm init stencil

# pnpm
pnpm create stencil

执行完后出现交互性的界面,有三种模式,构建组件库选择第一个 component

  • component:是构建一个可以用到任何地方的 Web Components 组件库
  • app:是用来构建一个静态网站或者一个应用,支持路由
  • ionic-pwa:构建一个 pwa 应用

选择“component”选项将提示您输入项目的名称。

1
2
css复制代码✔ Pick a starter › component
? Project name › components

成功创建我们的项目之后,CLI将向控制台输出如下内容

安装依赖,执行 npm start 启动项目

目录结构

初始化目录结构,最外层的目录结构

  • dist、loader:默认的输出目录,编译后的文件
  • src:源文件目录,包含了组件的代码、样式和测试
  • tsconfig.json 是用来配置 ts 的一些 config 配置。
  • www :启动本地开发环境后,经过 stencil 编译后 src 的 js 文件和 index.html 会被执行复制过移动到 www 文件夹中去,而启动的热服务器(live server)访问就会被映射到此文件夹,从而进行渲染和调试。
1
2
3
4
5
6
7
8
lua复制代码├── dist
├── loader
├── src
├── package.json
├── readme.md
├── stencil.config.ts
├── tsconfig.json
└── www

组件和配置目录结构

创建组件

在新建项目后,package.json 默认添加了如下脚本命令,其中 stencil generate 是创建组件的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码{
"scripts": {
# 构建组件
"build": "stencil build",
# 启动开发
"start": "stencil build --dev --watch --serve",
# 端对端测试
"test": "stencil test --spec --e2e",
"test.watch": "stencil test --spec --e2e --watchAll",
# 创建组件
"generate": "stencil generate"
}
}

运行 pnpm generate,提示创建的组件是否要添加 样式、单元测试、端对端测试文件

输入组件名称 my-button,直接回车在 components 目录下创建一个 my-button 组件目录

Stencil 使用 tsx 进行开发,类似 react ,提供完整的声明周期函数和装饰器等功能,查看 my-component.tsx 文件

  • @Component() 声明一个新的 Web 组件
  • @Prop() 声明一个公开的属性/属性
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
jsx复制代码import { Component, Prop, h } from '@stencil/core';
import { format } from '../../utils/utils';

@Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true,
})
export class MyComponent {
// 声明一个公开的属性/属性
@Prop() first: string;

/**
* The middle name
*/
@Prop() middle: string;

/**
* The last name
*/
@Prop() last: string;

private getText(): string {
return format(this.first, this.middle, this.last);
}

render() {
return <div>Hello, World! I'm {this.getText()}</div>;
}
}

使用组件像普通 html 标签一样使用,传入 first、last 会被 @Prop 声明的属性接收,修改它们的值,组件内部会响应式的更新渲染视图

1
jsx复制代码<my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>

stencil 工作原理

入口文件

src/index.html 是用来启动本地 dev 环境时候的一个入口文件,在初始化过程中,它会默认添加需要的 js 产物的路径,如下:

1
2
3
4
5
6
7
html复制代码  <head>
<script type="module" src="/build/editor.esm.js"></script>
<script nomodule src="/build/editor.js"></script>
</head>
<body>
<my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
</body>

在启动后,根据 stencil.config.ts 命名空间字段设置如 namespace: 'editor',会将编译 js 文件生成在 dist/editor 目录下,同时会拷贝一份到 www/build 文件夹提供给服务器访问,src/index.html 也会一并被复制过去

可以看到在启动后服务器访问的 js 目录 www/build 和 dist/editor 文件是一样的

www 本地调式目录

在启动本地命令 stencil build --dev --watch --serve 后,改动下组件的内容后会发现,www 文件夹内的资源文件也随之更新,浏览器相应界面的也发生变化,执行逻辑

所以 www 文件夹存放的是开发调试所包含资源文件,build 文件夹是编译的产物;host.config.json 会存放一些本地热更新服务器的配置,比如缓存的配置等;而 index.html 则是经过压缩的 src/index.html 文件。

stencil.config.ts 配置

stencil.config.ts 文件是 stencil 工具的配置文件,可以在此个性化的配置打包功能,比如:

  • globalScript:全局脚本入口文件,可以用来实现在初始化组件,进行挂载的时候进行一些逻辑的处理
  • globalStyle: 全局的样式路径,如设置 var 函数的主题变量
  • plugins 插件: 配置一些编译过程中的额外功能,比如 sass 插件可以用来适配 scss 文件的编译,postcss 可以用来压缩、增加一些兼容性的代码来适配各种浏览器等等,官方还提供了以下插件:@stencil/less、@stencil/postcss、@stencil/sass、@stencil/stylus
  • outputTargets:根据配置 target 进行打包

dist / loader 打包产物

执行 pnpm run build 命令进行打包,最终生成编译产物如下,包括 dist 和 loader 两个目录

通过观察 stencil 默认生成的 package.json 文件中 files 自动的配置也能知道,最终输出的文件资源 dist 和 loader 发布到 npm 上

1
2
3
4
json复制代码"files": [
"dist/",
"loader/"
],

那么这两个文件夹有什么关系呢,我们先看下 loader 文件夹 package.json 的内容和其他文件的内容

loader 目录

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
json复制代码{
"name": "editor-loader",
"private": true,
"typings": "./index.d.ts",
"module": "./index.js",
"main": "./index.cjs.js",
"jsnext:main": "./index.es2017.js",
"es2015": "./index.es2017.js",
"es2017": "./index.es2017.js",
"unpkg": "./cdn.js"
}

// index.js

export * from '../dist/esm/polyfills/index.js';
export * from '../dist/esm/loader.js';

// index.es2017.js

export * from '../dist/esm/polyfills/index.js';
export * from '../dist/esm/loader.js';


//index.cjs.js

module.exports = require('../dist/cjs/loader.cjs.js');
module.exports.applyPolyfills = function() { return Promise.resolve() };

可以看出,loader 文件夹是兼容 ESModule 和 Common.js 模块规范,根据当前的使用环境,分别引入 dist 的不同的文件夹产物内,比如 cjs 的引入类型会加载 ../dist/cjs/loader.cjs.js ;es6+ 的引入会加载 polyfills 文件和 esm 模式的资源文件 ../dist/esm/loader.js 。

dist 目录

在默认的 stencil.config.ts 配置下,打包结果为

1
2
3
4
5
6
7
8
复制代码├── cjs
├── collection
├── components
├── esm
├── editor
├── index.cjs.js
├── index.js
└── types

其中 cjs 和 esm 的逻辑几乎相同,esm 只是多了一些兼容性的代码,它们都是为了适配不同的环境所构造的不同产物

stencil 初始化配置里面默认包含了 dist-custom-elements,意思就是可以单独打包出符合 web components 规范的组件 class

打包的组件单独引入会进行自动注册

1
2
3
js复制代码import { HelloWorld } from 'my-library/dist/components/hello-world';

customElements.define('hello-world', HelloWorld);

打包的文件夹的内容与 www/build 文件内容相同,因为 stencil 也默认开启了 www 编译的 outTarget,所以也会编译出一份可以直接用于本地环境的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
json复制代码outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader',
},
{
type: 'dist-custom-elements',
customElementsExportBehavior: 'auto-define-custom-elements',
externalRuntime: false,
},
{
type: 'docs-readme',
},
{
type: 'www',
serviceWorker: null, // disable service workers
},
],

如果能对新技术开发组件库感兴趣,也欢迎加入stencil-component-ui,给个 star 鼓励一下 👏👏

本文转载自: 掘金

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

作用域你真的了解吗?

发表于 2024-04-26

作用域

作用域是指在程序中变量、函数等可被访问的范围。JavaScript中有三种,全局作用域,函数作用域,块级作用域。

1、全局作用域

全局作用域是指变量或函数在整个脚本或程序中都可以被访问和使用的范围。在 JavaScript 中,全局作用域可以分为以下两种情况:

a. 直接编写在<script>标签中的 JavaScript 代码,或在一个单独的 JavaScript 文件中,都属于全局作用域。

1
2
3
scss复制代码var a = 1;
function foo(){}
if(){}

其中变量a,函数foo,if语句都在全局作用域下。

b. 在全局作用域中,有一个全局对象window,可以直接使用。全局作用域在页面打开时创建,页面关闭时销毁。

c. 变量如果声明在全局作用域下,在任意位置都可以使用。

1
2
3
4
5
6
7
8
scss复制代码var a = 1;
console.log(a);//1 在全局域中可以使用
function foo(){
console.log(a);//1 在函数内部可以使用
}
if(a){} //在语句中可以使用
for(var i = 1;i<10;i++){} //在语句中声明的变量也属于全局变量
console.log(i)//10

d. 如果变量没有使用关键字,则为全局变量

1
2
3
4
5
scss复制代码function foo() {
x= 1;
}
foo();
console.log(x);//1

2、函数作用域

函数作用域是指在函数内部定义的变量和声明的作用范围。在函数作用域中,变量仅在函数内部可访问,在函数外部无法直接访问这些变量。当函数执行完毕后,函数内部的变量会被销毁。

1
2
3
4
javascript复制代码function foo(){
var a = 1;
}
console.log(a)//报错 a is not defined

3、块级作用域

块级作用域是指在JavaScript 中,用花括号 {} 括起来的一段代码所形成的作用域。在块级作用域中,变量的定义和使用只在该块内有效,超出该块范围后,这些变量将无法访问。

在 ES6 之前的 JavaScript 中,只有全局作用域和函数作用域,没有块级作用域的概念。这导致了一些问题,例如内层变量可能会覆盖外层变量,以及用来计数的循环变量可能会泄露为全局变量。

就像这样

1
2
3
4
5
6
7
8
ini复制代码function foo() {
var x = 2;
function bar() {
var x = 3;
}
bar();
console.log(x); // 3
}

ES6 中新增了块级作用域,使得 JavaScript 语言更加严谨和安全。块级作用域可以用于解决上述问题,并且提供了更好的代码组织和封装能力,就像这样。

1
2
3
4
5
6
7
8
9
10
ini复制代码function foo() {
var x = 2;
{
function bar() {
var x = 3;
}
}
bar();
console.log(x); // 2
}

当我们将bar声明成块级作用域时,x就不会发生泄露。既然聊到了作用域,就不得不聊一下var,let,和const了。

var let const

前置小知识

1、属性声明提升

1
2
ini复制代码console.log(a); // undefined
var a = 2;

v8引擎将变量声明提升到代码的开头,使得变量可以在整个代码中使用。变成这样:

1
2
3
ini复制代码var a;
console.log(a); // undefined
a = 2;

2、函数声明提升

1
2
3
4
scss复制代码foo(); //输出 "foo"
function foo() {
console.log("foo");
}

var的短处:

当我们在使用var时,有时会因为声明提升导致出现一些难以预料的结果。var允许重复声明同一个变量,这样会导致变量被覆盖和产生混乱,当全局声明时,还可能造成全局污染。所以ES6专门更新了两种新的声明变量的关键字,let和const。

let的作用和var基本差不多,但是let不存在声明提升问题,例如:

1
2
ini复制代码console.log(a); // 报错
let a = 2;

并且不能重复声明同一个变量例如:

1
2
ini复制代码let a = 1;
let a = 2;//报错

let会和{}形成块级作用域,只在其内部访问才有效,例如:

1
2
3
4
scss复制代码for(let i =0;i<10;i++){
console.log(i);//可以
}
console.log(i);//报错

const用于声明一个常量,也会和{}形成块级作用域,常量的意思是不能被修改的量。

1
2
ini复制代码const a = 1;
a = 2;//错误

总结

var let const相同点:

var let他们都是用来声明一个变量 ,const用来声明一个常量,在其作用域下三者都能被访问的到。

不同点:

var可以重复声明同一个变量,并且可以随意修改他的值,而且他存在声明提升。

let会和他所在{}之间组成块级作用域并只在其内部有效,不存在声明提升,并且不能重复声明同一个变量。

const也是只在其块级作用域下有效,并且声明时必须初始化,后续不能修改其值。

欺骗词法作用域

聊完三大作用域的之后,我们聊聊一些奇奇怪怪的函数,他可以欺骗词法作用域规则。

eval():在 JavaScript 中,eval() 函数用于执行一段字符串表示的 JavaScript 代码。它可以将字符串转换为相应的对象,并返回表达式的结果。eval() 将原本不属于当前作用域的变量声明提升到当前作用域,所以根据这个特性,我们可以写出这样的代码。

1
2
3
4
5
6
scss复制代码function foo(str,a) {
eval(str);//返回var b = 3; 语句
console.log(a,b);// 1 2 ?错误
}
var b = 2;
foo("var b = 3",1);// 1 3

with(obj){}:在 with() 语句中,obj 是要访问的对象。在 {} 内部,可以直接使用 obj 的属性和方法,而无需使用点号操作符。with() 用于修改一个对象的属性值,但如果属性不存在,则会创建新的属性,泄露到全局作用域。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码var  obj = {
a: 3
}
with(obj){//正常使用,obj里面有a属性
a: 4
}
function foo(obj){
with (obj) {//不正常使用,obj里面没有b属性
b = 2;
}
}
foo(obj);
console.log(obj); // {a: 3, b: 2}

本文转载自: 掘金

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

Rust入门掌握这50个写法就够了

发表于 2024-04-26

Rust,被誉为系统编程语言的新星,以其内存安全和高效的并发控制吸引了大量开发者的关注。对于初学者来说,要想快速上手并灵活运用Rust,掌握一些基本的写法和技巧至关重要。以下是Rust入门的50个关键写法的前十个,帮助你轻松迈出Rust编程的第一步。

各位美女帅哥只要老实把下面的代码都敲一遍,以后就不要再说没听过 Rust 了吧~

image.png

1. Hello, World!

这是学习任何新编程语言的起点。在Rust中,你可以这样写:

1
2
3
rust复制代码fn main() {
println!("Hello, World!");
}

2. 变量声明

在Rust中,你可以使用let关键字来声明变量。默认情况下,这些变量是不可变的。

1
rust复制代码let x = 5; // 不可变变量

3. 可变变量

如果你需要一个可以修改的变量,可以在let之后添加mut关键字。

1
2
rust复制代码let mut y = 10; // 可变变量
y = 15; // 修改变量值

4. 数据类型

Rust有多种内置的数据类型,如整数、浮点数、布尔值等。

1
2
3
rust复制代码let i32_num: i32 = -12345; // 32位有符号整数
let f64_num: f64 = 3.14; // 64位浮点数
let is_true: bool = true; // 布尔值

5. 元组

元组是一种可以包含多种不同类型元素的复合数据类型。

1
rust复制代码let tup = (500, "hello", 4.5); // 元组包含整数、字符串和浮点数

6. 数组

数组是相同类型元素的连续集合。

1
rust复制代码let arr = [1, 2, 3, 4, 5]; // 包含5个整数的数组

7. 切片

切片是对数组的一部分的引用。

1
rust复制代码let slice = &arr[1..3]; // 引用数组的第2和第3个元素(索引从0开始)

8. 控制流 - 条件语句

使用if、else if和else来控制程序的流程。

1
2
3
4
5
6
7
8
rust复制代码let num = 3;
if num > 5 {
println!("大于5");
} else if num == 5 {
println!("等于5");
} else {
println!("小于或等于5");
}

9. 控制流 - 循环

使用for循环来遍历集合中的元素。

1
2
3
rust复制代码for i in 0..5 { // 遍历0到4的整数序列
println!("{}", i);
}

10. 函数定义

在Rust中,你可以定义自己的函数来执行特定的任务。

1
2
3
4
rust复制代码fn greet(name: &str) {
println!("Hello, {}!", name); // 打印问候语
}
greet("Rustacean"); // 调用函数并传递参数

11. 函数返回值

在Rust中,你可以定义有返回值的函数。

1
2
3
4
rust复制代码fn add(a: i32, b: i32) -> i32 {
a + b // 返回两个整数的和
}
let sum = add(5, 7); // 调用函数并将结果存储在变量sum中

12. 引用和借用

Rust通过引用(&)来借用变量的值,而不直接获取其所有权。

1
2
3
4
5
rust复制代码fn print_value(value: &i32) {
println!("Value: {}", value);
}
let x = 10;
print_value(&x); // 通过引用传递变量x

13. 可变引用

可变引用允许你修改引用的值。

1
2
3
4
5
rust复制代码fn modify_value(value: &mut i32) {
*value += 1; // 修改引用的值
}
let mut y = 20;
modify_value(&mut y); // 通过可变引用传递变量y

14. 结构体(Structs)

结构体允许你定义一个复合数据类型,可以包含多个字段。

1
2
3
4
5
rust复制代码struct Person {
name: String,
age: u8,
}
let person = Person { name: "Alice".to_string(), age: 30 };

15. 枚举(Enums)

枚举类型用于定义一组命名的值。

1
2
3
4
5
6
rust复制代码enum Color {
Red,
Green,
Blue,
}
let my_color = Color::Red; // 使用枚举的一个变体

16. 模式匹配(Match)

match表达式允许你根据枚举或其他类型的值进行分支处理。

1
2
3
4
5
rust复制代码match my_color {
Color::Red => println!("It's red!"),
Color::Green => println!("It's green!"),
Color::Blue => println!("It's blue!"),
}

17. Option枚举

Option是一个表示可能有值或没有值的枚举。

1
2
3
4
5
rust复制代码enum Option<T> {
Some(T),
None,
}
let optional_value: Option<i32> = Some(42); // 或者None表示没有值

18. Result类型

Result类型用于处理可能出错的操作,它包含Ok或Err两个变体。

1
2
3
4
5
6
7
8
9
10
11
rust复制代码enum Result<T, E> {
Ok(T),
Err(E),
}
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("Division by zero!")
} else {
Ok(a / b)
}
}

19. 泛型

泛型允许你编写适用于多种类型的函数或结构体。

1
2
3
4
rust复制代码fn largest<T: Ord>(a: T, b: T) -> T {
if a > b { a } else { b }
}
let largest_i32 = largest(5i32, 10i32); // 泛型函数用于i32类型

20. 闭包(Closures)和迭代器(Iterators)

闭包是可以捕获环境的匿名函数,而迭代器提供了一种遍历集合元素的方法。

1
2
rust复制代码let nums = vec![1, 2, 3, 4, 5];
let squares = nums.iter().map(|x| x * x).collect::<Vec<_>>(); // 使用闭包和迭代器计算平方并收集结果

21. 闭包作为函数参数

在Rust中,你可以将闭包作为函数的参数传递。

1
2
3
4
rust复制代码fn apply<F>(f: F, x: i32) -> i32 where F: Fn(i32) -> i32 {
f(x)
}
let result = apply(|x| x * 2, 5); // 传递闭包,将数字乘以2

22. 使用标准库中的集合

Rust的标准库提供了多种集合类型,如Vec<T>、HashMap<K, V>等。

1
2
3
rust复制代码let mut vec = Vec::new();
vec.push(1);
vec.push(2);

23. 字符串操作

在Rust中,字符串是不可变的,但你可以通过String类型来创建和修改字符串。

1
2
rust复制代码let mut s = String::from("Hello");
s.push_str(", World!"); // 修改字符串

24. 使用match进行错误处理

match可以用于处理Result类型的错误。

1
2
3
4
5
rust复制代码let result: Result<i32, &str> = Ok(42); // 或者Err("Error message")
match result {
Ok(value) => println!("Value: {}", value),
Err(e) => println!("Error: {}", e),
}

25. 生命周期

Rust中的生命周期参数用于指定引用的有效范围,确保数据竞争和悬垂引用的安全。

1
2
3
4
5
6
7
rust复制代码fn print_longest<'a>(x: &'a str, y: &'a str) {
if x.len() > y.len() {
println!("{} is longer", x);
} else {
println!("{} is longer", y);
}
}

26. 使用use语句导入模块

你可以使用use语句来导入模块,以便在代码中使用它们。

1
2
3
rust复制代码use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("key", "value");

27. 实现特征(Traits)

在Rust中,你可以通过实现特征来为类型添加行为。

1
2
3
4
5
6
7
8
9
10
11
rust复制代码trait Greeting {
fn greet(&self);
}
struct Person;
impl Greeting for Person {
fn greet(&self) {
println!("Hello!");
}
}
let p = Person;
p.greet(); // 调用实现的特征方法

28. 宏(Macros)

Rust支持宏,它们允许你在编译时生成代码。

1
2
3
4
5
6
rust复制代码macro_rules! print_hello {
() => {
println!("Hello from macro!");
};
}
print_hello!(); // 调用宏

29. 线程安全

Rust提供了多种同步原语,如Mutex和Arc,以确保线程安全。

1
2
3
rust复制代码use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
// 在多线程环境中安全地修改counter的值...

30. 测试

Rust内置了强大的测试框架,你可以使用#[test]属性来编写测试函数。

1
2
3
4
rust复制代码#[test]
fn test_addition() {
assert_eq!(1 + 1, 2); // 断言1+1等于2,用于测试
}

31. 条件编译

Rust支持条件编译,允许你根据某些条件来编译特定的代码块。

1
2
3
4
5
6
7
rust复制代码#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

32. 文档注释

Rust使用特定的注释语法来生成文档。

1
2
3
rust复制代码/// 这是一个文档注释,用于描述下面的函数。
/// # 示例
///

/// println!(“Hello, world!”);
///

1
2
3
pub fn hello_world() {
println!("Hello, world!");
}

33. 外部依赖管理

使用Cargo.toml文件来管理项目的外部依赖。

1
2
toml复制代码[dependencies]
rand = "0.8"

34. 模块系统

Rust有一个强大的模块系统,允许你将代码组织成多个文件和模块。

1
2
3
4
5
6
7
rust复制代码// lib.rs
pub mod my_module;

// my_module.rs
pub fn my_function() {
println!("This is my function in my_module");
}

35. 范围

Rust支持使用范围来表示一系列的数值。

1
2
3
rust复制代码for i in 1..=5 {
println!("{}", i); // 打印从1到5的数字
}

36. 所有权和借用

理解Rust的所有权系统和借用检查器是编写安全Rust代码的关键。

1
2
3
4
5
6
7
rust复制代码fn take_ownership(v: Vec<i32>) {
// ...
}

fn borrow_vector(v: &Vec<i32>) {
// ...
}

37. 默认参数和可变参数

Rust函数支持默认参数和可变参数列表。

1
2
3
4
5
6
7
rust复制代码fn function_with_default(param: i32 = 10) {
// ...
}

fn function_with_variadic_params(params: &[&str]) {
// ...
}

38. 静态生命周期

在Rust中,你可以使用静态生命周期来指定某个引用的生命周期与整个程序的执行时间一样长。

1
rust复制代码static MY_CONST: &'static str = "Hello, world!";

39. 内部可变性和单元化

Rust提供了Cell和RefCell等类型,允许你在不违反Rust所有权规则的情况下实现内部可变性。

1
2
3
rust复制代码use std::cell::Cell;
let x = Cell::new(1);
x.set(x.get() + 1); // 内部可变性,不需要mut关键字

40. 不安全代码

在某些情况下,你可能需要编写不安全代码来执行底层操作。Rust提供了unsafe关键字来处理这些情况。

1
2
3
4
5
6
7
rust复制代码unsafe fn dangerous_function() {
// 执行不安全操作...
}

unsafe {
dangerous_function(); // 必须在unsafe块中调用不安全函数
}

41. 泛型

Rust支持泛型编程,允许你编写灵活且可重用的代码。

1
2
3
4
5
6
7
8
9
rust复制代码fn largest<T: Ord>(list: &[T]) -> T {
let mut largest = &list[0];
for item in list {
if item > *largest {
largest = item;
}
}
*largest
}

42. 特性(Trait)对象

特性对象允许你使用动态分发来处理具有相同特性的多种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rust复制代码trait Animal {
fn make_sound(&self);
}

struct Dog;
impl Animal for Dog {
fn make_sound(&self) {
println!("Woof!");
}
}

fn animal_sound(animal: &dyn Animal) {
animal.make_sound();
}

43. 关联函数和关联类型

在Rust中,你可以在特性中定义关联函数和关联类型。

1
2
3
4
rust复制代码trait Graph {
type Node;
fn add_node(&mut self, node: Self::Node);
}

44. 从错误中恢复

使用?操作符可以方便地处理可能返回Err的结果。

1
2
3
4
rust复制代码fn read_file(path: &str) -> Result<String, std::io::Error> {
let content = std::fs::read_to_string(path)?; // 错误处理被简化
Ok(content)
}

45. 自定义错误类型

你可以定义自己的错误类型来实现更复杂的错误处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
rust复制代码enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}

impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> MyError {
MyError::Io(err)
}
}
// 类似地实现其他错误类型的转换...

46. 使用Option和Result进行错误处理

Rust使用Option<T>和Result<T, E>来表示可能失败的操作。

1
2
3
4
5
6
7
rust复制代码fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
if b == 0.0 {
Err("Cannot divide by zero")
} else {
Ok(a / b)
}
}

47. 迭代器

Rust的迭代器提供了一种处理集合中元素的高效方式。

1
2
3
4
rust复制代码let vec = vec![1, 2, 3, 4, 5];
for i in vec.iter() {
println!("{}", i);
}

48. 并发与并行

Rust提供了强大的并发和并行支持,如线程、消息传递和异步I/O。

1
2
3
4
5
rust复制代码use std::thread;
let handle = thread::spawn(|| {
println!("Running in a new thread!");
});
handle.join().unwrap();

49. 结构体更新语法

Rust支持结构体更新语法,可以方便地创建结构体的新实例,同时保留部分字段的值。

1
2
3
rust复制代码struct Point { x: i32, y: i32 }
let point = Point { x: 1, y: 2 };
let new_point = Point { x: 5, ..point }; // y字段保持不变,x字段更新为5

50. 宏的高级用法

Rust的宏系统非常强大,支持声明性宏和过程性宏,可以实现复杂的代码生成和抽象。

1
2
3
4
5
6
rust复制代码macro_rules! print_vec {
($($x:expr),*) => {
println!("Vector: [{:?}]", vec![$($x),*]);
}
}
print_vec!(1, 2, 3); // 输出:Vector: [1, 2, 3]

本文转载自: 掘金

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

面试官:聊一聊前端性能优化 前言 一、减少 http 请求

发表于 2024-04-26

前言

性能优化总是被面试官问,每次都只能答出那么几个,其实大家都清楚性能优化手段非常多,只要是能让用户体验更好的手段都可以称之为性能优化,本期我就总结下我所知道的性能优化手段,欢迎各位补充~

一、减少 http 请求

比如一个项目中,登录之后需要展示登录时的昵称,这个昵称就不需要重新发一次接口请求去拿到了,可以通过路由传参拿到

为何说减少 http 请求是一种性能优化?因为一个 http 的请求中间的过程非常多,常见的 输入url后到页面展示的过程 大家都清楚,步骤很多

这个过程还不清楚的,可以跳转这两篇文章,都写得很详细

输入url到页面渲染前半段

输入url到页面渲染后半段:回流,重绘,优化【一次性带你搞明白】

二、使用 http2.0

当然,现在基本上都是用 http2.0 这个版本的 http 了,那为什么使用 http2.0 会性能更优?

http2.0 更优是相对于此前的版本,此前 1.1 版本因为有多个 keep-alive 长连接导致了 http 的队头阻塞问题,同时多个长连接也带来了 带宽 用不满的问题

2.0 针对 1.1 的这些问题,在一个域名下,多个 tcp 长连接合并成了一个,这就是多路复用,并且 2.0 采用了二进制分帧层,将每个请求分成了一帧一帧的数据进行传输并打上标记,可以给特定的数据帧加急处理

同样,这个问题我在文章 聊聊http发展史 中讲得非常详细了

三、使用 SSR 服务端渲染

SSR 可以让首屏加载更快,带来更好的 SEO

前端基本上现在都是 SPA 单页应用,单页应用的缺陷就是首屏加载很慢。使用 SSR 服务端渲染可以带来更好的 SEO ,SEO 就是搜索引擎优化,搜索引擎就是爬虫,可以更好的爬数据

感觉掘金在 seo 上比不过 csdn ,每次搜文章, csdn 永远在前面😭

其实早期的前后端不分离开发方式就是服务端渲染,就是 jsp ,后端直接向前端返回一个 html 文件,既然如此为何如今又要搞一个分离式开发方式,这是为了开发效率,开发效率的优点受益比服务端渲染高,不分离开发方式前端工作量太少了,效率很低

下面可以看下 vue 是如何做 ssr 的

vue - ssr

其实 vue 的 ssr 在官网上就有

服务端渲染 (SSR) | Vue.js (vuejs.org)

1.png

我们可以在后端新建一个文件运行这段代码试试

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码// 此文件运行在 Node.js 服务器上
import { createSSRApp } from 'vue'
// Vue 的服务端渲染 API 位于 `vue/server-renderer` 路径下
import { renderToString } from 'vue/server-renderer'

const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})

renderToString(app).then((html) => {
console.log(html)
})

打印输出:<button>1</button>

createSSRApp 可以帮我们创建一个组件,这样我们就可以不需要 vue 文件创建 vue 组件了,这个组件里面可以创建数据源,模板等,然后借助 renderToString 帮我们把模板当成字符串渲染成 html

接下来借助 express 来实现一个后端 demo

express 相比 koa , express 的路由无需另外安装, koa 其实是基于 express 打造的

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
xml复制代码import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'

const server = express() // 不需要new

server.get('/', (req, res) => {
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})

renderToString(app).then((html) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
})

server.listen(3000, () => {
console.log('ready')
})

server.get 就是路由,里面的逻辑是创建一个 vue 组件,然后渲染成字符串,返回给浏览器,这就是 jsp , jsp 就是 ssr ,现在就可以访问 localhost:3000 看到一个 html 页面了,里面是一个按钮

这个按钮目前点击是不会生效的,因为浏览器端没有 vue 的请求,还需要将 app 挂载 amount 到 #app 上,我们还需要在项目根目录下新建一个 app.js ,如下,目的是在服务端和客户端之间共享

1
2
3
4
5
6
7
8
9
javascript复制代码// app.js (在服务器和客户端之间共享)
import { createSSRApp } from 'vue'

export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
}

根目录下新建一个 client.js

1
2
3
4
javascript复制代码// client.js
import { createApp } from './app.js'

createApp().mount('#app')

这么做就是把 server.js 中定义组件的代码搬出去写了

在 server.js 的 html 中引入 vue 源码,再引入 client.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
xml复制代码import express from 'express'
import { renderToString } from 'vue/server-renderer'
import { createApp } from './app.js'

const server = express()

server.get('/', (req, res) => {
const app = createApp()

renderToString(app).then((html) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module" src="/client.js"></script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`);
});

app.mount('#app')
})

server.use(express.static('.'))

server.listen(3000, () => {
console.log('ready')
})

就跟着 vue 官方文档一步一步就实现了一个 ssr 的 demo ,现在访问 localhost:3000 点击按钮可以实现累加的效果,也就是是实现了响应式

1.gif

然后你可以去查看页面源代码, button 可以看到,若是 vue 的项目,你是看不到任何标签的,这样搜索引擎这样的爬虫就可以爬到你的数据,假设这是个买衣服的网站,你就可以尽可能地把信息展现到 html 中来

2.png

像是这样的实现,其实我们就可以把这个按钮组件写成一个登录组件,然后点击后跳转到另一个页面,那个页面又可以写其他端口,这样就是解决了首屏加载过慢的问题

公司做的产品只要是给用户用得,基本上都会去做 ssr

四、合理使用 cdn

CDN(Content Delivery Network)全称为内容分发网络

像是 cdn 就涉及到服务器分布的问题了,谷歌的服务器放在深海中的,为了方便散热🤣。像是我们访问国外的网站,就算有魔法加持也会比较慢,就是因为一个网络请求跨越的距离太远了。像是国内,假设阿里的服务器在杭州,西藏的朋友访问淘宝就会有点慢,因此,这些大厂基本上都会再设置云服务器方便偏远地区访问。

合理使用 cdn ,为何要说合理使用,就是因为服务器资源太贵了,不可能每个城市都给你搞一个云服务器

合理使用 cdn 是运维部门该干的事情,和前后端无关

当然不排除某些公司或者小厂,为了降本增效,运维工作同时交给了后端来干。另外前后端会有一个测试服务器,可以随便测自己的项目。

五、将 css 放在文件头部,将 js 放在文件底部

css 放在 body 当中其实也可以,这么带来的效果确实 html 优先出现在用户面前,但是没有样式,降低用户体验!因此 css 放到文件头部非常重要

有时候页面炸了的时候,没有样式,全是文字和🔗,非常影响用户体验,这种效果还不如给用户看白屏

当然 js 也可以写在上面,得看情况,若 js 在页面加载中需要用到,就需要放到前面,这种情况很少。 js 引擎线程和浏览器渲染线程是互斥的,因为 js 也可以操作 dom 结构,可能会冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>

</style>
<script>
// 放在这里意味着页面加载过程中需要用到这个js,情况会比较少
</script>
</head>
<body>
<div id="app">hello</div>

<script src="***">
// 这个适合cdn引入,比如引入vue
</script>
<script>

</script>
</body>
</html>

当然,你也可以用 async 和 defer 解决 js 阻塞问题

async & defer

二者的共同点均是异步加载 js

js 是有两个步骤的,先是加载,后是执行

defer 是延迟的意思,就是让 js 延迟执行,等 html 加载完毕再去执行,因此 defer 的效果就是把 js 放到文件底部

async 不会延迟执行 js ,它的效果是 js 照常加载,加载完毕就需要立即执行 js ,期间 html 就不能执行了

因此 defer 看样子更好说话, js 加载完毕等 html 执行完毕再去执行 js ,而 async 就难说话一点,加载完 js 后立即执行 js 。 defer 带来的效果其实就是将 js 放到文件底部, async 的效果就是放在文件头部

六、使用精灵图(雪碧图)

精灵图(雪碧图)就是一种将多个小图标或图像组合到单个图像文件中的技术

有个很经典的🌰就是豆瓣官网右上角一排的文字图片

3.png

既然是一张图那如何实现可以点到每个 li 的呢

4.png

img1.doubanio.com/f/sns/82c3a…

这张图并不是直接完整的放到页面中去,否则无法实现各点各的,这些个 li 其实都用到了这张图,只是每个 li 都会单独去调整这张图的位置,比如读书用到了这张图,只把读书二字展现了出来

这么做的意义就是减少 http 的请求次数,若这 8 个栏目都用自己单独的图片,就意味着要发 8 个 http 请求,用雪碧图就一次请求多处使用

ui 设计师若是不懂这个优化,我们前端可以跟她提这个需求,做成一张图

七、善用 http 缓存:强缓存 & 协商缓存

关于这两个缓存,此前专门出过一期文章详细讲解过:深入浅出【强缓存 & 协商缓存】,下面这段话不太理解可以详细看下这篇

二者都是后端控制的东西,强缓存是响应头添加 'Cache-Control': 'max-age=xxx' 字段, max-age 是过期时间,强缓存后无法缓存输入 url 后的 get 请求,想要缓存这个请求需要靠协商缓存来实现,协商缓存的实现是在强缓存的基础上添加一个 'Last-Modified': stats.mtimeMs 或者 etag 字段,若检查到前端返回的 If-Modified-Since 时间一致,后端就返回 304 状态码给前端,浏览器就从缓存中读取静态资源

八、压缩文件

压缩文件前后端都可以做,前端压缩就是打包,将代码打包成密密麻麻的样子,剔除掉无用的换行,空格等,这样文件大小就可以变小,文件变小下载速度就会快

后端的压缩就是向前端返回静态资源进行压缩,比如图片转格式等等

九、懒加载

懒加载的核心思想就是图片不在可视区范围中不加载,滚到可视区范围内才给予加载。这么做就是没必要页面初次加载所有图片

懒加载的实现早期就出过一篇文章:手把手教你实现js懒加载

懒加载的实现需要获取到可视区范围的高度,以及每张图片的高度,监听用户滚动的过程中图片是否进入范围内,进入时才赋值 src , src 只要有值就一定会发送 http 请求,此前存放 src 的属性可以任意取名,当然一般我们取名为 data- 前缀,比如下面这样

<img src="" data-src="****">

IntersectionObserver

IntersectionObserver - Web API | MDN (mozilla.org)

IntersectionObserver 这个 api 也可以用于实现懒加载,这个方法可以用来监听目标元素和祖先元素是否相交,交叉多少都可以实现

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
ini复制代码<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.img-item {
display: block;
height: 300px;
margin-top: 50px;
}
</style>

</head>

<body>

<body>
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.5053ecdbef05fa7726aa489d27b52e40&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.c5db2c88af1a76f18d0efe02fcded91d&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.c5129de8701c4a933d92cd6bf832b233&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.afe7f6448d6eba0055cd8ce9ac9fdf62&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.e168b9c5da30772083104ed0f4ef0ecf&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.8025ce5a977b3826589022cede69e110&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.a58ae29e32e20a27d498eed19528ee3c&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.2049b527600b31b2cd863a380be59848&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.9f51912b8b6c19a9891b380ad526db85&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">
<img class='img-item' src=""
data-original="https://th.bing.com/th?id=ORMS.1b6375ea147b5704f9d073a326e1fc2a&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
alt="">

<script>
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.original
entry.target.removeAttribute('data-original')
io.unobserve(entry.target)
}
})
})
const imgs = document.querySelectorAll('img[data-original]')
imgs.forEach(item => {
io.observe(item)
})
</script>
</body>
</body>

</html>

当观察到一个图片元素进入视口时,触发 IntersectionObserver 的回调函数。在回调函数中,首先检查触发事件的元素是否进入了视口entry.isIntersecting,如果是,则将该元素的 src 属性设置为 data-original 属性的值,即加载图片。然后,移除 data-original 属性,以确保图片只加载一次。最后,调用 io.unobserve(entry.target) 停止对该元素的观察,避免不必要的性能消耗。

十、尽量用 css ,字体来代表图片

有些背景图就是一个渐变色,这个时候就不需要放图片了, css 能够实现就用 css

反正能用 css 就用 css ,能不用图片就不用图片。当然对于程序员来讲,谁不想早点下班呢,用 css 就意味着自己多点工作,一张图片多简单

十一、使用 webp 格式的图片

webp 格式的图片是谷歌推出的,这种格式的图像压缩算法能力要优于传统的 jpg , png 等格式,在相同图片质量的情况下,空间大小会优化 30% 左右的样子

关于图片的性能优化就是小图用雪碧图,大图用 webp 格式

十二、webpack:tree-shaking | 打包文件名 + hash

webpack 一般来说都是配置好的,默认配置的中规中矩,不算差也不算好,我们可以在此基础上增加一个 tree-shaking , tree-shaking 的作用就是帮我们把项目中无用的代码给找出来,比如我们调试用的 console.log ,其实 console.log 对浏览器的开销还是蛮大的

以及项目中打包后的文件名被拼接了一个哈西值,这样就能剔除上一次打包的内容,或者做一个动态的组件引入

像是打包文件生成的 dist 目录,里面的文件默认就会有 hash 值, vite 默认会有这个操作,但是其他的打包工具就不一定会有

在实际开发中,打包工具不是由自己决定的,公司会统一好,不可能别人用 webpack ,你一个人用 vite

十三、尽量减少回流重绘

这个问题此前也详细讲过:输入url到页面渲染后半段:回流,重绘,优化【一次性带你搞明白】

回流(重排)就是计算布局,重绘就是给页面上色

尽量不用 js 去直接修改 css

我们可以看下下面两种情况

1
2
3
4
5
6
7
arduino复制代码// 案例一
box.style.width = '200px'
// 案例二
.more{
width: '200px'
}
box.classList.add('more')

第一种方案就是直接修改 css ,第二种是添加类名。方案一会导致回流,方案二不会导致回流,因为添加类名并没有修改几何属性,它是间接交给了 css ,上面就说了, css 一般放在文件顶部,提前加载好了,因此浏览器已经准备好了,做好了回流这个计算,就是等你把类名加上去

display: none

当涉及需要对 dom 进行一系列的操作时,可以先利用 display: none 将 dom 脱离文档流,再修改 display: block 带回文档流

fragment

这是文档虚拟片段, js 中被当成 dom ,但是在 css 中又不会当成真实的 dom 加载出来,涉及多个操作 dom 时,可以对 fragment 操作,最后把 fragment 挂到真实 dom 上

clone

深克隆节点,对副本进行操作,最后将副本插入到文档中进行回流,跟前面的方法原理是一致的,具体语法上面晾出的文章都详细讲过

十四、事件委托

同样,事件委托之间也单独拿出来讲过 js事件委托 ,这里就不大费周章重复去讲

事件委托的机制是借助冒泡机制,把原本需要批量操作子组件的操作代理到一个父组件上

十五、if-else VS switch

当涉及到多个判断条件时,我们可以用 switch 去写判断语句

比如下面两个案例

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
ini复制代码// 检查成绩等级(if-else)
function checkGrade(score) {
let grade;
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 70) {
grade = 'C';
} else if (score >= 60) {
grade = 'D';
} else {
grade = 'F';
}
return grade;
}

// 检查成绩等级(使用 switch)
function checkGradeSwitch(score) {
let grade;
switch (true) {
case (score >= 90):
grade = 'A';
break;
case (score >= 80):
grade = 'B';
break;
case (score >= 70):
grade = 'C';
break;
case (score >= 60):
grade = 'D';
break;
default:
grade = 'F';
}
return grade;
}

if-else 有个判断顺序的,一定是从上往下走逐个走到目标,每次都判断一下,浪费性能。而 switch 不然, switch 是直接命中目标,只有一次判断

if-else 会更加灵活,但是性能又没有 switch 来得好

十六、requestAnimationFrame避免页面卡顿

下面用 js 实现一个效果:小球滚动

2.gif

用一个定时器 setInterval ,让 left 值每间隔 16ms 就去自增

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
xml复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
width: 200px;
height: 200px;
border-radius: 50%;
background-color: red;
position: relative;
}
</style>
</head>
<body>
<div class="box"></div>

<script>
let box = document.querySelector('.box')

let left = 0
setInterval(() => {
left++
box.style.left = left + 'px'
}, 16)
</script>
</body>
</html>

上面展示的动画其实是有卡顿的,当然,光看这个效果肯定大部分是 gif 的原因,为何 16ms 也会有卡顿?屏幕设计成 60Hz 不就是因为人眼最低的时间辨别时间就是 16.7ms 么,确实,但是 16 和 16.7 就是不匹配,这个小球从第 A 位置移到 B 位置确实不会卡,但是下一次开始,移动的时候页面刷新一下,此时才会带来卡顿

这个时候我们就去采用 requestAnimationFrame ,这也是个定时器,这个定时器的执行时间不需要我们设置,它是根据屏幕刷新率来定的, 60Hz 就是 16.7ms 执行一次。

关于宏微任务,这个方法有歧义,暂且不讨论。总之这个方法的设计就是在每一帧渲染完准备去显示下一帧去执行的,它就是个动画帧

1
2
3
4
5
6
7
8
9
10
ini复制代码let box = document.querySelector('.box')

let left = 0

function fn () {
left++
box.style.left = left + 'px'
window.requestAnimationFrame(fn)
}
window.requestAnimationFrame(fn)

采用这个方法实现的动画将会非常丝滑,这里用 gif 不便展示其效果

十七、Web Worker 开启多线程

给 js 开多线程是这个话题中非常加分的点

js 默认情况下是单线程,但是 v8 引擎执行 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
xml复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<img src="" alt="" id="previewImage">
<input type="file" id="imageInput">

<script>
const previewImage = document.getElementById('previewImage');
const imageInput = document.getElementById('imageInput')

imageInput.addEventListener('change', (e) => {
const file = e.target.files[0]
if (file) {
const render = new FileReader() // js中专门用于读取上传图像文件的
render.readAsArrayBuffer(file) // 将文件读成二进制流
render.onload = (e) => {
const imageData = e.target.result // 二进制流
createImageBitmap(new Blob([imageData])).then((imageBitmap) => { // 将buffer流转成真实的数组,且是异步方法
console.log(imageBitmap);
// 其他逻辑:比如你想要实现水印

})
}
}
})
</script>
</body>
</html>

代码先写到这里,这里代码还没有实现黑白效果。举个例子,以预览为例,预览对于用户而言,其实慢点是没有关系的,也就是说这个功能不应该阻塞线程,因此像是这样的逻辑,如果你正常往下实现,是会占用这个线程的。

或者说,假设你把像是预览图片的代码写在 js 主线程中,若预览需要 3s ,此时下面假设还有其他按钮,这 3s 内,你就无法进行其他操作了

接下来就把像是预览这样的操作交给第二个线程来实现,这里实现的是变黑白

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码render.onload = (e) => {
const imageData = e.target.result // 二进制流

// 开一个新的线程
const worker = new Worker('worker.js')

createImageBitmap(new Blob([imageData])).then((imageBitmap) => { // 将buffer流转成真实的数组,且是异步方法
worker.postMessage({ imageBitmap }, [imageBitmap]) // 将目前的图片数据传给第二个线程

worker.onmessage = () => { // 从第二个线程中获取数据

}
})
}

postMessge 和 onmessge 上一次见过,可以用管道通信的方式实现深拷贝 让面试官眼前一亮的深拷贝

这两个方法是异步的,这个异步是针对主线程而言,但是里面的耗时是交给了另一个线程

接下来同级目录下新建一个文件 worker.js ,我们在这里面实现其他逻辑

我们可以试着在 worker.js 中打印 this 或者 self 看看指向, self 就是 this ,我们在这身上监听 message 事件

1
2
3
4
javascript复制代码// console.log(self);
self.addEventListener('message', (e) => {
console.log(e);
})

我们打印 e ,你会发现主线程中的 imageBitmap 在里面,这就是因为 self 监听到了主线程 postMessage 发布过来的 imageBitmap

5.png

1
2
3
4
5
6
7
javascript复制代码// console.log(self);
self.addEventListener('message', (e) => {
console.log(e.data.imageBitmap); // 获取到了主线程的图片资源
const imageBitmap = e.data.imageBitmap;

createImageBitmap(processImage(imageBitmap)) // processImage就是取到进程中的资源处理成canvas需要的格式
})

processImage 就是将参数转成 Uint8Array 二进制数组

es8 新推出了一个 Uint8Array 就是专门用于存放数据流的格式

接下来实现 processImage 这个函数,这个函数需要做的就是将从主线程读到的文件流变成黑白,通过 canvas 实现,最终 worker.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
arduino复制代码// console.log(self);
self.addEventListener('message', (e) => {
// console.log(e.data.imageBitmap); // 获取到了主线程的图片资源
const imageBitmap = e.data.imageBitmap;

createImageBitmap(processImage(imageBitmap)).then(processImageBitmap => { // processImage就是取到进程中的资源处理成canvas需要的格式
console.log(processImageBitmap);
self.postMessage({processImageBitmap}, [processImageBitmap]) // 触发主线程的onmessage
})
})

function processImage(inputImageBitmap) {
const canvas = new OffscreenCanvas(inputImageBitmap.width, inputImageBitmap.height) // 实例化一个画布

const ctx = canvas.getContext('2d')
ctx.drawImage(inputImageBitmap, 0, 0)

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const inputData = new Uint8Array(imageData.data.buffer)
// console.log(inputData); // 每四个一组,一组表示一个16进制像素点
const outputData = new Uint8Array(inputData.length) // 创建一个大小相同的容器

// 变黑白
for (let i = 0; i < inputData.length; i+=4) {
const avg = (inputData[i] + inputData[i + 1] + inputData[i + 2]) / 3
outputData[i] = avg // 取平均值,颜色就不会很深或者很浅
outputData[i + 1] = avg
outputData[i + 2] = avg
outputData[i + 3] = inputData[i + 3] // 保留颜色饱和度
}

return new ImageData(new Uint8ClampedArray(outputData.buffer), canvas.width, canvas.height)
}

这些过程中用上 canvas 纯粹是因为 canvas 身上有个方法 toDataURL 可以把 image 转成真实的 url 供你放到 img 上展示,这个 url 其实就是 base64 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码createImageBitmap(new Blob([imageData])).then((imageBitmap) => { // 将buffer流转成真实的数组,且是异步方法
worker.postMessage({ imageBitmap }, [imageBitmap]) // 将目前的图片数据传给第二个线程

worker.onmessage = (e) => { // 从第二个线程中获取数据
console.log(e.data.processImageBitmap);
const processImageBitmap = e.data.processImageBitmap
const previewCanvas = document.createElement('canvas')
previewCanvas.width = processImageBitmap.width
previewCanvas.height = processImageBitmap.height

const previewCtx = previewCanvas.getContext('2d')
previewCtx.drawImage(processImageBitmap, 0, 0) // 往previewCtx上绘制
console.log(previewCanvas.toDataURL());
}
})

我们可以打印看下这个 url,此时的图片已经 worker.js 处理回来了,黑白格式

6.png

最后把这个 base64 的 url 放入到 src 中即可

1
ini复制代码previewImage.src = previewCanvas.toDataURL()

7.png

面试官:如何将一个图片转成 base64 格式

顺带提这个面试题,看到之前有人被问到过

用 canvas 就可以实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码let img = new Image(); // 创建一个新的 Image 对象

img.src = 'image.jpg'; // 设置图片的源

img.onload = function() { // 当图片加载完成后执行操作
var canvas = document.createElement('canvas'); // 创建一个 Canvas 元素
var ctx = canvas.getContext('2d');

// 设置 Canvas 元素的宽高与图片一致
canvas.width = img.width;
canvas.height = img.height;

// 在 Canvas 上绘制图片
ctx.drawImage(img, 0, 0);

// 将 Canvas 内容转换为 base64 格式
var base64Data = canvas.toDataURL('image/jpeg');

// base64Data 就是转换后的 base64 格式的图片数据
console.log(base64Data);
};

除了给图片变换其他效果需要开启多线程外,其余还有很多情景,比如一个 for 循环需要循环一万次产生了一个阻塞,我们就可以把这个任务交给另一个线程来实现

十八、 css 选择器复杂性要低

我们看下面这案例

1
2
3
less复制代码#app .text p{

}

浏览器读取 css 是从右往左读,因此上面的代码,就是先读到 p 标签,再是 text 类名,最后是 id

这样就是先去找到所有的 p 标签,然后再去找带有 text 类名的部分,最后再去找还得带有 app 这个 id 的部分,这个工作量将会非常大,因此可以选择设置一个唯一的类名进行优化

1
2
3
sql复制代码.only{

}

因此,尽量给每个标签打上类名,不要去通过父容器

当然,这样就需要耗费精力去取类名了

十九、尽量使用弹性布局

flexbox 性能会比之前的布局好, flexbox 之前的布局就有浮动,定位, flexbox 的性能是它们的四倍左右

最后

其实当被问到这个问题的时候,你内心应该是开心的,这个问题答案太多了,面试时间是有限的,你需要把握主动权

性能优化比较发散,可能你还清楚一些我不知道的手段,欢迎各位大佬进行补充

如果你对面试感兴趣,可以关注我的公众号:Dolphin海豚,可以加微信进面试群,讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请 ”点赞+评论+收藏“ 一键三连,感谢支持!

本文转载自: 掘金

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

git revert后再次merge到master会丢失部分

发表于 2024-04-26

提前声明,这个问题你在公司可能一辈子碰不上,只有很多野路子协同开发可能会遇到这个问题。不过,这个对于理解revert的本质确实很有帮助。

一、实验复现问题

在feature分支上开发了一个新功能,其中有部分带点bug

image.png
恰巧,代码评审时没有发现这个问题,将这个feature开发的新功能合并到了master上

image.png
自然而然的,master在线上运行的过程中出现了问题,这个时候我们选择了通过revert回滚

image.png
按照正常流程,有bug就要修复,于是在feature上修复了这个bug

image.png
经过更加严格的代码评审,这个代码没问题,于是又将修复后的feature合并到了master

image.png

这个时候问题出现了,master上feture1和feature2的文件没有了,仅留下feature3

二、分析下问题如何造成的

在我们将feature的修复代码合并到master之前,master和feature的分支如下:

initfeaturerevert feature
initfeaturefix
此时按照常理来说,合并时会进行差异检查。两个分支会因为revert feature和fix两个commit出现了冲突需要进行合并。但是,事实上并没有。

非常反直觉的是,在操作合并时revert feature这个记录像失效了一样,fix直接合并了上来。

此时fix仅变更了含有bug的feature3而revert feature中没有任何fearue的部分。

三、出现问题的原因

对此,Linus早就给出了解释:

Reverting a regular commit just effectively undoes what that commit did, and is fairly straightforward. But reverting a merge commit also undoes the data that the commit changed, but it does absolutely nothing to the effects on history that the merge had.

So the merge will still exist, and it will still be seen as joining the two branches together, and future merges will see that merge as the last shared state - and the revert that reverted the merge brought in will not affect that at all.

So a “revert” undoes the data changes, but it’s very much not an “undo” in the sense that it doesn’t undo the effects of a commit on the repository history.

So if you think of “revert” as “undo”, then you’re going to always miss this part of reverts. Yes, it undoes the data, but no, it doesn’t undo history.

简单的梳理下:

  1. 我们在feature分支上revert一个常规commit,就仅仅是撤销这个commit做的改动
  2. 但是如果我们在master分支上revert一个merged commit(合并上来的commit),也仅仅撤销这个commit的改动,而不会对master分支产生任何对这个merge状态的影响
  3. 这就意味着意味着在master分支上merge feature的状态仍然存在
  4. 我们在feature修复后再发起merge,检查时会看到master分支上存在merged状态
  5. 因此只会带上merge状态之后(也就是feature之后)的改动,即fix,之前被revert的改动(即feature)不会再被merge到master分支上

最后总结下核心原因:revert撤回了代码的变更,但是没有撤回合并的记录,在master分支上慎用此方法。

四、如何解决这个问题

  1. 在master上复原feature的改动,也就是revert revert feature, 让master和feature分支上的代码一致,然后再进行合并。

initfeaturerevert featurerevert revert feature
initfeaturefix
2. 在feature上同步master,然后在进行修复,最后合并也能解决这个问题。

这就是为什么这个问题很难在公司遇到的原因,正经人谁不先看一下master直接提merge啊。但是这样也会把feature分支上有用的代码全部弄没。

initfeaturerevert feature
initfeaturerevert featurefix
3. 虽然,revert没有撤回合并的状态,但是那个合并的状态绑定的是原有的commit ID,我们可以从feature将原commit的内容带到master,但是需要产生和原merged commit不同的状态(不同的commit SHA ID)

在feature分支reset + commit,产生新的commits:feature-copy 修复后再merge到master

initfeaturerevert feature
initfeature-copyfix
参考文章


Git revert 某次merge后再重新 merge代码被丢失(第一次想merge的代码再也merge不上了)_git revert 后 再merge-CSDN博客

git 分回滚后无法合并代码问题 - 赵坤坤 - 博客园 (cnblogs.com)

如何再次合并已经被revert的合并? | furthergo

git撤销merge,彻底学会git revert的用法 - 我的诗和远方 - 博客园 (cnblogs.com)

本文转载自: 掘金

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

1…8910…399

开发者博客

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