Vue开发
搭建环境
node.js搭建
- node.js官网(https://nodejs.org/zh-cn/),下载最新的长期版本,直接运行安装完成之后,即具备node和npm的环境
- 下载完成后,在cmd 输入 node -v npm -v
安装vue
在node.js的安装盘符
安装淘宝npm
npm install -g cnpm - -registry = https://registry.npm.taobao.org
vue-cli 安装依赖包
cnpm install - -g vue-cli
分别安装了淘宝npm,cnpm是为了提高我们安装依赖的速度
vue ui
-
vue ui是@vue/cli3.0增加一个可视化项目管理工具,可以运行项目、打包项目,检查等操作
-
在IDEA项目的部署目录(需要建立vue项目的地方)中,打开powerShell(按住shift再右击目录会显示在此打开power shell 或者在文件夹左上角点击文件,然后有以管理员身份运行powershell)
-
注意在上一步下载的vue,在打开的powershell中执行 vue ui 无法打开可视化界面
- vue -V发现为2.9.6版本,需要3X以上版本可以使用vue ui命令
- npm uninstall vue-cli -g 删除现在已有的vue,再用 npm install -g @vue/cli 重新下载,
- 再重新执行 vue ui 即可
-
运行时依然报错如下,解决方法时再系统环境变量上添加发现是:用户变量的path没有配置C:\Windows\System32,添加即可
- 运行后依然报错,找不到应用程序
-
解决方法是重新安装Chrome谷歌浏览器,然后重新vue ui(可能是本地的浏览器不支持vue ui打开界面,重新安装chrome后,会弹出浏览器选择框,选择新安装的chrome浏览器即可)
新建项目
成功执行vue ui后 如下图所示
-
创建—>再此创建新项目—>填写必要信息后—>下一步
选择手动配置项目
勾选上Router Vuex(可以选择去掉Linter/Formatter) 下一步
选择使用历史路由—>创建项目—>创建项目,不保存预设
-
创建项目后,为了方便开发,可以使用webstorm,vscode,也可以使用IDEA 只是需要给IDEA集成一个Vue.js插件(然后再重启IDEA),此处采用IDEA来进行开发
-
项目创建完成后如下所示
项目导入IDEA
在IDEA中 File导入该项目即可(已经集成插件的基础上)
- /src/components 公共组件
- /sec/router 前端路由
- /src/views 路由跳转的view,页面目录
- /src/store 组件可以监视store中的数据
- App.vue
集成element-ui
-
直接在IDEA中对项目进行布置
-
引入element-ui组件(https://element.eleme.cn)可以获得好看的vue组件用以渲染数据,开发好看的博客界面
-
注意安装命令均在IDEA的控制台Terminal执行
# 切换到项目根目录 cd vueblog-vue# 安装element-ui # 安装element-ui cnpm install element-ui --save
-
在main.js中引入elenment-ui
打开项目src目录下的main.js,引入element-ui依赖 import Element from 'element-ui' import "element-ui/lib/theme-chalk/index.css" Vue.use(Element) //全局进行使用element-ui组件
-
至此可以在官网上选择组件复制代码到我们项目中直接使用
-
测试element-ui组件时,启动项目 ,发现组件成功渲染在页面时,即成功
- 在控制台执行: npm run serve
- 在Run/Debug Configurations 配置
安装axios
-
基于promise的HTTP库,这个这个工具可以再前后端对接时大大提高开发效率
-
IDEA的控制台Terminal执
cnpm install axios --save
-
main.js引入axios依赖
打开项目src目录下的main.js,在main.js中全局引入axios import axios from 'axios' Vue.prototype.$axios = axios
-
至此在组件中可以通过this.$axios.get()来发起请求
页面路由
定义页面
本项目由于页面较少,可以先定义好路由页面,在需要链接的地方就可以直接使用
- 在views文件夹下定义几个页面(new component)
- BlogDetail.vue(博客详情页)
- BlogEdit.vue(编辑博客)
- Blogs.vue(博客列表)
- Login.vue(登录页面)
- 删除掉原生的Home.vue
页面简单定义
- .vue下template标签内只能放一个标签(入放一个< div>)
路由中心配置
- 注意要先把上一步定义的几个.vue导入进index.js
router\index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import BlogDetail from '../views/BlogDetail.vue'
import BlogEdit from '../views/BlogEdit.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Blogs',
redirect: { name: 'Blogs' }//主页面,重定向
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/blogs',
name: 'Blogs',
// 懒加载
component: () => import('../views/Blogs.vue')
},
{
path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
name: 'BlogAdd',
meta: {
requireAuth: true
},
component: BlogEdit
},
{
path: '/blog/:blogId',
name: 'BlogDetail', //显示某篇博客详情,需要id
component: BlogDetail
},
{
path: '/blog/:blogId/edit', //需要制定修改哪一个blog故需要传递id
name: 'BlogEdit',
meta: {
requireAuth: true
},
component: BlogEdit
}
];
const router = new VueRouter(
{
mode: 'history',
base: process.env.BASE_URL,
routes
}
)
export default router
- 带有meta:requireAuth: true说明是需要登录字后才能访问的受限资源
- 注意路由的顺序严格要求,例如把path: '/blog/:blogId’这个路由放在path: '/blog/add’之前,则访问blog/add可能路由到/blog/:blogId,则不会进入add
登录页面开发
布局
- 在element-ui中 Container(布局容器),有许多布局样式
- 选择常见的三项布局,将其代码放入login.vue的< template>标签中,(将附带的一些样式拷进< script> 中)
- 在element-ui找到布局表单Form控件,找到一个有表单校验规则控件,修改一下,只留下两个输入框和一个提交按钮(注意同时修改校验规则)
views/Login.vue
-
表单校验(固定写法)
-
登录按钮的点击登录事件
< template> < div> <el-container> //el-header是el-container的头部区域 <el-header> <img class="mlogo" src="xxxxx" alt=""> </el-header> <el-main> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm"> <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>
< 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 到 15 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请选择密码', trigger: 'change' }
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios.post('/login', this.ruleForm).then(res => {
console.log(res.data)
const jwt = res.headers['authorization']
const userInfo = res.data.data
// 把数据共享出去,存入/strore/index.js
_this.$store.commit("SET_TOKEN", jwt)
_this.$store.commit("SET_USERINFO", userInfo)
// 获取
console.log(_this.$store.getters.getUser)
_this.$router.push("/blogs")
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
< style>
<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: 160px;
}
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: 60%;
margin-top: 10px;
}
.demo-ruleForm {
max-width: 500px;
margin: 0 auto;
}
</style>
发起请求
- 将表单的数据提交给后端,由后端进行校验返回jwt(token)
- 以前通过ajax发起异步请求,在Vue中采用基于promise的axios
views/Login.vue的提交表单方法
submitForm(formName) {
this.$ refs[formName].validate((valid) => {
if (valid) {
const _this = this//发起axios请求后,后面的this代表的是该请求的this,想要调用index.js中定义的全局参数必须先预存该代表整个vue项目的this
// 提交逻辑
//注意,此处的post请求括号内指令前的URL在axios中定义了,此处会自动加上,避免硬编码
this.$axios.post(‘/login', this.ruleForm).then((res)=>{
const token = res.headers['authorization']
//发起请求后的结果需要赋给
//登陆成功还会返回用户信息通过res.data.data获取
//传统项目token是放在cookie中的,vue项目一般会放在localstorage中
//Vue项目时单页面项目SPA(改变路由时不会刷新整个页面,而是刷新App.vue中<router-view/>中的内容)
//因此多组件的SPA保证都能获取如Jwt,则需要配置到全局参数
//获取参数
_this.$store.commit('SET_TOKEN', token)
_this.$store.commit('SET_USERINFO', res.data.data)
//跳转页面
_this.$router.push("/blogs")
})
} else {
console.log('error submit!!');
return false;
}
});
},
配置全局参数
- 进行token等数据状态同步,存储token,用localStorage;存储用户信息用sessionStorage
- localStorage,sessionStoraage在页面上
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: { //类似于java bean的setter方法
//第一个参数上面定义,第二个为传入的值
SET_TOKEN: (state, token) => {
state.token = token
//赋值后存到localStorage,浏览器关闭后仍保存一段时间
localStorage.setItem("token", token)
},
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo
//sessionStorage不能存储对象,只能存字符串之类的值
sessionStorage.setItem("userInfo", JSON.stringify(userInfo))
},
//调用remove方法删除掉localStorage,sessionStorage中的数据
REMOVE_INFO: (state) => {
localStorage.setItem("token", '')
sessionStorage.setItem("userInfo", JSON.stringify('')) state.userInfo = {}
}
},
//初始化时直接从sessionStorage中获取(拿到的是序列化的字符串,需要反序列化)
getters: {
getUser: state => { return state.userInfo }
},
}
actions: {},
modules: {}
})
- 完成上述配置后,输入用户名密码正确时会跳转到博客界面,但是输入错误时却不会由弹窗提示
- 此时需要一个全局处理登录异常,可以弹窗提示
- 做一个全局axios拦截对登陆错误的结果进行弹窗处理
全局axios拦截
-
对axios做了个后置拦截器,就是返回数据时候,如果结果的code或者status不正常,由对应弹窗提示
-
src目录下创建一个文件axios.js(与main.js同级),定义axios的拦截,需要在main.js 导入
import axios from 'axios' import Element from "element-ui"; import store from "./store"; import router from "./router"; //将域名:端口定义成常量,避免硬编码, axios.defaults.baseURL='http://localhost:8081' //前置拦截, axios.interceptors.request.use(config => { console.log("前置拦截") // 可以统一设置请求头 return config }) //后置拦截 axios.interceptors.response.use(response => { const res = response.data; console.log("后置拦截") //注意这里是采用了http的状态码 // 当结果的code是否为200的情况 if (res.code === 200) { return response } else { // 弹窗异常信息 //element-ui的一个组件,要在上方先导入依赖 Element.Message({ //弹窗信息 message: response.data.msg, type: 'error', //弹窗过期时间 duration: 2 * 1000 }) // 直接拒绝往下面返回结果信息 (不再执行login.vue中后续的逻辑) return Promise.reject(response.data.msg) } }, error => { console.log('err' + error)// for debug if(error.response.data) { error.message = error.response.data.msg } // 根据请求状态觉得是否登录或者提示其他 if(error.response.status= =401 ){ store.commit('REMOVE_INFO'); //跳转到登陆页面 router.push({ path: '/login' }); error.message = '请重新登录'; } if (error.response.status = = 403) { error.message = '权限不足,无法访问'; } Element.Message({ message: error.message, type: 'error', duration: 3 * 1000 }) return Promise.reject(error) })
-
编写完axios.js,再往main.js中导入axios.js
博客列表
公共组件Header
- 登录完成之后直接进入博客列表页面,然后加载博客列表的数据渲染出来
- 同时页面头部我们需要把用户的信息展示出来,很多地方都用到这个模块,把页面头部的用户信息单独抽取出来作为一个组件
构建Header组件
首先在/src新建一个目录components,新建一个Header.vue
头部用户信息
头部的用户信息,应该包含三部分信息:id,头像、用户名,而这些信息在登录之后就已经存在了sessionStorage。可以通过store的getters获取到用户信息
Header代码
-
created()中初始化用户的信息
-
通过hasLogin的状态来控制登录和退出按钮的切换,以及发表文章链接的disabled,这样用户的信息就能展示出来了(发表文章这个控件绑定了url指向/blog/add)
-
退出按钮,在methods中有个logout()方法,直接访问/logout(因为之前axios.js中已经设置axios请求的baseURL,不再需要链接的前缀),清除掉srore中的数据,并跳转到登陆页面
-为什么退出仍然要 this.$axios.get(‘http://localhost:8081/logout’?在AccountController中的logout要将当前角色退出shiro
-
登录之后才能访问的受限资源,所以在header中带上了Authorization。返回结果清除store中的用户信息和token信息,跳转到登录页面
< template><template> <div class="m-content"> <h3>欢迎来到MarkerHub的博客</h3> <div class="block"> //el的头像组件 <el-avatar :size="50" :src="user.avatar"></el-avatar> <div>{{ user.username }}</div> </div> <div class="maction"> // el-link标签 <span><el-link href="/blogs">主页</el-link></span> <el-divider direction="vertical"></el-divider> <span><el-link type="success" href="/blog/add">发表博客</el-link></span> <el-divider direction="vertical"></el-divider> <span v-show="!hasLogin"><el-link type="primary" href="/login">登录</el-link></span> <span v-show="hasLogin"><el-link type="danger" @click="logout">退出</el-link></span> </div> </div> </template>
< script>
<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>
< syle>
<style scoped>
.m-content {
max-width: 960px;
margin: 0 auto;
text-align: center;
}
.maction {
margin: 10px 0;
}
</style>
集成Header组件
import Header from "@/components/Header";
data() {
components: {Header}}
}
# 然后模板中调用组件
<Header></Header>
博客分页
- 列表页面,需要做分页,列表我们在element-ui中直接使用时间线组件来作为我们列表样式和分页组件
- 需要几部分信息
- 分页信息
- 博客列表内容,包括id、标题、摘要、创建时间
views\Blogs.vue
- data()中直接定义博客列表blogs、以及一些分页信息
- methods()中定义分页的调用接口page(currentPage),参数是需要调整的页码currentPage,得到结果之后直接赋值即可
- 初始化时候,直接在mounted()方法中调用第一页this.page(1)
- 注意标题这里我们添加了链接,使用的是标签
- < el-card>展示了每个标签的内容会展示博客名字,同时有个超链接指向具体的博客详情(需要传递参数blog的id)
< template>
<template>
<div class="mcontaner">
<Header></Header>
<div class="block">
<el-timeline>
<el-timeline-item :timestamp="blog.created" placement="top" v-for="blog in blogs">
//卡片组建用于展示分页上的博客信息,如下用标题形式展示了博客的id等
<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>
<script>
import Header from "../components/Header";
export default {
name: "Blogs.vue",
components: {Header},
data() {
return {
blogs: {},
//传给pagination分页插件的参数
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.current
_this.total = res.data.data.total
_this.pageSize = res.data.data.size
})
}
},
created() {
this.page(1)
}
}
</script>
< style>
< style scoped>
.mpage {
margin: 0 auto;
text-align: center;
}
</style>
博客编辑(发表)
- 在博客(展示页面)页面,点击header上的发表文章(页面路由)跳转到博客编辑页面
- 点击击发表博客链接调整到/blog/add页面,需要用到一个markdown编辑器,在vue组件中,比较好用的是mavon-editor,先来安装mavon-editor相关组件
安装mavon-editor
-
基于Vue的markdown编辑器mavon-editor
cnpm 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)
view\BlogEdit.vue
-
校验表单,然后点击按钮提交表单,注意头部加上Authorization信息,返回结果弹窗提示操作成功,然后跳转到博客列表页面
-
编辑和添加是同一个页面,所以有了create()(在内容开始渲染时就开始)方法,比如从编辑连接/blog/7/edit中获取blogId为7的这个id。然后回显博客信息。获取方式是const blogId = this.$route.params.blogId
-
mavon-editor因为已经全局注册,所以我们直接使用组件即可
<mavon-editor v-model="editForm.content">
< template>
< 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="内容" prop="content">
<mavon-editor v-model="ruleForm.content"></mavon-editor>
</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>
</div>
</div>
</template>
< script>
<script>
import Header from "../components/Header";
export default {
name: "BlogEdit.vue",
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: [
{ trequired: true, message: '请输入内容', 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() {
//拿到blog id
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this
if(blogId) {
//发送ajax请求,获得进行查询拿到blodid对于的blog内容, //生命周期函数在加载后拿到id发送ajax请求将得到的博客内容展示在页面上行
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>
< Script>
博客详情
- 博客详情中需要回显博客信息,
- 后端传过来的是博客内容是markdown格式的内容,需要进行渲染然后显示出来,使用一个插件markdown-it,用于解析md文档,然后导入github-markdown-c,所谓md的样式
安装插件
- 用于解析md文档
- cnpm install markdown-it --save
- md样式
- cnpm install github-markdown-css
然后就可以在需要渲染的地方使用
views\BlogDetail.vue
- 初始化create()方法中调用getBlog()方法,请求博客详情接口
- 返回的博客详情content通过markdown-it工具进行渲染
- 导入样式 import ‘github-markdown.css’
- 在content的div中添加class为markdown-body
- 另外标题下添加了个小小的编辑按钮,通过ownBlog (判断博文作者与登录用户是否同一人)来判断按钮是否显示出来
< template>
<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>
<script>
import 'github-markdown-css'
import Header from "../components/Header";
export default {
name: "BlogDetail.vue",
components: {Header},
data() {
return {
blog: {
id: "",
title: "",
content: ""
},
ownBlog: false
}
},
created() {
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this
this.$axios.get('/blog/' + blogId).then(res => {
const blog = res.data.data
_this.blog.id = blog.id
_this.blog.title = blog.title
var MardownIt = require("markdown-it")
var md = new MardownIt()
var result = md.render(blog.content)
_this.blog.content = result
_this.ownBlog = (blog.userId === _this.$store.getters.getUser.id)
})
}
}
</script>
< style>
<style scoped>
.mblog {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
width: 100%;
min-height: 700px;
padding: 20px 15px;
}
</style>
路由权限拦截
-
页面已经开发完毕之后,控制一下哪些页面是需要登录之后才能跳转的
-
如果未登录访问就直接重定向到登录页面
-
在src目录下定义一个js文件:src\permission.js
import router from "./router";// 路由判断登录 根据路由配置文件的参数 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的状态,觉得是否需要跳转到登录页面
-
再在main.js中import我们的permission.js
- import ‘./permission.js’ // 路由拦截