Vue:在线博客项目开发日志

前言

  • 在线博客项目基于Vue2.6及相关技术栈vue-router,vuex完成,单页项目。
  • 基于前后端接口约定开发

本文约定:星星,旗子,水滴与赞
⭐标注为踩坑总结;
🚩标注为封装优化;
💧标注为搁置,暂时不打算实现;
👍心得写在这里:

项目搭建

  1. 参照官方文档
  2. 使用less语法
  3. 使用vue-router, axios, vuex, element-ui, marked
  4. 部署至github,gitee

创建路由

  1. 路由懒加载
  2. 🚩动态匹配路由
  3. 🚩路由完善
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接口封装

  1. axios的请求封装(请求头参数:jwt鉴权机制)
  2. 对登录以及博客相关接口进行封装:将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没有改变

静态布局

目的

  1. 设计稿
  2. Header, Footer组件
  3. 使用element-ui,💧添加组件样式
  4. less_变量@backgroundColor: 特定文件定义变量:@/assets/base.less
  5. 使用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组件

  1. 条件渲染
  2. button跳转登录、注册页面

vuex状态管理

  1. ⭐什么是vuex状态管理
  2. 如何使用(管理鉴权相关的状态:isLogin, user控制header组件的条件渲染)

vuex状态管理

官网介绍:
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。

1.应用的状态:拿本项目的header说事,每个client端对应的都是一个user,它们的header组件渲染的user,isLogin数据应该放进store里。

  1. 更改 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)调用;

开发:

  1. @/src/pages/login/template.vue -静态样式
  2. @/src/store/index.js - 使用Vuex_Modules
  3. @/src/store/modules/authStore.js -[state, getters,mutations, actions]
  4. @/components/Header.vue
  5. @/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 || '/'})
        })
    }
  }
}

动态匹配路由 || 完善路由

  1. <router-link to="{name:xxx, params:xxx}">跳转动态路由
  2. 路由元信息:仅登录后允许跳转

1.<router-link to="{name:xxx, params:xxx}">在to属性中,path与params不能同时使用。
2.参考: 全局导航守卫中检查元字段
3.this.$router.push({path: this.$route.query.redirect || '/'})

页面的完善

  1. 列表的循环渲染,使用<router-link>进行跳转;
  2. 分页组件(element-ui);
  3. 创建博客/修改博客(marked.js);
  4. 删除博客(确认后删除);
  5. 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. 修改项目页面的名字和图标
    方法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>

参考:
栈溢出
掘金/蛮蛮

  1. 移动端的媒体查询
    注册界面由于表单的样式问题导致网页扩张。
    Github_commit

部署到Github

  1. 将axios请求修改为https协议
  2. /dist文件夹上传至独立的github项目并设置Github Pages预览
  3. 部署上线时发现,github page请求资源的路径有问题,过去的解决方案是修改assetsPublicPath,我一番摸索后,是这样解决的:在vue ui中vue-cli_打开vue配置_修改publicPath为相对路径。还是懵逼,不过问题解决了...项目已在github上线!
请求路径应该为github.io/easy-vue-blog/css/....
image.png

项目总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343