前面vue简书篇已经搭建好了脚手架,这里就忽略,直接进入主题,由于要通信,简单写了个登录界面。这里的通信仅仅是代码开发时期的通信,不涉及到部署之后。
一、登录界面
<template>
<el-container>
<el-header>
<div class="logo">
<img src="@/assets/images/logo.png" />
<span>Welcome To Login</span>
</div>
</el-header>
<el-main>
<div class="login-form">
<p class="login-tab">用户登录</p>
<el-form :model="ruleForm" status-icon :rules="rules" ref="loginForm" label-width="100px">
<el-form-item label="username" prop="username">
<el-input v-model="ruleForm.username" maxlength="25" clearable></el-input>
</el-form-item>
<el-form-item label="password" prop="password">
<el-input type="password" show-password v-model="ruleForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item class="bottom">
<el-checkbox>remember the password</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm()" style="width: 100%">Login</el-button>
</el-form-item>
</el-form>
</div>
</el-main>
<el-footer>
<FooterPage />
</el-footer>
</el-container>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import FooterPage from '@/pages/footer/footer.vue'
import { useRouter } from 'vue-router'
import { reqLogin } from '@/api/ajax.js'
const router = useRouter()
const checkUsername = (rule, value, callback) => {
if (value.length < 1) {
return callback(new Error('请输入正确的用户名'))
} else {
callback()
}
}
const checkPassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'))
} else {
callback()
}
}
const loginForm = ref(null)
const ruleForm = reactive({
password: '',
username: ''
})
const rules = reactive({
username: [{ validator: checkUsername, trigger: 'blur' }],
password: [{ validator: checkPassword, trigger: 'blur' }]
})
const submitForm = () => {
loginForm.value.validate(async (valid) => {
if (valid) {
const result = await reqLogin(ruleForm)
if (result.code === 1) {
sessionStorage.setItem('username', result.value)
// this.$cookie.set('token', result.token)
if (router.currentRoute.value.name === 'login') {
router.push({ path: 'home' })
console.log('去到主界面')
} else {
console.log('去到该去的界面')
}
} else {
ElMessage({
type: 'error',
message: result.value,
showClose: true
})
}
} else {
ElMessage({
type: 'error',
message: '请正确填写登录信息',
showClose: true
})
return false
}
})
}
</script>
<script>
export default {
name: 'Login-Page'
}
</script>
<style scoped>
.el-container {
height: 100%;
margin: 0;
padding: 0;
}
.el-header {
height: 10%;
display: flex;
/* 设置header下子元素,主轴居中就,即上下居中 */
align-items: center;
}
.logo {
margin-left: 19%;
}
.logo img {
width: 168px;
border-radius: 10px;
}
.logo span {
color: #4f4d4e;
font-size: 24px;
position: relative;
left: 20px;
bottom: 7px;
}
.el-main {
height: 72%;
background-size: cover;
background-image: url("@/assets/images/login_background.jpg");
display: flex;
align-items: center;
justify-content: flex-end;
}
.login-form {
width: 400px;
padding: 40px 30px 50px 20px;
margin-right: 7%;
background: white;
/* box-shadow: 2px 3px 7px rgb(0 0 0 / 20%); */
}
.login-tab {
text-align: left;
margin-left: 28px;
padding-bottom: 20px;
font-size: 18px;
color: gray;
font-weight: 500;
border-bottom: 2px solid #f0f0f0;
}
.el-footer {
height: 18%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
版权footer代码:
<template>
<div class="footer-info">
<p>
加些邮编+公司地址啥的 深圳七八九十十一二三公有限公司
</p>
<p>
Copyright © {{ year }} xxxxx International Corp. | All
Rights Reserved
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const year = ref(`2004-${new Date().getFullYear()}`)
</script>
<script>
export default {
name: 'FooterPage'
}
</script>
<style scoped>
.footer-info {
text-align: center;
}
.footer-info > p {
margin-bottom: 10px;
color: #666;
font: 14px simhei;
}
</style>
1、这里简单总结一下css问题,登录界面采用的是element的布局容器,header main footer常见的上中下响应式布局,各自高度都随着内容撑开。所以对屏幕的大小不能正常适应。这里采用高度用%的形式对这三部分进行设置高度。
设置元素的高度还要看它的父元素,所以这里必须在app.vue中设置下面,否则会设置失败。
因为:el-container高度等于父元素#app的高度,#app的高度的父节点是body,body的父节点是html,默认情况下html和body的高度为auto,浏览器不会自动给标签添加高度,因此自然就无效了。
html, body, #app{
height: 100%
}
2、简单说一下box-sizing属性,很重要的一个属性。
一般有两个值:content-box和border-box
content-box 是默认值:如果你设置一个元素的宽为 100px,那么这个元素的内容区会有 100px 宽,并且任何边框和内边距的宽度都会被增加到最后绘制出来的元素宽度中。最后肯定就不止100px了。
border-box 告诉浏览器:你想要设置的边框和内边距的值是包含在 width 内的。也就是说,如果你将一个元素的 width 设为 100px,那么这 100px 会包含它的 border 和 padding,
内容区的实际宽度是 width 减去 (border + padding) 的值。大多数情况下,这使得我们更容易地设定一个元素的宽高。这里的boder是:border: 10px solid black
如果给盒子添加box-sizing:border-box;之后,盒子的width、height就表示盒子实际占有的宽高(不含margin),即padding、border变为“内缩”的,不再“外扩”
注意:margin(外边距)跟这个属性无关, 跟padding(内边距)才有关,不管box-sizing是什么margin都是单独计算。
3、获取当前url 以及 router路由
import { useRouter } from 'vue-router'
const router = useRouter()
这种方法获取当前路由最简单,name和path分别对应自己定义路由时候的名字
router.currentRoute.value.name === 'login'
router.currentRoute.value.path=== '/login'
js中通过location.pathname获取,下面axios封装中有用到
路由跳转(前端路由跳转),有路由跳转的时候可以配置name 、path,通过path和name都行。
router.push( { path: '/home' } )
router.push( {name: 'home' } )
4、路由之间传参和参数接收
1、传参
router.push( { path: '/home', query: { msg: 'hello' } } )
传参方式也有 params 、query 两种。
- params 隐式路由传参
- query 显式路由传参
(显式时数据会跟在 url 之后,而隐式不会。隐式时要注意刷新页面params可能会消失,可将数据存在缓存中)
2、参数接收(注意一个useRouter, 一个useRoute)
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.query) 这样就可以获取到传的参数
二、axios promise封装拦截
因为是promise封装的函数方法,所以组件在使用的时候要加上async 函数(){ await 数据 } 不然会报错,因为没有完成异步转同步。
promise封装代码,命名一个文件夹如api,再命名一个文件如axios_promise.js
import router from '@/router/index'
import Qs from 'qs'
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 向外部暴露一个函数 ajax
// url 请求路径,默认为空
// parameter 请求参数,默认为空对象
// type 请求方法,默认为POST
// 在周期函数中调用函数不行,周期函数执行太快了,没等到结果返回就执行完,所以都是undefined
//Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句,所以并不是上面说的周期函数执行太快
// 所以用Promise就不会产生这种状况,函数要搭配async和await 使用
// 注意:get请求与post请求不一样,参数不一样,params和data,不能变,变了后端获取不到,post还得Qs转换类型
// indices: false 处理传递数组问题,后端request.data.getlist()获取完整列表
export default function ajax (url = '', parameter = {}, type = 'post') {
return new Promise(function (resolve, reject) {
let promise
if (type === 'get' || type === 'delete') {
promise = axios({
method: type,
url: url,
params: parameter
})
} else {
promise = axios({
method: type,
url: url,
data: Qs.stringify(parameter, { indices: false })
})
}
promise.then(res => {
resolve(res.data)
}).catch(error => {
console.log(error)
// reject(error) 注意这里只能打印,不能执行这个函数,否则会报异常
})
})
}
// 请求拦截器,在发送请求之前除登录之外所有请求都要先获取token,没有token信息直接回到登录界面
// 有token信息则请求头带上token用于后端认证使用
axios.interceptors.request.use(config => {
if (location.pathname !== '/login') {
// if (Vue.prototype.$cookie.get('token')) {
// config.headers.common['Authorization'] = 'JWT ' + Vue.prototype.$cookie.get('token')
// }
if (sessionStorage.getItem('token')) {
config.headers.common['Authorization'] = 'JWT ' + sessionStorage.getItem('token')
}
}
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器
axios.interceptors.response.use(response => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据 否则的话抛出错误
if (response && response.status === 200) {
if (response.data.token) {
// 请求成功后刷新token
sessionStorage.setItem('token', response.data.token)
// if(Vue.prototype.$cookie.get('token')){
// // 登录时也会保存token,这里匹配每个请求都更新token,提高安全性
// Vue.prototype.$cookie.set('token', response.data.token)
// }
}
return Promise.resolve(response)
}
return Promise.reject(response)
},
// 服务器状态码不是2开头的的情况
error => {
if (error.response) {
switch (error.response.status) {
// 401 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 401:
sessionStorage.clear()
// 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
})
}, 1000)
ElMessage({ message: 'Login expired, please login again!', type: 'warning' })
break
// 403: 没有权限访问
case 403:
ElMessage({ showClose: true, message: 'Forbidden!', type: 'warning' })
break
// 404请求不存在
case 404:
ElMessage({ message: 'The page or url you visited does not exist!', type: 'warning' })
break
// 406 token无效或者未携带token
case 406:
ElMessage({ showClose: true, message: 'UnAuthorization!', type: 'warning' })
break
// 500 服务器内部错误
case 500:
ElMessage({ showClose: true, message: 'Internal Server Error', type: 'warning' })
break
case 504:
ElMessage({ message: 'Gateway Timeout!Please check the service startup', type: 'warning' })
break
// 其他错误,直接抛出错误提示
default:
ElMessage({ message: error.response.statusText, type: 'warning' })
}
return Promise.reject(error.response)
}
})
然后在相同目录下命名一个如ajax.js的文件专门调用这个函数,代码如下。
/*
与后台交互模块 (依赖已封装的ajax函数)
包含n个接口请求函数的模块,函数的返回值是promise对象
put 和 delete请求 如果对应后端 update destory函数且url用的Register注册URL,则路由中需要带上id
*/
import ajax from './axios_promise'
const BASE_URL = '/api'
// User部分 1. 用户登录
export const reqLogin = (param) => ajax(BASE_URL + '/access/login/', param, 'post')
这样就可以在组件中调用了,如在登录中调用 import { reqLogin } from '@/api/ajax.js'
vue3的拦截封装和vue2差不多一样。有个地方不一样就是当后端服务器没有启动时,vue3无法识别504网关超时,而是下面的还是500,而vue2是可以的。不知道部署之后还是不是这样。
然后截取下面小块代码说一下解释;
promise.then(res => {
resolve(res.data)
}).catch(error => {
console.log(error)
// reject(error)
})
http通信的过程是先执行请求拦截器和响应拦截器,如果这两个拦截都正常运行,且运行成功那么执行这里.then,失败或者有异常则运行这里.catch。
1、.then:resolve(res.data),res.data是后端返回的数据,resolve函数即Promise.resolve()看官网没怎么懂,简单理解就是这里http请求成功了,然后数据可以正常返回到组件中,这时候组件中可以正常获取这个数据然后继续执行后面的代码。当然如果这里我不执行resolve(res.data)而是只打印一下,那组件的请求不会有反应(相当于代码执行到打印这里就结束了),那么组件没法获取到后端数据,但是没有报错。
2、.catch:这里理解为捕捉异常,即http通信的时候出现的异常,差不多就是状态码不是200我们都视为异常,返回到这里处理,所以一般的我们这里只打印一下就好了,因为异常提示已经做过了,这里注意的是如果执行reject(error),即Promise.reject()函数,官网也没怎么懂,我理解就是拒绝处理,然后把错误信息返回谁处理(谁调用谁自己处理,反正错误信息已经给你了)。但是坑的是这里我理解是把一个异常给调用它的函数处理,所以如果我们在的组件中使用如 const result = await reqLogin(ruleForm)时候,这里不加try catch捕捉的话就会报错。所以我们只打印一下就好了
三、proxy代理
vue3跟vue2不同,vue2搭建脚手架的时候proxy就已经给你弄好了,只需改一下路由地址就行了,vue3得自己搞。这里vue3搭建好之后会有一个文件夹vue.config.js,如果没有就自己创建。配置如下即可,部署打包的时候没搞不知道。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
//下面是自己配的,上面是搭建的时候自带的
module.exports = {
devServer: {
// 配置服务器代理
proxy: {
'/api': { // 代理接口前缀为/api的请求,以/api开头实际路径会自动加上target
"target": 'http://localhost:8888/', // 对应的代理地址
"secure": true, // 接受运行在https上,默认不接受
"changeOrigin": false, // 如果设置为true,那么本地会虚拟一个服务器接收你的请求并代你发送该请求,这样就不会有跨域问题(只适合开发环境)
"pathRewrite": { //重写路径: 比如我要调用'http://127.0.0.1:8000/index/',直接写‘/api/index/’即可
'^/api': '/'
}
},
// 如果要配置多个代理,继续
// "/service": {
// "target": 'https://www.google.com/',
// "secure": false,
// "changeOrigin": true
// }
}
}
}
到此开发阶段正常和后端通信就没问题了,至于部署打包后续再说。
tips: lightbox2 可以查看图片缩略图的原图,直接点击即可,下载插件直接用,非常方便,看官网