新闻发布系统,vue3+springboot,前后端分离项目
前言,适合人群
嘿,小伙伴,这是本人大二课程作业新闻发布系统的实现过程。如果您对前后端是如何交互感到困惑,或者想要初步学习vue3或后端的springboot框架的实践,又或是想要以此为基石完成属于自己课程作业或项目,那就上车,随我一起出发吧。(再次声明本文并非手把手教你的文章,需要您配合源码,以及我的半成品的图加以理解,因为这篇文章是我编写代码过程中成功经验给记录集合而已)
前提知识储备:springboot,mybatis sql(这部分不会讲)
主要分享:vue3
效果演示视频
Springboot+vue3新闻发布系统演示
项目启动引导教程
方便各位看的懂我是怎么做的,
vue3 vite项目启动
我使用的是Intellij idea,直接新建项目找到vite项目即可
![在这里插入图片描述](https://img-blog.csdnimg.cn/ba854858a5d445598ded0a385ed270e3.png
兄弟们刚创建完毕定启动项目会出现vite的欢迎界面
我这里的news部分的位置应当直接在src目录下(不要学我,我这里只是为了管理多个页面,配置就挺麻烦的,vite本身就是做单页面的。通过改变组件来实现页面切换)在src下建立news下文件夹,给大家上传的源码部分只会包含news部分,vite-config.js请君自行上网查阅,或私聊
springboot项目启动
数据库依赖请自行选择
Day1 -2 新闻管理页面的基本搭建,后端搭建
问题1:请求跨域问题
在vite-config.js处加上对server的配置
//前后端分离,跨域配置,后端服务器地址为http://localhost:8080
//想要访问http://localhost:8080/news来获取新闻数据,那么就需要配置路由重写
// 重写规则如下:
// 1. 以/api开头的请求,都会被代理到target配置
// 2. 重写后的地址为:target(去掉/api) + /api(去掉/api)
// 3. 例如:/api/news 重写后就是 http://localhost:8080 + /news
// 4. 最终得到 http://localhost:8080/news
// 5. 重写后的地址就会被代理到target配置
server: {
proxy: {
'/local': {
target: 'http://localhost:8080',
rewrite: (path) => path.replace(/^\/local/, '')
}
}
}
问题2 增删改中子组件与父组件的通信问题
需求描述; 点击编辑按钮/加号,弹出对话框(标题相应不同)填写表单,对话框组件封装在newsForm.vue中,父组件为news,为实现数据回显,以及是否弹出对话框,以下提供一个解决思路,当然也可以使用v-if
父组件通过ref获得子组件的实例,通过@向子组件传递自己的方法
子组件通过暴露自己的属性和方法,让父组件通过 const formInstance=ref()
之后使用formInstance可以访问到暴露的方法。
子组件通过使用emit访问到父组件的getNewsList方法。
问题3: element-plus怎么获取表格组件怎么获取行对象
需求描述:在点击编辑/删除时,我们需要知道究竟是修改了哪一行的数值
可以通过如下方法通过scope.row获得行对象
问题 4 前端如何发起请求
首先在httpRequest.js中配置请求基本路径以及拦截器(拦截器里面的处理可以暂时不写,在后面有业务需求在写)/local在这里要配合前面的跨域配置来写
然后在相应文件中按照这种方式发出请求,url地址与后端的接收的url要一致,传输与接收的参数,键值对中键名要与双方接收数据的属性名一致。传输对象时无须提前转换成json格式,object对象,hashmap对象都可以直接传入
Day3-4 后端全局异常处理函数,后端模糊,分页查询实现
package com.kinman.exception;
import com.kinman.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)//捕获所有异常
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
这里给各位说明一下,分页查询的工作实际上是后端在做,利用sql语句实现的(当然可以借助插件)前端element-plus分页组件并不能直接与表格显示的内容相关联,仅仅是为了好看,用于通知前端或者后端pageSize,以及currentPage的数据,利用这我们可以使用前端或者后端进行数据剪枝。模糊查询使用like即可
前后端分页查询参考https://blog.csdn.net/ws6afa88/article/details/108955852
数据库异常原因总结
数据库服务未开启,库名,表明字段名出现了该数据库专有的关键字,例如postgresql中user与name。 博主因为这个原因浪费一个上午的时光和一天的好心情。
其余原因比较简单容易察觉的我就不再这里列出了
Day5-6 登录校验与请求拦截,token
问题一 怎么样登录校验
- 准备表单
- 准备规则
- 将规则绑定到表单上(到这一步只是提示无法防止用户直接提交表单)
- 获取表单对象
- 对表单对象进行整体校验
<script lang="ts" setup>
import {reactive, ref} from "vue";
import 'element-plus/theme-chalk/el-message.css'
import { ElMessage } from 'element-plus'
import {useRouter} from "vue-router";
import {useUserStore} from "@/pages/news/dataStore/userdata.js";
const userStore = useUserStore()
//reactive 用于将对象转化为响应式对象,在vue3中,ref只能用于基本类型,reactive可以用于对象
const form = reactive({
username: '',
password: '',
agree: false
})
//校验规则,校验名必须与form中的属性名一致,按照产品经理的要求,用户名和密码都是必填项
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 4, max: 14, message: '长度在 4 到 14 个字符', trigger: 'blur' }
],
agree: [
{
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请勾选协议'))
} else {
callback()
}
}
}
]
}
//获取form实例
const formRef = ref(null)
//获取路由实例
const router = useRouter()
const handleLogin = () => {
//触发表单校验
formRef.value.validate(async (valid) => {
if (valid) {
//校验成功
console.log('校验成功')
//todo 登录逻辑
const {username, password} = form
await userStore.getUserInfo({username, password})
console.log(userStore.userInfo)
//todo 登录成功后跳转到首页
if (userStore.userInfo.code === 1) {
//todo 提示登录成功
ElMessage.success("登录成功")
//todo 保存token
//localStorage.setItem('token', res.data.token)
//todo 跳转到首页
await router.replace('/')
}
} else {
//校验失败
console.log('校验失败')
}
})
console.log(form)
}
</script>
<template>
<div class="background">
<!--让elcard居中透明,上下也居中,不要遮挡背景-->
<el-card class="loginCard" shadow="hover" style="width: 400px; margin: 200px auto 0;background: rgba(7,189,255,0.3)">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="80px" status-icon>
<!-- prop指定校验名-->
<el-form-item prop="username" >
<!-- #号的作用是将内容放到label中-->
<template #label>
<span style="color: #eeff00">用户名</span>
</template>
<el-input v-model="form.username" />
</el-form-item>
<el-form-item prop="password" >
<template #label>
<span style="color: #eeff00">密码</span>
</template>
<el-input v-model="form.password" />
</el-form-item>
<el-form-item prop="agree" label-width="22px">
<el-checkbox size="large" v-model="form.agree">
<i style="color: #00ffc4"> 我已同意隐私条款和服务条款</i>
</el-checkbox>
</el-form-item>
</el-form>
<!-- 登录按钮水平居中-->
<div style="text-align: center">
<el-button type="primary" @click="handleLogin">登录</el-button>
</div>
</el-card>
</div>
</template>
<style scoped>
.background{
width: 100%;
height: 100%;
background: url("../../assets/loginbg.jpg");
background-size:100% 100%;
position: fixed;
top: 0;
left: 0;
}
.loginCard{
margin-top: 200px;
}
</style>
token校验
- 向后端请求登录(一般校验到这就结束了,但因为是登录我们还需要之后保持token完成跳转功能)
- 后端返回token,配置拦截器
import com.kinman.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration//配置类
public class WebConfigure implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
后端拦截器
package com.kinman.interceptor;
import com.kinman.pojo.Result;
import com.kinman.utils.JwtUtils;
import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url。
String url = req.getRequestURL().toString();
log.info("请求的url: {}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
return true;
}
//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
//手动写入响应体,返回给前端,前端解析json,如果是未登录,跳转到登录页面
resp.getWriter().write(notLogin);
return false;
}
//5.解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//6.放行。
log.info("令牌合法, 放行");
return true;
}
@Override //目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ...");
}
@Override //视图渲染完毕后运行, 最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
- 前端使用pinia或者使用localstorage进行持久化存储,使用方法见登录校验处的代码
import {defineStore} from "pinia";
import {ref} from "vue";
import {loginAPI} from "@/pages/news/apis/usersHandler"; // 1️⃣ import defineStore
export const useUserStore = defineStore("user", ()=>{ // 2️⃣ define a store
const userInfo=ref({})// 3️⃣ add some state
// 4️⃣ 定义一些actions
const getUserInfo=async ({username,password})=>{
userInfo.value=await loginAPI({username, password})
}
const clearUserInfo=()=>{
userInfo.value={}
}
// 5️⃣ return the state and the actions
return {
userInfo,
getUserInfo,
clearUserInfo
}
},{
persist: true,
});
10.前端在请求拦截器请求处加上token字段,以及在后端响应处理后端传来token校验失败的信息
httpRequest.interceptors.request.use(config => {
// 1.当发送网络请求时, 在页面中添加一个loading组件, 作为动画
// 2.某些网络请求要求用户必须登录, 判断用户是否有token, 如果没有token跳转到login页面
// 3.对请求的参数进行序列化(看服务器是否需要序列化)
const userStore = useUserStore()
const token=userStore.userInfo.data?userStore.userInfo.data.token:''
if (token) {
config.headers['token'] = token
}
//拦截管理员请求,验证用户权限,如果identity 为true放行,否则抛出错误
if (config.url.includes("/ad")){
if (!userStore.userInfo.data.user.identity){
ElMessage({
type: 'warning',
message: '您没有权限访问该页面'
})
throw new Error('您没有权限访问该页面')
}
}
return config;
}, error => {
return Promise.reject(error);
}
)
// 响应拦截器
httpRequest.interceptors.response.use(response => {
// console.log(response)
//一般而言,只需要返回data即可
if(response.data.code === 0){
ElMessage({
type: 'warning',
message: response.data.msg
})
if (response.data.msg === 'NOT_LOGIN') {
const userStore = useUserStore()
userStore.clearUserInfo()
router.push('/login')
}
}
return response.data;
}, error => {
return Promise.reject(error);
}
)
export default httpRequest;
前端项目地址
前端项目地址
现在已经暂时完结,感谢各位陪伴