MyBatis详细教程!!(入门版)
目录
什么是MyBatis?
MyBatis入门
1)创建工程
2)数据准备
3)配置数据库连接字符串
4)写持久层代码
5)生成测试类
MyBatis打印日志
传递参数
MyBatis的增、删、改
增(Insert)
删(Delete)
改(Update)
查(Select)
使用XML方式
增删改查
增(Insert)
删(Delete)
改(Update)
查(Select)
补充:MySQL开发规范
什么是MyBatis?
MyBatis是一款持久层框架,用于简化JDBC开发
持久层:持久化操作的层,通常指数据访问层(DAO),是用来操作数据库的
MyBatis入门
- 准备工作(创建springboot工程、数据库表准备、实体类)
- 引入MyBatis的相关依赖,配置MyBatis(数据库连接信息)
- 编写SQL语句(注解/XML)
- 测试
1)创建工程
创建springboot工程,并引入MyBatis、MySQL依赖
<!--Mybatis 依赖包-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.1</version>
</dependency>
<!--mysql驱动包-->
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId>
</dependency>
MyBatis是一个持久层框架,具体的数据库存储和数据操作还是在MySQL中操作的
2)数据准备
创建用户表
DROP DATABASE IF EXISTS mybatis_test; CREATE DATABASE mybatis_test;
DEFAULT CHARACTER SET utf8mb4;
USE mybatis_test;
DROP TABLE IF EXISTS userinfo;
CREATE TABLE `userinfo` ( `id` INT ( 11 ) NOT NULL AUTO_INCREMENT, `username` VARCHAR ( 127 ) NOT NULL, `password` VARCHAR ( 127 ) NOT NULL, `age` TINYINT ( 4 ) NOT NULL, `gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-⼥ 0-默认', `phone` VARCHAR ( 15 ) DEFAULT NULL, `delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除', `create_time` DATETIME DEFAULT now(), `update_time` DATETIME DEFAULT now(), PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone ) VALUES ( 'admin', 'admin', 18, 1, '18612340001' ); INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone ) VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' ); INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone ) VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' ); INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone ) VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );
创建对应实体类UserInfo
(注意实体类中的属性名与表中的字段名一一对应)
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {private Integer id;private String username;private String password;private Integer age;private Integer gender;private String phone;private Integer deleteFlag;private Date createTime;private Date updateTime;
}
3)配置数据库连接字符串
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: root #改为你的数据库密码driver-class-name: com.mysql.cj.jdbc.Driver
4)写持久层代码
创建持久层接口UserInfoMapper
@Mapper
public interface UserInfoMapper {@Select("select username,`password`,age,gender,phone from userinfo")public List<UserInfo> queryAllUser();
}
Mybatis的持久层接⼝规范⼀般都叫XxxMapper
@Mapper注解:表⽰是MyBatis中的Mapper接⼝
程序运⾏时,框架会⾃动⽣成接⼝的实现类对象(代理对象),并给交Spring的IOC容器管理
5)生成测试类
在需要测试的Mapper接口中,右键->Generate->Test
书写测试代码
@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Testvoid queryAllUser() {List<UserInfo> userInfoList=userInfoMapper.queryAllUser();System.out.println(userInfoList);}
}
注解@SpringBootTest,该测试类在运行时,就会自动加载Spring的运行环境
MyBatis打印日志
通过日志看到sql语句的执行、执行传递的参数以及执行结果
mybatis:configuration: # 配置打印 MyBatis⽇志log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
①:查询语句
②:传递参数及类型
③:SQL执行结果
传递参数
@Mapper
public interface UserInfoMapper {@Select("select username,`password`,age,gender,phone from userinfo where id=#{id}")public List<UserInfo> queryAllUser(Integer id);
}
@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Testvoid queryAllUser() {List<UserInfo> userInfoList=userInfoMapper.queryAllUser(4);System.out.println(userInfoList);}
}
MyBatis的增、删、改
增(Insert)
(Mapper中)
@Mapper
public interface UserInfoMapper {@Options(useGeneratedKeys = true,keyProperty = "id")@Insert("insert into userinfo(username,`password`,age,gender,phone) values (#{username},#{password},#{age},#{gender},#{phone})")public Integer insert(UserInfo userInfo);}
(MapperTest中)
@SpringBootTest
class UserInfoMapperTest {@Testvoid insert() {UserInfo userInfo=new UserInfo();userInfo.setAge(10);userInfo.setGender(2);userInfo.setUsername("赵六");userInfo.setPhone("123456");userInfo.setPassword("123");userInfoMapper.insert(userInfo);}
}
注意:如果使用了@Param属性来重命名,#{...}需要使用“参数.属性”来获取
@Insert("insert into userinfo(username,`password`,age,gender,phone) values (#{userinfo.username},#{userinfo.password},#{userinfo.age},#{userinfo.gender},#{userinfo.phone})")
public Integer insert(@Param("userinfo") UserInfo userInfo);
Insert语句返回值是收影响的行数
有些情况下我们需要获得新插入数据的id,此时使用@Options注解
@Options(useGeneratedKeys = true,keyProperty = "id")@Insert("insert into userinfo(username,`password`,age,gender,phone) values (#{username},#{password},#{age},#{gender},#{phone})")public Integer insert(UserInfo userInfo);
@Test
void insert() {UserInfo userInfo=new UserInfo()userInfo.setAge(10);userInfo.setGender(2);userInfo.setUsername("赵六");userInfo.setPhone("123456");userInfo.setPassword("123");Integer count = userInfoMapper.insert(userInfo);//设置useGeneratedKeys=true后,方法返回值仍然是受影响行数System.out.println("添加数据条数:" +count +",数据id:"+userInfo.getId());
}
useGeneratedKeys:令MyBatis使⽤JDBC的getGeneratedKeys⽅法来取出由数据库内部⽣成的主键,默认值:false.
keyProperty:这指定了实体类中用于存储数据库生成的主键的属性的名称,当 MyBatis 从数据库获取到新生成的主键后,它会将这个值设置到实体类的
id
属性中。
删(Delete)
@Delete("delete from userinfo where id=#{id}")public void delete(Integer id);
@Test
void delete() {userInfoMapper.delete(4);
}
改(Update)
@Update("update userinfo set username=#{username} where id=#{id}")
public void update(String username,Integer id);
@Test
void update() {userInfoMapper.update("王五",3);
}
查(Select)
上述查询中,我们发现,只有在java对象属性和数据库字段名一样(忽略大小写)时,才会进行赋值
(java对象中deleteFlag、createTime、updateTime属性与数据库中字段delete_flag、create_time、update_time对应不上)
解决办法
①起别名
@Select("select id, username, `password`, age, gender, phone, delete_flag as
deleteFlag,create_time as createTime, update_time as updateTime from userinfo")
public List<UserInfo> queryAllUser();
②结果映射
@Select("select id, username, `password`, age, gender, phone, delete_flag,
create_time, update_time from userinfo")
@Results({@Result(column = "delete_flag",property = "deleteFlag"),@Result(column = "create_time",property = "createTime"),@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();
可以给Results定义一个名称,使其他sql也能复用这个映射关系
@Select("select id, username, `password`, age, gender, phone, delete_flag,
create_time, update_time from userinfo")
@Results(id = "resultMap",value = {@Result(column = "delete_flag",property = "deleteFlag"),@Result(column = "create_time",property = "createTime"),@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();
@Select("select id, username, `password`, age, gender, phone, delete_flag,
create_time, update_time " +"from userinfo where id= #{userid} ")
@ResultMap(value = "resultMap")
UserInfo queryById(@Param("userid") Integer id);
③开启驼峰命名(推荐)
数据库中字段通常使用蛇形命名,java属性通常使用驼峰命名,可以通过配置使得这两种命名方式自动映射(abc_xyz => abcXyz)
mybatis:configuration:map-underscore-to-camel-case: true #配置驼峰⾃动转换
MyBatis有使用注解和XML两种方式
下面,我们介绍
使用XML方式
①配置数据库连接字符串
#数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: root //改成你自己的密码driver-class-name: com.mysql.cj.jdbc.Driver
#配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
mybatis:mapper-locations: classpath:mapper/**Mapper.xml
②写持久层代码
1)方法定义Interface
@Mapper
public interface UserInfoXMLMapper {List<UserInfo> queryAllUser();
}
2)方法实现:UserInfoXMLMapper.xml(路径参考yml中的配置 mapper-locations: classpath:mapper/**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="com.example.sql.mapper.UserInfoMapper"><select id="queryAllUser" resultType="com.example.sql.UserInfo">select username,`password`,age,gender,phone from userinfo</select>
</mapper>
- <mapper>标签:需要指定namespace 属性,表⽰命名空间,值为mapper接⼝的全限定 名,包括全包名.类名。
- <select>查询标签:是⽤来执⾏数据库的查询操作的:
◦id :是和Interface (接⼝)中定义的⽅法名称⼀样的,表⽰对接⼝的具体实现⽅法。
◦ resultType :是返回的数据类型,也就是开头我们定义的实体类
单元测试
@SpringBootTest
public class UserInfoXMLMapperTest {@Autowiredprivate UserInfoXMLMapper userInfoXMLMapper;@Testvoid queryAllUser(){List<UserInfo> userInfoList=userInfoXMLMapper.queryAllUser();System.out.println(userInfoList);}}
增删改查
增(Insert)
@Mapper
public interface UserInfoXMLMapper {Integer insertUser(UserInfo userInfo);
}
<?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="com.example.sql.mapper.UserInfoXMLMapper"><insert id="insertUser" useGeneratedKeys="true" keyProperty="id">insert into userinfo (username,`password`,age,gender,phone) values(#{username},#{password},#{age},#{gender},#{phone})</insert>
</mapper>
@SpringBootTest
public class UserInfoXMLMapperTest {@Autowiredprivate UserInfoXMLMapper userInfoXMLMapper;@Testvoid insertUser(){UserInfo userInfo=new UserInfo();userInfo.setAge(10);userInfo.setGender(2);userInfo.setUsername("赵七");userInfo.setPhone("123457");userInfo.setPassword("123");Integer count=userInfoXMLMapper.insertUser(userInfo);System.out.println(count);}
}
使用@Param设置参数名称与注解类似
Integer insertUser(@Param("userinfo") UserInfo userInfo);
<insert id="insertUser">insert into userinfo (username, `password`, age, gender, phone) values(#{userinfo.username},#{userinfo.password},#{userinfo.age},#
{userinfo.gender},#{userinfo.phone})
</insert>
返回自增id
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">insert into userinfo (username, `password`, age, gender, phone) values(#{userinfo.username},#{userinfo.password},#{userinfo.age},#
{userinfo.gender},#{userinfo.phone})
</insert>
删(Delete)
UserInfoXMLMapper接⼝
Integer deleteUser(Integer id);
UserInfoXMLMapper.xml实现:
<delete id="deleteUser">delete from userinfo where id = #{id}
</delete>
改(Update)
UserInfoXMLMapper接⼝:
Integer updateUser(UserInfo userInfo);
UserInfoXMLMapper.xml实现:
update id="updateUser"> update userinfo set username=#{username} where id=#{id} update>
查(Select)
与注解方式类似,查找操作也涉及到java对象和数据库字段命名问题
解决办法
①起别名
②结果映射
③开启驼峰命名
(①③与注解一样),下面介绍结果映射
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo"><id column="id" property="id"></id><result column="delete_flag" property="deleteFlag"></result><result column="create_time" property="createTime"></result><result column="update_time" property="updateTime"></result>
</resultMap>
<select id="queryAllUser" resultMap="BaseMap">select id, username,`password`, age, gender, phone, delete_flag,
create_time, update_time from userinfo
</select>
开发中,建议简单sql使用注解方式,复杂sql(如动态sql)使用xml方式
注解方式和xml方式可以一起使用
补充:MySQL开发规范
①表名、字段名使⽤⼩写字⺟或数字,单词之间以下划线分割.尽量避免出现数字开头或者两个下划线,中间只出现数字.数据库字段名的修改代价很⼤,所以字段名称需要慎重考虑。
MySQL在Windows下不区分⼤⼩写,但在Linux下默认是区分⼤⼩写.
因此,数据库名、表名、字段名都不允许出现任何⼤写字⺟,避免节外⽣枝
②表必备三字段:id,create_time,update_time,id必为主键,类型为bigintunsigned,create_time,update_time的类型均为datetime类型,create_time表⽰创建时间, update_time表⽰更新时间
③在表查询中,避免使⽤*作为查询的字段列表,标明需要哪些字段
1. 增加查询分析器解析成本
2. 增减字段容易与resultMap配置不⼀致
3. ⽆⽤字段增加⽹络消耗,尤其是text类型的字段
相关文章:

MyBatis详细教程!!(入门版)
目录 什么是MyBatis? MyBatis入门 1)创建工程 2)数据准备 3)配置数据库连接字符串 4)写持久层代码 5)生成测试类 MyBatis打印日志 传递参数 MyBatis的增、删、改 增(Insert࿰…...
c++ using 关键字
在C中,using 关键字有多种用途,但最常见的用途之一是在命名空间(namespace)中引入名称,以避免在代码中频繁使用命名空间前缀。此外,using 还可以用于类型别名(typedef 的替代品)和模…...

AIGC时代算法工程师的面试秘籍(2024.4.29-5.12第十三式) |【三年面试五年模拟】
写在前面 【三年面试五年模拟】旨在整理&挖掘AI算法工程师在实习/校招/社招时所需的干货知识点与面试方法,力求让读者在获得心仪offer的同时,增强技术基本面。也欢迎大家提出宝贵的优化建议,一起交流学习💪 欢迎大家关注Rocky…...

Docker Portainer使用
Portainer是什么 Docker Portainer是一个轻量级的 Web UI 管理界面,可以用来管理Docker环境。它提供了一个直观的控制台,用户可以通过它来管理Docker主机、容器、网络、卷等Docker资源。 Portainer的主要功能和特点包括: 容器管理:可以查看、启动、停止、删除容器,以及查看容器…...
创新系列-既要保留<a/>标签右键功能, 又不要href导致点击页面刷新, 希望click实现vue-router跳转
发布时间:2024/05/22 如果您有适合我的项目机会给到我,这是我的简历:Resume 思路: 思路原理:实践发现href为null或者" "的时候是不起作用的 将href属性绑定的固定路径设置为响应式数据变量a,a初…...
【OceanBase诊断调优】—— KVCache 排查手册
原文链接:OceanBase分布式数据库-海量数据 笔笔算数 本文介绍 KVcache 相关问题的排查方法。 KVCache 相关概念 在进行排查前,需要了解几个概念。 pin 一个 cache 块 ( memblock ) 被 pin 住,表示它正在被引用。 cache 的由多个定长的块组成…...

核函数的介绍
1.核函数的介绍: 1、用线性核等于没有用核。 2、多项式核:随着d越大,则 fai(X) 对应的维度将越高。(可以通过d得到对应的fai(X)函数)。 3、高斯核函数:无限维度。 4、tanh核。 2.如何选择核函数的参数&am…...
使用pytorch写一个简单的vae网络用于生成minist手写数字图像
文章目录 代码结果代码 import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision impo...
Windows平台让标准输入(stdin)的阻塞立即返回
文章目录 背景介绍代码示例版本1-基本命令处理版本2-多线程命令处理,不阻塞主函数版本3-即使没有用户输入,也能立即退出 背景介绍 在开发命令行工具或控制台应用程序时,经常需要处理用户输入。常规做法是使用标准输入(stdin&…...

Spring中的Aware接口
Spring中的Aware接口 Aware接口介绍 Aware是Spring中的接口,它的作用是可以让Bean获取到运行环境的相关信息。比如获取到上下文、Bean在容器中的名称等。 Spring中提供了很多Aware接口的子类,具体如下: 常用接口的作用如下: …...
FFmpeg滤镜完整列表
FFmpeg滤镜完整列表 滤镜名称 用途 acompressor 压缩音频信号,当输入信号超过某个预设阈值时,压缩器就会开始工作。该滤镜使音量大的部分变得不那么响亮,而音量小的部分相对变得响亮,这样就可以使整体听起来更加均衡,常用于音乐…...

深入探索Python基础:两个至关重要的函数
新书上架~👇全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一、初学者的基石:print与input函数 二、类型转换:从字符串到浮点数…...

探索集合python(Set)的神秘面纱:它与字典有何不同?
新书上架~👇全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一、集合(Set)与字典(Dictionary)的初识 1. …...

火山引擎“奇袭”阿里云
图片|电影《美国队长3》剧照 ©自象限原创 作者丨程心 编辑丨罗辑 大模型价格战,已经不是什么新闻。 从OpenAI发布GPT-4o,将API价格下调50%,并宣布面向普通用户免费开始,就标志着大模型的竞争从性能进入到了成本…...

牛客网刷题 | BC94 反向输出一个四位数
目前主要分为三个专栏,后续还会添加: 专栏如下: C语言刷题解析 C语言系列文章 我的成长经历 感谢阅读! 初来乍到,如有错误请指出,感谢! 描述 将一个四位数&…...

记一次MySQL执行修改语句超时问题
异常问题 原因分析 这个问题发生在开发环境,怀疑是提交事务时终止项目运行,没有提交该事务,造成死锁 调试该事务时时间太长,为什么说有这个原因呢,因为通过查找日志显示 The client was disconnected by the server …...
linux fork()函数调用原理
在Linux中,fork函数用于创建一个新的进程,该进程是调用进程的子进程。fork函数的实现涉及用户态和内核态之间的交互。下面我将详细说明fork函数在代码流程中的原理和用户态与内核态的交互过程。 用户态调用fork函数: 用户程序调用fork函数,该函数是libc库提供的一个封装函数…...
【电控笔记5.9】编码器脉冲计算速度MT法
总结 编码器的脉冲计算速度可以使用多种方法,其中一种常用的方法是“MT法” (Measuring Time Method),即测量时间法。该方法通过测量编码器脉冲间的时间来计算速度。这种方法在高精度速度测量中非常有效,特别是在速度较低时。 MT法计算速度的基本原理 MT法的基本原理是通过…...

go-zero 实战(4)
中间件 在 userapi 项目中引入中间件。go项目中的中间可以处理请求之前和之后的逻辑。 1. 在 userapi/internal目录先创建 middlewares目录,并创建 user.go文件 package middlewaresimport ("github.com/zeromicro/go-zero/core/logx""net/http&q…...
go语言泛型Generic最佳实践 --- slices包
在go的内置包slices中, 所有的函数函数都使用了泛型, 各种各样的泛型, 可以说这个包绝对是go语言泛型学习的最佳实践之一! 来,先来瞄一眼,看看这个slices包里面的函数原型定义: func BinarySe…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
mongodb源码分析session执行handleRequest命令find过程
mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程,并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令,把数据流转换成Message,状态转变流程是:State::Created 》 St…...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...

2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...

ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill
视觉语言模型(Vision-Language Models, VLMs),为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展,机器人仍难以胜任复杂的长时程任务(如家具装配),主要受限于人…...
上位机开发过程中的设计模式体会(1):工厂方法模式、单例模式和生成器模式
简介 在我的 QT/C 开发工作中,合理运用设计模式极大地提高了代码的可维护性和可扩展性。本文将分享我在实际项目中应用的三种创造型模式:工厂方法模式、单例模式和生成器模式。 1. 工厂模式 (Factory Pattern) 应用场景 在我的 QT 项目中曾经有一个需…...
WEB3全栈开发——面试专业技能点P7前端与链上集成
一、Next.js技术栈 ✅ 概念介绍 Next.js 是一个基于 React 的 服务端渲染(SSR)与静态网站生成(SSG) 框架,由 Vercel 开发。它简化了构建生产级 React 应用的过程,并内置了很多特性: ✅ 文件系…...

算术操作符与类型转换:从基础到精通
目录 前言:从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符:、-、*、/、% 赋值操作符:和复合赋值 单⽬操作符:、--、、- 前言:从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...
深入浅出WebGL:在浏览器中解锁3D世界的魔法钥匙
WebGL:在浏览器中解锁3D世界的魔法钥匙 引言:网页的边界正在消失 在数字化浪潮的推动下,网页早已不再是静态信息的展示窗口。如今,我们可以在浏览器中体验逼真的3D游戏、交互式数据可视化、虚拟实验室,甚至沉浸式的V…...