Django-Vue搭建个人博客:资料更新与异步
6122 views, 2021/03/26 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 源码。