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/awaitasync 表示函数里含有异步操作,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()" 调用方法,在方法中删除 localStoragewindow.location.reload(false) 原地刷新页面。

实在不知道咋实现的,参考Github 源码




本文作者: 杜赛
发布时间: 2021年03月26日 - 15:22
最后更新: 2021年03月26日 - 15:22
转载请保留原文链接及作者