Django-Vue搭建个人博客:资料更新与异步
6400 views, 2023/10/16 updated Go to Comments
上一章用户可以登录了,本章接着完成用户资料的更新和登出。
组件化
前面做搜索功能时,为了美观我们定义了按钮的样式。正巧用户更新也需要按钮,为了避免样式相互冲突,先做点准备工作:把搜索框组件化。
组件化的方法前面已经讲过了,再来一遍加深印象。
新建一个 SearchButton.vue
文件,把 BlogHeader.vue
中与搜索相关的内容全部搬运过来:
<!-- frontend/src/components/SearchButton.vue -->
<template>
<div class="search">
<form>
<input v-model="searchText" type="text" placeholder="输入搜索内容...">
<button v-on:click.prevent="searchArticles"></button>
</form>
</div>
</template>
<script>
export default {
name: 'SearchButton',
data: function () {
return {
searchText: '',
}
},
methods: {
searchArticles() {...}
},
}
</script>
<style scoped>
/* 相关样式全部搬运到这里 */
.search {...}
* {...}
form {...}
input, button {...}
input {...}
button {...}
.search input {...}
.search button {...}
.search button:before {...}
</style>
注意这里的搬运是有些小改动的,比如组件导出的名字,不改就乱套了。
接着把 BlogHeader.vue
对应搜索的部分删掉(特别是样式):
<!-- frontend/src/components/BlogHeader.vue -->
<template>
<div id="header">
<div class="grid">
...
<!--引入搜索框组件-->
<SearchButton/>
</div>
...
</div>
</template>
<script>
import axios from 'axios';
import SearchButton from '@/components/SearchButton.vue'
export default {
name: 'BlogHeader',
// 定义组件
components: {SearchButton},
data: function () {
return {
username: '',
hasLogin: false,
// searchText 变量删除
}
},
// methods 删除掉
mounted() {...}
}
}
</script>
<style scoped>
/*与搜索框相关的 css 删除*/
...
</style>
同样也记得把用不到的库、组件、名称都修改正确。
完成后刷新页面,确保功能正常就 OK。
异步与重构
用户资料的更改、删除最好有个单独的页面,这就带来两个很头疼的问题:
- 用户资料页面涉及 POST/PATCH 等操作,毫无疑问需要验证用户的身份和 token 有效性;巧的是前面写的
BlogHeader.vue
也有类似的需求。因此需要将验证代码重构为一个单独的函数,供大家调用。 - 把验证代码抽象为单独的函数后,由于
axios
发送的请求是异步的,所以要将此处的异步代码转换为同步代码,否则 localStorage 的存取顺序会因为网速的快慢而不可预测,带来潜在 bug。
综合上述两条,让我们先来处理这最麻烦的部分。
新建路径及文件 frontend/src/utils/authorization.js
,写入代码:
// frontend/src/utils/authorization.js
import axios from 'axios';
async function authorization() {
const storage = localStorage;
let hasLogin = false;
let username = storage.getItem('username.myblog');
const expiredTime = Number(storage.getItem('expiredTime.myblog'));
const current = (new Date()).getTime();
const refreshToken = storage.getItem('refresh.myblog');
// 初始 token 未过期
if (expiredTime > current) {
hasLogin = true;
console.log('authorization access')
}
// 初始 token 过期
// 申请刷新 token
else if (refreshToken !== null) {
try {
let response = await axios.post('/api/token/refresh/', {refresh: refreshToken});
const nextExpiredTime = Date.parse(response.headers.date) + 60000;
storage.setItem('access.myblog', response.data.access);
storage.setItem('expiredTime.myblog', nextExpiredTime);
storage.removeItem('refresh.myblog');
hasLogin = true;
console.log('authorization refresh')
}
catch (err) {
storage.clear();
hasLogin = false;
console.log('authorization err')
}
}
// 无任何有效 token
else {
storage.clear();
hasLogin = false;
console.log('authorization exp')
}
console.log('authorization done');
return [hasLogin, username]
}
export default authorization;
看起来和之前写的验证代码很像,但是有两个非常重要的区别:
async/await
:async
表示函数里含有异步操作,await
表示紧跟在后面的表达式需要等待结果。await
关键字只能用在async
函数中,并且由于它返回的Promise
对象运行的结果可能是rejected
,所以最好放到try...catch
语句中。async
函数返回的不再是return
后面的数据,而是包含数据的Promise
对象,因此调用它的位置需要改为Promise.then().catch()
进行异常处理。(有点像axios.then().catch()
)
Promise 对新手来说稍麻烦,篇幅有限不展开讲原理,请善用搜索。
用户中心
封装好身份验证的函数后,用户中心这个页面相对就好弄了。
新建 frontend/src/views/UserCenter.vue
,写入代码:
<!-- frontend/src/views/UserCenter.vue -->
<template>
<BlogHeader/>
<div id="user-center">
<h3>更新资料信息</h3>
<form>
<div class="form-elem">
<span>用户名:</span>
<input v-model="username" type="text" placeholder="输入用户名">
</div>
<div class="form-elem">
<span>新密码:</span>
<input v-model="password" type="password" placeholder="输入密码">
</div>
<div class="form-elem">
<button v-on:click.prevent="changeInfo">更新</button>
</div>
</form>
</div>
<BlogFooter/>
</template>
<script>
import axios from 'axios';
import BlogHeader from '@/components/BlogHeader.vue'
import BlogFooter from '@/components/BlogFooter.vue'
import authorization from '@/utils/authorization';
const storage = localStorage;
export default {
name: 'UserCenter',
components: {BlogHeader, BlogFooter},
data: function () {
return {
username: '',
password: '',
token: '',
}
},
mounted() {
this.username = storage.getItem('username.myblog');
},
methods: {
changeInfo() {
const that = this;
// 验证登录状态
authorization()
.then(function (response) {
// 检查登录状态
// 若登录已过期则不执行后续操作
if (!response[0]) {
alert('登录已过期,请重新登录');
return
}
console.log('change info start');
// 密码不能小于 6 位
if (that.password.length > 0 && that.password.length < 6) {
alert('Password too short.');
return
}
// 旧的 username 用于向接口发送请求
const oldName = storage.getItem('username.myblog');
// 获取已填写的表单数据
let data = {};
if (that.username !== '') {
data.username = that.username
}
if (that.password !== '') {
data.password = that.password
}
// 获取令牌
that.token = storage.getItem('access.myblog');
// 发送更新数据到接口
axios
.patch(
'/api/user/' + oldName + '/',
data,
{
headers: {Authorization: 'Bearer ' + that.token}
}
)
.then(function (response) {
const name = response.data.username;
storage.setItem('username.myblog', name);
that.$router.push({name: 'UserCenter', params: {username: name}});
})
});
}
},
}
</script>
<style scoped>
#user-center {
text-align: center;
}
.form-elem {
padding: 10px;
}
input {
height: 25px;
padding-left: 10px;
}
button {
height: 35px;
cursor: pointer;
border: none;
outline: none;
background: gray;
color: whitesmoke;
border-radius: 5px;
width: 200px;
}
</style>
核心就是脚本里的 authorization()
函数,这就是我们刚封装的验证函数嘛。在它的 .then()
里,干了下面这两件事情:
- 检查函数返回的数据,如果登录失效,或者密码太短,则拒绝执行后面的逻辑。
- 拿到用户填写的表单数据,并取出保存在本地的令牌,发送到后端接口更新用户数据。
模板和样式都没有新内容,读者捎带看一下就可以。
收尾工作
由于验证身份有了独立的函数,因此页眉的验证代码可以都删了,调用它即可。此外,用户中心的入口可以作为用户提示语的下拉框,就像大部分平台做的那样。
修改 BlogHeader.vue
的代码:
<!-- frontend/src/components/BlogHeader.vue -->
<template>
<div id="header">
...
<hr>
<div class="login">
<div v-if="hasLogin">
<div class="dropdown">
<button class="dropbtn">欢迎, {{username}}!</button>
<div class="dropdown-content">
<router-link :to="{ name: 'UserCenter', params: { username: username }}">用户中心</router-link>
</div>
</div>
</div>
<div v-else>...</div>
</div>
</div>
</template>
<script>
import SearchButton from '@/components/SearchButton.vue';
import authorization from '@/utils/authorization';
export default {
name: 'BlogHeader',
components: {SearchButton},
data: function () {
return {
username: '',
hasLogin: false,
}
},
mounted() {
// 千言万语汇成此句
authorization().then((data) => [this.hasLogin, this.username] = data);
}
}
</script>
<style scoped>
/* 样式来源: https://www.runoob.com/css/css-dropdowns.html* /
/* 下拉按钮样式 */
.dropbtn {
background-color: mediumslateblue;
color: white;
padding: 8px 8px 30px 8px ;
font-size: 16px;
border: none;
cursor: pointer;
height: 16px;
border-radius: 5px;
}
/* 容器 <div> - 需要定位下拉内容 */
.dropdown {
position: relative;
display: inline-block;
}
/* 下拉内容 (默认隐藏) */
.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 120px;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
text-align: center;
}
/* 下拉菜单的链接 */
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
/* 鼠标移上去后修改下拉菜单链接颜色 */
.dropdown-content a:hover {
background-color: #f1f1f1
}
/* 在鼠标移上去后显示下拉菜单 */
.dropdown:hover .dropdown-content {
display: block;
}
/* 当下拉内容显示后修改下拉按钮的背景颜色 */
.dropdown:hover .dropbtn {
background-color: darkslateblue;
}
</style>
<style scoped>
/* 旧的样式 */
...
</style>
- 将之前写的验证代码全部删除,变为调用
authorization()
函数。 - 将欢迎词替换为下拉框,选项里包含用户中心的入口。
最后把路由注册到 index.js
:
// frontend/src/router/index.js
...
import UserCenter from "@/views/UserCenter.vue";
const routes = [
...
{
path: "/user/:username",
name: "UserCenter",
component: UserCenter
},
];
大功告成,愿关二爷保佑没 BUG 吧。
测试
登录用户后,鼠标悬停主页面右上角登录提示,出现,用户中心下拉框:
点击后跳转至用户中心页面。
尝试更改用户名(从 Ivanka 到 Trump)。点击更新按钮后,url 自动跳转到 .../user/Trump
,后端用户名也会顺利的更新为 Trump。
美中不足的是右上角的欢迎词没正确的更新,没关系下一章来解决。
打开控制台多等一会儿,顺利的话 token 的提交、刷新、过期都应该能正常工作,请自行测试吧。
教程为了测试方便,token 的过期时间设置为 1 分钟。实际项目中根据你的需求,可以设置得更长一些。
用户登出
用户登出就非常轻松了,简单的把 localStorage 里存储的数据删除就 OK 了,五六行代码即可搞定。
如何实现就不讲了,作为你的课后作业自由发挥吧。
小提示:下拉框中增加一个
router-link
,用v-on:click.prevent="logout()"
调用方法,在方法中删除localStorage
并window.location.reload(false)
原地刷新页面。实在不知道咋实现的,参考Github 源码。