当前位置: 首页 > news >正文

手写mybatis之解析和使用ResultMap映射参数配置

前言
学习源码是在学习什么呢?
就是为了通过这些源码级复杂模型中,学习系统框架的架构思维、设计原则和设计模式。在这些源码学习手写的过程中,感受、吸收并也是锻炼一种思维习惯,并尝试把这些思路技术迁移到平常的复杂业务设计开发中。
目标
通常在数据库表字段的命名中,所定义的规范是希望使用小写的英文字母和下划线的方式组合使用,例如:雇员表中的雇员姓名,则使用 employee_name 表字段描述。但这样的字段定义与 Java 代码开发中的 PO 数据库对象中的字段,是不能一一匹配的,因为 Java 代码中会使用驼峰的方式进行命名,同样是雇员姓名在 Java 代码中则是 employeeName 的方式进行表示。

那么这里就有一个问题了,我们在使用 Mybatis 框架的时候,如果遇到这样的字段,则需要通过把数据库表中的下划线的字段名称,映射成 Java 代码中的驼峰字段,这样才能在执行查询操作的时候,正确的把数据库中的结果映射到 Java 代码的返回对象上。注意:另外在 Mybatis 中也可以使用例如 employee_name as employeeName 的方式进行处理,但在整个编程中并不是太优雅,因为所有的查询都要做 as 映射,那么使用一个统一的字段映射更加合理。
设计
在这里插入图片描述
以 XMLMapperBuilder 解析为入口,扩展 resultMapElements 方法,解析 resultMap 映射参数。解析过程涉及到了 MapperBuilderAssistant 映射器构建助手类的使用,所以需要在 XMLMapperBuilder 构造函数中进行初始化。
参数的解析细节主要在 MapperBuilderAssistant 映射构建器助手中完成,包括解析 javaTypeClass、typeHandlerInstance,以及封装 XML 配置的基本字段映射信息。
解析映射配置
整个配置解析都以围绕在 Mybatis ORM 框架中使用 resultMap 映射为主,而 resultMap 的参数映射配置也是用于解决数据库表中的字段与 Java 代码中的对象字段不一致的情况。
在这里插入图片描述
解析入口
基于这样对映射字段的解决方案,所以需要扩展 Mapper XML 映射构建器 configurationElement 方法的处理内容,添加解析 resultMap 操作。这部分的操作也就是在解析整个 select、insert、update、delete 之前就操作这个部分,因为后续解析 select 标签时,如果遇到有 resultMap 的配置参数,则可以直接调用已经解析的 resultMap 参数进行关联。

private void configurationElement(Element element) {// 1.配置namespaceString namespace = element.attributeValue("namespace");if (namespace.equals("")) {throw new RuntimeException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);// 2. 解析resultMapresultMapElements(element.elements("resultMap"));// 3.配置select|insert|update|deletebuildStatementFromContext(element.elements("select"),element.elements("insert"),element.elements("update"),element.elements("delete"));
}

在 XMLMapperBuilder#configurationElement 配置元素解析的方法中,新增加了关于 resultMap 元素的解析,由于可能在一个 Mapper XML 中有多组这样的映射参数配置,所以这里获取的是一个 elements 集合元素。
解析过程

private ResultMap resultMapElement(Element resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {String id = resultMapNode.attributeValue("id");String type = resultMapNode.attributeValue("type");Class<?> typeClass = resolveClass(type);List<ResultMapping> resultMappings = new ArrayList<>();resultMappings.addAll(additionalResultMappings);List<Element> resultChildren = resultMapNode.elements();for (Element resultChild : resultChildren) {List<ResultFlag> flags = new ArrayList<>();if ("id".equals(resultChild.getName())) {flags.add(ResultFlag.ID);}// 构建 ResultMappingresultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));}// 创建结果映射解析器ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, resultMappings);return resultMapResolver.resolve();
}

解析的核心过程包括读取 resultMap 标签中,如: 的 id、type 信息。之后开始循环解析标签内的每一条配置元素 如 中的 colum、property 信息。同时这里会把id的配置专门用 ResultFlag 枚举类进行标记。
基础信息解析完成后,就开始调用结果映射器把解析的信息封装成 ResultMap 进行存放。
存放映射对象

public class ResultMapResolver {private final MapperBuilderAssistant assistant;private String id;private Class<?> type;private List<ResultMapping> resultMappings;// ... 省略构造函数public ResultMap resolve() {return assistant.addResultMap(this.id, this.type, this.resultMappings);}}

ResultMapResolver 结果映射器是本章节新增加的内容,其实它的作用就是对解析结果内容的一个封装处理,最终调用的是还是 MapperBuilderAssistant 映射构建器助手,所提供 ResultMap 封装和保存操作。
封装 ResultMap

public class ResultMap {private String id;private Class<?> type;private List<ResultMapping> resultMappings;private Set<String> mappedColumns;private ResultMap() {}public static class Builder {private ResultMap resultMap = new ResultMap();public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings) {resultMap.id = id;resultMap.type = type;resultMap.resultMappings = resultMappings;}public ResultMap build() {resultMap.mappedColumns = new HashSet<>();// 添加 mappedColumns 字段for (ResultMapping resultMapping : resultMap.resultMappings) {final String column = resultMapping.getColumn();if (column != null) {resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));}}return resultMap;}}// ... 省略get方法}

ResultMap 中 Builder 建造者负责完成字段的处理,通过把字段统一转换为大写存放到 mappedColumns 映射字段中。并返回 resultMap 对象。其余的信息都可以通过构造函数进行传递。
添加 ResultMap

public class MapperBuilderAssistant extends BaseBuilder {private String currentNamespace;private String resource;// ... 省略部分方法public MapperBuilderAssistant(Configuration configuration, String resource) {super(configuration);this.resource = resource;}public ResultMapping buildResultMapping(Class<?> resultType,String property,String column,List<ResultFlag> flags) {Class<?> javaTypeClass = resolveResultJavaType(resultType, property, null);TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, null);ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property, column, javaTypeClass);builder.typeHandler(typeHandlerInstance);builder.flags(flags);return builder.build();}public ResultMap addResultMap(String id, Class<?> type, List<ResultMapping> resultMappings) {// 补全ID全路径,如:cn.bugstack.mybatis.test.dao.IActivityDao + activityMapid = applyCurrentNamespace(id, false);ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration,id,type,resultMappings);ResultMap resultMap = inlineResultMapBuilder.build();configuration.addResultMap(resultMap);return resultMap;}}

在 MapperBuilderAssistant 映射构建器助手中,本章新增加了2个方法,构建 Mapping 方法 buildResultMapping、添加 ResultMap 方法 addResultMap。
而构建 buildResultMapping 方法就是在最开始 Mapper XML 映射构建器解析 buildResultMappingFromContext 所调用的 XMLMapperBuilder#buildResultMapping 方法,封装映射配置中 column、property 字段。
另外一个是 MapperBuilderAssistant#addResultMap 方法,从 ResultMapResolver 结果映射器调用添加 ResultMap,最终就是把这个配置保存到 Configuration 配置项中。
使用映射对象
从 DefaultSqlSession 调用方法,执行 SQL 后,就是对结果的封装,主要体现在 DefaultResultSetHandler#handleResultSets 结果收集器的操作中。

private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);boolean foundValues = false;final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();for (ResultMapping propertyMapping : propertyMappings) {final String column = propertyMapping.getColumn();if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {// 获取值final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();Object value = typeHandler.getResult(rsw.getResultSet(), column);// 设置值final String property = propertyMapping.getProperty();if (value != NO_VALUE && property != null && value != null) {// 通过反射工具类设置属性值metaObject.setValue(property, value);foundValues = true;}}}return foundValues;
}

从 DefaultResultSetHandler#handleResultSets 方法开始,调用 handleResultSet 方法,创建结果处理器、封装数据和保存结果。

那么在封装数据阶段,则包括了创建对象和封装对象属性,源码参考DefaultResultSetHandler#getRowValue
测试

CREATE TABLE activity  (id  bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',activity_id  bigint(20) NOT NULL COMMENT '活动ID',activity_name  varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '活动名称',activity_desc  varchar(128) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '活动描述',create_time  datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',update_time  datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (id ),UNIQUE KEY unique_activity_id (activity_id )
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='活动配置';

配置数据源

<environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&characterEncoding=utf8"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment>
</environments>

通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
在这里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。
配置Mapper加载方式

<mappers><!-- XML 配置 --><mapper resource="mapper/Activity_Mapper.xml"/>
</mappers>

配置Mapper XML 语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.bugstack.mybatis.test.dao.IActivityDao"><resultMap id="activityMap" type="cn.bugstack.mybatis.test.po.Activity"><id column="id" property="id"/><result column="activity_id" property="activityId"/><result column="activity_name" property="activityName"/><result column="activity_desc" property="activityDesc"/><result column="create_time" property="createTime"/><result column="update_time" property="updateTime"/></resultMap><select id="queryActivityById" parameterType="java.lang.Long" resultMap="activityMap">SELECT activity_id, activity_name, activity_desc, create_time, update_timeFROM activityWHERE activity_id = #{activityId}</select></mapper>

映射对象类

public class Activity {/*** 自增ID*/private Long id;/*** 活动ID*/private Long activityId;/*** 活动名称*/private String activityName;/*** 活动描述*/private String activityDesc;/*** 创建人*/private String creator;/*** 创建时间*/private Date createTime;/*** 修改时间*/private Date updateTime;// ... 省略 get/set}

单元测试

@Before
public void init() throws IOException {// 1. 从SqlSessionFactory中获取SqlSessionSqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));sqlSession = sqlSessionFactory.openSession();
}@Test
public void test_queryActivityById(){// 1. 获取映射器对象IActivityDao dao = sqlSession.getMapper(IActivityDao.class);// 2. 测试验证Activity res = dao.queryActivityById(100001L);logger.info("测试结果:{}", JSON.toJSONString(res));
}

测试结果
在这里插入图片描述
总结

结合着整个框架和 ResultMap 提前预留出来的参数解析框架,添加映射类参数的处理操作。在整个解析的过程中,一个 ResultMap 对应多个 ResultMapping 的关系,把每一条映射都存处理成 ResultMapping 信息,都存放到配置项中。前面我们就提到过 Configuration 伴随整个 session 生命周期
所有的解析操作完成以后就是到了接触处理封装中,这里你可以思考怎么把 SQL 执行的结果和对象封装到一起,普通的对象默认按照对象字段即可封装,而带有下划线的属性字段,则需要根据映射的2个字段,下划线对应非下划线的方式,进行匹配处理。最终返回统一的封装对象结果。

好了到这里就结束了手写手写mybatis之解析和使用ResultMap映射参数配置的学习,大家一定要跟着动手操作起来。需要源码的 可si我获取;

相关文章:

手写mybatis之解析和使用ResultMap映射参数配置

前言 学习源码是在学习什么呢&#xff1f; 就是为了通过这些源码级复杂模型中&#xff0c;学习系统框架的架构思维、设计原则和设计模式。在这些源码学习手写的过程中&#xff0c;感受、吸收并也是锻炼一种思维习惯&#xff0c;并尝试把这些思路技术迁移到平常的复杂业务设计开…...

LDR6500:低成本一拖二快充线解决方案

随着科技的飞速发展&#xff0c;我们的电子设备日益增多&#xff0c;从智能手机到平板电脑&#xff0c;再到各种可穿戴设备&#xff0c;它们已成为我们日常生活不可或缺的一部分。然而&#xff0c;随之而来的充电问题也日益凸显。为了解决这一难题&#xff0c;Type-C接口一拖二…...

DS线性表之单链表的讲解和实现(2)

文章目录 前言一、链表的概念二、链表的分类三、链表的结构四、前置知识准备五、单链表的模拟实现定义头节点初始化单链表销毁单链表打印单链表申请节点头插数据尾插数据头删数据尾删数据查询数据在pos位置之后插入数据删除pos位置之后的数据 总结 前言 本篇的单链表完全来说是…...

LeetCode 73 Set Matrix Zeroes 题目解析和python代码

题目&#xff1a; Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0’s. You must do it in place. Example 1: Input: matrix [[1,1,1],[1,0,1],[1,1,1]] Output: [[1,0,1],[0,0,0],[1,0,1]] Example 2: Input: matrix …...

鸿蒙--WaterFlow 实现商城首页

目录结构 ├──entry/src/main/ets // 代码区 │ ├──common │ │ ├──constants │ │ │ └──CommonConstants.ets // 公共常量类 │ │ └──utils │ │ └──Logger.ets // 日志打印类 │ ├──entryability │ │ └──EntryAbility.ets // 程序入口…...

QT 中如何保存matlab 能打开的.mat数据矩阵!

Windows 上安装并使用 MATIO 库来保存 MATLAB 格式的 .mat 文件&#xff0c;需要进行以下步骤&#xff1a; 1. 下载并安装 CMake MATIO 使用 CMake 构建项目&#xff0c;因此你需要先安装 CMake。 前往 CMake 官网下载适用于 Windows 的安装程序并安装。 2. 下载 MATIO 库源…...

菱形继承(多继承)

1. 什么是菱形继承 也就是多继承&#xff0c;C独有的特性。 2. 菱形继承有什么问题&#xff1f; &#xff08;1&#xff09;存在内存浪费&#xff0c;多存一份父类的父类。 &#xff08;2&#xff09;容易造成二义性&#xff08;不知道修改哪一个基本属性&#xff09;。 3. 如…...

【功能安全】什么是Aspice?

背景 如何设计开发一个符合功能安全的模块&#xff0c;大多都是按照Aspice的规范去做。所以理解Aspice就很重要。 什么是Aspice 英文全称&#xff1a;Automotive Software Process Improvement Capability dEtermanition ASPICE4.0文档 汽车软件过程改进及能力评定&#xf…...

基于SpringBoot的国家基础信息管理功能的设计与实现

目录 前言 一、标准信息参考 1、信息来源 二、后台基础信息的维护管理 1、实体类和Mapper类 2、业务层和控制层设计 3、前端界面实现 三、管理页面效果 1、列表管理界面 2、国家信息调整 四、总结 前言 在之前的博客中&#xff0c;我们基于GeoTools工具实现了全球各个…...

Python酷库之旅-第三方库Pandas(145)

目录 一、用法精讲 656、pandas.Timestamp.resolution属性 656-1、语法 656-2、参数 656-3、功能 656-4、返回值 656-5、说明 656-6、用法 656-6-1、数据准备 656-6-2、代码示例 656-6-3、结果输出 657、pandas.Timestamp.second属性 657-1、语法 657-2、参数 6…...

最懂生活的年轻人,都在喝十元奶茶

文 | 螳螂观察 作者 | 如意 以前的打工人&#xff0c;总把二三十的高价奶茶当成身份的象征&#xff0c;喝上了高价奶茶才能叫做在生活中富养自己。 只是&#xff0c;到盘开支的时候&#xff0c;打工人才猛然发觉&#xff0c;动辄二三十一杯的奶茶&#xff0c;不知不觉刮走了…...

MinIO 学习订阅服务

MinIO 的入门非常简单 — 只需几个简单的命令和一个 100 MB 的小二进制文件&#xff0c;您就可以立即启动并运行一个功能性开发环境。但是&#xff0c;为了在生产规模上利用 MinIO 的全部功能&#xff0c;我们鼓励专业人士更多地了解 MinIO 的广泛功能。我们推出了 MinIO 学习订…...

【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…...

用python做一个简单的画板

一&#xff0c;画板的介绍 画板&#xff08;Paint Board&#xff09;是一个提供用户绘图、涂鸦和创作的平台。现代数字画板通常是由软件程序实现的&#xff0c;具有多种功能。以下是画板的一些主要特征和功能&#xff1a; 1. 基本绘图工具 画笔和铅笔&#xff1a;用户可以选…...

根据传入的文件流链接实现前端下载

后端传入一个下载的url&#xff0c;实现点击按钮&#xff0c;下载文件。 方式一&#xff1a; 通过window.open(“URL”, _blank) 方式 PS&#xff1a;会打开一个新的页面 import React from react;const DownloadButton () > {// window.open("URL", "_…...

大数据新视界 --大数据大厂之大数据环境下的零信任安全架构:构建可靠防护体系

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...

基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于springboot的高校招生系统1拥有两种角色&#xff1a;管理员和用户 管理员&#xff1a;学生管理、专业管理、报名管理、录取通知管理、招生公告管理等 用户&#xff1a;登录注册、报…...

【C++设计模式】行为型模式:观察者模式

文章目录 行为型模式&#xff1a;观察者模式 行为型模式&#xff1a;观察者模式 观察者模式定义了一种一对多的依赖关系&#xff1a;它让一个主题&#xff08;被观察者&#xff09;对象关联多个观察者对象&#xff0c;并且当主题对象的状态发生变化时&#xff0c;它会主动通知…...

本篇5K,立志最细,FreeRtos中的信号量Semaphore教程详解!!!

前言&#xff1a;本篇教程&#xff0c;参考韦东山&#xff0c;开发文档&#xff0c;连接放在最后 目录 Semaphore基本概念 二值信号量&#xff08;Binary Semaphore&#xff09; 计数信号量&#xff08;Couting Semaphore&#xff09; 互斥信号量&#xff08;Mutex&…...

【Postman】接口测试工具使用

干就完啦 Postman发送get请求案例1&#xff1a; Postman发送post请求案例2 Postman发送其他请求Postman测试实战 学习目标&#xff1a;能够使用Postman发送get/post/put/delete请求并获取响应结果 Postman发送get请求 首先postman是一款接口调试工具&#xff0c;支持win&…...

springboot 整合 rabbitMQ(1)

目录 一、MQ概述 二、MQ的优势和劣势 三、常见的MQ产品 RabbitMQ使用步骤 第一步&#xff1a;确保rabbitmq启动并且可以访问15672 第二步&#xff1a;导入依赖 第三步&#xff1a;配置 auto自动确认 manual手工确认&#xff08;推荐使用&#xff01;可以防止消息丢失&a…...

Appium Device Farm安装教程

环境要求&#xff1a;Appium version ≥ 2.4.X 安装appium npm install -g appium2.11.3 如果安装提示如下问题 npm error code EEXIST npm error syscall rename npm error path /Users/wan/.npm/_cacache/tmp/d5787519 npm error dest /Users/wan/.npm/_cacache/content-…...

异常、基类

异常 人生和世界总是充满着意外&#xff0c;争议、冲突和战争似乎是人类必然经历的过程。程序执行也有不满的时候。 不同编程语言的异常 Ada/Modula-3是早期引入异常处理的语言。 C语言没有标准意义的异常&#xff0c;使用goto或setjmp模拟错误或异常发生时的处理流程。 C/Java…...

VScode 自定义代码配色方案

vscode是一款高度自定义配置的编辑器, 我们来看看如何使用它自定义配色吧 首先自定义代码配色是什么呢? 看看我的代码界面 简而言之, 就是给你的代码的不同语义(类名, 函数名, 关键字, 变量)等设置不同的颜色, 使得代码的可读性变强. 其实很多主题已经给出了定制好的配色方案…...

MuMu模拟器12 KitsumeMask安装教程

这里是引用"> 在MuMu模拟器上安装KitsumeMask的时候遇到安装失败的情况。 一、下载APK安装包 如果你没有apk安装包可以通过下面的百度网盘进行下载 通过网盘分享的文件:KitsumeMask 链接: https://pan.baidu.com/s/1yeq3I6BsUD7J6uI-bnk-Vw?pwd=7n3v 提取码: 7n3v 二…...

Perforce静态分析工具2024.2新增功能:Helix QAC全新CI/CD集成支持、Klocwork分析引擎改进和安全增强

Perforce Helix QAC和Klocwork的最新版本对静态分析工具进行了重大改进&#xff0c;通过尽早修复错误、降低开发成本和加快发布速度&#xff0c;使开发团队实现左移。 本文中&#xff0c;我们将概述2024.2版本的新特性和新功能。 CI/CD和左移以实现持续合规性 现代软件开发实…...

太阳能电池特性及其应用

中南民族大学-通信工程2024-大学物理下实验 目录 代码实现结果显示 &#x1f6e0;工具使用 MarsCode&#xff08;插件&#xff0c;集成在PyCharm&#xff09;&#xff1b; python编程&#xff08;豆包AI智能体&#xff09; &#x1f4bb;编程改进 此处是用「Matplotlib」来作图…...

日语学习零基础生活日语口语柯桥外语学校|股票用日语怎么说?

在日语中&#xff0c;“股票”可以说&#xff1a; • 株&#xff08;かぶ&#xff09; 这是最常用的表达方式&#xff0c;直接表示“股票”。 例如&#xff1a; 株を買う - 买股票 株を売る - 卖股票 • 株式&#xff08;かぶしき&#xff09; 这个词也是“股票”的意…...

第2关:寻找一个序列中的第K小的元素(即第k小元问题)

[TOC]寻找一个序列中的第K小的元素&#xff08;即第k小元问题&#xff09; 对于给定的含有n(n<100)元素的无序序列&#xff0c;求这个序列中第k&#xff08;1≤k≤n&#xff09;小的元素。 任务描述 本关任务&#xff1a;编写一个能计算数组中的第k小的元素的小程序。 相关…...

docker 搭建 vue3 + vite

vue3发布了,今天就分享一下我使用docker 搭建 vue3 vite 开发环境。至于为什么使用docker搭建&#xff0c;因为多版本可以快速切换&#xff0c;和本地环境避免冲突。好了话不多说我们开始吧。 1. 准备资料 Docker Desktop wsl2 ubuntu 下载地址 : https://www.docker.…...