1、前言
技術棧:
- vue
- element-ui
- axios http請求庫
- markdown編輯器
- mavon-editor
- markdown-it
- github-markdown-css
2、項目演示
3、環(huán)境準備
安裝Vue的環(huán)境,Vue官方文檔
1、Nodejs安裝
Node.js:http://nodejs.cn/download/
安裝就是無腦的下一步就好,安裝在自己的環(huán)境目錄下-
檢查時候安裝成功
- cmd下輸入node -v,查看是否能夠正確打印出版本號即可!
- cmd下輸入npm -v,查看是否能夠正確打印出版本號即可!
- 這個npm,就是一個軟件包管理工具,就和linux下的apt軟件安裝差不多!
-
修改全局依賴包下載路徑
可以通過
npm root -g查看當前存放的位置我們不想讓全局包放在這里,我們可以自定義存放目錄,在
CMD窗口執(zhí)行以下兩條命令修改默認路徑:npm config set prefix "D:\Program Files (x86)\nodejs\node_global"npm config set cache "D:\Program Files (x86)\nodejs\node_globalnode_cache" -
設置淘寶源,讓我們下載速度快的飛起
# 安裝淘寶npm npm config set registry http://registry.npm.taobao.org/ -
安裝 vue-cli
# vue-cli 安裝依賴包 npm install -g vue-cli
4、新建項目
參考官方文檔:https://cli.vuejs.org/zh/guide/creating-a-project.html
可以通過vue create或者vue ui創(chuàng)建項目,這里我使用vue ui,是@vue/cli3.0增加一個可視化項目管理工具,可以運行項目、打包項目,檢查等操作。
# 打開vue的可視化管理工具界面
vue ui
運行vue ui之后,會為我們打開一個http://localhost:8080的頁面:

然后點擊創(chuàng)建,填寫項目名稱
注意:創(chuàng)建的目錄最好是和你運行vue ui同一級。這樣方便管理和切換。

下一步,選擇【手動】,再點擊下一步,如圖點擊按鈕,勾選上路由Router、狀態(tài)管理Vuex,去掉js的校驗。

下一步,也選上【Use history mode for router】,點擊創(chuàng)建項目,然后彈窗中選擇按鈕【創(chuàng)建項目,不保存預設】,就進入項目創(chuàng)建啦。
上述步驟,幫助我們創(chuàng)建了一個vue項目,并且安裝了Router、Vuex。這樣我們后面就可以直接使用。
我們可以看一下vueblog-vue的項目結(jié)構
將項目導入idea
安裝element ui
安裝element-ui組件(https://element.eleme.cn/#/zh-CN/component/installation), 幫助我們開發(fā)出好看的博客頁面
在我們的項目根目錄用命令
# 安裝element-ui
npm i element-ui -S
在main.js中引入elementui依賴
//引入elementui
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
以上代碼便完成了 Element 的引入。需要注意的是,樣式文件需要單獨引入。
安裝Axios
接下來,安裝Axios(http://www.axios-js.com/zh-cn/docs/)
使用 npm:
npm install --save axios vue-axios
將下面代碼加入main.js:
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
我們可以利用Axios完成,從瀏覽器中創(chuàng)建、轉(zhuǎn)換請求數(shù)據(jù)和響應數(shù)據(jù)、攔截請求和響應
組件中,我們就可以通過this.$axios.get()來發(fā)起我們的請求了。
配置頁面路由
開始前我們先定義好頁面、配置好路由,由于項目簡單,頁面較少,所有提前鏈接好,后續(xù)慢慢開發(fā),等用到鏈接的時候就可以直接使用:
我們在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"; import BlogDetail from "../views/BlogDetail"; import Blogs from "../views/Blogs"; import BlogEdit from "../views/BlogEdit"; Vue.use(VueRouter) //路由是按照順序訪問的,所以/blog/:blogId 要放在/blog/:blogId/edit前面 const routes = [ { //博客列表 path: '/', name: 'Index', //也可以這樣寫 redirect: {name: "Blogs"} component: Blogs, }, { //博客列表 path: '/blogs', name: 'Blogs', component: Blogs }, { //登錄頁面 path: '/login', name: 'Login', component: Login }, { //編輯博客,增加blog path: '/blog/add', name: 'BlogEdit', component: BlogEdit }, { //博客詳情頁 path: '/blog/:blogId', name: 'BlogDetail', component: BlogDetail }, { //編輯博客 //接受前端傳遞來的參數(shù) blogId path: '/blog/:blogId/edit', name: 'BlogEdit', component: BlogEdit }, ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router清空APP.vue的style
測試

5、頁面開發(fā)
登錄頁面
1、頁面原型設計
由于頁面設計簡單 ,這里就略過了
2、代碼
- views.Login.vue
<template>
<div>
<el-container>
<el-header>
<img class="mlogo" src="../static/images/logo.png">
</el-header>
<el-main>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<span class="login-title">歡迎登錄</span>
<el-form-item label="用戶名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="密碼" prop="password">
<el-input type="password" v-model="ruleForm.password"></el-input>
</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>
</el-main>
</el-container>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
ruleForm: {
username: 'markerhub',
password: '111111'
},
rules: {
username: [
{ required: true, message: '請輸入用戶名', trigger: 'blur' },
{ min: 3, max: 15, message: '長度在 3 到 5 個字符', trigger: 'blur' }
],
password: [
{ required: true, message: '請輸入密碼', trigger: 'change' }
],
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
//表單提交是post
const _this = this;
this.axios.post('http://localhost:8081/login',this.ruleForm).then(res => {
const jwt = res.headers['authorization']
//將jwt放入 jwt
const userInfo = res.data.data;
console.log(jwt);
console.log(userInfo);
//這里this指向的是axios,所以需要在外面另存this,為_this
//把數(shù)據(jù)共享出去
_this.$store.commit("SET_TOKEN",jwt);
_this.$store.commit("SET_USERINFO",userInfo);
console.log(_this.$store.getters.GETUSER)
//頁面跳轉(zhuǎn)
_this.$router.push("/blogs")
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style scoped>
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
/*background-color: #E9EEF3;*/
color: #333;
text-align: center;
line-height: 80px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
.mlogo {
height: 80%;
margin-top: 5px;
}
.demo-ruleForm{
border:1px solid #DCDFE6;
width: 350px;
margin:0px auto;
padding: 35px 35px 15px 35px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
.login-title{
width: 350px;
height: 40px;
text-align:center;
margin: 0 auto 40px auto;
color: #303133;
}
</style>
上述代碼主要做了兩件事:
1、表單校驗
2、登錄按鈕的點擊登錄事件
表單校驗規(guī)則,固定寫法,element-ui組件的demo有
發(fā)起登錄事件之后的代碼:
//這里this指向的是axios,所以需要在外面另存this,為_this
//把數(shù)據(jù)共享出去
_this.$store.commit("SET_TOKEN",jwt);
_this.$store.commit("SET_USERINFO",userInfo);
console.log(_this.$store.getters.GETUSER)
//頁面跳轉(zhuǎn)
_this.$router.push("/blogs")
從返回的結(jié)果請求頭中獲取到token的信息,然后使用store提交token和用戶信息的狀態(tài)。完成操作之后,我們調(diào)整到了/blogs路由,即博客列表頁面。
token的狀態(tài)同步
所以在store/index.js中,代碼是這樣的:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: '',
userInfo: JSON.parse(sessionStorage.getItem("userInfo"))
},
mutations: {
//state的值不能直接修改 mutations相當于set方法
SET_TOKEN: (state,token) => {
state.token = token;
localStorage.setItem("token",token);
},
SET_USERINFO: (state,userInfo) => {
state.userInfo = userInfo;
//session里面不能傳遞字符串,這里用JSON轉(zhuǎn)化一下,序列化
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.token;
}
},
actions: {
},
modules: {
}
})
存儲token,我們用的是localStorage,存儲用戶信息,我們用的是sessionStorage。畢竟用戶信息我們不需要長久保存,保存了token信息,我們隨時都可以初始化用戶信息。當然了因為本項目是個比較簡單的項目,考慮到初學者,所以很多相對復雜的封裝和功能我沒有做,當然了,學了這個項目之后,自己想再繼續(xù)深入,完成可以自行學習和改造哈
定義全局axios攔截器
點擊登錄按鈕發(fā)起登錄請求,成功時候返回了數(shù)據(jù),如果是密碼錯誤,我們是不是也應該彈窗消息提示。為了讓這個錯誤彈窗能運用到所有的地方,所以我對axios做了個后置攔截器,就是返回數(shù)據(jù)時候,如果結(jié)果的code或者status不正常,那么我對應彈窗提示。
在src目錄下創(chuàng)建一個文件axios.js(與main.js同級),定義axios的攔截:
import axios from "axios";
import ElementUI from 'element-ui';
import store from './store'
import router from './router'
//方便管理請求,方便修改請求鏈接的域名
axios.defaults.baseURL="http://localhost:8081";
//前置攔截
axios.interceptors.request.use(config =>{
return config
})
//后置攔截
axios.interceptors.response.use(response => {
let res = response.data;
console.log("==============")
console.log(res)
console.log("==============")
if (res.code == 200) {
return response
} else {
ElementUI.Message.error('錯了哦,這是一條錯誤消息', {duration : 3*1000});
//不讓進入Login.vue
return Promise.reject(response.data.msg)
}
},
error => {
console.log(error)
if (error.response.data){
error.message = error.response.data.msg
}
if (error.response.status === 401){
store.commit("REMOVE_INFO")
router.push("/login")
}
ElementUI.Message.error(error.message, {duration : 3*1000});
return Promise.reject(error)
}
)
前置攔截,其實可以統(tǒng)一為所有需要權限的請求裝配上header的token信息,這樣不需要在使用是再配置,我的小項目比較小,所以,還是免了吧~
然后再main.js中導入axios.js
//引入axios攔截器
import "./axios"
后端因為返回的實體是Result,succ時候code為200,fail時候返回的是400,所以可以根據(jù)這里判斷結(jié)果是否是正常的。另外權限不足時候可以通過請求結(jié)果的狀態(tài)碼來判斷結(jié)果是否正常。這里都做了簡單的處理。
登錄異常時候的效果如下:

博客頁面
登錄完成之后直接進入博客列表頁面,然后加載博客列表的數(shù)據(jù)渲染出來。同時頁面頭部我們需要把用戶信息展示處理。因為很多地方都用到這個模塊,所以我們把頁面頭部的用戶信息單獨抽取處理作為一個組件。
頭部用戶信息
頭部用戶信息包括三部分信息:id,頭像,用戶名,而這些信息我們是在登錄之后就存在sessionStorage。因此,我們可以通過store的getters獲取用戶信息。
- components\Header.vue
<template>
<div class="m-content">
<h3>歡迎來到Ergou博客</h3>
<div class="block"><el-avatar :size="50" :src="user.avatar"></el-avatar></div>
<div>{{user.username}}</div>
<div class="m-action">
<span> <el-link href="/blogs" >主頁</el-link></span>
<el-divider direction="vertical"></el-divider>
<span><el-link type="success" href="/blog/add">發(fā)表博客</el-link></span>
<el-divider direction="vertical"></el-divider>
<span v-if="!hasLogin"><el-link type="primary" @click="login">登錄</el-link></span>
<span v-if="hasLogin"> <el-link type="danger" @click="logout">退出</el-link></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", {
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;
}
.m-action{
margin: 10px 0;
}
</style>
上面代碼created()中初始化用戶的信息,通過hasLogin的狀態(tài)來控制登錄和退出按鈕的切換,以及發(fā)表文章鏈接的disabled,這樣用戶的信息就能展示出來了。 然后這里有個退出按鈕,在methods中有個logout()方法,邏輯比較簡單,直接訪問/logout,因為之前axios.js中我們已經(jīng)設置axios請求的baseURL,所以這里我們不再需要鏈接的前綴了哈。因為是登錄之后才能訪問的受限資源,所以在header中帶上了Authorization。返回結(jié)果清楚store中的用戶信息和token信息,跳轉(zhuǎn)到登錄頁面。
然后需要頭部用戶信息的頁面只需要幾個步驟:
import Header from "@/components/Header";
data() {
components: {Header}
}
# 然后模板中調(diào)用組件
<Header></Header>
博客分頁
接下來就是列表頁面,需要做分頁,列表我們在element-ui中直接使用時間線組件來作為我們的列表樣式,還是挺好看的。還有我們的分頁組件。
需要幾部分信息:
- 分頁信息
- 博客列表內(nèi)容,包括id、標題、摘要、創(chuàng)建時間
- views\Blogs.vue
<template>
<div>
<Header> </Header>
<div class="block">
<el-timeline>
<el-timeline-item :timestamp="blog.created" placement="top" v-for="blog in blogs">
<el-card>
<h4>
<router-link :to="{name: 'BlogDetail',params:{blogId: blog.id}}">
{{blog.title}}
</router-link>
</h4>
<p>{{blog.description}}</p>
</el-card>
</el-timeline-item>
</el-timeline>
<el-pagination class="mpage"
background
layout="prev, pager, next"
:current-page=currentPage
:page-size=pageSize
:total=total
@current-change=page>
</el-pagination>
</div>
</div>
</template>
<script>
import Header from "../components/Header"
export default {
name: "Blogs",
components: {
Header
},
data() {
return {
blogs: {},
currentPage: 1,
total: 0,
pageSize: 5
}
},
methods: {
page(currentPage) {
const _this = this
_this.axios.get("/blogs?currentPage=" + currentPage).then(res => {
console.log(res)
_this.blogs = res.data.data.records
_this.currentPage = res.data.data.currentPage
_this.total = res.data.data.total
_this.pageSize = res.data.data.size
})
}
},
created() {
//調(diào)用分頁程序
this.page(1)
}
}
</script>
<style scoped>
.mpage{
margin: 0 auto;
text-align: center;
}
</style>
data()中定義博客列表blogs、已經(jīng)一些分頁信息。method()中定義分頁的調(diào)用接口page(currentPage),參數(shù)是需要調(diào)整的頁碼currentPage,得到結(jié)果之后直接賦值即可。然后初始化時候,直接在mounted()方法中調(diào)用第一頁this.page(1),
博客編輯
我們點擊發(fā)表博客鏈接調(diào)整到/blog/add頁面,這里我們需要用到一個markdown編輯器,在vue組件中,比較好用的是mavon-editor,那么我們直接使用哈。先來安裝mavon-editor相關組件:
安裝mavon-editor
基于Vue的markdown編輯器mavon-editor
npm 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)
ok,那么我們?nèi)ザx我們的博客表單:
<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="內(nèi)容" prop="content">
<mavon-editor v-model="ruleForm.content"></mavon-editor>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">立即創(chuàng)建</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: 3, max: 25, message: '長度在 3 到 25 個字符', trigger: 'blur' }
],
description: [
{ required: true, message: '請輸入摘要', trigger: 'blur' }
],
content: [
{ required: true,message: '請輸入內(nèi)容', trigger: 'blur' }
],
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this;
this.axios.post("/blog/edit",this.ruleForm ,{
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() {
const blogId = this.$route.params.blogId;
const _this = this;
//內(nèi)容回顯
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{
text-align: center;
}
</style>
邏輯依然簡單,校驗表單,然后點擊按鈕提交表單,注意頭部加上Authorization信息,返回結(jié)果彈窗提示操作成功,然后跳轉(zhuǎn)到博客列表頁面。emm,和寫ajax沒啥區(qū)別。熟悉一下vue的一些指令使用即可。 然后因為編輯和添加是同一個頁面,所以有了create()方法,比如從編輯連接/blog/7/edit中獲取blogId為7的這個id。然后回顯博客信息。獲取方式是const blogId = this.$route.params.blogId。
對了,mavon-editor因為已經(jīng)全局注冊,所以我們直接使用組件即可:
<mavon-editor v-model="editForm.content"/>
效果如下:

博客詳情頁
博客詳情中需要回顯博客信息,然后有個問題就是,后端傳過來的是博客內(nèi)容是markdown格式的內(nèi)容,我們需要進行渲染然后顯示出來,這里我們使用一個插件markdown-it,用于解析md文檔,然后導入github-markdown-c,所謂md的樣式。
方法如下:
# 用于解析md文檔
npm install markdown-it --save
# md樣式
npm install github-markdown-css
-
views\BlogDetail.vue
<template> <div> <Header> </Header> <div class="mblog"> <h2>{{blog.title}}</h2> <el-link icon="el-icon-edit" v-if="ownBlog"> <router-link :to="{name: 'BlogEdit',params: {blogId: blog.id} }"> 編輯 </router-link> </el-link> <el-divider></el-divider> <div class="markdown-body" v-html="blog.content"></div> </div> </div> </template> <script> import Header from "../components/Header"; import "github-markdown-css/github-markdown.css" export default { name: "BlogDetail", components: {Header}, data() { return { blog: { id: "", title: "默認", content: "內(nèi)容" }, ownBlog: false } }, created() { const blogId = this.$route.params.blogId; const _this = this; //內(nèi)容回顯 if (blogId){ this.axios.get("/blog/" + blogId).then(res =>{ const blog = res.data.data; _this.blog.id = blog.id _this.blog.title = blog.title var markdownIt = require("markdown-it") var md =new markdownIt(); var result = md.render(blog.content) _this.blog.content = result _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: 700px; padding: 20px 15px; } </style>
具體邏輯還是挺簡單,初始化create()方法中調(diào)用getBlog()方法,請求博客詳情接口,返回的博客詳情content通過markdown-it工具進行渲染。
再導入樣式:
import 'github-markdown.css'
然后在content的div中添加class為markdown-body即可哈。 效果如下:

另外標題下添加了個小小的編輯按鈕,通過ownBlog (判斷博文作者與登錄用戶是否同一人)來判斷按鈕是否顯示出來。
6、路由權限攔截
頁面已經(jīng)開發(fā)完畢之后,我們來控制一下哪些頁面是需要登錄之后才能跳轉(zhuǎn)的,如果未登錄訪問就直接重定向到登錄頁面,因此我們在src目錄下定義一個js文件:
- src\permission.js
import router from "./router";
// 路由判斷登錄 根據(jù)路由配置文件的參數(shù)
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)) { // 判斷該路由是否需要登錄權限
const token = localStorage.getItem("token")
console.log("------------" + token)
if (token) { // 判斷當前的token是否存在 ; 登錄存入的token
if (to.path === '/login') {
} else {
next()
}
} else {
next({
path: '/login'
})
}
} else {
next()
}
})
通過之前我們再定義頁面路由時候的的meta信息,指定requireAuth: true,需要登錄才能訪問,因此這里我們在每次路由之前(router.beforeEach)判斷token的狀態(tài),覺得是否需要跳轉(zhuǎn)到登錄頁面。
{
path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
name: 'BlogAdd',
meta: {
requireAuth: true
},
component: BlogEdit
}
然后我們再main.js中import我們的permission.js
import './permission.js' // 路由攔截
7、小結(jié)
前端到這就算到一段落,對于組件的使用,vue的聲明周期還是理解的太淺,后面多接觸 肯定沒問題