供应链开发第一天
总结复习下该篇博文大致写了些啥?! 并补充一些知识点.
前端的细节很多!!很多细节未在此处总结.无伤大雅,遇到了翻阅对应笔记即可!
- 前后端项目的创建以及必要模块的安装,按照总结好的步骤实现,准没错.
- 前端页面的完整布局,前端路由的设计+<router-link/>+<router-view>,需仔细悟一悟.
大体框架 => 顶部导航栏 + 左侧侧栏 + 点击侧栏按钮右侧显示对应内容
ps:此处的设计登陆注册等都是包含顶部导航栏的,详见前端路由设计
- 表单的校验分为 前端校验 + 后端校验
- 前端校验 = "输入框失去焦点时"表单字段的校验 + "点击登陆按钮时"主动整体校验 + "点击发送短信验证码按钮时"主动的表单字段校验
Ps:校验的规则可自定义,比较硬核的操作是注册的表单中自定义规则用于验证重复密码输入是否一致!!
- 后端校验 = 数据格式校验 + 数据库合法性校验
- 前端拦截器 = 路由拦截器(导航守卫) + axios拦截器
- 后端跨域问题用CORS解决,本质在于返回数据时候设置响应头!!往往通过编写中间件来实现!
需要特别注意的是,简单请求和复杂请求 简单,直接发送;复杂,先预检OPTIONS,后真正的请求.
- jwt Token,校验是无需经过数据库的,用计算换取空间! 我们使用PyJWT模块来实现的!
"★ 登陆的整体流程图 务必烂熟于心!"
▲ To do List:
1. 在该篇博文中设计完整布局,不是很完美,还存在“默认选中菜单”等问题,稍安勿躁,后续会解决.
2. 后续的涉及到表单的功能需求,为了省事,就不进行前端校验的编写啦!
3. 表单某个字段的主动校验,该篇博文中没有涉及,在后续发送短信验证码时会用到!关键代码如下:
// 会应用写好的的校验规则里mobile字段对应的规则进行校验!!
proxy.$refs.userRef.validateField("mobile",(valid)=>{
if (!valid) { // 校验失败
console.log("校验失败");
return false;
}
console.log("校验成功", userModel) // 校验成功,发送网络请求(基于axios发送)
})
4. 思考一个问题,若想在页面初始化时就发送请求获取数据赋值给表单中的字段,如何实现呢? 关键思想如下:
★★★ const常量是不可变的,直接给它赋值肯定报错,
但要明白,若是复杂数据类型,比如对象,对象内存地址不变,但里面的属性是可以变的!!
5. 后端解决跨域问题,除了CORS,还可以JSONP!但JSONP,只能发送GET请求的跨域!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
上面思考问题对应的解决方案:
供应链系统
供应商 发布需求。 -- vue+drf
管理中心 后台管理 -- vue+drf
运输端 接受需求。 -- 小程序或app+drf
供应商、管理中心、运输端 不同的三个项目. 部署时还可以部署在不同的服务器上.
有三个客户端,但它们的后端用的都是同样的api,涉及到的数据库也是相同的.
2
3
4
5
6
接下来,我们先来开发 客户端-供应商!
# 创建供应商项目
利用webstorm快速创建vue项目
- 利用webstorm快速创建vue项目
- 暂时关闭eslint检查,以便开发阶段保存运行时允许有一些不规范的写法出现.
// vue.config.js
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
linkOnSave: false,
})
- 安装必备依赖
vue add router 规划整个页面的设计,路由,模版
vue add vuex 登录功能,会话保持
它两都会自动创建文件并配置.
2
3
4
5
6
7
8
9
10
11
12
# 顶栏初步实现
简单的打了个草稿, 实现了顶栏!
效果如下:
代码截图:
# 顶栏布局调整
安装element-plus 并引入配置文件, 进行布局调整
继续开发,将 src/App.vue 改巴改巴.. 有几个需注意的点:
1> Container布局容器 注意 el-header
标签 它有个默认的高度60px.
2> Layout布局 "el-row el-col" , 默认采用的是flex布局. SO,下面代码中自己写的原生flex可以用el-row来实现一样的效果!!
3> img大小自适应的方法.
4> 在elementPlus的按钮中使用icon图标.
效果如下:
src/App.vue
<template>
<el-container>
<el-header height="72px" style="border-bottom: 1px solid #f5f5f5;">
<div class="pg-header">
<div class="logo">
<img src="./assets/logo.png" alt="">
</div>
<div class="top-menu">
<el-button type="warning" :icon="Star" circle/>
<router-link :to="{name:'Login'}">
<el-button>登陆</el-button>
</router-link>
<router-link :to="{name:'Register'}">
<el-button>注册</el-button>
</router-link>
</div>
</div>
</el-header>
<router-view/>
</el-container>
</template>
<style>
body {
margin: 0;
}
.pg-header {
display: flex;
flex-direction: row; /* 横向 */
justify-content: space-between; /* 横向两边 */
align-items: center; /* 纵向居中 */
/* elementPlus的顶栏<el-header>的默认高度是60px,
其子标签".pg-header"的高度为72px,顶栏会被撑起来!
我们往往会给<el-header height="72px">设置高度,保持一致!! */
height: 72px;
}
.pg-header .logo {
height: 48px;
}
.pg-header .logo img {
height: 100%; /* !!img这样设置,会使用它父级标签的高度,图片宽度自适应*/
}
.pg-header .top-menu a {
padding: 0 5px;
text-decoration: none;
}
</style>
<script setup>
import {Star,} from '@element-plus/icons-vue' // 引入图标
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
来康康用el-row实现顶栏的布局是如何做的!! (可结合截图中画的结构图辅助理解!)
简单来说, 就是 el-row
标签 代替了 pg-header
标签. el-row
本身就支持flex, 可设置标签属性 justify 、align 等属性进行布局!!
# 完整布局
顶栏固定,在页面上加入侧边栏!! 点击侧边栏上的菜单,在右边显示对应的内容.
完整的布局使用的是下面container页面布局的这个结构,只不过vue是单页面应用, 多个vue组件的组合需多琢磨琢磨下.
elmentPlus官网的示例代码是这样的:
<template>
<div class="common-layout">
<el-container>
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
效果如下:
Ps: 还有两个bug! 后续开发过程中会慢慢解决! 其1是加载 Front组件时, 默认选中 的菜单的内容没有在右侧展示; 其二是, 选中某个菜单后刷新,没有绑定该菜单!!
代码实现
src/views/FrontView.vue
<template>
<el-container>
<el-aside width="250px" style="height: calc(100vh - 72px)">
<!--<router-link :to="{name:'Basic'}">基本认证</router-link>
<router-link :to="{name:'Auth'}">实名认证</router-link>-->
<el-scrollbar>
<el-menu default-active="1-1" :router="true" style="border: none;">
<el-sub-menu index="1">
<template #title>
<el-icon>
<Location/>
</el-icon>
<span>Navigator One</span>
</template>
<el-menu-item index="1-1" :route="{name:'Basic'}">item one</el-menu-item>
<el-menu-item index="1-2" :route="{name:'Auth'}">item two</el-menu-item>
</el-sub-menu>
<el-menu-item index="2">
<el-icon>
<IconMenu/>
</el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon>
<Document/>
</el-icon>
<span>Navigator Three</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon>
<Setting/>
</el-icon>
<span>Navigator Four</span>
</el-menu-item>
<el-menu-item v-for="index in 15" :key="index" index="index">
<el-icon>
<Setting/>
</el-icon>
<span>Navigator Four</span>
</el-menu-item>
</el-menu>
</el-scrollbar>
</el-aside>
<el-main style="background-color: #f5f5f5">
<router-view></router-view>
</el-main>
</el-container>
</template>
<style scoped>
</style>
<script setup>
import {Document, Menu as IconMenu, Location, Setting,} from '@element-plus/icons-vue'
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
完整的布局写完后,到目前为止.. 看router里,共有5个 path/路由 , 接下来会完善这5个路由对应页面的内容!
# 登陆页面实现
实现 用户名密码登陆 和 短信登陆 的页面效果!
效果如下:
代码实现
回顾一个知识点, 数据变,页面跟着变化,vue3组合式API中需要加上ref !!
若还想页面变, 数据跟着变, 就需要使用 v-model (注意,它只针对于input标签)!!
src/views/LoginView.vue
<template>
<div class="main">
<div class="login-box">
<div class="tab-box-switch">
<ul class="switch-ul">
<li @click="tabSelected = index" :class="tabSelected === index?'tab-active':''"
v-for="(txt, index) in tabList" :key="index"> {{ txt }}
</li>
</ul>
<div v-show="tabSelected === 0">
<el-form size="large" :model="userModel">
<el-form-item style="margin-top: 24px;">
<el-input v-model="userModel.user" placeholder="手机号"/>
</el-form-item>
<el-form-item style="margin-top: 24px;">
<el-input v-model="userModel.pwd" placeholder="密码"/>
</el-form-item>
<el-form-item style="margin-top: 24px;">
<el-button type="primary" size="default">登录</el-button>
</el-form-item>
</el-form>
</div>
<div v-show="tabSelected === 1">
<el-form size="large" :model="smsModel">
<el-form-item style="margin-top: 24px;">
<el-input v-model="smsModel.mobile" placeholder="手机号"/>
</el-form-item>
<el-form-item style="margin-top: 24px;">
<el-row justify="space-between" style="width: 100%">
<el-input v-model="smsModel.code" placeholder="验证码" style="width: 60%"/>
<el-button :disabled="btnSmsDisabled" @click="doSendSms" style="width: 30%">
{{ btnSmsText }}
</el-button>
</el-row>
</el-form-item>
<el-form-item style="margin-top: 24px;">
<el-button type="primary" size="default">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref, reactive} from 'vue'
let tabSelected = ref(0)
const tabList = reactive(["密码登录", "免密码登录"])
const userModel = reactive({
name: '',
pwd: '',
})
const smsModel = reactive({
mobile: '',
code: '',
})
const btnSmsText = ref("发送验证码")
const btnSmsDisabled = ref(false)
function doSendSms() {
btnSmsDisabled.value = true;
let txt = 5;
let interval = window.setInterval(() => {
txt -= 1
btnSmsText.value = `${txt}秒后重发`
if (txt < 1) {
btnSmsText.value = '重新发送'
window.clearInterval(interval)
btnSmsDisabled.value = false;
}
}, 1000)
}
</script>
<style scoped>
.main {
height: calc(100vh - 72px);
background-color: #f5f5f5;
}
.login-box {
width: 350px;
min-height: 200px;
background-color: white;
margin: 150px auto;
border-radius: 5px;
padding: 0 44px 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.tab-box-switch .switch-ul {
list-style: none;
padding: 0;
margin: 0;
}
.tab-box-switch .switch-ul li {
display: inline-block;
height: 60px;
line-height: 60px;
font-size: 16px;
margin-right: 24px;
cursor: pointer;
}
/* 下面这两个css用于,实现选项卡激活状态的样式 */
.tab-active {
position: relative; /*设定为相对定位,这意味着该元素的定位是相对于其自身原本的位置进行的.*/
color: #1a1a1a; /*文本颜色*/
font-weight: 500; /*文本粗细*/
font-synthesis: style; /*设置字体综合风格.*/
}
.tab-active::before {
display: block; /*将伪元素显示为块级元素,这样它就可以独占一行,而不会与其他元素在同一行上。*/
position: absolute; /*将伪元素的定位方式设定为绝对定位,这样它可以相对于最近的已定位祖先元素进行定位.*/
bottom: 0; /*将伪元素定位在父元素的底部.*/
content: ""; /*定义伪元素的内容为空,因为我们希望它只是一个装饰性的元素.*/
width: 100%; /*将伪元素的宽度设定为父元素的100%,以便填满父元素的宽度.*/
height: 2px; /*定义伪元素的高度为3像素.*/
background-color: #0084ff; /*设置伪元素的背景颜色为#0084ff,即蓝色.*/
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# 前端表单校验
需要对用户输入的内容进行表单校验(前端), 减轻服务端的压力.
大体需求
客户端的校验
1> 点击输入框后失去焦点时候触发对该表单字段的校验 [技术点]el-form标签的属性指令rules,并给el-form-item标签设置属性prop
2> 点击登陆按钮,对表单里的数据主动进行校验 [技术点]给el-form标签设置属性ref,以便于找到整个表单
PS: 前端的校验还包括主动的表单字段校验,该处示例中没用到,暂且不论后续的短信验证码处会用到,望周知!!
服务端的校验
3> 当客户端的校验都通过后,会携带表单数据向后端API发送axios请求
若请求成功,token写入vuex+持久化+跳转
若后端校验不通过,后端会返回错误信息(eg:用户名密码不匹配;验证码失效等),需在前端页面进行展示.如何展示呢?
- 表单字段少,提示整体错误. 使用ElementPlus上的消息弹出框、消息提示
import { ElMessage } from 'element-plus'
ElMessage.error('Oops, this is a error message.')
- 表单字段多,在对应字段下方显示错误提示信息 [技术点]el-form-item标签的属性指令error
4> 连续点击登陆按钮,后端错误提示信息在页面上不会消失.
5> 点击重置按钮后,重置该表单项,将其值重置为初始值,并移除校验结果.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
效果如下:
关键代码实现
注: ref="userRef"
、:model="userModel"
、prop
, 若要使用ref,这三者都得设置哦!!
注意哈, 在 validateFormError 方法中, errorDict[key] = resError[key][0];
表明前后端的字段名最好一致!!
src/views/LoginView.vue
<template>
<div class="main">
<div class="login-box">
<div class="tab-box-switch">
<ul class="switch-ul">
<li @click="tabSelected = index" :class="tabSelected === index?'tab-active':''"
v-for="(txt, index) in tabList" :key="index"> {{ txt }}
</li>
</ul>
<div v-show="tabSelected === 0">
<el-form size="large" :model="userModel" :rules="userRules" ref="userRef">
<el-form-item style="margin-top: 24px;" prop="user" :error="userError.user">
<el-input v-model="userModel.user" placeholder="用户名"/>
</el-form-item>
<el-form-item style="margin-top: 24px;" prop="pwd" :error="userError.pwd">
<el-input v-model="userModel.pwd" placeholder="密码"/>
</el-form-item>
<el-form-item style="margin-top: 24px;">
<el-button type="primary" size="default" @click="doPwdLogin">登录</el-button>
<el-button size="default" @click="resetUserForm">Reset</el-button>
</el-form-item>
</el-form>
</div>
<div v-show="tabSelected === 1">
<el-form size="large" :model="smsModel" :rules="smsRules" ref="smsRef">
<el-form-item style="margin-top: 24px;" prop="mobile" :error="smsError.mobile">
<el-input v-model="smsModel.mobile" placeholder="手机号"/>
</el-form-item>
<el-form-item style="margin-top: 24px;" prop="code" :error="smsError.code">
<el-row justify="space-between" style="width: 100%">
<el-input v-model="smsModel.code" placeholder="验证码" style="width: 60%"/>
<el-button :disabled="btnSmsDisabled" @click="doSendSms" style="width: 30%">
{{ btnSmsText }}
</el-button>
</el-row>
</el-form-item>
<el-form-item style="margin-top: 24px;">
<el-button type="primary" size="default" @click="doSmsLogin">登录</el-button>
<el-button size="default" @click="resetSmsForm">Reset</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {getCurrentInstance, reactive, ref} from 'vue'
const {proxy} = getCurrentInstance()
// 登陆模式
let tabSelected = ref(0)
const tabList = reactive(["密码登录", "免密码登录"])
// 用户和密码登陆
const userModel = reactive({user: '', pwd: '',}) // 模型
const userError = reactive({user: '', pwd: '',}) // 后端校验相关
const userRules = reactive({ // 前端校验规则
user: [
{required: true, message: '用户名必填', trigger: 'blur'},
{min: 3, max: 10, message: '用户名长度在3到10位之间', trigger: 'blur'},
],
pwd: [
{required: true, message: '密码必填', trigger: 'blur'},
{
pattern: /^(?=.*[0-9])(?=.*[a-zA-Z]).{6,20}$/,
message: '密码长度在6到20位之间且包含至少一个数字和一个字母',
trigger: 'blur'
},
],
})
function doPwdLogin() {
clearFormError(userError)
proxy.$refs.userRef.validate((valid) => {
if (!valid) {
return false
}
// console.log("校验成功!", userModel)
let pwdLoginError = {code: -1, error: {user: ["用户名或密码错误"], pwd: ["用户名或密码错误"]}}
validateFormError(userError, pwdLoginError.error);
})
}
function resetUserForm() {
proxy.$refs.userRef.resetFields()
}
// 短信登陆
const smsModel = reactive({mobile: '', code: '',})
const smsError = reactive({mobile: '', code: '',})
const btnSmsText = ref("发送验证码")
const btnSmsDisabled = ref(false)
const smsRules = reactive({
mobile: [{required: true, message: '手机号必填', trigger: 'blur'},],
code: [{required: true, message: '验证码必填', trigger: 'blur'},],
})
function doSendSms() {
btnSmsDisabled.value = true;
let txt = 5;
let interval = window.setInterval(() => {
txt -= 1
btnSmsText.value = `${txt}秒后重发`
if (txt < 1) {
btnSmsText.value = '重新发送'
window.clearInterval(interval)
btnSmsDisabled.value = false;
}
}, 1000)
}
function doSmsLogin() {
clearFormError(smsError)
proxy.$refs.smsRef.validate((valid) => {
if (!valid) {
return false
}
// console.log("校验成功!", smsModel)
let smsLoginError = {code: -1, error: {mobile: ["手机号码不存在"], code: ["验证码失效"]}}
validateFormError(smsError, smsLoginError.error);
})
}
function resetSmsForm() {
proxy.$refs.smsRef.resetFields()
}
// 后端错误信息相关
function validateFormError(errorDict, resError) {
for (let key in resError) {
errorDict[key] = resError[key][0];
}
}
function clearFormError(errorDict) {
for (let key in errorDict) {
errorDict[key] = "";
}
}
</script>
<style scoped>
/* 略 跟前面的并无啥出入 */
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# 注册页面实现
实现 注册 的页面效果!
新增知识点:
表单 label的设置 ; 自定义校验规则用于重复密码输入是否一致的校验!!
使用status-icon
属性为输入框添加了表示校验结果的反馈图标!
<el-form size="large" :model="regModel" :rules="regRules" ref="regRef" label-width="80px" status-icon>
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('重复密码不能为空'))
} else if (value !== regModel.pwd) {
callback(new Error("两次密码输入不一致!"))
} else {
callback()
}
}
const regRules = reactive({
pwd: [
{required: true, message: '密码必填', trigger: 'blur'},
{min: 6, max: 10, message: '密码至少有6位', trigger: 'blur'},
],
pwdConfirm: [
{validator: validatePass, trigger: 'blur'}, // ★★★
],
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
效果如下:
src/views/RegisterView.vue
<template>
<div class="main">
<div style="width: 1000px;margin: 10px auto;">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>用户注册</span>
</div>
</template>
<div style="width: 460px;margin: 10px auto;">
<el-form size="large" :model="regModel" :rules="regRules"
ref="regRef" label-width="80px" status-icon>
<el-form-item style="margin-top: 24px;" prop="name" :error="regError.name" label="企业简称">
<el-input v-model="regModel.name" placeholder="请输入企业简称"/>
</el-form-item>
<el-form-item style="margin-top: 24px;" prop="mobile" :error="regError.mobile" label="手机号">
<el-input v-model="regModel.mobile" placeholder="请输入手机号"/>
</el-form-item>
<el-form-item style="margin-top: 24px;" prop="code" :error="regError.code" label="验证码">
<el-row justify="space-between" style="width: 100%">
<el-input v-model="regModel.code" placeholder="请输入验证码" style="width: 60%"/>
<el-button style="width: 30%">发送验证码</el-button>
</el-row>
</el-form-item>
<el-form-item style="margin-top: 24px;" prop="pwd" :error="regError.pwd" label="密码">
<el-input type="password" v-model="regModel.pwd" placeholder="请输入密码"/>
</el-form-item>
<el-form-item style="margin-top: 24px;" prop="pwdConfirm" :error="regError.pwdConfirm"
label="重复密码">
<el-input type="password" v-model="regModel.pwdConfirm" placeholder="请再次输入密码"/>
</el-form-item>
<el-form-item style="margin-top: 24px;">
<el-button type="primary" size="default" @click="doRegister">注册</el-button>
<el-button size="default" @click="resetRegisterForm">Reset</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import {getCurrentInstance, reactive} from 'vue'
const {proxy} = getCurrentInstance()
const regModel = reactive({name: '', mobile: '', code: '', pwd: '', pwdConfirm: ''})
const regError = reactive({name: '', mobile: '', code: '', pwd: '', pwdConfirm: ''})
// ★ 自定义校验规则用于重复密码输入是否一致的校验!!
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('重复密码不能为空'))
} else if (value !== regModel.pwd) {
callback(new Error("两次密码输入不一致!"))
} else {
callback()
}
}
const regRules = reactive({
name: [
{required: true, message: '企业名称必填', trigger: 'blur'},
],
mobile: [
{required: true, message: '手机号必填', trigger: 'blur'},
{pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur'},
],
code: [
{required: true, message: '验证码必填', trigger: 'blur'},
],
pwd: [
{required: true, message: '密码必填', trigger: 'blur'},
{min: 6, max: 10, message: '密码至少有6位', trigger: 'blur'},
],
pwdConfirm: [
{validator: validatePass, trigger: 'blur'},
],
})
function doRegister() {
clearFormError(regError)
proxy.$refs.regRef.validate((valid) => {
if (!valid) {
return false
}
// console.log("校验成功!", regModel)
let RegisterError_res = {code: -1, error: {name: ["该企业已经注册"], code: ["验证码不正确"]}}
validateFormError(regError, RegisterError_res.error);
})
}
function resetRegisterForm() {
proxy.$refs.regRef.resetFields()
}
function validateFormError(errorDict, resError) {
for (let key in resError) {
errorDict[key] = resError[key][0];
}
}
function clearFormError(errorDict) {
for (let key in errorDict) {
errorDict[key] = "";
}
}
</script>
<style scoped>
.main {
height: calc(100vh - 72px);
background-color: #f5f5f5;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 创建后端项目
创建纯净版的Django项目
- 创建名为dahe的Django项目,并创建了名为shipper的app => Django版本3.2 + drf
- 纯净版 + 改一下时区,中文
"""
LANGUAGE_CODE = 'en-us' 英文
LANGUAGE_CODE = 'zh-hans' 中文
"""
LANGUAGE_CODE = 'zh-hans'
"""
datetime.datetime.now() - utc时间 / datetime.datetime.utcnow() - utc时间
TIME_ZONE = 'UTC'
datetime.datetime.now() - 东八区时间 / datetime.datetime.utcnow() - utc时间
TIME_ZONE = 'Asia/Shanghai'
"""
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
"""
影响自动生成数据库时间字段
USE_TZ = True, 创建UTC时间写入到数据库
USE_TZ = False, 根据TIME_ZONE设置的时区进行创建时间并写入数据库
"""
USE_TZ = False
- 启动Djaong项目时,解决匿名用户认证错误
REST_FRAMEWORK = {
"UNAUTHENTICATED_USER": None,
"UNAUTHENTICATED_TOKEN": None,
}
- 解决跨域问题,在项目根目录下创建 ext文件夹,并在里面创建middlewares目录,并在该目录下创建auth.py文件
from django.utils.deprecation import MiddlewareMixin
class AuthMiddleware(MiddlewareMixin):
def process_response(self, request, response):
response["Access-Control-Allow-Origin"] = "*" # 任意网址
response["Access-Control-Allow-Methods"] = "*" # 任意请求方式 "PUT,DELETE,GET,POST等"
response["Access-Control-Allow-Headers"] = "*" # 任意请求头
return response
记得应用中间件 'ext.middlewares.auth.AuthMiddleware',
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 后端数据校验
用户输入的表单数据在前端校验成功后, 会通过axios传递到后端.
后端也会对它进行数据校验, 该校验主要分为两部分 1> 数据格式校验 2> 数据库合法性校验
后端校验成功后, 就登陆成功啦!! 还需要进行token相关的操作,这个下一小节进行阐述.
PS: 后端是可以返回某些个字段的错误的.. 只是gif示例中没有体现出来, 望周知!
# 表写在哪个app里?
创建数据库表的时候,关于表结构的设计!
(不存在哪种设计好于不好,取决于开发者的爱好,更取决于需求,需求多了就会进行拆分,再往后面说,还会涉及到分库分表)
常见的有三种设计方案:
方案1: 有多个APP
它们里面相应的表都是独立的,独立的表结构,没有太大的关系.
- app01, app01的表结构+相应的业务
- app02, app02的表结构+相应的业务
- app03, app03的表结构+相应的业务
方案2: 只有一个app
将所有的表结构都放到这个app里;所有的业务也放到该app中进行开发(将views.py变成views目录)
- app01, 所有表结构 + 所有业务
方案3: 有多个app
表结构单独写在一个app中,业务功能在多个app中进行了拆分.
- app01, 里面只写表结构
- app02, 里面只写相应的业务
- app03, 里面只写相应的业务
- app04, 里面只写相应的业务
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
像我们的供应链项目,一个后端对应多个前端"供应商、管理中心、运输端", 通过需求分析是用不了方案1的.
因为管理中心会涉及到对供应商的表进行操作,供应商需要提交一些数据由管理中心的管理人员审核.
它们的业务是杂糅到一起的.因此对它们独立拆表也拆不开! 所以不适合使用方案1.
我们的供应链项目用设计2和设计3皆可以!此处,我们用设计3进行示例!
# 数据库表
创建名为repository的App, 只用来放表
python manage.py startapp repository
记得注册.
在repository的models.py中设计ORM表结构, 并进行数据迁移.
创建模拟数据
也可以不要供应商认证表,若不要的话,供应商认证表中的字段应写在供应商表中,并允许其值为空!!
像营业执照、法人身份证等字段应该存储文件路径..此处暂时先用CharField类型,后续根据需求再进行调整!
repository/models.py
from django.db import models
class Company(models.Model):
""" 供应商 """
auth_type_choices = ((1, "未认证"), (2, "认证中"), (3, "已认证"))
name = models.CharField(verbose_name="企业简称", max_length=32)
mobile = models.CharField(verbose_name="手机号", max_length=11)
password = models.CharField(verbose_name="密码", max_length=32)
auth_type = models.SmallIntegerField(verbose_name="认证类型", choices=auth_type_choices, default=1)
ctime = models.DateTimeField(verbose_name="注册时间", auto_now_add=True)
class CompanyAuth(models.Model):
""" 供应商认证 """
company = models.OneToOneField(verbose_name="公司", to="Company", on_delete=models.CASCADE)
title = models.CharField(verbose_name="企业全称", max_length=64)
unique_id = models.CharField(verbose_name="信用代码", max_length=64)
licence_path = models.CharField(verbose_name="营业执照", max_length=64)
corp = models.CharField(verbose_name="法人", max_length=64)
corp_identity = models.CharField(verbose_name="法人身份证", max_length=64)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
创建的模拟数据
utils/encrypt.py
import hashlib
from django.conf import settings
def md5(data_string):
obj = hashlib.md5(settings.SECRET_KEY.encode('utf-8'))
obj.update(data_string.encode('utf-8'))
return obj.hexdigest()
2
3
4
5
6
7
8
repository/test.py 单独运行该文件,就可创建模拟数据出来!
import os
if __name__ == "__main__":
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dahe.settings')
import django
django.setup()
from repository import models
from datetime import datetime
from utils.encrypt import md5
models.Company.objects.create(
name="大和实业", mobile="13888888881", password=md5("root123"), auth_type=2, ctime=datetime.now())
2
3
4
5
6
7
8
9
10
11
12
13
14
# 后端校验逻辑
规定什么情况应该用什么样的返回码!!
0 成功 ; -1 字段 ; -2 整体.
urls.py 根路由
from django.urls import path
from shipper import views
urlpatterns = [
path('api/login/', views.LoginView.as_view()),
]
2
3
4
5
6
ext/ret.py
"""★★★ 返回码!!!"""
SUCCESS = 0 # 校验成功
FIELD_ERROR = -1 # 字段校验失败(前端表单提示)
SUMMARY_ERROR = -2 # 数据库相关,整体校验失败(前端messagebox提示)
2
3
4
shipper/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from ext import ret
from repository import models
from utils.encrypt import md5
class LoginSerializer(serializers.Serializer):
# 还可以使用自定义正则,钩子方法等对关于数据的校验规则进行补充.
mobile = serializers.CharField(required=True)
password = serializers.CharField(required=True)
def validate_password(self, value):
md5_string = md5(value)
return md5_string
class LoginView(APIView):
def post(self, request):
# 1.数据格式校验
ser = LoginSerializer(data=request.data)
if not ser.is_valid():
# 数据格式校验不通过,错误都在ser.errors里,看源码可验证!!
return Response({"code": ret.FIELD_ERROR, "msg": "error.", "detail": ser.errors})
# 2.数据库合法性校验
instance = models.Company.objects.filter(**ser.validated_data).first()
if not instance:
return Response({"code": ret.SUMMARY_ERROR, 'msg': "用户名或密码错误"})
# 3.登陆成功
# ▲ To do: 生成token,返回用户信息 (token应使用JWT生成)
return Response({"code": ret.SUCCESS, 'msg': "登陆成功!",
"data": {"token": "xxxx", "name": instance.name}})
"""
PS: ★ 若校验过程,某个步骤,校验出了某个字段的错误,还可以直接写进去! (eg:验证码失效)
指明具体哪个字段,前端开发会感谢你的.
return Response({"code": ret.FIELD_ERROR, "msg": "error.", "detail": {"字段名": ["某个错误."]})
一个想法思考以及曲线解决的过程,不重要了解即可.
我试过
ser.errors["字段名"] = ["某个错误."]
return Response({"code": ret.FIELD_ERROR, "msg": "error.", "detail": res.errors)
这样的操作,看似ser.errors是个字典,往里面加了东西,其实并没有加进去!!行不通的!
why?打印下 type(ser.errors) ser.errors.__dict__ 就大致明白啦!
如何解决?
errors是被@property修饰了的!那么最简单直接的办法就是:
ser._errors["字段名"] = ["某个错误."]
return Response({"code": ret.FIELD_ERROR, "msg": "error.", "detail": res.errors)
这样是可以的.
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 前端axios回调
根据 code值不同,进行不同的反馈.
提一嘴,axios有两种使用方式!!在以往的博文中有所总结,请自行翻阅.
PS: 为了保证前端表单传递到后端的字段与后端ORM表中的字段名一致,在LoginView.vue中对字段名进行了一点小调整.略.
const userModel = reactive({mobile: '', password: '',}) // el-input标签上是用了v-model的
function doPwdLogin() {
clearFormError(userError)
proxy.$refs.userRef.validate((valid) => {
if (!valid) {
return false
}
proxy.$axios.post(
"http://127.0.0.1:8000/api/login/",
userModel
).then((res) => {
if (res.data.code === 0) {
// res.data -- {"code": 0,"msg": "登陆成功!","data": {"token": "xxxx","name": "大和实业"}}
ElMessage.success(res.data.msg)
// ▲ To do: token写入vuex并做持久化操作+跳转到后台
} else if (res.data.code === -1) {
// 后端校验不通过,登陆失败 在表单上显示具体某些个字段的错误
// res.data -- {"code": -1, "msg": "error.", "detail": {"password": ["该字段不能为空"]}}
validateFormError(userError, res.data.detail);
} else {
// 后端校验不通过,登陆失败 利用ElMessage显示整体错误
// res.data -- {code: -2, msg: '用户名或密码错误'}
ElMessage.error(res.data.msg)
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 后端-JWT
认证/登陆成功 返回的数据, 有一个凭证很重要! 下一次请求携带该凭证.
凭证通常分为两类: session 以及 jwt token!!
切记, session 和 JWT token 各有优劣, 有自己的应用场景, 并不是jwt token 一定就比session 好.. 不是非得用jwt,望周知!
# jwt原理
关于jwt token作废,可参考下: https://wangbjun.site/2019/coding/jwt-token-usage.html
武sir在讲token原理时,说的是token和jwt token.. 其实准确点说 应该是 session和jwt token.
为何武sir会这么说呢?因为大家往往会将认证/用户登陆成功返回给前端的凭证称作为token.
所以session是token,jwt token也是token. token是一种思想!!
关于cookie到session到token的演变,不再赘述,详看以往在Django学习中关于cookie和session的笔记!
不仅需复习它们的原理,也要大致知道它们的用法,特别注意的是,session可以存储在文件、mysql以及redis中!
=== === ===
session的不好处:
1> 用户信息存储在后端, 量大就着不住,量大数据库的压力大
2> 每次请求来时,都得去数据源(mysql、redis等)做一次校验, 访问频繁.
session的好处:
可以将凭证相关信息在后端主动清除掉,也就意味着当前用户的登陆状态被消除啦
根据session的好处,可延伸出一些应用场景:
- 更新密码,密码更新后都应该让其重新登陆
- 在后台管理中变更了当前用户的权限,让其重新登陆,变更的权限信息就会生效啦
- 限定登陆设备的个数,已经在A设备上登陆了,继续在B设备上登陆,A设备上的登陆就会失效
jwt token是啥?
登陆成功后,后端返回前端点分三段式的字符串,前端下次请求携带该字符串,后端校验该字符串是否合法.
如何验证合法性呢?这就得理清楚jwt生成的过程以及验证的机制!
- 生成
header = {"typ":"jwt","alg":"SHA256"} # 声明类型是jwt,以及第三段的加密方式
payload = {"id":1,"name":"xxx","exp":"7day"} # 用户信息以及凭证过期时间
第一段: base64(header)
第二段: base64(payload)
第三段: SHA256(第一段 + "." + 第二段 , 盐) Ps:第三段无法反解,而且还加了盐!
jwt token => 第一段.第二段.第三段
- 验证
壹 第一段 base64 解密
贰 第二段 base64 解密
叁 将解密出的第一段和第二段内容以同样的方式进行SHA256加密加盐,得到的结果与jwt token中的第三段进行比较!
肆 还会进行有效期的校验!
PS:
1> 上述的生成和校验过程,使得我们可以放心使用校验通过后jwt token里的用户信息.
因为用户信息但凡被篡改一点,第三段的比较肯定不会通过!!
2> 关于,有效期检查,有效期过了,简单点会直接让用户重新登陆;复杂一点需要进行"更新token"的操作!
jwt token
好处,不需要格外存储,计算换资源嘛; 不好处,该jwt token在一定时间内肯定是有效的!
场景化来理解:
在A设备和B设备上都登陆了,接着在A设备上进行了更新密码或更新权限的操作.但这仅仅只是使得数据库中的数据变了.
在设备A上的操作没办法使得设备B的登陆状态token失效!只能等过期时间到了自动失效.
(当然你说,我每次进行token校验时通过jwt的第二段看看该用户在数据库中的数据是否改变
进而使得在设备A上更新后,让设备B的登陆状态失效. 这样操作阁下又该如何面对?
我只想说,那为何不直接使用session机制呢?session也会频繁的查询数据库. 要清楚,使用jwt的初衷就是减轻数据库的压力!!)
★ 我用apifox做了个实验,第一次登陆后,拿到TokenA;退出登陆,重新生成一个TokenB. 我在CURD的接口,拿着TokenA一样可以成功操作!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# jwt实现
上面我们已经了解了原理,此处我们自己用代码实现jwt !!
pip install pyjwt -i https://pypi.tuna.tsinghua.edu.cn/simple
★ 该模块的使用有啥不明白的直接看官方文档, 写的很明白: https://pyjwt.readthedocs.io/en/latest/index.html
# 生成代码
import jwt
import datetime
SALT = 'django-insecure-@j+_pttf$=8=e8i-x7c365yvncp5euokbw1&@f+m3)a^wral8s'
def create_token():
# 构造header
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
# 构造payload
payload = {
'user_id': 1, # 自定义用户ID
'username': 'dc', # 自定义用户名
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # 凭证过期时间
}
result = jwt.encode(headers=headers, payload=payload, key=SALT.encode('utf-8'), algorithm="HS256")
return result
if __name__ == '__main__':
token = create_token()
print(token)
"""
eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.
eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImRjIiwiZXhwIjoxNzAwNTQ0MzExfQ.
fHDN30QeaQ6hK4rl4zLnQlJVj2-thkTxTADf3GkHsas
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 验证代码
import jwt
import datetime
from jwt import exceptions
SALT = 'django-insecure-@j+_pttf$=8=e8i-x7c365yvncp5euokbw1&@f+m3)a^wral8s'
def get_payload(token):
"""根据jwt token获取payload"""
try:
# - 从token中获取payload【不校验合法性】
# unverified_payload = jwt.decode(token, options={"verify_signature": False})
# print(unverified_payload)
# - 从token中获取payload【校验合法性】
verified_payload = jwt.decode(token, SALT, algorithms=["HS256"])
return verified_payload
except exceptions.ExpiredSignatureError: # 当超过token的有效期引发该异常
return 'token已失效'
except jwt.DecodeError: # 当token因验证失败而无法解码时引发该异常
return 'token认证失败'
except jwt.InvalidTokenError: # 校验失败的基本异常
return '非法的token'
if __name__ == '__main__':
token = \
"eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImRjIiwiZXhwIjoxNzAwNTQ0MzExfQ.\
fHDN30QeaQ6hK4rl4zLnQlJVj2-thkTxTADf3GkHsas"
payload = get_payload(token)
print(payload)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 封装使用
将 jwt 生成和验证的代码进行封装, 以便在项目中使用!
utils/jwt_auth.py
import jwt
import datetime
from jwt import exceptions
from django.conf import settings
def create_token(payload, timeout=20):
"""
:param payload: 以字典格式存储的用户信息,比如{'user_id':1,'username':'dc'} 必传!
:param timeout: token的过期时间,默认设置为20分钟
:return:
"""
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(seconds=timeout) # seconds 秒; hours 小时
# 盐使用的是Django配置文件中的密钥SECRET_KEY
result = jwt.encode(
headers=headers, payload=payload, key=settings.SECRET_KEY.encode('utf-8'), algorithm="HS256")
return result
def parse_payload(token):
""" 对jwt token校验,校验成功返回元祖 (True/False , payload值/验证失败信息) """
try:
verified_payload = jwt.decode(token, settings.SECRET_KEY.encode('utf-8'), algorithms=["HS256"])
return True, verified_payload
except exceptions.ExpiredSignatureError:
error = 'token已失效'
except jwt.DecodeError:
error = 'token认证失败'
except jwt.InvalidTokenError:
error = '非法的token'
return False, error
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 代码完善
有了token后, 将 "用户密码登陆" 的前后端业务逻辑代码进行完善.
后端: 登陆成功,生成jwt token,返回用户信息
前端: token写入vuex+持久化操作+router跳转到后台+状态展示,即登陆按钮变成当前登陆用户+注销登陆
# 后端返回token
其实就是返回了后端自己生成的jwt token, 登陆凭证!
shipper/views.py
class LoginView(APIView):
def post(self, request):
"""用户登陆"""
# 1.数据格式校验
ser = LoginSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": ret.FIELD_ERROR, "msg": "error.", "detail": ser.errors})
# 2.数据库合法性校验
instance = models.Company.objects.filter(**ser.validated_data).first()
if not instance:
return Response({"code": ret.SUMMARY_ERROR, 'msg': "用户名或密码错误"})
# 3.登陆成功 ★ 生成token,返回用户信息
token = create_token({'user_id': instance.id, 'name': instance.name})
# ★ ★ ★ data键对应的值中,token用于凭证,name用于登陆后右上角显示登陆按钮变成谁登陆啦.
return Response(
{"code": ret.SUCCESS, 'msg': "登陆成功!", 'data': {"token": token, 'name': instance.name}})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 前端拿到token
前端代码的完善 在vuex的插件的学习示例中 所涉及到的知识点都尽数阐述了,此处不再赘述!
需要提醒一点的是, 是否使用计算属性的判定标准, 值得再思考下, 通过前端笔记中关于vuex的那三个QA再悟一悟.
src/store/index.js
import {createStore} from 'vuex'
export default createStore({
state: {
name: localStorage.getItem('name') || "",
token: localStorage.getItem('token') || "",
},
getters: {},
mutations: {
login(state, {name, token}) {
state.name = name;
state.token = token;
localStorage.setItem('name', name);
localStorage.setItem('token', token);
},
logout(state) {
state.name = "";
state.token = "";
localStorage.removeItem('name');
localStorage.removeItem('token');
},
},
actions: {},
modules: {}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
src/views/LoginView.vue token写入vuex+持久化+跳转
function doPwdLogin() {
clearFormError(userError)
proxy.$refs.userRef.validate((valid) => {
if (!valid) {
return false
}
proxy.$axios.post(
"http://127.0.0.1:8000/api/login/",
userModel
).then((res) => {
console.log(res.data)
if (res.data.code === 0) {
// res.data -- {"code": 0,"msg": "登陆成功!","data": {"token": "xxxx","name": "大和实业"}}
ElMessage.success(res.data.msg)
// ★ ★ ★ 登陆成功,token写入vuex+持久化+跳转
// res.data.data ==> {"token": token值, 'name': 当前登陆用户名字}
store.commit("login", res.data.data); // 也可以 proxy.$store.commit("login", res.data.data)
router.replace({name: "Basic"}) // 也可以 proxy.router({name: "Basic"})
} else if (res.data.code === -1) {
// 后端校验不通过,登陆失败 在表单上显示具体某些个字段的错误
// res.data -- {"code": -1, "msg": "error.", "detail": {"password": ["该字段不能为空"]}}
validateFormError(userError, res.data.detail);
} else {
// 后端校验不通过,登陆失败 利用ElMessage显示整体错误
// res.data -- {code: -2, msg: '用户名或密码错误'}
ElMessage.error(res.data.msg)
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
src/App.vue 状态展示,即登陆按钮变成当前登陆用户+注销登陆
<template>
<el-container>
<el-header height="72px" style="border-bottom: 1px solid #f5f5f5;">
<div class="pg-header">
<div class="logo">
<img src="@/assets/nav-log.png">
</div>
<div v-if="name" class="top-menu">
<el-dropdown>
<span class="el-dropdown-link">{{ name }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="User">基本信息</el-dropdown-item>
<el-dropdown-item :icon="CloseBold" @click="doLogout">注销登陆</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div v-else class="top-menu">
<router-link :to="{name:'Login'}">
<el-button>登录</el-button>
</router-link>
<router-link :to="{name:'Register'}">
<el-button>注册</el-button>
</router-link>
</div>
</div>
</el-header>
<router-view/>
</el-container>
</template>
<script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
import {useRouter} from 'vue-router'
import {User, CloseBold} from '@element-plus/icons-vue'
const router = useRouter();
const store = useStore();
// const name = ref(store.state.name); 这样不得行!
const name = computed(() => store.state.name);
function doLogout() {
//localStorage清空 + state值清空 + 跳转登录
store.commit("logout");
router.push({name: "Login"});
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 拦截器相关处理
拦截器相关处理(指在前端方面), ☆大致需要两个操作:
1> 全局导航守卫 实现了前端哪些页面/前端路由 能顺利加载.
2> axios拦截器
请求拦截器 axios请求时携带token登陆凭证
响应拦截器 前端可以知道后端让不让它进行此次axios请求背后的CURD操作../token凭证是否是有效合法的.
# ★ 登陆的整体流程
先来理一理下,用户名密码登陆的整个流程应该做些什么?!!
纵览全局,就丝毫不慌!
PS: 前端的校验还包括主动的表单字段校验,图中没画出来,后续的发送短信验证码时会用到,望周知!!
图中的阐述言简意赅, 登陆成功的部分已经在上一小节 "后端-JWT"中阐述完啦,接下来将阐述 拦截器相关处理!!
测试示例中, 是登陆成功后跳转后台页面,在BasicView.vue中发送了一个axios请求..是能成功的!!
5s之内,刷新BasicView.vue页面,重新发送的axios请求都没问题!
但当经过5s后,jwt token登陆凭证过期了,此时刷新当前页面,BasicView.vue开始新的生命周期,重新发送axios请求.
但因为jwt token过期,后端API的认证组件通过不了,返回401错误,前端的axios响应拦截器拦截到这个错误,做了相应的处理!!
(Ps:为了方便测试,token有效时间我设置的是5s)
/* BasicView.vue的代码,我是这样写的.所以,我用刷新页面的方式来重新发送axios请求! */
<template>
<h2>基础信息</h2>
</template>
<script setup>
import {getCurrentInstance} from 'vue'
const {proxy} = getCurrentInstance()
proxy.$axios.get("http://127.0.0.1:8000/api/basic/").then((res) => {
console.log(res);
})
</script>
<style scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
实验结果如下:
# 全局导航守卫
作用: 实现了前端哪些页面/前端路由 能顺利加载.
src/router/index.js
import {createRouter, createWebHistory} from 'vue-router'
const routes = [...]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
// 白名单
if (to.name === "Login" || to.name === "Register") {
next();
return;
}
let token = localStorage.getItem("token");
if (token) {
// 已登录,可以向目标地址访问
next();
return
}
// 未登录,重定向到登陆界面
next({name: "Login"});
})
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# axios拦截器
aixos请求拦截器 给每次axios请求加上token相关的请求头;
aixos响应拦截器 根据后端返回的错误状态码(eg:401认证错误)进行处理!
★★★ 有两种方式使用axios!! (详看对应部分的笔记!! 该处示例里我们使用的是方式二)
方式一: 创建axios对象,页面中导入并实现.
方式二: 创建axios对象,设置vue对象中,其他页面获取vue对象.
src/plugins/axios.js
注意, vue3项目,新的vue-store中规定 useStore只能在setup中使用!! vue-route同理!
即import {useStore} from 'vuex' const store = useStore();
这样的写法只能在vue组件的<script setup>
中使用!!
import axios from "axios";
import router from "@/router";
import store from "@/store";
import {ElMessage} from 'element-plus'
let config = {};
const _axios = axios.create(config);
_axios.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = localStorage.getItem("token");
if (token) {
config.headers.common['Authorization'] = token;
}
return config;
}
);
_axios.interceptors.response.use(
function (response) {
// Do something with response data
// console.log(response.status);
return response;
},
function (error) {
// Do something with response error
// return Promise.reject(error);
if (error.response.status === 401) {
store.commit("logout");
router.replace({name: "Login"});
ElMessage.error("认证失败,请重新登录");
} else {
// 如果是其他错误,会在前端页面显示报错,抛出异常!!
return Promise.reject(error);
}
}
);
export function installAxios(Vue) {
Vue.config.globalProperties.$axios = _axios;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 后端认证代码
关键就是认证组件!!
实验的关键代码如下:
class JwtAuthentication(BaseAuthentication):
def authenticate(self, request):
# “预检”其实做检查,检查后端的跨域策略,“预检”通过后才会发送一次请求用于数据传输!
# ★ “预检”请求里肯定没有请求头HTTP_AUTHORIZATION的!!若不写这两行语句,认证会一直不通过401.
if request.method == "OPTIONS":
return
# 1.读取请求头中的token
authorization = request.META.get('HTTP_AUTHORIZATION', '')
# 2.jwt token的校验
status, info_or_error = parse_payload(authorization)
# 3.校验失败,返回失败信息,前端重新登录
if not status:
raise exceptions.AuthenticationFailed({"code": 8888, 'msg': info_or_error})
# 4.校验成功 request.user request.auth
return info_or_error, authorization
def authenticate_header(self, request):
return 'API'
class BasicView(APIView):
authentication_classes = [JwtAuthentication, ]
def get(self, request):
print(request.data)
print(request.user)
print(request.auth)
return Response({"code": ret.SUCCESS, 'msg': "success"})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 侧边栏的收缩展开
就这玩意儿 倒腾了我1个半小时, 喵的, 前端真的难搞啊!!
关键代码如下:
还可以更简化一下,把那两个方法也省略了!!
<el-icon v-if="!isCollapse" @click="isCollapse=true" class="shrinkBtn">
<Fold/>
</el-icon>
<el-icon v-else @click="isCollapse=false" class="shrinkBtn">
<Expand/>
</el-icon>
2
3
4
5
6
7