三周搞定一个新项目(实际全部大框架写完花费将近11天,后续事件会不断学习细化)
Yin-Hongwei/music-website: 🎧 Vue + SpringBoot + MyBatis 音乐网站 (github.com) (该项目作者拒绝商用,仅供学习!)
git:IDEA导入Git中项目_idea如何导入git工程_asdfghjklor的博客-CSDN博客
建表
软件是Navicat,注意navicat的中文字符需要
共计 表:
创建项目
IJ:选择Spring Initializr
笔者因为是交到企业的项目,在组的名字有所修改,无需要的小伙伴可不修改
ps:Spring Initialize和Maven区别?
Spring Initialize在新建项目时可以直观地勾选依赖,在创建项目时直接导入;而Maven可以在自己导入需要的依赖,适合网络差,对依赖非常熟悉的开发者使用。
各个配置文件
-
src/main/java:主程序入口
-
src/mian/resources:配置目录,服务端口、应用名字、数据库配置文件等等。还有static和templates目录,前者放静态资源:图片、CSS、JavaScript等;后者放Web页面的模板文件
-
src/test:单元测试
-
application.properties/application.yml用于存放程序的各种依赖模块的配置信息
-
application.properties 用于连接数据库
-
pom.xml 配置项目相关依赖和插件
-
generatorConfig.xml
相关项目练习: generatorConfig.xml - 掘金 (juejin.cn)
一些可能出错的点
springboot 3.0之后都要求jdk17以上。如果是jdk8的,下图的版本位置改为任意8.0以下版本即可。
项目结构
- model包括实体类(vo)、request
- Mapper层 :向数据库发送SQL语句,完成数据库操作;分为Mapper接口和Mapper接口映射文件(因为不能直接定义操作数据库函数);Mapper接口映射文件在mapper目录下,完成对数据库的访问
- Service层 : 服务层,完成业务逻辑处理,调用Mapper层操作数据库;分为Service接口和Service实现(面向接口的思想,方便后续解耦及扩展)
- Controller层:控制层,对请求和响应进行控制,调用Service进行逻辑处理,将最后结果返回前端。
写好这个项目的预备重点知识
BaseMapper方法(Mybatis-Plus)
Mapper继承该接口后,无需编写mapper.xml文件,即可获CRUD功能
包括
- 插入一条记录
- 根据主键id删除、根据columnMap条件删除(字段map对象)、根据entity条件删除(实体对象,可以是null)、根据id批量删除
- 根据主键id更新、根据whereEntity更新
- 根据主键id查询、根据id批量查询、根据columnMap条件查询、根据entity查询一条记录、根据wrapper条件,查询总记录数、查询全部记录
- 查询并翻页
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteById(T entity);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> queryWrapper);
int deleteBatchIds(@Param("coll") Collection<?> idList);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {
List<T> ts = this.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(ts)) {
if (ts.size() != 1) {
throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records", new Object[0]);
} else {
return ts.get(0);
}
} else {
return null;
}
}
default boolean exists(Wrapper<T> queryWrapper) {
Long count = this.selectCount(queryWrapper);
return null != count && count > 0L;
}
Long selectCount(@Param("ew") Wrapper<T> queryWrapper);
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper);
}
IService方法(Mybatis-Plus)
需要BaseMapper和ServiceImpl的配合
public interface UserMapper extends BaseMapper<User> {
}
public interface UserService extends IService<User> {
}
//继承basemapper的UserMapper和实体类User
public class UserServiceImpl extends UserImpl<UserMapper,User> implements UserService {}
public class UserController{
@Autowired
UserService userService;
void a(){
User u = userService.某方法();
}
}
思路1
以往数据库增加一条记录为例
-
通过输入网址进入Controller层的addUser方法里
-
进入Service层接口的addUser方法(该接口继承了IService)
-
实际代码写在实现类里
- 判断注册账号名字是否已存在,存在返回提示信息,不存在进行下面步骤
- 新建一个用户类,对其进行复制(
BeanUtils.copyProperties(source, d))、密码md5加密、判断号码、邮箱是否为空、设置初始头像 - 调用Mapper的插入操作
-
进入继承了
BaseMapper<User>的Mapper层,执行操作的是继承了添加实体类方法的UserMapper的添加User方法
generatorConfig.xml生成代码后续完善 - 掘金 (juejin.cn)
思路2
完全不用generatorConfig.xml的思路
预备知识
Serializable序列化
一个类只有实现序列化接口,它的对象才能被序列化
序列化:将对象状态转换为可保持或传输的格式的过程。搭配反序列化(将流转换为对象),可以轻松地存储和传输数据。
用法:实体类实现Serializable接口
handler处理器
MyBatis-Plus自定义填充处理器 主要用于创建时间 修改更新时间等 再扩展可以添加人,修改人
热部署
Redis
在另一篇文章中有写到,这里简要讲一句:持久化数据,即将数据缓存下来,使得暂时无网条件下用户仍然有多一些耐心等待
- pom.xml
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.0.4</version>
</dependency>
- 创建类RedisConfig 继承 CachingConfigurerSupport
@EnableCaching //开启缓存注解
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
- 在对应Mapper接口中使用注解 达到优先查询redis中的缓存数据的结果,如果redis缓存对应不存在,则请求mysql查询数据,并将数据存储在redis中。
例如
@MyCacheable
public User findById(Integer id) {
System.out.println("find user by id: " + id);
User user = new User();
user.setId(id);
user.setName("Name" + id);
return user;
}
common-pool2 连接池管理类
降低 数据库连接、网络IO、大数据对象 时需要的时间
下次使用时,不再重新创建,直接从对象池缓存中取即可
common-pool2的介绍和使用_common pool2可视化页面_Tiffany_J的博客-CSDN博客
前端Vue
安装并创建项目
- 下载Download | Node.js (nodejs.org) 选择长期维护的windows版本即可
- 直接用官方提供的脚手架,运行如下命令全局安装vue
npm install -g @vue/cli
- 查看是否安装成功
node -v
npm -v
全部都有后进入下一步
笔者有过报错情况处理方式:
- 运行
npm cache clean --force将缓存文件清理 - 用管理者模式打开cmd
- 因为我们之前写的是后端(服务端),所以我们再创建一个包将后端代码都放进去
进到我们新创建的包下后(music-client即为我们新创建客户端的名字)
vue create music-client
- 进入新创建的服务端
npm run serve
- 输入查看对应网址
预备知识
axios
基于Promise的ajax封装库,例如 发送get、post请求,是一个轻量级的库,使用时可直接引入。
- 特点:异步的ajax请求库
- 在浏览器端和 node 端都可以使用。
- 支持 Promise API。
- 支持请求和响应拦截。
- 响应数据自动转换 JSON 数据。
- 支持请求取消。
- 可以批量发送多个请求。
- 客户端支持安全保护,免受 XSRF 攻击。
前后端数据交互(五)——什么是 axios? - 掘金 (juejin.cn)
跨源资源共享(CORS)
基于HTTP头的机制,该机制通过允许服务器标识除了它自己以外的其他源(域、协议、接口),使得浏览器允许这些源访问加载自己的资源。
跨域资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求。
vuex 插件来对数据进行管理(sessionStorage)
vuex 是没有持久化存储的,刷新之后需要重新从sessionStorage里面获取数据,所以我们需要监听页面刷新这一个动作,来达到刷新的时候,vuex里面的能自动获取sessionStorage里面的数据重新给自己赋值一遍。
// 为了防止刷新时vuex的数据丢失
if (sessionStorage.getItem("XXXXX") ) {
this.$store.replaceState(Object.assign({}, this.$store.state,JSON.parse(sessionStorage.getItem("XXXXX"))))
}
//在页面刷新时将vuex里的信息保存到sessionStorage里
// pagehide为了兼容ios
window.addEventListener("pagehide",()=>{
sessionStorage.setItem("XXXXX",JSON.stringify(this.$store.getters))
})
vuex配sessionStorage进行自动存储,解决刷新数据丢失的问题。_vuex中的内容是自动存到sessionstorage中吗_oduoke~~的博客-CSDN博客
Vue前端框架没有html格式的界面?
vue是单页应用程序,和一般的网站有些区别,一般网站是很多html页面构成的,页面跳转是去请求一个新的html页面,但是单页应用程序的话它只有一个html页面,vue是自己生成一个vue实例,然后挂到了当前的那个html页面里,看到的切换页面其实是切换组件。如果用的是官方的cli的话那个html页面是项目里的index.html。
ESLint
ESLint 是一种用于识别和报告在 ECMAScript/JavaScript 代码中发现的模式的工具,其目标是使代码更加一致并避免错误
配置文件 - ESLint 中文文档 (nodejs.cn)
browserslistrc
在使用脚手架搭建项目时,会自动生成.browserslistrc文件,该文件主要是 配置兼容浏览器
vue中的.browserslistrc 文件? - 掘金 (juejin.cn)
管理端
参考客户端创建(省略下载、安装脚手架步骤)
注意点:因为客户端默认启动端口是8080,所以我们需要先关闭客户端,再搭建管理端
改端口
- 临时改端口号
进入music-manage包下(不需要改serve 它不是名字)
npm run serve -- --port 8081
//输入http://localhost:8081/可查看
- 每次重启都为固定端口号
vue.config.js文件中
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
//更改默认端口
devServer:{
open: false,//自动打开浏览器
port:8081,
}
})
遇到了一个奇怪的报错
找了非常久才找到正确的方案:
解决方案:Angular 13 升级 - 错误:未知关键字格式最小 - Trung Vo (trungk18.com)
简单来说
npm ls ajv-keywords
查找到对应出错的提示(版本不一样)
npm update
更新版本后重新运行serve即可
一些零碎
- 8.0以上需要加cj
- 前端不同的组件有时候会用到相同的方法,当要修改方法时就要改好多地方,所以把它们单独拿出来放到 mixins 文件夹下面,当用到这些方法的时候在对应组件中引入即可。
- vue 支持很多的第三方组件,能给我们项目带来很好的交互和显示效果,具体在需要的时候引入就行了,当然了,一些样式和图片也可以放到 assets 文件夹下管理。
- 前端练习组件的封装很有帮助
- 服务端和客户端都是vue框架的情况下
node_modules都是可以直接复制过去的,不会出现问题 ../是返回上一级,@/是scr
参考来自:
Mybatis-Plus 之BaseMapper 方法详解 - 堇墨傾城 - 博客园 (cnblogs.com)
Spring boot配置文件书写规范(上) - 掘金 (juejin.cn)
npm run serve自定义端口和指定临时端口_npm run serve --port_欲饮琵琶码上催的博客-CSDN博客
推荐阅读: