title: 多人博客项目date: 2019-11-27 15:53:44 tags: 项目
blog3 说明 使用Vue-cli3搭建,高度集成webpack,不暴露webpack配置,相应插件需要自行创建vue.config.js,在其中配置.
前后端接口约定 后端接口规范
当前接口路径
当前接口提交数据类型,如:
GET: 获取数据
POST: 提交或者创建
PUT: 修改数据
DELETE: 删除数据
PATCH: 修改数据,部分修改
参数类型格式: json或者application/x-www-form-urlencoded的数据
参数字段及限制条件
返回成功的数据格式
返回失败的数据格式
下面和后端做以下接口约定,开发阶段使用postman或者curl命令测试接口
认证相关
POST /auth/register 功能: 用户注册
提交参数
参数类型: Content-type: application/x-www-form-urlencoded;chratset=utf-8
参数字段:
username: 用户名,长度1-15个字符,只能是字母数字下划线
password: 密码,长度6-16位任意字符
返回数据
失败:
返回格式: {"status": "fail", "msg": "错误原因"}
成功:
返回格式:
1 2 3 4 5 6 7 8 9 10 11 { "status" : "ok" , "msg" : "注册成功" , "data" : { "id" : 1 , "username" : "Tom" , "avatar" : "http://avatar.com/1.png" , "createdAt" : "2019-10-19T15:15:33.343Z" , "updatedAt" : "2019-10-19T15:15:33.343Z" } }
测试命令
# -d用来传递数据
# 对于POST和PUT可以: -X POST,对于GET,不加-X
curl -d "usernme=Tom&password=123456" -X POST "http://localhost:3000/auth/regitster"
GET /auth/login 功能: 用户登录
提交参数
参数类型: Content-type: application/x-www-form-urlencoded;chratset=utf-8
参数字段:
1 2 username: 用户名,长度1-15个字符,只能是字母数字下划线 password: 密码,长度6-16位任意字符
返回数据
失败:
1 返回格式: `{"status": "fail", "msg": "用户不存在"}或者 {"status": "fail", "msg": "密码不正确"}`
成功:
返回格式:
1 2 3 4 5 6 7 8 9 10 11 { "status" : "ok" , "msg" : "注册成功" , "data" : { "id" : 1 , "username" : "Tom" , "avatar" : "http://avatar.com/1.png" , "createdAt" : "2019-10-19T15:15:33.343Z" , "updatedAt" : "2019-10-19T15:15:33.343Z" } }
测试命令
# -d用来传递数据
# -i 可以显示响应头
# 会发现响应头里有setCookie信息,得到cookie
curl -d "usernme=Tom&password=123456" "http://localhost:3000/auth/login" -i
GET /auth 功能: 判断用户是否登录
提交参数: 无
返回数据:
已经登录的情况
返回格式:
1 2 3 4 5 6 7 8 9 10 { "status" : "ok" , "isLogin" : "true" , "data" : { "id" : 1 , "username" : "Tom" , "createdAt" : "2019-10-19T15:15:33.343Z" , "updatedAt" : "2019-10-19T15:15:33.343Z" } }
没有登录的情况
1 2 3 4 { "status": "ok" "isLogin": false }
测试命令
#先通过登录接口获取 cookie,带上 cookie 就能测试登录
curl "http://localhost:3000/auth" -b "connect.sid=s%3AmeDbrn03UtTM8fqChaPQ20wmWlnKeHiu.e3uMtu7j1zQ1iNeaajCmxkYYGQ%2FyHV1ZsozMvZYWC6s"
GET /auth/logout 功能: 注销登录
提交参数:无
返回数据:
1 2 3 4 5 失败: 返回格式: `{ "status": "fail", "msg": "用户尚未登录"}` 成功: 返回格式: `{ "status": "success", "msg": "注销成功"}`
测试命令
curl "http://localhost:3000/auth/logout" -b "connect.sid=s%3AmeDbrn03UtTM8fqChaPQ20wmWlnKeHiu.e3uMtu7j1zQ1iNeaajCmxkYYGQ%2FyHV11ZsozMvZYWC6s"
博客相关 GET /blog 功能: 获取博客列表
提交参数:
1 2 3 page: 页码,不传默认为1. userId: 用户ID,不传获取全部用户ID atIndex: 是否展示在首页.true只得到展示到首页的博客列表,false得到不展示到首页的列表,不传得到全部类型的博客列表
如 /blog?page=2&userId=1 获取属于用户1的第二页博客列表
返回数据:
成功:
返回格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "status" : "ok" , "msg" : "获取成功" , "total" : 200 , "page" : 2 , "totalPage" : 10 , "data" : { "id" : 1 , "title" : "博客标题" , "description" : "博客内容简要描述" , "user" : { "id" : 100 , "username" : "博客所属用户username" , "avatar" : "头像" }, "createdAt" : "2019-10-19T15:15:33.343Z" , "updatedAt" : "2019-10-19T15:15:33.343Z" , } }
测试命令
curl "http://localhost:3000/blog?page=1&userId=1"
curl "http://localhost:3000/blog?page=1"
curl "http://localhost:3000/blog"
GET /blog/:blogId 功能: 获取id为blogId的博客详情,如 /blog/1
提交参数: 无
返回数据:
失败: { "status": "fail", "msg": "系统异常"}
成功:
返回格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "status" : "ok" ,"msg" : "获取成功" ,"data" : { "id" : 1 , "title" : "博客标题" , "description" : "博客内容简要描述" , "content" : "博客内容" , "user" : { "id" : 100 , "username" : "博客所属用户username" , "avatar" : "头像" }, "createdAt" : "2019-10-19T15:15:33.343Z" , "updatedAt" : "2019-10-19T15:15:33.343Z" , } }
POST /blog 功能: 创建博客
提交参数:
1 2 3 4 5 参数类型: Content-type: application/x-www-form-urlencoded; charset=utf-8; 参数字段: title : 博客标题, 博客标题不能为空,且不超过100个字符 content : 博客内容, 博客内容不能为空,且不超过10000个字符 description: 博客内容简要描述,可为空,如果为空则后台自动从content 中提取
返回数据:
1 失败: { "status": "fail", "msg": "登录后才能操作"}
成功:
返回格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "status": "ok", "msg": 创建成功", "data": { "id": 1, "title": "博客标题", "description": "博客内容简要描述", "content": "博客内容", "user": { "id": 100, //博客所属用户id "username": "博客所属用户username", "avatar": "头像" }, "createdAt": "2019-10-19T15:15:33.343Z", //创建时间 "updatedAt": "2019-10-19T15:15:33.343Z", //更新时间 } }
测试命令
curl -d "title=hello&content=world&description=jirengu" -X POST "http://localhost:3000/blog" -b "connect.sid=s%3AdyZh-z5fqPU_ThG9Qn8nGD6euI0UI75e.8uso0k4P6WzqWv02iQCUwxbUML2RdlOCnpKp7RSJpj0"
PATCH /blog/:blogid 功能: 修改博客id为:blogid的博客
范例: /blog/1
提交参数
参数类型: Content-Type: application/x-www-form-urlencoded; charset=utf-8
参数字段:
1 2 3 4 title: 博客标题 content : 博客内容, 博客内容不能为空,且不超过10000个字符 description: 博客内容简要描述,可为空,如果为空则后台自动从content 中提取 atIndex: true/false, 展示到首页/从首页异常, 可选
返回数据
失败
返回格式:
1 2 3 {"status" : "fail" , "msg" : "登录后才能操作" } {"status" : "fail" , "msg" : "博客不存在" } {"status" : "fail" , "msg" : "无法修改别人的博客" }
成功
返回格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "status": "ok", "msg": 修改成功", "data": { "id": 1, "title": "博客标题", "description": "博客内容简要描述", "content": "博客内容", "user": { "id": 100, //博客所属用户id "username": "博客所属用户username", "avatar": "头像" }, "createdAt": "2019-10-19T15:15:33.343Z", //创建时间 "updatedAt": "2019-10-19T15:15:33.343Z", //更新时间 } }
测试命令
curl -d "title=hello100&content=world1&description=jirengu2234444444&atIndex=true" -X PATCH "http://localhost:3000/blog/12" -b "connect.sid=s%3At_9V2bMXA7U9oSAmr1dhRXpdRPAsNM2B.jlpWgkwiWdpgTjexeTHGNydt8gvc%2F%2BEkJpQ9yaAmTg0"
DELETE /bog/:blogid 功能: 删除id为:blogid的博客
提交参数:无
返回数据
失败
返回格式范例
1 2 3 {"status" : "fail" , "msg" : "登录后才能操作" } {"status" : "fail" , "msg" : "博客不存在" } {"status" : "fail" , "msg" : "无法删除别人的博客" }
成功
返回格式
1 2 3 4 { "status" : "ok" ,"msg" : "删除成功" }
测试命令
curl -X DELETE "http://localhost:3000/blog/12" -b "connect.sid=s%3AG_Chytg2F0RLWh2wTSCdLWVxpNg1MWWb.nPuMcgyMN6zxuxjSkyu8qSqM1boruol1Nce7egaXrPw"
文件介绍 main.js 项目入口
1 2 3 4 5 6 7 import Vue ,app,router from ...new Vue ({ el :"#app" , router, component : { App }, template : '<App /> })
App.vue 当前整个项目的模板
tempalte,js,style
component/xxx.vue 各个组件,也可以同名的文件夹,包含vue,css,js
router.js 路由组件
请求接口封装 helpers/request.js 请求组件
import axios
//约定请求文件格式
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
//接口路径
axios.defaults.baseURL = 'http://blog-server.hunger-valley.com'
//前后端分离,即使异步请求也带上cookie
axios.defaults.withCredentials = true
//函数导出
export default function request(){ ... }
api接口封装 @/api/auth.js //引入请求接口
import request from '@/helpers/request'
把各个接口进行封装,便于后续调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const URL = { REGISTER : '/auth/register' , LOGIN : '/auth/login' , LOGOUT : '/auth/logout' , GET_INFO : '/auth' } export default { register ({username, password} ){ return request (URL .REGISTER , 'POST' , { username, password }) }, login ({ username, password} ){ return request (URL .LOGIN , 'POST' , { username, password }) }, logout ( ){ return request (URL .LOGOUT ) }, getInfo ( ){ return request (URL .GET_INFO ) } }
封装blog接口 同样引入请求接口
import request from '@/helpers/request'
封装blog的各个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const URL = { GET_LIST : '/blog' , GET_DETAIL : '/blog/:blogId' , CREATE : '/blog' , UPDATE : '/blog/:blogId' , DELETE : '/blog/:blogId' } export default { getBlogs ({ page=1 , userId, atIndex } = { page:1 } ){ return request (URL .GET_LIST , 'GET' , { page, userId, atIndex }) }, getIndexBlogs ({ page=1 } = { page: 1 } ){ return this .getBlogs ({ userId, page, atIndex }) } getDetail ({ blogId } ){ return reuqest (URL .GET_DETAIL , replace (':blogId' , blogId)) }, updateBlog ({ blogId }, { title, content, description, atIndex } ){ return request (URL .UPDATE , replace (':blogId' , blogId), 'PATCH' , { title, content, description, atIndex }) }, deleteBlog ({ blogId } ){ return request (URL .DELETE , replace (':blogId' ,blogId), 'DELETE' ) }, createBlog ({ title = '' , content = '' , description = '' ,atIndex = false } = { title = '' , content = '' } ){ return request (URL .CREATE , 'POST' , { title, content, description }) } }
首页布局 …
状态管理 store.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 const state = { user : null , isLogin : false , }; const getters = { user : (state ) => state.user , isLogin : (state ) => state.isLogin , }; const mutations = { setUser (state, payload ) { state.user = payloag.user ; }, setLogin (state, payload ) { state.isLogin = payload.isLogin ; }, }; const actions = { async login ({ commit }, { username, password } ) { let res = await auth.login ({ username, password }); commit ("setUser" , { user : res.data }); commit ("setLogin" , { isLogin : true }); return res.data ; }, async register ({ commit }, { username, password } ) { let res = await auth.register ({ username, password }); commit ("setUser" , { user : res.data }); commit ("setLogin" , { isLogin : true }); return res.data ; }, async logout ({ commit } ) { await auth.logout (); commit ("setUser" , { user : null }); commit ("setLogin" , { isLogin : false }); }, async checkLogin ({ commit, state } ) { if (state.isLogin ) return true ; let res = await auth.getInfo (); commit ("setLogin" , { isLogin : res.isLogin }); if (!res.isLogin ) return false ; commit ("setUser" , { user : res.data }); return true ; }, }; export default { state, getters, mutations, actions, };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { mapState, mapActions } from 'vuex' export default { data ( ){ return {} }, computed ( ){ ...mapGetters ([ 'isLogin' , 'user' ]) }, created ( ){ this .checkLogin () }, methods : { ...mapActions ({ 'checkLogin' }) } }
登录和注册 1 2 3 4 5 6 7 8 9 10 11 <template> <div id ="login" > <h4 > 用户名</h4 > <input v-model ="username" placeholder ="用户名" > <h4 > 密码</h4 > <input v-model ="password" type ="password" placeholder ="密码" @key.enter ="onLogin" > <el-button size ="medium" @click ="onLogin" > 立即登录</el-button > <p class ="notice" > 没有账号?<router-link to ="/register" > 注册新用户</router-link > </p > </div > </template >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { mapActions } from 'vuex' export default { data ( ){ return { username : '' , password : '' } }, methods : { ...mapActions (['login' ]) onLogin ( ){ console .log (this .username + this .password ) this .login ({username : this .username , password : this .password }) .then (() => { this .$router .push ({path : this .$route .query .redirect || '/' }) }) } } }
注册 注册类似于登录
将 login 换成 register 即可.
路由组件 router.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import Vue from "vue" ;import Router from "vue-router" ;import store from "./src/store.js" ;Vue .use (Router );const router = new Router ({ routes : [ { path : "/" , component : () => import ("@/pages/Index/template.vue" ), }, { path : "/login" , component : () => import ("@/pages/Login/template.vue" ), }, { path : "/detail/:blogId" , component : () => import ("@/pages/Detail/template.vue" ), }, { path : "/edit/:blogId" , component : () => import ("@/pages/Edit/template.vue" ), meta : { requiresAuth : true }, }, { path : "/create" , component : () => import ("@/pages/Create/template.vue" ), meta : { requiresAuth : true }, }, { path : "/user/:userId" , component : () => import ("@/pages/User/template.vue" ), }, { path : "/my" , component : () => import ("@/pages/My/template.vue" ), meta : { requiresAuth : true }, }, { path : "/register" , component : () => import ("@/pages/Register/template.vue" ), }, ], }); router.beforeEach ((to, from , next ) => { if (to.matched .some ((record ) => record.meta .requiresAuth )) { store.dispatch ("checkLogin" ).then ((isLogin ) => { if (!isLogin) { next ({ path : "/login" , query : { redirect : to.fullPath }, }); } else { next (); } }); } else { next (); } }); export default router;
创建博客页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <template> <div id ="edit" > <h1 > 创建文章</h1 > <h3 > 文章标题</h3 > <el-input v-model ="title" > </el-input > <p class ="msg" > 限30个字</p > <h3 > 内容简介</h3 > <el-input type ="textarea" v-model ="description" :autosize ="{ minRow: 2, maxRows: 4 }" > </el-input > <p class ="msg" > 限30个字</p > <h3 > 文章内容</h3 > <el-input type ="textarea" v-model ="content" :autosize ="{ minRow: 4, maxRows: 30 }" > </el-input > <p class ="msg" > 限30个字</p > <p > <label > 是否展示到首页</label > <el-switch v-model ="atIndex" active-color ="#13ce66" inactive-color ="#fff" > </el-switch > </p > <el-button @click ="onCreate" > 确定</el-button > </div > </template> <script > import blog from '@/api/blog' export default { data ( ){ return { title : '' , description : '' , content : '' , atIndex : false } }, methods : { onCreate ( ){ blog.createBlog ({ title : this .title , content : this .content , description : this .description , atIndex : this .atIndex }) .then (res => { this .$message .success (res.msg ) this .$router .push ({ path : `/detail/${res.data.id} ` }) }) } } } </script >
完善首页 详情页 时间插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function friendlyDate (datsStr ) { let dateObj = typeof datsStr === "object" ? datsStr : new Date (datsStr); let time = dateObj.getTime (); let now = Date .now (); let space = now - time; let str = "" ; switch (true ) { case space < 60000 : str = "刚刚" ; break ; case space < 1000 * 3600 : str = Math .floor (space / 60000 ) + "分钟前" ; break ; case space < 1000 * 3600 * 24 : str = Math .floor (space / (1000 * 3600 )) + "小时前" ; break ; default : str = Math .floor (space / (1000 * 3600 * 24 )) + "天前" ; } return str; } export default { install (Vue, options ) { Vue .prototype .friendlyDate = friendlyDate; }, };
个人页面 user 有点东西
登陆者的个人页面 my 遇到过的问题
vuex 中 actions 错误(原因: modules 拼错了)
登录时 enter 无法使用.(解决方法: 加.native)
文章内容应有一定区域(原因: 没写 palceholder)
编辑时少一个设置为首页的按钮(原因:按钮初始颜色透明)
my 页面无法显示(原因: 因为错误 6)
过滤器 getMouth 错误,(原因: mouth 单词拼错)
my 页面编辑删除两个按钮靠的太近
header 布局错误(原因: h1 标签没有把 router-link 包裹,如果包裹,less 有设置 h1 下 a 标签的颜色为白色.
没有包裹的时候,a 标签的颜色是 common.less 设置的黑色,会覆盖)
标题颜色错误,把标题文本和普通文本颜色算在一起了.