前言
- 在线博客项目基于Vue2.6及相关技术栈vue-router,vuex完成,单页项目。
- 基于前后端接口约定开发
本文约定:星星,旗子,水滴与赞
⭐标注为踩坑总结;
🚩标注为封装优化;
💧标注为搁置,暂时不打算实现;
👍心得写在这里:
项目搭建
- 参照官方文档
- 使用less语法
- 使用vue-router, axios, vuex, element-ui, marked
- 部署至github,gitee
创建路由
- 路由懒加载
- 🚩动态匹配路由
- 🚩路由完善
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/register',
name: 'Register',
component: () => import('../pages/register/template.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('../pages/login/template.vue')
}
// More routes..
]
const router = new VueRouter({
routes
})
//动态匹配路由1
{
path:'/user/:id',
name:'user',
component: () => import('../pages/User/user.vue'),
},
{
path:'/edit/:blogId',
name:'edit',
component: () => import('../pages/Edit/edit.vue'),
}
//动态匹配路由2
//Home.vue<template>
<router-link
class="item"
v-for="blog in blogs"
:key="blog.id"
:to="{ name: 'detail', params: { blogId: blog.id } }"
>
//Detail.vue<template>
<div id="detail">
<!-- <section class="user-info" v-if="user"> -->
<section class="user-info">
<img :src="user.avatar" />
<h3>{{title}}</h3>
<p><router-link :to="{ name:'user', params: {id:user.id} }">{{user.username}}</router-link>更新于{{$friendlyDate(user.updatedAt)}}</p>
</section>
<section class="article" v-html="markdown">文章本体</section>
</div>
//路由守卫
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
// console.log(store.state.isLogin)
store.dispatch("checkLogin").then(isLogin => {
if (!isLogin) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
})
}
else {
next() // 确保一定要调用 next()
}
})
api接口封装
- axios的请求封装(请求头参数:
jwt
鉴权机制) - 对登录以及博客相关接口进行封装:将
request
函数import进来,在Auth对象的每个属性中发起请求。
//model
// @/helper/request2.js
import axios from 'axios'
import { Message } from 'element-ui'
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
axios.defaults.baseURL = 'http://blog-server.hunger-valley.com'
const request = (url, type='GET', data={}) => {
return new Promise( (resolve, reject) => {
//http://axios-js.com/zh-cn/docs/index.html#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE
//axios()传入对象,是使用axios(configs)
let option = {
url,
method: type
}
if(type.toLowerCase() ==='get') {
option.params = data
}else if(type.toLowerCase() === 'post') {
option.data = data
}
//-----------
if(localStorage.token) {
axios.defaults.headers.common['Authorization'] = localStorage.token
}
axios(option).then(res => {
// console.log(res.data)
//---------
if(res.data.status === 'ok') {
if(res.data.token) {
localStorage.token = res.data.token
}
resolve(res.data)
}else{
console.log("这里是request2..")
Message.error(res.data.msg)
reject(res.data)
}
})
})
}
export default request
// @/api/auth.js
import request from '../helpers/request2'
const URL = {
REGISTER: '/auth/register',
LOGIN: '/auth/login',
LOGOUT: '/auth/logout',
GET_INFO: '/auth'
}
const Auth = {
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)
},
}
export default Auth
测试A:
//@/helpers/request2.test.js
import request from './request2'
window.request = request
request("/auth/register", "POST", {username:'buool2',password:'42jdjk'})
// @/main.js
import './helpers/request.test';
测试B:
// @/api/blog.test.js
import Blog from './blog'
window.Blog = Blog
// Blog.getIndexBlogs()
// Blog.getIndexBlogs({page:2})
// Blog.getBlogs()
⭐jwt鉴权机制
store
store—modules
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import Auth from '../api/auth'
import AuthStore from './modules/AuthStore'
// import BlogStore from './modules/blog'
Vue.use(Vuex)
let store = new Vuex.Store({
modules: {
AuthStore,
// BlogStore
}
})
window.store = store
export default store
store—AuthStore
import Auth from '../../api/auth'
const state = {
user:null,
isLogin: false
}
const getters = {
user: state => state.user,
isLogin: state => state.isLogin
}
const mutations = {
setUser(state, payload) {
state.user = payload.user
},
setLogin(state, payload) {
state.isLogin = payload.isLogin
}
}
const actions = {
login({commit}, {username, password}) {
return Auth.login({username,password})
.then( res => {
console.log(res)
commit("setUser",{user: res.data})
commit("setLogin",{isLogin: true})
})
},
async register({commit}, {username, password}) {
let res = await Auth.register({username,password})
console.log(res)
commit("setUser",{user: res.data})
commit("setLogin",{isLogin: true})
return res.data
},
async logout({commit}){
// let res = await Auth.logout()
// console.log(res)
await Auth.logout()
commit('setUser', {user: null})
commit('setLogin', {isLogin: false})
},
async checkLogin({commit, state}){
if(state.isLogin) return true
// console.log("excuted??")
let res = await Auth.getInfo()
// console.log("excuted??")
// console.log(res.isLogin)
commit('setLogin', { isLogin: res.isLogin})
if(!res.isLogin) return false
commit('setUser', { user: res.data })
// console.log("excuted??")
return true
// if(state.isLogin) return true
// return false
}
}
const AuthStore = {
state,
getters,
mutations,
actions
}
export default AuthStore
//在page/login/template.js测试login 发现store没有变化,跳转header没有改变
静态布局
目的
- 设计稿
-
Header
,Footer
组件 - 使用element-ui,💧添加组件样式
- less_变量@backgroundColor: 特定文件定义变量:
@/assets/base.less
- 使用grid布局
SPA的首页布局
// App.vue
<template>
<div id="app">
<Header id="header"/>
<main>
<router-view/>
</main>
<Footer id="footer"/>
</div>
</template>
<script>
import Header from './components/Header'
import Footer from '@/components/Footer'
export default {
components: {
Header,
Footer
}
}
</script>
<style lang="less">
#app {
min-height: 100vh;
display: grid;
grid: ~"auto 1fr auto / 12% 1fr 12%";
}
#header {
grid-area: ~"1/1/2/4";
}
#footer {
grid-area: ~"3/1/4/4";
}
main {
grid-area: ~"2/2/3/3";
}
//媒体查询
@media (max-width: 768px) {
#app {
grid-template-columns: 10px auto 10px;
// grid: ~"auto 1fr auto / 10px auto 10px";
#header, #footer {
padding-left: 10px;
padding-right: 10px;
}
}
}
</style>
Header组件
- 条件渲染
- button跳转登录、注册页面
vuex状态管理
- ⭐什么是vuex状态管理
- 如何使用(管理鉴权相关的状态:isLogin, user控制header组件的条件渲染)
vuex状态管理
官网介绍:
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
1.应用的状态:拿本项目的header说事,每个client端对应的都是一个user,它们的header组件渲染的user,isLogin数据应该放进store里。
- 更改 Vuex 的 store 中的状态的唯一方法是提交 mutations:
2.1. store.commit(balabla, {guala:"ngualaala"}) 提交mutations
2.2. actions中异步提交mutations,通过参数解构得到commit;
2.3. store.dispatch("kalakallala") 提交actions进而提交mutations.
2.4. 在vue组件的methods中解构...mapActions(["kasnk","lulalala"])
,所有的actions通过vue实例(this)调用;
开发:
- @/src/pages/login/template.vue -静态样式
- @/src/store/index.js - 使用Vuex_Modules
- @/src/store/modules/authStore.js -[state, getters,mutations, actions]
- @/components/Header.vue
- @/src/pages/login/template.js
//3 维护user, isLogin
import Auth from '../../api/auth'
const state = {
user:null,
isLogin: false
}
const getters = {
user: state => state.user,
isLogin: state => state.isLogin
}
const mutations = {
setUser(state, payload) {
console.log(">>>>")
state.user = payload.user
},
setLogin(state, payload) {
state.isLogin = payload.isLogin
}
}
const actions = {
login({commit}, {username, password}) {
return Auth.login({username,password})
.then( res => {
console.log(res)
commit("setUser",{user: res.data})
commit("setLogin",{isLogin: true})
})
},
async register({commit}, {username, password}) {
let res = await Auth.register({username,password})
console.log(res)
commit("setUser",{user: res.data})
commit("setLogin",{isLogin: true})
return res.data
},
async logout({commit}){
// let res = await Auth.logout()
// console.log(res)
await Auth.logout()
commit('setUser', {user: null})
commit('setLogin', {isLogin: false})
},
async checkLogin({commit, state}){
if(state.isLogin) return true
// console.log("excuted??")
let res = await Auth.getInfo()
// console.log("excuted??")
// console.log(res.isLogin)
commit('setLogin', { isLogin: res.isLogin})
if(!res.isLogin) return false
commit('setUser', { user: res.data })
// console.log("excuted??")
return true
// if(state.isLogin) return true
// return false
}
}
const AuthStore = {
state,
getters,
mutations,
actions
}
export default AuthStore
//在page/login/template.js测试login 发现store没有变化,跳转header没有改变
// 4. 在UI层各种办法提交mutations
// @/components/Header.vue
<script>
import { mapGetters, mapActions } from "vuex"
export default {
data() {
return {
isTesting: false,
// isLogin: false,
}
},
computed: {
...mapGetters(["user","isLogin"])
},
methods: {
onLogout(){
this.logout()
},
...mapActions([
'logout',
'checkLogin'
]),
},
mounted() {
this.checkLogin()
}
};
</script>
// 5. @/src/pages/login/template.js
import { mapActions } from "vuex"
export default {
data(){
return {
username:"",
password:""
}
},
methods: {
...mapActions(["login"]),
onLogin() {
console.log(`{username:${this.username}, password:${this.password}}`)
this.login({username:this.username, password:this.password})
.then(()=>{
this.$router.push({path: this.$route.query.redirect || '/'})
})
}
}
}
动态匹配路由 || 完善路由
-
<router-link to="{name:xxx, params:xxx}">
跳转动态路由 - 路由元信息:仅登录后允许跳转
1.
<router-link to="{name:xxx, params:xxx}">
在to属性中,path与params不能同时使用。
2.参考: 全局导航守卫中检查元字段
3.this.$router.push({path: this.$route.query.redirect || '/'})
页面的完善
- 列表的循环渲染,使用<router-link>进行跳转;
- 分页组件(element-ui);
- 创建博客/修改博客(marked.js);
- 删除博客(确认后删除);
- vue中插件的使用:// 开发插件
5.1 定义插件
5.2 将friendlyDate()函数
作为插件使用;
// 1-2, 5.2
<template>
<div id="index">
<section class="blog-post">
<router-link
class="item"
v-for="blog in blogs"
:key="blog.id"
:to="{ name: 'detail', params: { blogId: blog.id } }"
>
<figure>
<img :src="blog.user.avatar" :alt="blog.user.username" />
<figcaption>{{ blog.user.username }}</figcaption>
</figure>
<h3>
{{ blog.title }}<span>{{ $friendlyDate(blog.user.createdAt) }}</span>
</h3>
<p>{{ blog.description }}</p>
</router-link>
</section>
<section class="pagination">
<el-pagination
layout="prev, pager, next"
:total="total"
@current-change="handleCurrentChange"
></el-pagination>
</section>
</div>
</template>
<script>
import Blog from "../api/blog";
export default {
data() {
return {
blogs: [],
page: 1,
total: 1,
totalPage: 1,
};
},
created() {
Blog.getIndexBlogs().then((res) => {
this.blogs = res.data;
this.page = res.page;
this.total = res.total;
this.totalPage = res.totalPage;
});
},
methods: {
handleCurrentChange(page) {
console.log(page);
Blog.getIndexBlogs({ page }).then((res) => {
this.blogs = res.data;
this.page = res.page;
this.total = res.total;
this.totalPage = res.totalPage;
});
},
},
};
</script>
// 5.1
// @/main.js
import Vue from 'vue'
import Util from './helpers/util'
Vue.use(Util);
// @/helpers/util.js
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
}
// 开发插件 https://cn.vuejs.org/v2/guide/plugins.html#%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6
const Util = {
install: function(Vue, options){
Vue.prototype.$friendlyDate = friendlyDate
}
}
export default Util
修缮细节
- 修改项目页面的名字和图标
方法1:document.title="easy-vue-blog"
方法2: 在page.vue中使用自定义指令v-title
方法3:Use an existing Component
//https://cn.vuejs.org/v2/guide/custom-directive.html
//1.在main.js 页面里添加自定义指令//
Vue.directive('title', {//单个修改标题
inserted: function (el, binding) {
document.title = el.dataset.title
}
})
//2.在需要修改的页面里添加v-title 指令
<div v-title data-title="我是新的标题"></div>
- 移动端的媒体查询
注册界面由于表单的样式问题导致网页扩张。
Github_commit
部署到Github
- 将axios请求修改为https协议
- 将
/dist
文件夹上传至独立的github项目并设置Github Pages预览 - 部署上线时发现,github page请求资源的路径有问题,过去的解决方案是修改assetsPublicPath,我一番摸索后,是这样解决的:在vue ui中vue-cli_打开vue配置_修改publicPath为相对路径。还是懵逼,不过问题解决了...项目已在github上线!