泽恩小站-教程区 Help

资料更新与异步

上一章用户可以登录了,本章接着完成用户资料的更新和登出

组件化

前面做搜索功能时,为了美观我们定义了按钮的样式。正巧用户更新也需要按钮,为了避免样式相互冲突,先做点准备工作: 把搜索框组件化

新建一个 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/awaitasync 表示函数里含有异步操作, await 表示紧跟在后面的表达式需要等待结果。 await 关键字只能用在 async 函数中,并且由于它返回的 Promise 对象运行的结果可能是 rejected ,所以最好放到 try...catch 语句中。

  • async 函数返回的不再是 return 后面的数据,而是包含数据的 Promise 对象,因此调用它的位置需要改为 Promise.then().catch() 进行异常处理。(有点像 axios.then().catch())

用户中心

封装好身份验证的函数后,用户中心这个页面相对就好弄了。

新建 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 吧。

测试

登录用户后,鼠标悬停主页面右上角登录提示,出现,用户中心下拉框:

P260 1

点击后跳转至用户中心页面。

P260 2

尝试更改用户名(从 Ivanka 到 Trump)。点击更新按钮后,url 自动跳转到 .../user/Trump ,后端用户名也会顺利的更新为 Trump。

美中不足的是右上角的欢迎词没正确的更新,没关系下一章来解决。

打开控制台多等一会儿,顺利的话 token 的提交、刷新、过期都应该能正常工作,请自行测试吧。

用户登出

用户登出就非常轻松了,简单的把 localStorage 里存储的数据删除就 OK 了,五六行代码即可搞定。

如何实现就不讲了,作为你的课后作业自由发挥吧。

Last modified: 06 January 2025