趁着寒假充实一下技能…
顺便更新一下自己的技术栈Springboot3+Vue3+MybatisPlus+Vite+TS+ElementPlus
路由守卫
import { jwtDecode } from 'jwt-decode';
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) { // 检查路由是否需要登录认证
const token = localStorage.getItem('token');
const isAuthenticated = token !== null;
if (isAuthenticated) {
// 如果本地存储中存在 token,则解析 token 并提取过期时间
try {
const decodedToken = jwtDecode(token);
const expirationDate = decodedToken.exp * 1000; // 将过期时间转换为毫秒
if (Date.now() < expirationDate) {
next(); // 如果 token 有效,则继续导航
} else {
next('/login'); // 如果 token 已过期,则重定向到登录页面
}
} catch (error) {
console.error('Token decoding failed:', error);
next('/login'); // 如果 token 无效或解码失败,则重定向到登录页面
}
} else {
next('/login'); // 如果用户未登录,则重定向到登录页面
}
} else {
next(); // 如果路由不需要登录认证,则继续导航
}
});
Mybatis plus
首先禁用所有的名称转换方法,以保持数据库字段名与实体类字段名完全一致:
mybatis-plus.configuration.map-underscore-to-camel-case=false
在典型的 Spring Boot 应用程序中,通常会遵循 MVC(Model-View-Controller)架构模式。在这个模式中,Controller
层负责处理用户请求,Service
层处理业务逻辑,而 Mapper
或 Repository
层负责数据访问。下面是这些层级的一般职责:
- Controller:接收客户端发送的请求,调用 Service 层的方法处理这些请求,然后返回响应。Controller 层是请求的入口点。
- Service:包含业务逻辑,通常会调用 Mapper/Repository 来访问数据库。Service 层位于 Controller 和 Mapper/Repository 层之间,起到了协调它们的作用。
- Mapper/Repository:直接与数据库交互,执行 CRUD 操作(创建、读取、更新、删除)。在 MyBatis 中,Mapper 接口定义了与数据库交互的方法。
这里放一个实例:
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
@Data
@TableName("comweb_users") //指定需要操作的表名
public class Users {
@TableId // 默认是不会自动生成主码且主码不需要注名
private String userName;
// @TableField("passwd") 指定需要操作的字段,如果不写这一行默认是按照下方定义的字段去匹配,写了可以指定一个不同于字段名的列名,不需要指定长度由数据库自己决定
private String passwd;
private String ywProf;
private String asset;
// 省略 getter 和 setter 方法
// userName = models.CharField(max_length=10, primary_key=True)
// passwd = models.CharField(max_length=10, null=True, blank=True)
// ywProf = models.CharField(max_length=16, null=True, blank=True) # 专业组名字
// asset = models.CharField(max_length=10, null=True, blank=True) # 权限
}
在 MyBatis Plus 中,BaseMapper
提供了一些基本的 CRUD 方法,但它不会自动生成特定于字段的查询方法,如 selectByUserNameAndPasswd
。这种方法需要您自行实现,要么通过定义 XML 映射文件,要么通过使用 MyBatis Plus 提供的注解或者使用 QueryWrapper 来构建查询。
如果您希望 MyBatis Plus 自动生成这样的方法,您需要使用 @Select
注解或者使用 MyBatis Plus 的 IService
和 ServiceImpl
来扩展基本的 CRUD 功能。但请注意,即使使用了 IService
和 ServiceImpl
,也不会自动生成特定的方法,如 selectByUserNameAndPasswd
。您仍然需要自定义这样的方法。
1. 使用 QueryWrapper
自定义查询:
如果您不想写额外的注解或 XML 映射,可以在服务层使用 QueryWrapper
来构建自定义查询条件。这样,您就可以利用 BaseMapper
提供的 selectOne
方法来执行查询,如前面的示例所示。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.yourpackage.entity.Users;
import com.example.yourpackage.mapper.UsersMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UsersService {
@Autowired
private UsersMapper usersMapper;
public Users getUserByUserNameAndPasswd(String userName, String passwd) {
// 创建 QueryWrapper 实例
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
// 设置查询条件
queryWrapper.eq("userName", userName).eq("passwd", passwd);
// 执行查询
return usersMapper.selectOne(queryWrapper);
}
}
对应mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.yourpackage.entity.Users;
public interface UsersMapper extends BaseMapper<Users> {
// 这里不需要添加任何额外的方法,因为BaseMapper已经提供了selectOne等CRUD方法
}
对应controller
import com.example.yourpackage.entity.Users;
import com.example.yourpackage.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UsersController {
@Autowired
private UsersService usersService;
@GetMapping("/login")
public Users getUserByUserNameAndPasswd(@RequestParam String userName, @RequestParam String passwd) {
return usersService.getUserByUserNameAndPasswd(userName, passwd);
}
}
2. 使用注解自定义 SQL:
如果您希望在 Mapper 接口中定义自定义方法并使用注解提供 SQL,您可以这样做:
public interface UsersMapper extends BaseMapper<Users> {
@Select("SELECT * FROM comweb_users WHERE userName = #{userName} AND passwd = #{passwd}")
Users selectByUserNameAndPasswd(@Param("userName") String userName, @Param("passwd") String passwd);
}
@Select("<script>" +
"SELECT * FROM comweb_users WHERE " +
"<foreach collection='filterOptions' item='option' separator=' OR '>" +
" <foreach collection='option.filterValues' item='filter' separator=' OR '>" +
" ${option.columnName} = #{filter}" +
" </foreach>" +
"</foreach>" +
"</script>")
List<Users> selectUsersByFilterOptions(@Param("filterOptions") List<Map<String, List<String>>> filterOptions);
对应controller
package com.example.comweb.controller;
import com.example.comweb.mapper.UsersMapper;
import com.example.comweb.model.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UsersController {
@Autowired
private UsersMapper usersMapper;
@GetMapping("/api/users/login")
public String login(@RequestParam String userName, @RequestParam String passwd) {
Users user = usersMapper.selectByUserNameAndPasswd(userName, passwd);
if (user != null) {
return "Login successful!"; // 登录成功
} else {
return "Login failed!"; // 登录失败
}
}
}
3. 使用 XML 映射文件(plus中可以不使用xml):
如果您更喜欢使用 XML 映射文件,您需要创建一个与 Mapper 接口同名的 XML 文件,在其中定义您的自定义 SQL 语句。然后,确保 MyBatis Plus 能够找到并加载这个 XML 映射文件。
在任何情况下,MyBatis Plus 都不会自动为您生成特定字段的查询方法,您需要手动提供查询逻辑。如果您想避免书写 SQL 语句,应该使用 QueryWrapper
或者 LambdaQueryWrapper
来构建查询条件。
如果你选择使用 MyBatis Plus 的 XML 映射文件而不是注解来定义 SQL 语句,你需要在 mapper
目录下为你的 UsersMapper
接口创建一个对应的 XML 文件。这个文件通常与接口同名,并且放在相同的包路径下。
以下是一个基本的 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.yourpackage.mapper.UsersMapper">
<!-- 定义resultMap,映射数据库列到Java实体属性 -->
<resultMap id="UserResultMap" type="com.example.yourpackage.entity.Users">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<result column="passwd" property="password"/>
<!-- 其他字段映射 -->
</resultMap>
<!-- 通过用户名和密码查询用户的SQL -->
<select id="selectByUserNameAndPasswd" resultMap="UserResultMap">
SELECT * FROM users
WHERE user_name = #{userName} AND passwd = #{password}
</select>
</mapper>
在这个文件中:
<!DOCTYPE>
声明了 MyBatis 的 DTD,这是为了验证 XML 文件的结构。<mapper>
元素的namespace
属性应该与你的 Mapper 接口的完全限定名匹配。<resultMap>
定义了如何将数据库表的列映射到你的 Java 实体类的属性。id
元素通常用于主键字段,而result
用于其他字段。<select>
元素定义了一个 SQL 查询语句。id
属性应该与你的 Mapper 接口中的方法名匹配。resultMap
属性指定了返回结果应该如何映射到 Java 对象。
现在在你的 UsersMapper
接口中,你可以定义一个与 XML 文件中的 select
语句相匹配的方法:
import com.example.yourpackage.entity.Users;
import org.apache.ibatis.annotations.Param;
public interface UsersMapper extends BaseMapper<Users> {
Users selectByUserNameAndPasswd(@Param("userName") String userName, @Param("password") String password);
}
在你的服务层中,你现在可以直接调用这个方法:
@Service
public class UsersService {
@Autowired
private UsersMapper usersMapper;
public Users getUserByUserNameAndPasswd(String userName, String passwd) {
return usersMapper.selectByUserNameAndPasswd(userName, passwd);
}
}
确保你的 MyBatis 配置文件已经配置了 mapper XML 文件的路径,否则 MyBatis Plus 找不到这些文件。例如,在 mybatis-config.xml
中,你需要有如下配置:
<configuration>
<!-- 其他配置... -->
<mappers>
<mapper resource="com/example/yourpackage/mapper/UsersMapper.xml"/>
<!-- 其他mapper文件 -->
</mappers>
</configuration>
这样,MyBatis Plus 在启动时会加载并解析这些 XML 文件,你就可以在你的应用程序中使用这些自定义的查询了。
4.Websocket通信
使用 JWT 拦截请求时,ws 请求不能和 http 同时处理,一般有两种方法,独立为 ws 写一个拦截器。或者通过 HeaderMapRequestWrapper
对 WebSocket 请求的 HttpServletRequest
进行包装,在包装过程中将 WebSocket 请求头中的 Sec-WebSocket-Protocol
值提取出来并放入一个名为 Authorization
的请求头中。将 WebSocket 请求转变为标准的 HTTP 请求,从而可以通过常规的 HTTP 过滤器来处理认证逻辑。
/**
* 修改header信息,key-value键值对儿加入到header中
*/
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
private static final Logger LOGGER = LoggerFactory.getLogger(HeaderMapRequestWrapper.class);
/**
* construct a wrapper for this request
*
* @param request
*/
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
private Map<String, String> headerMap = new HashMap<>();
/**
* add a header with given name and value
*
* @param name
* @param value
*/
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
@Override
public String getHeader(String name) {
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
/**
* get the Header names
*/
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
for (String name : headerMap.keySet()) {
names.add(name);
}
return Collections.enumeration(names);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
values = Arrays.asList(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
/**
* 通过request的url判断是否为websocket请求
* 如果为websocket请求,先处理Authorization
*/
if(request.getRequestURI().contains("/websocket")){
HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper((HttpServletRequest) request);
//修改header信息 将websocket请求中的Sec-WebSocket-Protocol值取出处理后放入认证参数key
//此处需要根据自己的认证方式进行修改 Authorization 和 Bearer
requestWrapper.addHeader("Authorization","Bearer "+request.getHeader("Sec-WebSocket-Protocol"));
request = (HttpServletRequest) requestWrapper;
}
//处理完后就可以走系统正常的登录认证权限认证逻辑了
//下边就是自己的验证token 登陆逻辑
LoginUser loginUser = getLoginUser(request);
·······
chain.doFilter(request, response);
}
5.异步处理
springboot支持异步处理,不影响正常的请求,比如路由守卫的时候可以加一些计算。
@Async
Element Plus
npm install -D unplugin-vue-components unplugin-auto-import
vite.config.ts 按需引入
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
全局配置,既然全面拥抱 ts,我们写ts
<template>
<el-config-provider :size="size" :z-index="zIndex">
<div id="app">
<router-view></router-view>
</div>
</el-config-provider>
</template>
<script setup lang="ts">
// 删除 export default,因为使用 script setup 会自动导出组件
//在 Vue 3 中,当你使用 `<script setup>` 语法时,组件的名称通常是由其文件名决定的。
import { ref } from 'vue';
// 使用 ref 创建响应式引用
const zIndex = ref(3000);
const size = ref('small');
</script>
<style scoped>
/* 全局样式... */
</style>
Icon
https://www.jb51.net/article/283664.htm
BackTop
回到顶部实现不难,但是容易被打断,第二滚动后立即点击会被中断,如
const scrollToTop = () => {
// 先停止当前所有滚动
window.scrollTo(0, window.scrollY);
// 稍等片刻后开始平滑滚动到顶部
setTimeout(() => {
window.addEventListener('wheel', disableScroll, { passive: false });
window.scrollTo({
top: 0,
behavior: 'smooth'
});
const checkIfAtTop = () => {
if (window.scrollY === 0) {
window.removeEventListener('wheel', disableScroll);
window.removeEventListener('scroll', checkIfAtTop);
}
};
window.addEventListener('scroll', checkIfAtTop);
}, 40); // 延迟40毫秒开始滚动到顶部
};
尝试n多种方法,都不太理想,包括监听滚轮
const disableScroll = (event) => {
event.preventDefault();
};
window.addEventListener('scroll', disableScroll, { passive: false });
// window.removeEventListener('scroll', disableScroll);
实际上这个bug不是鼠标滚轮导致的,因为通过监听可以发现,鼠标滚动停止后再点击回到顶部会被打断,而此时已经没有了滚动,所以阻止滚动是无效的,查看各大平台网站,我自己的hexo主题,还有淘宝他们的网站都是没有这个bug的,这里面包含了比较复杂的动画处理,而且Vue3和TS对Jquery支持有限,无法很好使用Jquery的animate。最后只能采用最简单朴素的方法,就是延迟一小段时间再回到顶部。255已经是不会触发这个bug的最小时间。
const scrollToTop = () => {
setTimeout(() => {
window.scrollTo({
top: 0,
behavior: "smooth" // 使用平滑滚动
});
}, 255); // 延迟
};
样式参考
<transition name="slide">
<div class="top-scroll" v-if="showScrollTop" @click="scrollToTop">
<a class="btn-floating" href="#!">
<font-awesome-icon :icon="['fas', 'arrow-up']" style="color: #fff; font-size: 28px;" />
</a>
</div>
</transition>
选用图标如下
import { faArrowUp } from '@fortawesome/free-solid-svg-icons';
library.add(faArrowUp)
sass参考
.top-scroll
position: fixed
bottom: -20px
right: -20px
z-index: 100
padding: 10px
width: 100px
height: 100px
.btn-floating
display: flex
justify-content: center
align-items: center
width: 60%
height: 60%
padding: 0
border-radius: 50%
background: #ff2a4e
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.163)
transition: background 0.5s ease
&:hover
background: #ff513a
.slide-enter-active, .slide-leave-active
transition: all 0.3s ease
.slide-enter-from, .slide-leave-to
transform: translateY(100%)
opacity: 0
这里还有一个坑,缓慢转变颜色不能用渐变色用函数也不行。
完整代码
<script setup lang="ts">
import { RouterLink, useRoute } from "vue-router";
import { ref, onMounted, onBeforeUnmount, watchEffect, onUnmounted } from "vue";
import { library } from '@fortawesome/fontawesome-svg-core'
import $ from 'jquery'
import { faSquareFacebook, faTwitter, faSquareInstagram } from '@fortawesome/free-brands-svg-icons'
import { faArrowUp } from '@fortawesome/free-solid-svg-icons';
// 将图标添加到库中
library.add(faTwitter, faSquareFacebook, faSquareInstagram, faArrowUp)
const route = useRoute();
const isTransparent = ref(false);
const showScrollTop = ref(false);
const scrollToTop = () => {
setTimeout(() => {
window.scrollTo({
top: 0,
behavior: "smooth" // 使用平滑滚动
});
}, 255); // 延迟 1000 毫秒(1 秒)执行回到顶部的操作
};
// 监听滚动事件
const handleScroll = () => {
showScrollTop.value = window.scrollY > 100;
isTransparent.value = route.path === '/' && window.scrollY < 50;
};
// 注册和注销滚动事件监听器
onMounted(() => {
window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
watchEffect(() => {
// 当路由为'/'时,根据滚动位置设置透明度
isTransparent.value = route.path === '/' && window.scrollY < 50;
console.log("Current route path:", route.path);
});
const isMenuOpen = ref(false);
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
};
</script>
<template>
<section id="header1" class="d-flex align-items-center justify-content-between top-0 left-0">
<div class="contactEmail">
Contact Email: <a href="mailto:info@jmymelody.com">info@jmymelody.com</a>
</div>
<!-- <img src="../../img/logo.png" class="logo" alt=""> -->
<nav id="navbar" :class="{ 'active': isMenuOpen }" class="d-flex align-items-center justify-content-center m-0">
<a href="https://www.facebook.com" target="_blank"><font-awesome-icon :icon="['fab', 'square-facebook']" /> Facebook</a>
<a href="https://www.twitter.com" target="_blank"><font-awesome-icon :icon="['fab', 'twitter']" /> Twitter</a>
<a href="https://www.instagram.com" target="_blank"><font-awesome-icon :icon="['fab', 'square-instagram']" /> Instagram</a>
</nav>
<div id="mobile" class="d-none align-items-center">
<a><i class="bi bi-bag bag"></i></a>
<span class="d-flex align-items-end" @click="toggleMenu">
<i class="fas fa-outdent bar"></i>
</span>
</div>
</section>
<section id="header" :class="{'bg-transparent': isTransparent}" class="d-flex align-items-center justify-content-between position-sticky top-0 left-0">
<div class="webName">JMYmelody</div>
<!-- <img src="../../img/logo.png" class="logo" alt=""> -->
<nav id="navbar" :class="{ 'active': isMenuOpen }" class="d-flex align-items-center justify-content-center m-0">
<RouterLink active-class="active" to="/">Home</RouterLink>
<RouterLink active-class="active" to="/shop">Shop</RouterLink>
<RouterLink active-class="active" to="/blog">Blog</RouterLink>
<RouterLink active-class="active" to="/about">About</RouterLink>
<RouterLink active-class="active" to="/contact">Contact</RouterLink>
<RouterLink active-class="active" to="/login">login</RouterLink>
<RouterLink active-class="active" to="/cart"><i id="lg-bag" class="bi bi-bag"></i></RouterLink>
<RouterLink active-class="active" to="/" href="#" id="close" @click="toggleMenu"><i class="bi bi-x-lg"></i></RouterLink>
</nav>
<div id="mobile" class="d-none align-items-center">
<a><i class="bi bi-bag bag"></i></a>
<span class="d-flex align-items-end" @click="toggleMenu">
<i class="fas fa-outdent bar"></i>
</span>
</div>
</section>
<transition name="slide">
<div class="top-scroll" v-if="showScrollTop" @click="scrollToTop">
<a class="btn-floating" href="#!">
<font-awesome-icon :icon="['fas', 'arrow-up']" style="color: #fff; font-size: 28px;" />
</a>
</div>
</transition>
</template>
<style src="../../assets/header.sass" scoped lang="sass"></style>