这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战
Hello,你好呀,我是灰小猿,一个超会写bug的程序猿!
利用国庆期间做了一个基于springboot+vue的前后端分离的个人博客网站,今天在这里将开发过程和大家分享一下,手把手教你搭建一个自己专属的个人博客。
完整源码放置在Gitee上了,【源码链接】
小伙伴们记得⭐star⭐哟!
小伙伴们一键三连➕关注!灰小猿带你上高速啦🎉🎉🎉**!**
上一篇文章和大家讲了后台实现,这一篇文章就继续来和大家讲一下搭建个人博客系统的前台界面是如何搭建起来的呢?
⚡项目目录⚡
三、Vue前端页面开发
前端页面的开发我们是基于Vue和Element-Ui的,同时涉及axios发送请求,markdown编辑器的引入、登录验证、跨域请求等问题。
博客主页的页面是这样的:
接下来和大家分享一下前端页面的开发流程。
(1)安装Element-UI
Element-UI是进行前端开发的一个组件库,官网地址。这里面提供了各种已经开发好的组件供我们使用。
Element - The world's most popular Vue UI framework
使用该组件库我们首先是需要引入的。在vue的根目录下,输入如下命令:
# 切换到项目根目录
cd vueblog-vue
# 安装element-ui
npm install element-ui --save
之后打开项目的src目录下的main.js文件,引入Element-UI依赖。
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
Vue.use(Element)
到现在,组件库中的组件我们就可以任意使用了。
(2)安装axios
axios是一个基于promise的HTTP库,在我们进行前后端项目开发的时候,使用该工具可以提高我们的开发效率。【axios官网】
Axios的安装命令如下:
cnpm install axios --save
同样需要在main.js中全局引入axios,
import axios from 'axios'
Vue.prototype.$axios = axios
之后我们就可以通过this.$axios.get()来发起我们的请求了!
(3)配置页面路由
接下来是定义页面路由,定义页面路由的目的是我们在访问相应路径的时候,可以根据路由来确定到我们将要访问的页面。
在views文件夹中的页面有:
- BlogDetail.vue(博客详情页)
- BlogEdit.vue(编辑博客)
- Blogs.vue(博客列表)
- Login.vue(登录页面)
页面路由设置在router文件下的index.js中。配置如下:
/**
* 路由注册中心
*/
import Vue from 'vue'
import VueRouter from 'vue-router'
//注册页面
import Login from '../views/Login.vue'
import Blogs from '../views/Blogs.vue'
import BlogEdit from '../views/BlogEdit.vue'
import BlogDetail from '../views/BlogDetail.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'index',
redirect: {name: "Blogs"} //页面重定向
},
{
path: '/blogs',
name: 'Blogs',
component: Blogs
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/blog/add',
name: 'BlogAdd',
component: BlogEdit,
//添加权限访问,表示只有登录之后才能进行该操作
meta: {
requireAuth: true
}
},
{
path: '/blog/:blogId/edit',
name: 'BlogEdit',
component: BlogEdit,
//添加权限访问,表示只有登录之后才能进行该操作
meta: {
requireAuth: true
}
},
{
path: '/blog/:blogId',
name: 'BlogDetail',
component: BlogDetail
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
**在上述代码中带有meta:requireAuth: true说明是需要登录之后才能访问的受限资源,**后面我们路由权限拦截时候会讲到这个。
(4)登录页面
登录页面我们这里是由用户名和密码进行登录的,组件我采用了element-ui中的组件,所以在登录中直接就有了登录校验的功能,
登录验证
在这里点击登录按钮之后,会有一个验证登录的过程,简单说一下验证的思路,
我们发起登录请求之后,获取到它返回的请求,查看请求中是否存在我们需要的jwttoken,如果存在的,那么我们就将获取到的token和用户信息共享给我们的浏览器,之后跳转到主页。如果不存在,就弹窗提示,并且不做任何操作。
代码如下:
methods: {
/**提交表单**/
async submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
// alert('submit!');
//提交登录信息
//获取到当前的this对象
const _this = this;
this.$axios.post("/login", this.ruleForm).then(res => {
console.log(res.data)
const jwt = res.headers["authorization"]
if (jwt === null){
this.$alert('用户名或密码错误!!', '提示', {
confirmButtonText: '确定',
callback: action => {
// _this.$router.push("/blogs")
}
});
}else {
const userInfo = res.data.data
console.log(jwt)
console.log(userInfo)
//把数据共享出去
_this.$store.commit("SET_TOKEN", jwt);
_this.$store.commit("SET_USERINFO", userInfo);
//获取
console.log(_this.$store.getters.getUser)
//页面跳转
_this.$router.push("/blogs")
}
});
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
token状态同步
在上述代码中,我们用到了$store来同步token和用户信息,那么这个同步是如何完成的呢,其实是我们在store文件下的index.js中进行了封装和设置。
存储token,我们用的是localStorage,存储用户信息,我们用的是sessionStorage。毕竟用户信息我们不需要长久保存,保存了token信息,我们随时都可以初始化用户信息。
index.js中的代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// token: "",
//用户的信息可以直接从浏览器中取出来
token: localStorage.getItem("token"),
//反序列化操作
userInfo: JSON.parse(sessionStorage.getItem("userInfo"))
},
mutations: {
/**类似set操作*/
//给token赋值
SET_TOKEN: (state, token) => {
state.token = token;
//将信息存储到浏览器中,以至于浏览器关闭时信息还在
localStorage.setItem("token", token);
},
//给userinfo赋值
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo;
//session会在每次浏览器关闭时清空,在重新登录后再生成
//由于sessionStorage不能存储对象,所以要将其存储成字符串的形式
sessionStorage.setItem("userInfo", JSON.stringify(userInfo));
},
//移除用户信息
REMOVE_INFO: (state) => {
//移除用户信息时将用户所有的信息都置为空
state.token = "";
state.userInfo = {};
localStorage.setItem("token", "");
sessionStorage.setItem("userInfo", JSON.stringify(""));
}
},
getters: {
/**类似get请求*/
//获取用户信息
getUser: state => {
return state.userInfo;
}
},
actions: {},
modules: {}
})
定义全局axios拦截器
由于我们的登录有时候会出现密码输出错误的情况,虽然不需要做任何操作,但是有时候我们还是需要进行弹窗提示,这样对于这种错误信息的弹窗,我们就可以对其进行统一的封装和设置。所以我对axios设置一个拦截器,包括前置拦截和后置拦截,如果说我们返回数据的code或者status不正常就会弹窗提示相应的信息。
操作是在在src目录下创建一个文件axios.js(与main.js同级),定义axios的拦截:
import axios from "axios"
import Element from "element-ui"
import router from "../router"
import store from "../store";
//设置统一请求路径
axios.defaults.baseURL = "/api"
//前置拦截
axios.interceptors.request.use(config => {
return config
})
/**
* 对请求的返回数据进行过滤
*/
axios.interceptors.response.use(response => {
let res = response.data;
console.log("=================")
console.log(res)
console.log("=================")
//如果状态码是200,直接放行
if (res.code === 200) {
return response
} else {
//如果是用户名错误会直接断言处理,不会到达这一步!
//弹窗提示!
Element.Message.error('用户名或密码错误!', {duration: 3 * 1000})
//返回错误信息
return Promise.reject(response.data.msg)
}
},
//如果是非密码错误,会到达这一步
error => {
console.log(error)
//如果返回的数据里面是空
if (error.response.data){
error.message = error.response.data.msg;
}
//如果状态码是401,
if (error.response.status === 401){
store.commit("REMOVE_INFO")
router.push("/login")
}
//弹出错误信息
Element.Message.error(error.message, {duration: 3 * 1000})
return Promise.reject(error)
})
之后别忘了在main,js文件中导入axios,js文件。
import './axios.js' // 请求拦截
简单说一下这几个拦截的作用:
**前置拦截:**在请求之前的拦截,可以在其中统一为所有需要权限的请求装配上header的token信息,这样就不要在使用的时候再配置。
**后缀拦截:**在请求返回之后的拦截,可以在请求之后对返回的数据进行处理和验证,
(5)博客列表
在我们登录完成之后就会进入了博客的主页面,在该页面主要是展示了当前录入到系统中的博客信息,界面如下:
整个博客的显示是按照时间线的方式展开的,最后发布的博客会在第一个出现,同时你会发现在博客主页的头部会展示我们的一些基本信息,包括个人信息以及编辑和退出的功能,这个头部信息会一直显示在我们的页面中,所以为了能够实现代码复用,减少代码的使用量,我们将头部信息全部都抽取了出来,放置在了Header.vue页面中,
<template>
<div class="m_content">
<h3>欢迎来到{{user.username}}的博客</h3>
<div class="block">
<el-avatar :size="50" :src="user.avatar"></el-avatar>
<div>{{user.username}}</div>
</div>
<div class="maction">
<span><el-link type="primary" href="/blogs">主页</el-link></span>
<el-divider direction="vertical"></el-divider>
<span><el-link type="success" href="/blog/add">发表博客</el-link></span>
<span v-show="!haslogin">
<el-divider direction="vertical"></el-divider>
<span><el-link type="warning" href="/login">登录</el-link></span>
</span>
<span v-show="haslogin">
<el-divider v-show="haslogin" direction="vertical"></el-divider>
<span ><el-link type="danger" @click="logout">退出</el-link></span>
</span>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {
user: {
username: "请先登录",
avatar: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
},
haslogin: false
}
},
//写入方法
methods: {
//退出登录
logout() {
const _this = this;
//发送退出登录请求
_this.$axios.get("/logout", {
//由于只有在登录的时候才能进行退出,所以将token信息携带回去
headers: {
"authorization": localStorage.getItem("token")
}
}).then(res => {
//清空数据信息
_this.$store.commit("REMOVE_INFO")
//返回到登录界面
_this.$router.push("/login")
})
},
},
//执行一个初始化操作
created() {
//如果用户名存在,就回显出来
if (this.$store.getters.getUser.username) {
this.user.username = this.$store.getters.getUser.username
this.user.avatar = this.$store.getters.getUser.avatar
this.haslogin = true
}
}
}
</script>
<style scoped>
.m_content {
max-width: 960px;
margin: 0 auto;
text-align: center;
}
.maction {
margin: 10px 0px;
}
</style>
如果在其他页面中需要该头部信息时,只需要将Header页面引用到该页面中,之后在内容中写入即可。如下:
import Header from "@/components/Header";
data() {
components: {Header}
}
然后模板中调用组件
<Header></Header>
在该博客主页中包括博客分页,以及博客排列,因为我们使用了分页组件,所以在返回的信息会直接带有分页信息,我们直接拿来用就可以了。
<template>
<div>
<Header></Header>
<div class="block">
<el-timeline>
<el-timeline-item :timestamp="blog.created" placement="top" v-for="blog in blogs">
<el-card>
<router-link :to="{name:'BlogDetail',params:{blogId:blog.id}}">
<h4>{{blog.title}}</h4>
</router-link>
<p>{{blog.description}}</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<el-pagination class="mpage"
background
layout="prev, pager, next"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change=page
>
</el-pagination>
</div>
</template>
<script>
//导入公共的Header
import Header from "../components/Header";
export default {
name: "Blogs",
//将Header注册进去
components: {Header},
//返回的数据
data() {
return {
blogs: {},
currentPage: 1, //当前页
total: 0, //总共多少页
pageSize: 5 //每一页的数据个数
}
},
mounted() {
},
methods: {
//请求指定页的方法
page(currentPage) {
const _this = this;
_this.$axios.get("/blogs?currentPage=" + currentPage,{
headers: {
"authorization": localStorage.getItem("token")
}
}).then(res => {
console.log(res)
//从获取到的数据中进行赋值
_this.blogs = res.data.data.records
_this.currentPage = res.data.data.current
_this.total = res.data.data.total
_this.pageSize = res.data.data.size
})
}
},
created() {
this.page(1)
}
}
</script>
<style scoped>
.mpage {
margin: 0 auto;
text-align: center;
}
</style>
data()中直接定义博客列表blogs、以及一些分页信息。methods()中定义分页的调用接口page(currentPage),参数是需要调整的页码currentPage,得到结果之后直接赋值即可。然后初始化时候,直接在mounted()方法中调用第一页this.page(1)。
(6)博客编辑
博客编辑页面中我们可以对已经发布的博客进行编辑,也可以发布新的博文,但是该项功能是只有在登录的状态下才能使用的,在博客编辑页面中,我们引入了markdown编辑器,该编辑器有关于vue的支持。我们直接导入相关依赖拿来用就可以了,
Markdown编辑器引入
第一步、进入插件
Markdown编辑器中比较好用的插件是mavon-editor,首先我们需要安装相关插件。
cnpm install mavon-editor --save
第二步、全局注册
引入之后如果想要使用,当然是需要在main.js文件中全局注册的,
// 全局注册
import Vue from 'vue'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)
第三步、定义到页面中
Markdown编辑器的使用,在注册到全局页面中之后,只需要我们在页面中使用如下代码引入即可。
<mavon-editor v-model="editForm.content"/>
以上就是vue引入markdown编辑器的步骤了,
另外附上博客编辑页面的代码:
<template>
<div>
<Header></Header>
<div class="m_content">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="标题" prop="title">
<el-input v-model="ruleForm.title"></el-input>
</el-form-item>
<el-form-item label="摘要" prop="description">
<el-input type="textarea" v-model="ruleForm.description"></el-input>
</el-form-item>
<el-form-item label="内容" prop="content">
<mavon-editor v-model="ruleForm.content"></mavon-editor>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">立即发布</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import Header from "../components/Header";
export default {
name: "BlogEdit",
components: {Header},
data() {
return {
ruleForm: {
id: "",
title: '',
description: '',
content: '',
},
rules: {
title: [
{required: true, message: '请输入标题', trigger: 'blur'},
{min: 5, max: 100, message: '长度在 5 到 100 个字符', trigger: 'blur'}
],
description: [
{required: true, message: '请输入摘要', trigger: 'blur'}
],
content: [
{required: true, message: '请输入内容', trigger: 'blur'}
],
}
};
},
mounted() {
console.log(localStorage.getItem("token"))
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
console.log(this.ruleForm)
//发送编辑的请求
_this.$axios.post("/blog/edit", this.ruleForm, {
//添加请求头部token
headers: {
"authorization": localStorage.getItem("token")
}
}).then(res => {
console.log(res)
this.$alert('编辑操作成功!', '提示', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push("/blogs")
}
});
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
//页面渲染前的后置方法
created() {
//将博客信息回显,作用在进行编辑的时候可以在编辑页面出现用户的信息
//用户发送“/blog/{id}/edit”类型的请求才会回显
//用户发送“/blog/add”添加博客的请求时,获取不到值
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this;
if (blogId){
_this.$axios.get("/blog/" + blogId).then(res=>{
const blog = res.data.data;
_this.ruleForm.id = blog.id
_this.ruleForm.title = blog.title
_this.ruleForm.description = blog.description
_this.ruleForm.content = blog.content
})
}
}
}
</script>
<style scoped>
.m_content {
margin: 0 auto;
}
</style>
效果如下:
(7)博客详情页
在博客详情页中,我们需要回显我们的博客信息,但是我们在发布博客的时候,使用的是markdown编辑器,所以在回显的时候,我们回显的内容带有markdown标签的,那么应该如何回显我们正式编辑的文本呢?
在这里需要使用一个插件markdown-it,它的作用是解析md文档,之后再导入github-markdown-it,使用md样式。
# 用于解析md文档
cnpm install markdown-it --save
# md样式
cnpm install github-markdown-css
在需要写入博客文本的地方的使用方法是:
<div class="content markdown-body" v-html="blog.content"></div>
其中还涉及到一些md相关的渲染,可以看代码,
**具体逻辑是这样的;**初始化create()方法中调用getBlog()方法,请求博客详情接口,返回的博客详情content通过markdown-it工具进行渲染。
再导入样式:
import 'github-markdown.css'
然后在content的div中添加class为markdown-body即可
<template>
<div>
<Header></Header>
<div class="mblog">
<h2>{{blog.title}}</h2>
<div>
<el-link icon="el-icon-edit" v-if="ownBlog">
<!--携带博客ID跳转到编辑页面,对博客进行修改-->
<router-link :to="{name:'BlogEdit',params:{blogId: blog.id}}">
编辑
</router-link>
</el-link>
<el-divider direction="vertical"></el-divider>
<el-link type="danger" @click="messageHandel" v-if="ownBlog">
删除
</el-link>
</div>
<el-divider></el-divider>
<div class="markdown-body" v-html="blog.content"></div>
</div>
<el-dialog
:title="title"
:visible.sync="centerDialogVisible"
width="30%"
center>
<span slot="footer" class="dialog-footer">
<el-button @click="centerDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="deleteBlog">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
// 导入所需依赖
import Header from "../components/Header";
//为了让markdown转换的文本更好看,使用这个文件进行渲染
import "github-markdown-css/github-markdown.css"
import messageModel from "../components/messageModel";
export default {
name: "BlogDetail",
components: {Header, messageModel},
data() {
return {
centerDialogVisible: false,
title:'您确定要删除此条博客吗?',
blog: {
id: "",
title: "",
content: ""
},
ownBlog: false
}
},
methods:{
messageHandel(){
this.centerDialogVisible = true
},
deleteBlog(){
const _this = this
const blogid = this.$route.params.blogId
_this.$axios.post("/blog/delete/" + blogid,{
headers:{
"authorization":localStorage.getItem("token")
}
}).then(res=>{
console.log(res)
const code = res.data.code
if (code === 200){
this.centerDialogVisible = false
_this.$router.push("/blogs")
}else {
this.$alert('删除失败!', '提示', {
confirmButtonText: '确定',
});
}
})
}
},
created() {
//从路径中获取到博客id
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this
//按照博客id找到指定的博客,
_this.$axios.get("/blog/" + blogId).then(res => {
//获取到返回的博客信息,并且将该信息回显给页面
const blog = res.data.data;
_this.blog.id = blog.id
_this.blog.title = blog.title
_this.blog.description = blog.description
/**
* 将markdown的文本转为正常文本
* */
//拿到markdown渲染资源对象
var markDownIT = require("markdown-it")
var md = new markDownIT() //获取到markdown-it的对象
var result = md.render(blog.content) //将markdown文本渲染成html文本
_this.blog.content = result //将正常文本赋值给页面显示
//只有在当前的博客的用户id和当前登录的用户的id相符合的时候,才会显示编辑按钮
_this.ownBlog = (blog.userId === _this.$store.getters.getUser.id)
})
}
}
</script>
<style scoped>
.mblog {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
width: 100%;
min-height: 760px;
margin: 10px 15px;
}
/deep/.el-dialog__body{
padding: 0 !important
}
</style>
得到的效果如下,其中的博文是我之前在CSDN发布过的,效果基本上一样。
(8)权限路由拦截
因为我们最开始也提到了部分页面是需要在登录的状态下才能访问的,那么在前台应该如何进行拦截呢?其实思路是很简单的,主要就是给每一个页面请求添加一个参数,标记其是否是需要在登录状态下才能访问,同时过滤拦截每一个请求,如果该请求是需要登录才能访问的,那么就从浏览器中获取token,如果能够够获取到,就放行,否则就跳转到登录页面。
关于这个知识点我单独总结了一篇博客,欢迎小伙伴们学习!《两步教你在Vue中设置登录验证拦截!》****
四、写在最后&项目总结
到这里整个项目就算是开发完成了,其中也参考了b站大佬的视频讲解,自己也学到到了很多东西,整个项目对学习前后台分离的新手来说还是非常友好的。其中的shiro+jwt安全验证、统一结果封装、以及前台页面中路由的设置、存储和获取token、跨域解决、登录验证等都是非常值得学习的。
项目参考自MarkerHub老师的《4小时开发一个SpringBoot+vue前后端分离博客项目》,本文章中的讲解是我个人的开发总结和思路,如有问题欢迎批评指正!MarkerHub老师的视频链接。
项目源码我放在gitee了,【源码链接】,小伙伴们别忘了⭐star⭐哟!
**一键三连加关注!灰小猿带你上高速啦!**✨✨✨
我是灰小猿,我们下期见!