云对象存储进阶

news2024/11/25 4:31:08

《使用Minio搭建文件服务器》一文对minio作了简单的介绍,本文为进阶学习。

1.对象存储产品介绍

目前市场上流行各种对象存储服务,诸如以下:

  • Amazon S3:亚马逊提供的服务, 是市场上最成熟的产品,拥有最大的市场份额

  • OSS:阿里巴巴提供

  • COS:腾讯云提供
  • Minio: 开源版本免费,企业版需要付费,开源版本免费,企业版需要付费

总结

  • 市场占有率:Amazon S3 是市场上最成熟的产品,拥有最大的市场份额。
  • 成本:所有服务都采用按需付费模式,但具体成本取决于使用量和具体配置。
  • 兼容性:MinIO 提供与 Amazon S3 的兼容性,这使得它可以作为私有云环境中的替代方案。
  • 部署灵活性:MinIO 提供了更高的部署灵活性,可以在本地或云环境中部署。
  • 安全性:所有服务都提供了强大的安全特性,包括数据加密和访问控制

2.兼容性api设计

MinIO 与 Amazon S3 的API是兼容的,这意味着使用minio的sdk可以访问AmazonS3,反之亦然。

从实际表示来看,只能说大部分兼容,真正用起来还是偶尔出现一些异常,例如,笔者用minio的sdk来访问docker下的minio组件,从没发生IO异常,但用来访问AmazonS3,则偶尔会出现IO异常。

网上说有可能是网络不稳定,实在找不出原因,于是就想重构一下,对于内网使用minio的sdk库访问minio组件,外网使用amazons3的sdk访问亚马逊的云存储。

针对业务上,我们对于s3的基本操作是资源的上传,删除,拷贝,获取(比较少用),而对数据桶的操作,例如创建、桶删除等,由于比较少用,就不介绍了。

2.1.API接口设计

(generatePresignedUrl方法后面再介绍)

public interface S3Client {

    /**
     * 创建数据桶
     * @param filePath s3内部路径
     */
    String upload(InputStream input, String filePath, String contentType) throws OssException;

    /**
     * 获取指定对象输入流
     */
    InputStream getObject(String fileName) throws OssException;

    /**
     * 删除对象
     */
    void remove(String objectName) throws OssException;

    /**
     * 复制对象,只支持同一个桶内部复制(简化API)
     */
    void copyResource(String copy, String target) throws OssException;

    /**
     * 生成客户端临时上传路径
     */
    String generatePresignedUrl(String path) throws OssException;
}

配置项,不作过多解释

@ConfigurationProperties(prefix = "s3")
@Component
@Data
public class OssConfig {

    /**
     * 这个值对于 amazon统一为 s3.amazonaws.com,
     * 对于MinIO,可自定义
     */
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucketName;
    /**
     * cdn域名,只在客户端读取文件使用,服务端上传文件统一用endpoint
     * 对于一些文件内容可能会被修改,则不适用cdn。因为cdn在缓存期间是不会重新刷新资源的
     */
    private String cdn;
    /**
     * 使用minio,该值可以配置为amazon任何一个有效区域,唯一作用只是为了兼容amazonAPI
     */
    private String region;

}

2.2.minio实现(引入io.minio:minio依赖)

public class MinIoS3Client implements S3Client {

    private MinioClient minioClient;

    private OssConfig ossConfig;

    public MinIoS3Client(MinioClient minioClient, OssConfig ossConfig) {
        this.minioClient = minioClient;
        this.ossConfig = ossConfig;
    }

    @Override
    public void createBucket(String name) throws OssException {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public String upload(InputStream input, String filePath, String contentType) throws OssException {
        try {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(ossConfig.getBucketName())
                            .object(filePath).stream(input, input.available(), -1)
                            .contentType(contentType)
                            .build());
            return filePath;
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public InputStream getObject(String fileName) throws OssException {
        GetObjectArgs request = GetObjectArgs.builder().bucket(ossConfig.getBucketName()).object(fileName).build();
        try {
            return minioClient.getObject(request);
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public void remove(String objectName) throws OssException {
        RemoveObjectArgs request = RemoveObjectArgs.builder().bucket(ossConfig.getBucketName()).object(objectName).build();
        try {
            minioClient.removeObject(request);
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public void copyResource(String copy, String target) throws OssException {
        try {
            String bucket = ossConfig.getBucketName();
            minioClient.copyObject(
                    CopyObjectArgs.builder()
                            .bucket(bucket)
                            .object(target)
                            .source(
                                    CopySource.builder()
                                            .bucket(bucket)
                                            .object(copy)
                                            .build())
                            .build());
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public String generatePresignedUrl(String path) throws OssException {
        try {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.PUT)
                            .bucket(ossConfig.getBucketName())
                            .object(path)
                            .expiry(1, TimeUnit.HOURS)
                            .build());
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

}

2.3.amazons3实现(引入com.amazonaws:aws-java-sdk-s3依赖)

public class AmazonS3Client implements S3Client {

    private AmazonS3 client;

    private OssConfig ossConfig;

    public AmazonS3Client(AmazonS3 client, OssConfig ossConfig) {
        this.client = client;
        this.ossConfig = ossConfig;
    }

    @Override
    public void createBucket(String name) throws OssException {
        CreateBucketRequest createBucketRequest = new CreateBucketRequest(ossConfig.getBucketName());
        try {
            client.createBucket(createBucketRequest);
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public String upload(InputStream input, String filePath, String contentType) throws OssException {
        try {
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(contentType);
            metadata.setContentLength(input.available());
            PutObjectRequest putObjectRequest = new PutObjectRequest(ossConfig.getBucketName(), filePath, input, metadata);
            client.putObject(putObjectRequest);
            return filePath;
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public InputStream getObject(String fileName) throws OssException {
        return null;
    }

    @Override
    public void remove(String path) throws OssException {
        try {
            client.deleteObject(ossConfig.getBucketName(), path);
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public void copyResource(String copy, String target) throws OssException {
        try {
            CopyObjectRequest request = new CopyObjectRequest()
                    .withSourceBucketName(ossConfig.getBucketName())
                    .withSourceKey(copy) // 源对象键(文件名)
                    .withDestinationBucketName(ossConfig.getBucketName())
                    .withDestinationKey(target); // 目标对象
            client.copyObject(request);
        } catch (Exception e) {
            throw new OssException(e);
        }
    }

    @Override
    public String generatePresignedUrl(String path) throws OssException {
        long expiration = System.currentTimeMillis() + TimeUtil.MILLIS_PER_HOUR;
        try {
            GeneratePresignedUrlRequest request =
                    new GeneratePresignedUrlRequest(ossConfig.getBucketName(), path)
                            .withMethod(HttpMethod.PUT)
                            .withExpiration(new Date(expiration));
            return client.generatePresignedUrl(request).toString();
        } catch (Exception e) {
            throw new OssException(e);
        }
    }
}

2.4.利用springboot的condition机制作配置切换

spring自4.X提供了Condition条件机制,springboot在此基本上实现了一系列ConditionOnXXX注解。我们可以用来实现,当配置s3.type="minio",启动minio客户端;当配置s3.type="amazon",启动amazon客户端。代码如下:

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OssAutoConfig {

    @ConditionalOnProperty(name = "s3.type", havingValue = "minio")
    public static class MinioAutoConfig {

        @Autowired
        private OssConfig ossConfig;

        @Bean
        public S3Client createS3Client() throws Exception {
            MinioClient minioClient = minioClient();
            return new MinIoS3Client(minioClient, ossConfig);
        }

        private MinioClient minioClient() throws Exception {
            return MinioClient.builder().endpoint(ossConfig.getEndpoint())
                    .region(ossConfig.getRegion())
                    .credentials(ossConfig.getAccessKey(), ossConfig.getSecretKey())
                    .build();
        }
    }

    @ConditionalOnProperty(name = "s3.type", havingValue = "amazon")
    public static class AmazonAutoConfig {
        @Autowired
        private OssConfig ossConfig;

        @Bean
        public S3Client createS3Client() throws Exception {
            AWSCredentials credentials = new BasicAWSCredentials(ossConfig.getAccessKey(), ossConfig.getSecretKey());
            AwsClientBuilder.EndpointConfiguration endpointConfig = new AwsClientBuilder.EndpointConfiguration(
                    ossConfig.getEndpoint(), // amazon统一为s3.amazonaws.com,MinIO可自定义
                    ossConfig.getRegion()// 签名区域,对于 MinIO 来说,这个值可以是任意字符串
            );
            AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
                    .withCredentials(new AWSStaticCredentialsProvider(credentials))
                    .withEndpointConfiguration(endpointConfig)
                    .build();

            return new AmazonS3Client(s3Client, ossConfig);
        }
    }
}

3. 客户端文件直达存储服务

对于文件上传,有两种机制,一种是浏览器将文件上传到服务器,再由服务器转发到s3等云对象存储。另外一种,是客户端将需要发送的文件告知服务器,服务器验证通过后,生成一个临时上传路径,客户端通过临时路径直传s3。两者各有优缺点,如下:

转发直传
优点对文件等信息进行强验证不影响服务器吞吐量
缺点浪费服务器资源,io按双倍算客户端可能会作弊,例如验证是a文件,实际发送的是b文件。资源状态可能不同步,例如服务器已保存相关资源信息,但客户端中断上传。

相关逻辑见S3Client接口

/**
 * 生成客户端临时上传路径
 */
String generatePresignedUrl(String path) throws OssException;

4.cdn加速访问

4.1.配置cloudfront

当云对象存储与cdn(内容分发网络)结合在一起,可以极大的提供网站的性能以及用户体验。CDN通过将内容缓存到全球分布的边缘节点,可以更快地将内容传递给用户,减少延迟。

Amazon CloudFront是亚马逊提供的CDN服务,可以与AmazonS3完美结合在一起,开通CloudFront服务后,可以设置各种访问策略,cors跨域策略,资源防盗策略等等。

如下的s3配置

需要注意两个地方:

1.亚马逊是全球服务,endpoint是固定的,如果采用minio,则采用部署的实际地址。region是一个区域列表,代表s3服务的部署区域,如果采用minio,则随便选一个。

2.cdn是针对桶进行建立的,如果使用了cdn地址,则返回给客户端的资源路径无需带上桶名称。

4.2.清除缓存

由于CDN自带缓存机制,意味着浏览器从cdn拿到数据之后,只要不过期(过期时间可设置)就不会重新请求。但如果你的资源是可变的,例如json文件,html代码,cdn反而导致你的更新无法被所有人感知。因此,对于内容可能发生变化的资源,可以选择使用S3作为原始地址

或者使用cloudfrone的sdk,手动触发缓存失效。

首先,引入sdk(好像还有2.x版本)

  <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-cloudfront</artifactId>
            <version>1.12.540</version>
  </dependency>

示例代码

 // 创建CloudFront客户端
        // 使用您的AWS访问密钥和秘密密钥初始化凭证对象
        String accessKey = "xxxx";
        String secretKey = "yyyyyyyyyyyyy";
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        AmazonCloudFrontClient cloudFrontClient = new AmazonCloudFrontClient(credentials);

        String distributionId = "zzzzzzzzz";
        // 创建无效项请求
        Paths paths = new Paths();
        paths.withItems("database/picture/cf1eed1fcd6f42bb8f42de725da374e3.jpg");
        InvalidationBatch invalidationBatch = new InvalidationBatch()
                .withPaths(
                        paths);

        // 请求流水号,同一个流水号,cdn服务器只会执行一次
        invalidationBatch.withCallerReference("" + System.currentTimeMillis());
        // 创建无效请求
        CreateInvalidationRequest createInvalidationRequest = new CreateInvalidationRequest(distributionId, invalidationBatch);
        // 发送无效请求并获取结果
        CreateInvalidationResult createInvalidationResult = cloudFrontClient.createInvalidation(createInvalidationRequest);
        // 输出无效ID
        System.out.println("Invalidation ID: " + createInvalidationResult.getInvalidation().getId());

5.资源防盗

一些平台网站会限制只允许自己的网站访问内容资源(特别是以资源作卖点的产品),不允许用户直接通过浏览器访问资源(包括但不限于图片,视频等)。例如下面的图片链接,直接在浏览器上进行访问,会报错(403代表服务器禁止回应)。CDN文章的图片没有如此限制,随便访问。

5.1原理

图片等大部分资源都是放在s3类产品,其禁止资源防盗的原理,一般都是通过http请求的Referer参数(由浏览器自行注入),Referer代表当前访问的页面(区别于origin请求头),如果Referer的域名不在s3配置的域名列表,则不允许访问。

理解该原理之后,破解该策略也就很容易了。我们只需把正常访问里的http请求头带到自己的http请求即可(任何能发起http请求的工具Curl或编程语言都可以)。

5.2.python带上header绕过检测

import requests
from PIL import Image
from io import BytesIO
def query_sth():
    url = '此处填写目标地址'
    # 定义你的header信息
    headers = {
        'method': 'GET',
        'scheme': 'https',
        'Accept-Encoding': 'gzip, deflate, br, zstd',
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Referer': '浏览器正常访问的referer参数',
        'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
        'Sec-Ch-Ua-Mobile': '?0',
        'Sec-Ch-Ua-Platform': '"Windows"',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-site',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
    }
    # 发送请求
    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        # 获取二进制图像数据
        image_data = response.content
        # print(response.content)
        # 使用BytesIO创建一个文件类对象
        image_stream = BytesIO(image_data)

        # 使用Pillow打开图像
        image = Image.open(image_stream)
        # 显示图像
        image.show()
        # 如果你想保存图像到本地
        # image.save('local_image.jpg')
    else:
        print('Failed to retrieve image')
    print(response)

query_sth()

总结:

通过referer限制浏览器访问资源,只是提高了资源安全的系数,只能针对小白,懂点相关知识的人员可以轻易破解,这个问题无法从根本上解决。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2224583.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ATom:2016-2018 年沿飞行轨迹的 CAM-chem/CESM2 模型输出

目录 简介 摘要 代码 引用 网址推荐 知识星球 机器学习 ATom: CAM-chem/CESM2 Model Outputs Along Flight Tracks, 2016-2018 ATom&#xff1a;2016-2018 年沿飞行轨迹的 CAM-chem/CESM2 模型输出 简介 该数据集包含沿 ATom 飞行轨迹的 CAM-chem&#xff08;带化学的…

[ARM-2D 专题]5 MDK编译器一个旧版本-Ofast优化bug的问题及解决办法

最近开始大量基于ARM-2D开发应用项目&#xff0c;为了达到最佳性能&#xff0c;我们使用了编译器的许多特殊技能&#xff0c;其中就包含了-Ofast优化&#xff0c;很不幸&#xff0c;一不小心踩坑了。 案发情况如下&#xff1a; 使用的MDK版本5.36&#xff0c;编译器6.16 优化选…

在GeoTools中的Shapefile属性表读取效率之Shp与Dbf对比

目录 前言 一、POI测试数据简介 1、选用的POI数据 2、关于数据的属性数据 二、属性数据读取的两种方式实现 1、基于DbaseFileReader的读取 2、基于SimpleFeatureSource的读取 三、实际运行对比 1、内存和CPU占用情况 2、运行耗时情况 四、总结 前言 众所周知&#x…

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper+代码——交叉注意力(Cross-Attention)

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 文章目录 【…

‌Spring MVC的主要组件有哪些?

前言 SpringMVC的核心组件包括DispatcherServlet、Controller、HandlerMapping、HandlerAdapter、ViewResolver、ModelAndView等&#xff0c;它们协同工作以支持基于MVC架构的Web应用程序开发。这些组件使得开发人员能够以一种声明式和模块化的方式构建Web应用程序&#xff0c…

小程序开发实战:PDF转换为图片工具开发

目录 一、开发思路 1.1 申请微信小程序 1.2 编写后端接口 1.3 后端接口部署 1.4 微信小程序前端页面开发 1.5 运行效果 1.6 小程序部署上线 今天给大家分享小程序开发系列&#xff0c;PDF转换为图片工具的开发实战&#xff0c;感兴趣的朋友可以一起来学习一下&#xff01…

ECharts饼图-基础南丁格尔玫瑰图,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个饼图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供详…

一、在cubemx下RTC配置调试实例测试

一、rtc的时钟有lse提供。 二、选择rtc唤醒与闹钟功能 内部参数介绍 闹钟配置 在配置时间时&#xff0c;注意将时间信息存储起来&#xff0c;防止复位后时间重新配置。 if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0)! 0x55AA)//判断标志位是否配置过&#xff0c;没有则进…

qt EventFilter用途详解

一、概述 EventFilter是QObject类的一个事件过滤器&#xff0c;当使用installEventFilter方法为某个对象安装事件过滤器时&#xff0c;该对象的eventFilter函数就会被调用。通过重写eventFilter方法&#xff0c;开发者可以在事件处理过程中进行拦截和处理&#xff0c;实现对事…

WSL2 Ubuntu22.04编译安装LLVM

前提 这两天因为工作需要&#xff0c;要编译一个Debug版本的llvm。这里对编译安装过程进行一个简单的记录&#xff0c;同时也记录下这个过程中遇到的几个问题。 下载源码并编译 有关llvm编译安装的官方文档在这里。 从git仓库clone llvm的源码。 git clone https://github.c…

FPGA搭建PCIE3.0通信架构简单读写测试,基于XDMA中断模式,提供3套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的PCIE方案本博客方案的PCIE2.0版本 3、PCIE基础知识4、工程详细设计方案工程设计原理框图XDMA配置及使用XDMA中断模块数据缓存架构用户逻辑Windows版本XDMA驱动安装Linux版本XDMA驱动安装测试应用程序工程源码架构PCIE上板…

电磁场-Laplace算子与冲激函数的关系

csdn重新打一遍公式太麻烦了。欢迎转到我的知乎账号上查阅原版文章&#xff0c;也可后台私信我发送原版PDF或者markdown。 电磁场-Laplace算子与冲激函数的关系 - 知乎 下面的文章是一张超大的图片。

论1+2+3+4+... = -1/12 的不同算法

我们熟知自然数全加和&#xff0c; 推导过程如下&#xff0c; 这个解法并不难&#xff0c;非常容易看懂&#xff0c;但是并不容易真正理解。正负交错和无穷项计算&#xff0c;只需要保持方程的形态&#xff0c;就可以“预知”结果。但是这到底说的是什么意思&#xff1f;比如和…

C++扑克牌(poker)2024年CSP-J认证第二轮第一题 CCF信息学奥赛C++ 中小学初级组 第二轮真题解析

目录 C扑克牌&#xff08;poker&#xff09; 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、运行结果 五、考点分析 六、推荐资料 C扑克牌&#xff08;poker&#xff09; 2024年CSP-J认证第二轮第一题 一、题目要求 1、编程实现 小 P 从同学…

HarmonyOS 组件样式@Style 、 @Extend、自定义扩展(AttributeModifier、AttributeUpdater)

1. HarmonyOS Style 、 Extend、自定义扩展&#xff08;AttributeModifier、AttributeUpdater&#xff09; Styles装饰器&#xff1a;定义组件重用样式   ;Extend装饰器&#xff1a;定义扩展组件样式   自定义扩展&#xff1a;AttributeModifier、AttributeUpdater 1.1. 区…

HarmonyOS 5.0应用开发——应用打包HAP、HAR、HSP

【高心星出品】 目录 应用打包HAP、HAR、HSPModule类型HAPHAR创建HAR建立依赖HAR共享内容 HSP创建HSP建立依赖同上HSP共享内容同上 HAR VS HSP 应用打包HAP、HAR、HSP 一个应用通常会包含多种功能&#xff0c;将不同的功能特性按模块来划分和管理是一种良好的设计方式。在开发…

【哈工大_操作系统实验】Lab9 proc文件系统的实现

本节将更新哈工大《操作系统》课程第九个 Lab 实验 proc文件系统的实现。按照实验书要求&#xff0c;介绍了非常详细的实验操作流程&#xff0c;并提供了超级无敌详细的代码注释。 实验目的&#xff1a; 掌握虚拟文件系统的实现原理&#xff1b;实践文件、目录、文件系统等概念…

【C++开篇】

首先初阶的数据结构相信大家已经学习的差不多了&#xff0c;关于初阶数据结构排序的相关内容的总结随后我也会给大家分享出来。C语言和C有许多相同的地方&#xff0c;但也有许多不相同的地方。接下来的C部分&#xff0c;我们主要是针对C与C语言不同的地方来与大家进行分享。其中…

量子变分算法 (python qiskit)

背景 变分量子算法是用于观察嘈杂的近期设备上的量子计算效用的有前途的候选混合算法。变分算法的特点是使用经典优化算法迭代更新参数化试验解决方案或“拟设”。这些方法中最重要的是变分量子特征求解器 (VQE)&#xff0c;它旨在求解给定汉密尔顿量的基态&#xff0c;该汉密尔…

这是一篇vue3 的详细教程

Vue 3 详细教程 一、Vue 3 简介 Vue.js 是一款流行的 JavaScript 前端框架&#xff0c;用于构建用户界面。Vue 3 是其最新版本&#xff0c;带来了许多新特性和性能优化&#xff0c;使开发更加高效和灵活。 二、环境搭建 安装 Node.js 前往Node.js 官方网站下载并安装适合你…