原创文章,转载请注明出处。

前言

像Word,Excel,PPT等类似的编辑软件都有一个特点,就是可以保存成一个文件, 方便传阅。比如
我们的PPT里内置了视频图片文字等,它的后缀是pptx。其实这个pptx就是一个压缩文件,你如果把 .pptx 的后缀改成 .rar 的话, 对其解压后你会发现其实这个.pptx就是个压缩文件, 里面存了很多文件。如下图。其实这个就是用到了压缩和解压。 如果你也是做类似的软件,那么使用7z的库还是挺有必要的。
在这里插入图片描述
将.pptx改成rar解压后如下图,比如media文件夹就是放你的ppt内视频的。
在这里插入图片描述

解决方案

并不是通过7z.exe直接调用命令的方式, 而是通过bit7z+7z.dll进行调用(方便拿到解压缩进度)
点击跳转到7z官网链接地址,可以了解一些7z的信息

1>拿到 bit7z64.lib和7z.dll

bit7z是一个C++的静态库,里面封装了接口,用来调用7-zip的库;
对bit7z进行编译的话,需要下载下面的两个源码:

1.1>下载编译bit7z64.lib需要的源代码

在这里插入图片描述
bit7z-3.1.3.zip | bit7z-master.zip 这两个分支我都测试过是可以用的,建议用比较新的。

1.2>解压下载文件

解压,将《lzma1805.7z》解压,将解压后的(下图的文件)拷贝
在这里插入图片描述
放到《 bit7z-master.zip》解压后的路径里(bit7z-master\lib\7zSDK\)如下图
在这里插入图片描述

1.3>编译《bit7z-master》,拿到 bit7z64.lib

打开 bit7z-master\bit7z.vcxproj 对其编译, 编译选项Win64+Release
如下图,我编译的是Win64下用的。编译成功后在 bit7z-master\bin\x64 目录下会有你编译完的bit7z64.lib。这个就是我们要用的。
在这里插入图片描述

1.4> 准备7z.dll

由于bit7z依赖7z的库,所以还要下载7z的dll库才能用。
7z.dll 下载链接:下载链接
打开上面下载链接后,

  • 安装下图《.exe 64 位 x64 7-Zip Windows 64 位(Intel 64 或 AMD64)》

  • 在这里插入图片描述

  • 安装时候让你选安装路径,拷贝记下这个路径在这里插入图片描述

  • 安装完毕后,找到安装后的目录,拷贝7-zip.dll出来,和上面编译好的bit7z64.lib放到你的exe路径下,接下来就可以写代码了
    在这里插入图片描述

1.2>编码

我直接将代码贴出来,下面1.2.3小节有使用的例子代码。
然后再传一个整体的使用到csdn上: 下载链接 ,我发现我每次传文件时候都是选择的下载不需要积分(0分),但是下载的时候还要做任务,这对大家属实有点不友好。

1.2.1>头文件

// Fill out your copyright notice in the Description page of Project Settings.
/*
*	Author:田华健 tianhj
*	Data: 2021-07-30 09:02:36
*	Description:提供调用7z.exe进行解压缩
*	并不是通过7z.exe直接调用命令的方式, 而是通过bit7z+7z.dll进行调用(方便拿到解压缩进度)

解压缩例子: 两种回调监听方式, 代理和std::bind都可以, 二选一.
{
	//移除回调
	F7ZDelegates::SevenZProgressDelegate.Remove(m_Delegate1);
	F7ZDelegates::SevenZExceptionDelegate.Remove(m_Delegate2);

	//绑定解压缩的进度回调
	m_Delegate1 = F7ZDelegates::SevenZProgressDelegate.AddUObject(this, &USPGameInstance::OnSevenZProgressListen);
	//绑定解压缩的异常回调
	m_Delegate2 = F7ZDelegates::SevenZExceptionDelegate.AddUObject(this, &USPGameInstance::OnSevenZExceptionListen);

	//新建一个7zHelper类
	TSharedPtr<FSevenZHelper> p7Z = MakeShared<FSevenZHelper>();
	if (p7Z.Get())
	{
		//解压
		{
			//设置进度回调
			p7Z->SetprogressCallback(std::bind(&USPGameInstance::progressCallback, this, std::placeholders::_1));
			//设置异常回调
			p7Z->SetBitExceptionCallback(std::bind(&USPGameInstance::BitExceptionCallback, this, std::placeholders::_1));
			//执行解压 将TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian.gspp") 用密码password解压到D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian文件夹
			p7Z->CompressDecompression(ESPSevenZType::OpType_Decompression, TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian.gspp"), TEXT(""), TEXT("password"));
		}

		//压缩
		{
			//设置进度回调
			p7Z->SetprogressCallback(std::bind(&USPGameInstance::progressCallback, this, std::placeholders::_1));
			//设置异常回调
			p7Z->SetBitExceptionCallback(std::bind(&USPGameInstance::BitExceptionCallback, this, std::placeholders::_1));
			//执行压缩 将TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian") 用密码password压缩成TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian.gspp")文件
			p7Z->CompressDecompression(ESPSevenZType::OpType_Compress, TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian"), TEXT(""), TEXT("password"));
		}
	}
}
*/

#pragma once
#pragma warning(disable : 4530)

//7Z类型
UENUM()
enum class ESPSevenZType : uint8
{
	OpType_None,
	OpType_Compress,		//压缩
	OpType_Decompression,	//解压
};

//#ifndef NIMO_OBS_ZLIB_HASAS
//#define NIMO_OBS_ZLIB_HASAS

#include "CoreMinimal.h"
#include <iostream>
#include <string>
#include <functional>
#include "../../bit7z-3.1.3/include/bit7z.hpp"
#include "../../bit7z-3.1.3/include/bit7zlibrary.hpp"

using namespace bit7z;

//7z代理类
class F7ZDelegates
{
public:
	/*
	*	7z解压缩进度反馈函数
	*	ESPSevenZType:解压还是压缩?
	*	FString:传入的文件名
	*	double:真正的进度
	*/
	DECLARE_MULTICAST_DELEGATE_ThreeParams(FSevenZProgressDelegate, ESPSevenZType, const FString&, double);
	static FSevenZProgressDelegate SevenZProgressDelegate;

	/*
	*	7z异常抛	
	*	ESPSevenZType:解压还是压缩?
	*	FString:传入的文件名
	*	BitException:真正的异常原因
	*/
	DECLARE_MULTICAST_DELEGATE_ThreeParams(FSevenZExceptionDelegate, ESPSevenZType, const FString&, const bit7z::BitException&);
	static FSevenZExceptionDelegate SevenZExceptionDelegate;
};


class FSevenZHelper : public TSharedFromThis<FSevenZHelper>
{
private:
	typedef std::function<void(double progress)> ProgressCallback;		//参数是进度回调
	typedef std::function<void(std::string filename)> MyFileCallback;	//其参数是当前正在进行的操作所处理的文件的名称
	typedef std::function<void(const bit7z::BitException&)> BitExceptionCallback;	//参数是异常项目

	ProgressCallback m_pProgressFunc;
	MyFileCallback m_pFileFunc;
	BitExceptionCallback m_pExpFunc;

public:
	FSevenZHelper();
	~FSevenZHelper();

	/*
	*	外部调用这个函数进行解压缩
	*	
	*	参数1:	ESPSevenZType 传入是解压还是压缩的枚举
	*	参数2:	const FString& Src 你要压缩或解压的全路径
	*	参数3:	你的最终路径
	*			如果该参数传值的时候为空的话
	*			1>解压时候会在同级文件夹下创建一个根据Src去掉压缩文件后缀的文件夹
	*			2>压缩的话会在同级文件夹下默认压缩成一个.gspp的文件, 其实就是7z格式
	*	参数4:	解压或者压缩时候带的密码
	*	
	*	Notes:不支持压缩成rar
	*/
	void CompressDecompression(ESPSevenZType type, const FString& Src, const FString& Dest, const FString& Password);

	//外部调用设置进度的回调函数
	void SetProgressCB(ProgressCallback upcFunc);

	//外部调用设置异常的回调函数
	void SetBitExceptionCallback(BitExceptionCallback ExceptionFunc);

	//外部调用设置获取当前正在进行操作所处理的文件的名称
	void SetFileNameCurOperatorCallback(MyFileCallback ufcFunc);

private:
	void Compress(ESPSevenZType type, const FString& Src, const FString& Dest, const FString& Password);	//压缩
	void Extract(ESPSevenZType type, const FString& Src, const FString& Dest, const FString& Password);		//解压

	void GetFileSuffix(const FString& Path);//获取文件后缀

	void TotalSizeCB(uint64_t total_size);	//返回操作的文件总大小的回调函数
	void ProgressCB(uint64_t size);			//返回进度
	void FileNameCB(std::wstring filename);	//返回当前正在进行的操作所处理的文件的名称

	FString m_SourcePath;		//源文件路径
	FString m_DestDir;			//目标路径
	FString m_FileSuffix;		//根据原路径拆出来的文件后缀
	uint64_t m_FileSize;		//源文件大小
	FString DefaultSuffix;		//默认后缀
	ESPSevenZType m_OpType;		//当前是解压还是压缩
	uint64_t TotalSize;			//当前正在处理的文件的总大小
};
//#endif

1.2.2>cpp文件

#include "SevenZHelper.h"
#include "GetRegValue.h"
#include "RegionRiskReadLibrary.h"


F7ZDelegates::FSevenZProgressDelegate F7ZDelegates::SevenZProgressDelegate;
F7ZDelegates::FSevenZExceptionDelegate F7ZDelegates::SevenZExceptionDelegate;


FSevenZHelper::FSevenZHelper() 
	: m_pProgressFunc(nullptr)
	, m_pFileFunc(nullptr)
	, m_SourcePath(TEXT(""))
	, m_DestDir(TEXT(""))
	, m_FileSuffix(TEXT(""))
	, m_FileSize(0)
	, DefaultSuffix(TEXT("gspp"))
	, m_OpType(ESPSevenZType::OpType_None)
{
	
}

FSevenZHelper::~FSevenZHelper()
{
}

void FSevenZHelper::CompressDecompression(ESPSevenZType type, const FString& Src, const FString& Dest, const FString& Password)
{
	m_OpType = type;
	if (type == ESPSevenZType::OpType_Compress)
	{
		Compress(type, Src, Dest, Password);
	}
	else if (type == ESPSevenZType::OpType_Decompression)
	{
		Extract(type, Src, Dest, Password);
	}
}

void FSevenZHelper::GetFileSuffix(const FString& Path)
{
	//默认后缀格式
	m_FileSuffix = DefaultSuffix;

	FString Temp = FPaths::ConvertRelativePathToFull(Path);

	//获取文件后缀
	//将其按照路径拆分开
	TArray<FString> arrSplit;
	Temp.ParseIntoArray(arrSplit, TEXT("/"), true);
	if (arrSplit.Num() > 1)
	{
		Temp.RemoveFromEnd(arrSplit[arrSplit.Num() - 1]);

		//拿到最后一个, 再按照.拆分
		FString Fin = arrSplit[arrSplit.Num() - 1];
		TArray<FString> arrSplitPoint;
		Fin.ParseIntoArray(arrSplitPoint, TEXT("."), true);
		if (arrSplitPoint.Num() == 2)
		{
			m_FileSuffix = arrSplitPoint[1];
		}
	}
}

void FSevenZHelper::TotalSizeCB(uint64_t total_size)
{
	TotalSize = total_size;
}

void FSevenZHelper::ProgressCB(uint64_t size)
{
	double progress = ((1.0 * size) / TotalSize);
	F7ZDelegates::SevenZProgressDelegate.Broadcast(m_OpType, m_SourcePath, progress);
	//std::wcout << progress << "%" << std::endl;
	if (m_pProgressFunc) {
		m_pProgressFunc(progress);
	}
}

void FSevenZHelper::FileNameCB(std::wstring filename)
{
	std::string temp = wstring2string(filename);
	//std::cout << temp.c_str() << std::endl;
	if (m_pFileFunc) {
		m_pFileFunc(temp);
	}
}

//压缩
void FSevenZHelper::Compress(ESPSevenZType type, const FString& Src, const FString& Dest, const FString& Password)
{
	if (Src.IsEmpty()) return;

	//1>源文件(路径或者单个文件) 你要对其压缩
	m_SourcePath = FPaths::ConvertRelativePathToFull(Src);

	//2>获取文件后缀
	GetFileSuffix(Dest);

	if (m_FileSuffix.Compare(TEXT("rar")) == 0)
	{
		//不支持rar压缩
		return;
	}

	//3>判断当前传入的文件路径是否和解析的后缀一样
	if (!Dest.IsEmpty())
	{
		m_DestDir = FPaths::ConvertRelativePathToFull(Dest);
		if (!m_DestDir.EndsWith(m_FileSuffix))
		{
			m_DestDir.Append(TEXT("."));
			m_DestDir.Append(m_FileSuffix);
		}
	}
	else
	{
		//3>如果目标路径不设置的话, 会根据当前的压缩文件名称或者压缩文件路径名称创建一个.gspp(7z)的压缩文件出来
		m_DestDir = m_SourcePath;
		if (!m_FileSuffix.IsEmpty())
		{
			//创建一个同级文件夹路径
			FString RemoveEndStr = TEXT(".");
			RemoveEndStr.Append(m_FileSuffix);
			m_DestDir.RemoveFromEnd(RemoveEndStr);
		}
		m_DestDir.Append(TEXT("."));
		m_DestDir.Append(DefaultSuffix);
	}

 	try {
		bit7z::Bit7zLibrary lib(L"7z.dll");
		std::shared_ptr<BitCompressor> compressor = nullptr;
		if (m_FileSuffix.Compare(TEXT("gsp")) == 0 || m_FileSuffix.Compare(TEXT("gspp")) == 0 || m_FileSuffix.Compare(TEXT("7z")) == 0)
		{
			//后缀L".7z"
			compressor = std::make_shared<BitCompressor>(lib, BitFormat::SevenZip);
		}
		else if (m_FileSuffix.Compare(TEXT("zip")) == 0)
		{
			//后缀L".zip"
			compressor = std::make_shared<BitCompressor>(lib, BitFormat::Zip);
		}
		else if (m_FileSuffix.Compare(TEXT("rar")) == 0)
		{
			//后缀L".rar"
			//compressor = std::make_shared<BitCompressor>(lib, BitFormat::Rar5); //Rar5会报错
		}
		else if (m_FileSuffix.Compare(TEXT("bz2")) == 0)
		{
			//后缀L".bz2
			compressor = std::make_shared<BitCompressor>(lib, BitFormat::BZip2);
		}
		else if (m_FileSuffix.Compare(TEXT("xz")) == 0)
		{
			//后缀L".xz
			compressor = std::make_shared<BitCompressor>(lib, BitFormat::Xz);
		}
		else if (m_FileSuffix.Compare(TEXT("wim")) == 0)
		{
			//后缀L".wim
			compressor = std::make_shared<BitCompressor>(lib, BitFormat::Wim);
		}
		else if (m_FileSuffix.Compare(TEXT("tar")) == 0)
		{
			//后缀L".tar
			compressor = std::make_shared<BitCompressor>(lib, BitFormat::Tar);
		}
		else if (m_FileSuffix.Compare(TEXT("gz")) == 0)
		{
			//后缀L".gz
			compressor = std::make_shared<BitCompressor>(lib, BitFormat::GZip);
		}

		bit7z::TotalCallback TotalCB = std::bind(&FSevenZHelper::TotalSizeCB, this, std::placeholders::_1);
		bit7z::ProgressCallback ProCB = std::bind(&FSevenZHelper::ProgressCB, this, std::placeholders::_1);
		bit7z::FileCallback fc = std::bind(&FSevenZHelper::FileNameCB, this, std::placeholders::_1);
		compressor->setTotalCallback(TotalCB);
		compressor->setProgressCallback(ProCB);
		compressor->setFileCallback(fc);
		compressor->setPassword(*Password);
		compressor->compressDirectory(*m_SourcePath, *m_DestDir);
		compressor->setUpdateMode(true);
	}
	catch (const BitException& ex) {
		//do something with ex.what()...
		cout << ex.what() << endl;
		F7ZDelegates::SevenZExceptionDelegate.Broadcast(type, m_SourcePath, ex);
		if (m_pExpFunc)
		{
			m_pExpFunc(ex);
		}
	}
}

//解压
void FSevenZHelper::Extract(ESPSevenZType type, const FString& Src, const FString& Dest, const FString& Password)
{
	//Src不能为空
	if (Src.IsEmpty()) return;

	//1>源文件(压缩文件格式) 你要对其解压
	m_SourcePath = FPaths::ConvertRelativePathToFull(Src);

	//2>获取文件后缀
	GetFileSuffix(Src);

	//3>设置目标路径
	if (!Dest.IsEmpty())
	{
		m_DestDir = FPaths::ConvertRelativePathToFull(Dest);
	}
	else
	{
		//3>如果目标路径不设置的话, 则会在同级目录创建一个同级文件夹出来
		m_DestDir = m_SourcePath;
		if (!m_FileSuffix.IsEmpty())
		{
			//创建一个同级文件夹路径
			FString RemoveEndStr = TEXT(".");
			RemoveEndStr.Append(m_FileSuffix);
			m_DestDir.RemoveFromEnd(RemoveEndStr);
		}
		else
		{
			//后缀读不到, 解压到桌面上
			m_DestDir = URegionRiskReadLibrary::GetUserDesktopFullPath();
			m_DestDir = FPaths::ConvertRelativePathToFull(m_DestDir);
		}
	}

	try {
		bit7z::Bit7zLibrary lib(L"7z.dll");
		std::shared_ptr<bit7z::BitExtractor> extractor = nullptr;
		if (m_FileSuffix.Compare(TEXT("gsp")) == 0 || m_FileSuffix.Compare(TEXT("gspp")) == 0 || m_FileSuffix.Compare(TEXT("7z")) == 0)
		{
			//后缀L".7z"
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::SevenZip);
		}
		else if (m_FileSuffix.Compare(TEXT("zip")) == 0)
		{
			//后缀L".zip"
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::Zip);
		}
		else if (m_FileSuffix.Compare(TEXT("rar")) == 0)
		{
			//后缀L".rar"
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::Rar5);
		}
		else if (m_FileSuffix.Compare(TEXT("bz2")) == 0)
		{
			//后缀L".bz2
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::BZip2);
		}
		else if (m_FileSuffix.Compare(TEXT("xz")) == 0)
		{
			//后缀L".xz
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::Xz);
		}
		else if (m_FileSuffix.Compare(TEXT("wim")) == 0)
		{
			//后缀L".wim
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::Wim);
		}
		else if (m_FileSuffix.Compare(TEXT("tar")) == 0)
		{
			//后缀L".tar
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::Tar);
		}
		else if (m_FileSuffix.Compare(TEXT("gz")) == 0)
		{
			//后缀L".gz
			extractor = std::make_shared<bit7z::BitExtractor>(lib, bit7z::BitFormat::GZip);
		}

		bit7z::TotalCallback TotalCB = std::bind(&FSevenZHelper::TotalSizeCB, this, std::placeholders::_1);
		bit7z::ProgressCallback ProCB = std::bind(&FSevenZHelper::ProgressCB, this, std::placeholders::_1);
		bit7z::FileCallback fc = std::bind(&FSevenZHelper::FileNameCB, this, std::placeholders::_1);
		extractor->setTotalCallback(TotalCB);
		extractor->setProgressCallback(ProCB);
		extractor->setFileCallback(fc);
		extractor->setPassword(*Password);
		extractor->extract(*m_SourcePath, *m_DestDir);
	}
	catch (const BitException& ex) {
		//do something with ex.what()...
		cout << ex.what() << endl;
		F7ZDelegates::SevenZExceptionDelegate.Broadcast(type, m_SourcePath, ex);
		if (m_pExpFunc)
		{
			m_pExpFunc(ex);
		}
	}
}

void FSevenZHelper::SetProgressCB(ProgressCallback upcFunc)
{
	this->m_pProgressFunc = upcFunc;
}

void FSevenZHelper::SetBitExceptionCallback(BitExceptionCallback ExceptionFunc)
{
	this->m_pExpFunc = ExceptionFunc;
}

void FSevenZHelper::SetFileNameCurOperatorCallback(MyFileCallback ufcFunc)
{
	this->m_pFileFunc = ufcFunc;
}

1.2.3>使用介绍

压缩支持的格式:
7z、zip、bz2、xz、wim、tar、gz
解压支持的格式:
7z、zip、rar、bz2、xz、wim、tar、gz

进度是double: 0到1
异常是char*: 用ex.what()

压缩暂时不支持.rar的压缩,我代码里面没有做弹框提示,其实建议大家改下代码,做个明确提示。

因为我这个是在UE4引擎里扩展的,所以内部增加了一个UE4的广播。如果你是C++项目的话,将里面的 F7ZDelegates 类的相关内容删除掉,将FString改成std::string,头文件包含目录改一下。
我里面还有个字符串叫gspp,这个是我们自定义的后缀,大家也可以修改。其实对外封装使用来讲,我不该里面写这些格外的格式。大家拿过去自己修改一下吧。
我只是搭建了个基础模板,大家再自己丰富吧。
要是想自己再丰富其他格式其实也是支持的,详情可以看
bit7z-3.1.3\includebitformat.hpp 这个文件,里面定义了很多格式。

解压代码+解压进度监听 +解压异常监听
同样下面例子代码用是UE4智能指针,同理你改成C++11智能指针shared_ptr是一样的。

C++11

std::shared_ptr<FSevenZHelper> p7Z = std::make_shared<FSevenZHelper>();

UE4C++

//新建一个7zHelper类
TSharedPtr<FSevenZHelper> p7Z = MakeShared<FSevenZHelper>();
if (p7Z.Get())
{
	//解压
	{
		//设置进度回调
		p7Z->SetprogressCallback(std::bind(&USPGameInstance::progressCallback, this, std::placeholders::_1));
		//设置异常回调
		p7Z->SetBitExceptionCallback(std::bind(&USPGameInstance::BitExceptionCallback, this, std::placeholders::_1));
		//执行解压 将TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian.gspp") 用密码password解压到D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian文件夹
		p7Z->CompressDecompression(ESPSevenZType::OpType_Decompression, TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian.gspp"), TEXT(""), TEXT("password"));
	}

压缩代码+压缩进度监听+压缩异常监听

//新建一个7zHelper类
TSharedPtr<FSevenZHelper> p7Z = MakeShared<FSevenZHelper>();
if (p7Z.Get())
{
	//压缩
	{
		//设置进度回调
		p7Z->SetprogressCallback(std::bind(&USPGameInstance::progressCallback, this, std::placeholders::_1));
		//设置异常回调
		p7Z->SetBitExceptionCallback(std::bind(&USPGameInstance::BitExceptionCallback, this, std::placeholders::_1));
		//执行压缩 将TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian") 用密码password压缩成TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian.gspp")文件
		p7Z->CompressDecompression(ESPSevenZType::OpType_Compress, TEXT("D:\\GSP交付\\西安广联达\\WindowsNoEditor\\Engine7zTianhuajian"), TEXT(""), TEXT("password"));
	}
}

到这就结束了,谢谢。

谢谢,创作不易,大侠请留步… 动起可爱的双手,点个赞再走呗
ღ( ´・ᴗ・` )比心<( ̄︶ ̄)>*

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐