我是markerhub作者的一名粉丝,最初在markerhub公众号关注了博主,去B站白嫖了博主的基于SpringBoot+Vue开发的前后端分离博客项目完整教学的视频教程,看一遍跟着老师敲一遍,有的地方是直接Ctrl+C,Ctrl+V哈哈哈,今天转载了博主的项目博客,非常推荐这个博主(B站UP主;公众号号主;markerhub网站站长),嘻嘻嘻

小Hub领读:

前后端分离的博客项目终于出来啦,真是花了好多心思录制咧。文末直接进入B站看视频哈!


作者:吕一明

项目代码:https://github.com/MarkerHub/vueblog

项目视频:https://www.bilibili.com/video/BV1PQ4y1P7hZ/

转载请保留此引用,感谢!

Vue前端页面开发

1、前言

接下来,我们来完成vueblog前端的部分功能。可能会使用的到技术如下:

  • vue

  • element-ui

  • axios

  • mavon-editor

  • markdown-it

  • github-markdown-css

本项目实践需要一点点vue的基础,希望你对vue的一些指令有所了解,这样我们讲解起来就简单多了哈。

2、项目演示

我们先来看下我们需要完成的项目长什么样子,考虑到很多同学的样式的掌握程度不够,所以我尽量使用了element-ui的原生组件的样式来完成整个博客的界面。不多说,直接上图:

在线体验:https://markerhub.com:8083

3、环境准备

万丈高楼平地起,我们下面一步一步来完成,首先我们安装vue的环境,我实践的环境是windows 10哈。

1、首先我们上node.js官网(https://nodejs.org/zh-cn/),下载最新的长期版本,直接运行安装完成之后,我们就已经具备了node和npm的环境啦。

安装完成之后检查下版本信息:

2、接下来,我们安装vue的环境


  
  
  1. # 安装淘宝npm
  2. npm install -g cnpm --registry=https: //registry.npm.taobao.org
  3. # vue-cli 安装依赖包
  4. cnpm install --g vue-cli

4、新建项目


  
  
  1. # 打开vue的可视化管理工具界面
  2. vue ui

上面我们分别安装了淘宝npm,cnpm是为了提高我们安装依赖的速度。vue ui是@vue/cli3.0增加一个可视化项目管理工具,可以运行项目、打包项目,检查等操作。对于初学者来说,可以少记一些命令,哈哈。3、创建vueblog-vue项目

运行vue ui之后,会为我们打开一个http://localhost:8080 的页面:

然后切换到【创建】,注意创建的目录最好是和你运行vue ui同一级。这样方便管理和切换。然后点击按钮【在此创建新羡慕】

下一步中,项目文件夹中输入项目名称“vueblog-vue”,其他不用改,点击下一步,选择【手动】,再点击下一步,如图点击按钮,勾选上路由Router、状态管理Vuex,去掉js的校验。

下一步中,也选上【Use history mode for router】,点击创建项目,然后弹窗中选择按钮【创建项目,不保存预设】,就进入项目创建啦。

稍等片刻之后,项目就初始化完成了。上面的步骤中,我们创建了一个vue项目,并且安装了Router、Vuex。这样我们后面就可以直接使用。

我们来看下整个vueblog-vue的项目结构


  
  
  1. ├── README.md            项目介绍
  2. ├── index.html           入口页面
  3. ├── build              构建脚本目录
  4. │  ├── build-server.js         运行本地构建服务器,可以访问构建后的页面
  5. │  ├── build.js            生产环境构建脚本
  6. │  ├── dev-client.js          开发服务器热重载脚本,主要用来实现开发阶段的页面自动刷新
  7. │  ├── dev-server.js          运行本地开发服务器
  8. │  ├── utils.js            构建相关工具方法
  9. │  ├── webpack.base.conf.js      wabpack基础配置
  10. │  ├── webpack.dev.conf.js       wabpack开发环境配置
  11. │  └── webpack.prod.conf.js      wabpack生产环境配置
  12. ├── config             项目配置
  13. │  ├── dev.env.js           开发环境变量
  14. │  ├── index.js            项目配置文件
  15. │  ├── prod.env.js           生产环境变量
  16. │  └── test.env.js           测试环境变量
  17. ├── mock              mock数据目录
  18. │  └── hello.js
  19. ├── package.json          npm包配置文件,里面定义了项目的npm脚本,依赖包等信息
  20. ├── src               源码目录 
  21. │  ├── main.js             入口js文件
  22. │  ├── app.vue             根组件
  23. │  ├── components           公共组件目录
  24. │  │  └── title.vue
  25. │  ├── assets             资源目录,这里的资源会被wabpack构建
  26. │  │  └── images
  27. │  │    └── logo.png
  28. │  ├── routes             前端路由
  29. │  │  └── index.js
  30. │  ├── store              应用级数据(state)状态管理
  31. │  │  └── index.js
  32. │  └── views              页面目录
  33. │    ├── hello.vue
  34. │    └── notfound.vue
  35. ├── static             纯静态资源,不会被wabpack构建。
  36. └── test              测试文件目录(unit&e2e)
  37.   └── unit              单元测试
  38.     ├── index.js            入口脚本
  39.     ├── karma.conf.js          karma配置文件
  40.     └── specs              单测 case目录
  41.       └── Hello.spec.js

5、安装element-ui

接下来我们引入element-ui组件(https://element.eleme.cn),这样我们就可以获得好看的vue组件,开发好看的博客界面。

命令很简单:


  
  
  1. # 切换到项目根目录
  2. cd vueblog-vue
  3. # 安装element-ui
  4. cnpm install element-ui --save

然后我们打开项目src目录下的main.js,引入element-ui依赖。


  
  
  1. import Element from 'element-ui'
  2. import "element-ui/lib/theme-chalk/index.css"
  3. Vue.use(Element)

这样我们就可以愉快得在官网上选择组件复制代码到我们项目中直接使用啦。

6、安装axios

接下来,我们来安装axios(http://www.axios-js.com/),axios是一个基于 promise 的 HTTP 库,这样我们进行前后端对接的时候,使用这个工具可以提高我们的开发效率。

安装命令:

cnpm install axios --save

  
  

然后同样我们在main.js中全局引入axios。


  
  
  1. import axios from 'axios'
  2. Vue.prototype.$axios = axios //

组件中,我们就可以通过this.$axios.get()来发起我们的请求了哈。

7、页面路由

接下来,我们先定义好路由和页面,因为我们只是做一个简单的博客项目,页面比较少,所以我们可以直接先定义好,然后在慢慢开发,这样需要用到链接的地方我们就可以直接可以使用:

我们在views文件夹下定义几个页面:

  • BlogDetail.vue(博客详情页)

  • BlogEdit.vue(编辑博客)

  • Blogs.vue(博客列表)

  • Login.vue(登录页面)

然后再路由中心配置:

  • router\index.js


  
  
  1. import Vue from 'vue'
  2. import VueRouter from 'vue-router'
  3. import Login from '../views/Login.vue'
  4. import BlogDetail from '../views/BlogDetail.vue'
  5. import BlogEdit from '../views/BlogEdit.vue'
  6. Vue.use(VueRouter)
  7. const routes = [
  8. {
  9. path: '/',
  10. name: 'Index',
  11. redirect: { name: 'Blogs' }
  12. },
  13. {
  14. path: '/login',
  15. name: 'Login',
  16. component: Login
  17. },
  18. {
  19. path: '/blogs',
  20. name: 'Blogs',
  21. // 懒加载
  22. component: () => import( '../views/Blogs.vue')
  23. },
  24. {
  25. path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
  26. name: 'BlogAdd',
  27. meta: {
  28. requireAuth: true
  29. },
  30. component: BlogEdit
  31. },
  32. {
  33. path: '/blog/:blogId',
  34. name: 'BlogDetail',
  35. component: BlogDetail
  36. },
  37. {
  38. path: '/blog/:blogId/edit',
  39. name: 'BlogEdit',
  40. meta: {
  41. requireAuth: true
  42. },
  43. component: BlogEdit
  44. }
  45. ];
  46. const router = new VueRouter({
  47. mode: 'history',
  48. base: process.env.BASE_URL,
  49. routes
  50. })
  51. export default router

接下来我们去开发我们的页面。其中,带有meta:requireAuth: true说明是需要登录字后才能访问的受限资源,后面我们路由权限拦截时候会用到。

8、登录页面

接下来,我们来搞一个登陆页面,表单组件我们直接在element-ui的官网上找就行了,登陆页面就两个输入框和一个提交按钮,相对简单,然后我们最好带页面的js校验。emmm,我直接贴代码了~~

  • views/Login.vue


  
  
  1. <template>
  2.   <div>
  3.     <el-container>
  4.       <el-header>
  5.         <router-link to= "/blogs">
  6.         <img src= "https://www.markerhub.com/dist/images/logo/markerhub-logo.png"
  7.              style= "height: 60%; margin-top: 10px;">
  8.         </router-link>
  9.       </el-header>
  10.       <el-main>
  11.         <el-form :model= "ruleForm" status-icon :rules= "rules" ref= "ruleForm" label-width= "100px"
  12.                  class= "demo-ruleForm">
  13.           <el-form-item label= "用户名" prop= "username">
  14.             <el-input type= "text" maxlength= "12" v-model= "ruleForm.username"></el-input>
  15.           </el-form-item>
  16.           <el-form-item label= "密码" prop= "password">
  17.             <el-input type= "password" v-model= "ruleForm.password" autocomplete= "off"></el-input>
  18.           </el-form-item>
  19.           <el-form-item>
  20.             <el-button type= "primary" @click= "submitForm('ruleForm')">登录</el-button>
  21.             <el-button @click= "resetForm('ruleForm')">重置</el-button>
  22.           </el-form-item>
  23.         </el-form>
  24.       </el-main>
  25.     </el-container>
  26.   </div>
  27. </template>
  28. <script>
  29.   export default {
  30.     name: 'Login',
  31.     data() {
  32.       var validatePass = (rule, value, callback) => {
  33.         if (value === '') {
  34.           callback( new Error( '请输入密码'));
  35.         } else {
  36.           callback();
  37.         }
  38.       };
  39.       return {
  40.         ruleForm: {
  41.           password: '111111',
  42.           username: 'markerhub'
  43.         },
  44.         rules: {
  45.           password: [
  46.             {validator: validatePass, trigger: 'blur'}
  47.           ],
  48.           username: [
  49.             {required: true, message: '请输入用户名', trigger: 'blur'},
  50.             {min: 3, max: 12, message: '长度在 3 到 12 个字符', trigger: 'blur'}
  51.           ]
  52.         }
  53.       };
  54.     },
  55.     methods: {
  56.       submitForm(formName) {
  57.         const _this = this
  58.         this.$refs[formName].validate((valid) => {
  59.           if (valid) {
  60.             // 提交逻辑
  61.             this.$axios.post( 'http://localhost:8081/login', this.ruleForm).then((res)=>{
  62.               const token = res.headers[ 'authorization']
  63.               _this.$store.commit( 'SET_TOKEN', token)
  64.               _this.$store.commit( 'SET_USERINFO', res.data.data)
  65.               _this.$router.push( "/blogs")
  66.             })
  67.           } else {
  68.             console.log( 'error submit!!');
  69.             return false;
  70.           }
  71.         });
  72.       },
  73.       resetForm(formName) {
  74.         this.$refs[formName].resetFields();
  75.       }
  76.     },
  77.     mounted() {
  78.       this.$notify({
  79.         title: '看这里:',
  80.         message: '关注公众号:MarkerHub,回复【vueblog】,领取项目资料与源码',
  81.         duration: 1500
  82.       });
  83.     }
  84.   }
  85. </script>

找不到啥好的方式讲解了,之后先贴代码,然后再讲解。上面代码中,其实主要做了两件事情

1、表单校验

2、登录按钮的点击登录事件

表单校验规则还好,比较固定写法,查一下element-ui的组件就知道了,我们来分析一下发起登录之后的代码:


  
  
  1. const token = res.headers[ 'authorization']
  2. _this.$store.commit( 'SET_TOKEN', token)
  3. _this.$store.commit( 'SET_USERINFO', res.data.data)
  4. _this.$router.push( "/blogs")

从返回的结果请求头中获取到token的信息,然后使用store提交token和用户信息的状态。完成操作之后,我们调整到了/blogs路由,即博客列表页面。

token的状态同步

所以在store/index.js中,代码是这样的:


  
  
  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. Vue.use(Vuex)
  4. export default new Vuex.Store({
  5.   state: {
  6.     token: '',
  7.     userInfo: JSON.parse(sessionStorage.getItem( "userInfo"))
  8.   },
  9.   mutations: {
  10.     SET_TOKEN: (state, token) => {
  11.       state.token = token
  12.       localStorage.setItem( "token", token)
  13.     },
  14.     SET_USERINFO: (state, userInfo) => {
  15.       state.userInfo = userInfo
  16.       sessionStorage.setItem( "userInfo", JSON.stringify(userInfo))
  17.     },
  18.     REMOVE_INFO: (state) => {
  19.       localStorage.setItem( "token", '')
  20.       sessionStorage.setItem( "userInfo", JSON.stringify( ''))
  21.       state.userInfo = {}
  22.     }
  23.   },
  24.   getters: {
  25.     getUser: state => {
  26.       return state.userInfo
  27.     }
  28.   },
  29.   actions: {},
  30.   modules: {}
  31. })

存储token,我们用的是localStorage,存储用户信息,我们用的是sessionStorage。毕竟用户信息我们不需要长久保存,保存了token信息,我们随时都可以初始化用户信息。当然了因为本项目是个比较简单的项目,考虑到初学者,所以很多相对复杂的封装和功能我没有做,当然了,学了这个项目之后,自己想再继续深入,完成可以自行学习和改造哈。

定义全局axios拦截器

点击登录按钮发起登录请求,成功时候返回了数据,如果是密码错误,我们是不是也应该弹窗消息提示。为了让这个错误弹窗能运用到所有的地方,所以我对axios做了个后置拦截器,就是返回数据时候,如果结果的code或者status不正常,那么我对应弹窗提示。

在src目录下创建一个文件axios.js(与main.js同级),定义axios的拦截:


  
  
  1. import axios from 'axios'
  2. import Element from "element-ui";
  3. import store from "./store";
  4. import router from "./router";
  5. axios.defaults.baseURL= 'http://localhost:8081'
  6. axios.interceptors.request.use(config => {
  7.   console.log( "前置拦截")
  8.   // 可以统一设置请求头
  9.   return config
  10. })
  11. axios.interceptors.response.use(response => {
  12.     const res = response.data;
  13.     console.log( "后置拦截")
  14. // 当结果的code是否为200的情况
  15.     if (res.code === 200) {
  16.       return response
  17.     } else {
  18. // 弹窗异常信息
  19.       Element.Message({
  20.         message: response.data.msg,
  21.         type: 'error',
  22.         duration: 2 * 1000
  23.       })
  24. // 直接拒绝往下面返回结果信息
  25.       return Promise.reject(response.data.msg)
  26.     }
  27.   },
  28.   error => {
  29.     console.log( 'err' + error) // for debug
  30.      if(error.response.data) {
  31.       error.message = error.response.data.msg
  32.     }
  33. // 根据请求状态觉得是否登录或者提示其他
  34.     if (error.response.status === 401) {
  35.       store.commit( 'REMOVE_INFO');
  36.       router.push({
  37.         path: '/login'
  38.       });
  39.       error.message = '请重新登录';
  40.     }
  41.     if (error.response.status === 403) {
  42.       error.message = '权限不足,无法访问';
  43.     }
  44.     Element.Message({
  45.       message: error.message,
  46.       type: 'error',
  47.       duration: 3 * 1000
  48.     })
  49.     return Promise.reject(error)
  50.   })

前置拦截,其实可以统一为所有需要权限的请求装配上header的token信息,这样不需要在使用是再配置,我的小项目比较小,所以,还是免了吧~

然后再main.js中导入axios.js

import './axios.js' // 请求拦截

  
  

后端因为返回的实体是Result,succ时候code为200,fail时候返回的是400,所以可以根据这里判断结果是否是正常的。另外权限不足时候可以通过请求结果的状态码来判断结果是否正常。这里都做了简单的处理。

登录异常时候的效果如下:

9、博客列表

登录完成之后直接进入博客列表页面,然后加载博客列表的数据渲染出来。同时页面头部我们需要把用户的信息展示出来,因为很多地方都用到这个模块,所以我们把页面头部的用户信息单独抽取出来作为一个组件。

头部用户信息

那么,我们先来完成头部的用户信息,应该包含三部分信息:id,头像、用户名,而这些信息我们是在登录之后就已经存在了sessionStorage。因此,我们可以通过store的getters获取到用户信息。

看起来不是很复杂,我们贴出代码:

  • components\Header.vue


  
  
  1. <template>
  2. <div class= "m-content">
  3. <h3>欢迎来到MarkerHub的博客</h3>
  4. <div class= "block">
  5. <el-avatar :size= "50" :src= "user.avatar"></el-avatar>
  6. <div>{{ user.username }}</div>
  7. </div>
  8. <div class= "maction">
  9. <el-link href= "/blogs">主页</el-link>
  10. <el-divider direction= "vertical"></el-divider>
  11. <span>
  12. <el-link type= "success" href= "/blog/add" :disabled= "!hasLogin">发表文章</el-link>
  13. </span>
  14. <el-divider direction= "vertical"></el-divider>
  15. <span v-show= "!hasLogin">
  16. <el-link type= "primary" href= "/login">登陆</el-link>
  17. </span>
  18. <span v-show= "hasLogin">
  19. <el-link type= "danger" @click= "logout">退出</el-link>
  20. </span>
  21. </div>
  22. </div>
  23. </template>
  24. <script>
  25. export default {
  26. name: "Header",
  27. data() {
  28. return {
  29. hasLogin: false,
  30. user: {
  31. username: '请先登录',
  32. avatar: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
  33. },
  34. blogs: {},
  35. currentPage: 1,
  36. total: 0
  37. }
  38. },
  39. methods: {
  40. logout() {
  41. const _this = this
  42. this.$axios.get( 'http://localhost:8081/logout', {
  43. headers: {
  44. "Authorization": localStorage.getItem( "token")
  45. }
  46. }).then((res) => {
  47. _this.$store.commit( 'REMOVE_INFO')
  48. _this.$router.push( '/login')
  49. });
  50. }
  51. },
  52. created() {
  53. if(this.$store.getters.getUser.username) {
  54. this.user.username = this.$store.getters.getUser.username
  55. this.user.avatar = this.$store.getters.getUser.avatar
  56. this.hasLogin = true
  57. }
  58. }
  59. }
  60. </script>

上面代码created()中初始化用户的信息,通过hasLogin的状态来控制登录和退出按钮的切换,以及发表文章链接的disabled,这样用户的信息就能展示出来了。然后这里有个退出按钮,在methods中有个logout()方法,逻辑比较简单,直接访问/logout,因为之前axios.js中我们已经设置axios请求的baseURL,所以这里我们不再需要链接的前缀了哈。因为是登录之后才能访问的受限资源,所以在header中带上了Authorization。返回结果清楚store中的用户信息和token信息,跳转到登录页面。

然后需要头部用户信息的页面只需要几个步骤:


  
  
  1. import Header from "@/components/Header";
  2. data() {
  3. components: {Header}
  4. }
  5. # 然后模板中调用组件
  6. <Header></Header>

博客分页

接下来就是列表页面,需要做分页,列表我们在element-ui中直接使用时间线组件来作为我们的列表样式,还是挺好看的。还有我们的分页组件。

需要几部分信息:

  • 分页信息

  • 博客列表内容,包括id、标题、摘要、创建时间

  • views\Blogs.vue


  
  
  1. <template>
  2. <div class= "m-container">
  3. <Header></Header>
  4. <div class= "block">
  5. <el-timeline>
  6. <el-timeline-item v-bind:timestamp= "blog.created" placement= "top" v- for= "blog in blogs">
  7. <el-card>
  8. <h4><router-link :to= "{name: 'BlogDetail', params: {blogId: blog.id}}">{{blog.title}}</router-link></h4>
  9. <p>{{blog.description}}</p>
  10. </el-card>
  11. </el-timeline-item>
  12. </el-timeline>
  13. </div>
  14. <el-pagination class= "mpage"
  15. background
  16. layout= "prev, pager, next"
  17. :current-page=currentPage
  18. :page-size=pageSize
  19. @current-change=page
  20. :total= "total">
  21. </el-pagination>
  22. </div>
  23. </template>
  24. <script>
  25. import Header from "@/components/Header";
  26. export default {
  27. name: "Blogs",
  28. components: {Header},
  29. data() {
  30. return {
  31. blogs: {},
  32. currentPage: 1,
  33. total: 0,
  34. pageSize: 5
  35. }
  36. },
  37. methods: {
  38. page(currentPage) {
  39. const _this = this
  40. this.$axios.get( 'http://localhost:8081/blogs?currentPage=' + currentPage).then((res) => {
  41. console.log(res.data.data.records)
  42. _this.blogs = res.data.data.records
  43. _this.currentPage = res.data.data.current
  44. _this.total = res.data.data.total
  45. _this.pageSize = res.data.data.size
  46. })
  47. }
  48. },
  49. mounted () {
  50. this.page( 1);
  51. }
  52. }
  53. </script>

data()中直接定义博客列表blogs、以及一些分页信息。methods()中定义分页的调用接口page(currentPage),参数是需要调整的页码currentPage,得到结果之后直接赋值即可。然后初始化时候,直接在mounted()方法中调用第一页this.page(1)。完美。使用element-ui组件就是简单快捷哈哈!注意标题这里我们添加了链接,使用的是标签。

10、博客编辑(发表)

我们点击发表博客链接调整到/blog/add页面,这里我们需要用到一个markdown编辑器,在vue组件中,比较好用的是mavon-editor,那么我们直接使用哈。先来安装mavon-editor相关组件:

安装mavon-editor

基于Vue的markdown编辑器mavon-editor

cnpm install mavon-editor --save

  
  

然后在main.js中全局注册:


  
  
  1. // 全局注册
  2. import Vue from 'vue'
  3. import mavonEditor from 'mavon-editor'
  4. import 'mavon-editor/dist/css/index.css'
  5. // use
  6. Vue.use(mavonEditor)

ok,那么我们去定义我们的博客表单:


  
  
  1. <template>
  2. <div class= "m-container">
  3. <Header></Header>
  4. <div class= "m-content">
  5. <el-form ref= "editForm" status-icon :model= "editForm" :rules= "rules" label-width= "80px">
  6. <el-form-item label= "标题" prop= "title">
  7. <el-input v-model= "editForm.title"></el-input>
  8. </el-form-item>
  9. <el-form-item label= "摘要" prop= "description">
  10. <el-input type= "textarea" v-model= "editForm.description"></el-input>
  11. </el-form-item>
  12. <el-form-item label= "内容" prop= "content">
  13. <mavon-editor v-model= "editForm.content"/>
  14. </el-form-item>
  15. <el-form-item>
  16. <el-button type= "primary" @click= "submitForm()">立即创建</el-button>
  17. <el-button>取消</el-button>
  18. </el-form-item>
  19. </el-form>
  20. </div>
  21. </div>
  22. </template>
  23. <script>
  24. import Header from "@/components/Header";
  25. export default {
  26. name: "BlogEdit",
  27. components: {Header},
  28. data() {
  29. return {
  30. editForm: {
  31. id: null,
  32. title: '',
  33. description: '',
  34. content: ''
  35. },
  36. rules: {
  37. title: [
  38. {required: true, message: '请输入标题', trigger: 'blur'},
  39. {min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur'}
  40. ],
  41. description: [
  42. {required: true, message: '请输入摘要', trigger: 'blur'}
  43. ]
  44. }
  45. }
  46. },
  47. created() {
  48. const blogId = this.$route.params.blogId
  49. const _this = this
  50. if(blogId) {
  51. this.$axios.get( '/blog/' + blogId).then((res) => {
  52. const blog = res.data.data
  53. _this.editForm.id = blog.id
  54. _this.editForm.title = blog.title
  55. _this.editForm.description = blog.description
  56. _this.editForm.content = blog.content
  57. });
  58. }
  59. },
  60. methods: {
  61. submitForm() {
  62. const _this = this
  63. this.$refs.editForm.validate((valid) => {
  64. if (valid) {
  65. this.$axios.post( '/blog/edit', this.editForm, {
  66. headers: {
  67. "Authorization": localStorage.getItem( "token")
  68. }
Logo

前往低代码交流专区

更多推荐