Skip to content

Latest commit

 

History

History
362 lines (290 loc) · 12.7 KB

270-资料删除与组件通信.md

File metadata and controls

362 lines (290 loc) · 12.7 KB

在实现用户资料的删除之前,先解决上一章的遗留问题:更新用户资料信息后,右上角欢迎词依然显示旧的用户名,必须强制刷新页面后才显示更新后的用户名

有的读者不理解,更新资料时已经通过 $router.push() 刷新过页面了,为什么路径、表单数据都更新了,唯独欢迎词没有更新?原因在于 Vue 太高效了。因为 $router 跳转的是同一个页面,那么 Vue 就只会重新渲染此页面发生变化的组件,而那些 Vue 觉得没变化的组件就不再重新渲染。很显然 Vue 觉得页眉里的数据没发生变化,页眉的生命周期钩子 mounted() 没执行,欢迎词也就未更新了。

Vue 查看的仅它自己管理的数据。显然 localStorage 里保存的登录标志变量不在此列。

我们用两种方式来解决此小问题。

组件通信

Vue 是基于组件的一套系统,如果组件和组件之间无法通信传递数据,那无疑是没办法接受的。Vue 中父组件向子组件传递信息的方式就是 Props 了,接下来就用 Props 来“拐弯抹角”的实现欢迎词更新的功能。

首先修改 UserCenter.vue

<!-- frontend/src/views/UserCenter.vue -->

<template>
    <BlogHeader :welcome-name="welcomeName" />
    ...
</template>

<script>
    ...
    export default {
        ...
        data: function () {
            return {
                ...
                // 新增这里
                welcomeName: '',
            }
        },
        mounted() {
            ...
            // 新增这里
            this.welcomeName = storage.getItem('username.myblog');
        },
        methods: {
            changeInfo() {
                ...
                authorization()
                    .then(function (response) {
                        ...
                        axios
                            .patch(...)
                            .then(function (response) {
                                ...
                                // 新增这里
                                that.welcomeName = name;
                            })
                    });
            }
        },
    }
</script>
...

可以看到组件是可以带参数的(也就是 Props 了),这个参数会传递到子组件中使用。

又一次看到了 :welcome-name 这种带冒号的写法了。重申一次,冒号表示这个属性被双向绑定到了 Vue 所管理的数据或表达式上。如果你只是传递一个固定值(如字符串),那么去掉冒号即可。: 就是 v-bind: 的简写形式。

然后修改 BlogHeader.vue

<!-- frontend/src/components/BlogHeader.vue -->

<template>
    <div id="header">
        ...
        <div ...>
            <div ...>
                <div class="dropdown">
                    <button class="dropbtn">欢迎, {{name}}!</button>
                    ...
                </div>
            </div>
            ...
        </div>
    </div>
</template>

<script>
    ...
    export default {
        ...
        props: ['welcomeName'],
        computed: {
            name() {
                return (this.welcomeName !== undefined) ? this.welcomeName : this.username
            }
        },
        ...
    }
</script>

需要注意的有两点。

由于 HTML 对大小写不敏感,所以 Vue 规定 camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名。所以就有了模板中是 welcome-name 而脚本中是 welcomeName ,它两是对应的。

出现了一个新家伙:computed 计算属性。乍一看这玩意儿和 methods 没啥区别,但实际上区别大了:

  • 计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要与它有关系的参数没有发生改变,多次访问此计算属性会立即返回之前的计算结果,而不必再次执行函数。相比之下,每当触发重新渲染时,方法总会再次执行函数。
  • **计算属性默认不接受参数,并且不能产生副作用。**也就是说,在它的执行过程中不能改变任何 Vue 所管理的数据,否则将会报错。计算属性是依赖数据工作的,副作用会使代码不可预测(鸡生蛋,蛋生鸡)。

一般来说,能用 computed 就尽量用它,不能的再考虑 methods ,算是用空间(缓存)换取时间(效率)吧。

测试看看,几行代码就修补了上一章的 bug。

事件

你可能会问,既然父组件可以向子组件传递数据,那能不能子组件返过来传递 Props 给父组件呢?很遗憾这是不行的。

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

那 Vue 的子组件能不能给父组件传递信息?能,采用的是事件的形式。

看看官网的例子

// ---js---

Vue.component('welcome-button', {
  template: `
    <button v-on:click="$emit('welcome')">
      Click me to be welcomed
    </button>
  `
})


//  ---html---

<div id="emit-example-simple">
  <welcome-button v-on:welcome="sayHi"></welcome-button>
</div>


// ---js---

...
methods: {
  sayHi: function () {
    alert('Hi!')
  }
}

虽然不能直接反馈给父组件数据,但可以通过事件的形式传递信息。

数据结构

大型项目中的数据和状态量是惊人的,建立一个蜘蛛网般复杂的数据通信结构,想想都很可怕。这时候你就需要用到 Vuex 了。

Vuex 是一个专为 Vue 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。也就是说,Vuex 把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的;应用够简单,最好不要使用 Vuex。一个简单的 store 模式就足够了。如果需要构建一个中大型单页应用,Vuex 将会成为自然而然的选择。

截止笔者撰文时,很遗憾 Vuex 还不支持 Vue 3。不过相信很快了,耐心等待吧。

关于数据通信和数据管理的讨论暂时就到这里了,继续了解请阅读官方文档吧。

访问子组件

Props 虽然能够解决我们的问题,但总感觉有点别扭:为什么我要持有 welcomeNameusername 两个状态?这两货不应该是同一个东西吗?

幸好,还有一种更简单的方法来处理此问题: 用 ref 访问子组件。

先把刚才写的代码都还原。

先在 BlogHeader.vue 中写一个刷新数据的方法:

<!-- frontend/src/components/BlogHeader.vue -->
...
<script>
    ...
    export default {
        ...
        methods: {
            ...
            refresh() {
                this.username = localStorage.getItem('username.myblog');
            }
        }
    }
</script>
...

然后在 UserCenter.vue 更新用户数据时访问此函数:

<!-- frontend/src/views/UserCenter.vue -->

<template>
    <BlogHeader ref="header"/>
    ...
</template>

<script>
    ...
    export default {
        ...
        methods: {
            changeInfo() {
                ...
                authorization()
                    .then(...) {
                        ...
                        axios
                            .patch(...)
                            .then(function (response) {
                                ...
                                that.$refs.header.refresh();
                            })
                    });
            }
        },
    }
</script>
...

是不是比 Props 的方式要更加适合一些呢。

关于组件通信的介绍就告一段落了。接下来处理用户删除的功能。

用户删除

删除用户按钮通常会放在用户中心页面,并且为了避免用户误操作,点击后还要进行第二次确认,方可删除。

修改 UserCenter.vue 文件:

<!-- frontend/src/views/UserCenter.vue -->

<template>
    ...
    <div ...>
        ...
        <form>
            ...
            
            <div class="form-elem">
                <button 
                    v-on:click.prevent="showingDeleteAlert = true" 
                    class="delete-btn"
                >删除用户</button>
                <div :class="{ shake: showingDeleteAlert }">
                    <button
                            v-if="showingDeleteAlert"
                            class="confirm-btn"
                            @click.prevent="confirmDelete"
                    >确定?
                    </button>
                </div>
            </div>
        </form>
    </div>
    ...
</template>

<script>
    ...
    export default {
        ...
        data: function () {
            return {
                ...
                showingDeleteAlert: false,
            }
        },
        mounted() {...},
        methods: {
            confirmDelete() {
                const that = this;
                authorization()
                    .then(function (response) {
                        if (response[0]) {
                            // 获取令牌
                            that.token = storage.getItem('access.myblog');
                            axios
                                .delete('/api/user/' + that.username + '/', {
                                    headers: {Authorization: 'Bearer ' + that.token}
                                })
                                .then(function () {
                                    storage.clear();
                                    that.$router.push({name: 'Home'});
                                })
                        }
                    })
            },
            changeInfo() {...}
        },
    }
</script>

<style scoped>
    ...

    .confirm-btn {
        width: 80px;
        background-color: darkorange;
    }
    .delete-btn {
        background-color: darkred;
        margin-bottom: 10px;
    }
    .shake {
        animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
        transform: translate3d(0, 0, 0);
        backface-visibility: hidden;
        perspective: 1000px;
    }
    @keyframes shake {
        10%,
        90% {
            transform: translate3d(-1px, 0, 0);
        }
        20%,
        80% {
            transform: translate3d(2px, 0, 0);
        }
        30%,
        50%,
        70% {
            transform: translate3d(-4px, 0, 0);
        }
        40%,
        60% {
            transform: translate3d(4px, 0, 0);
        }
    }
</style>

删除本身没什么好说的,与用户更新的实现方式大同小异。需要注意的倒是另外的小知识点:

  • 确认删除按钮的出现是带有动画的(横向抖动)。上面代码的一半内容都是样式,定义了按钮的外观、动画和关键帧。Vue 2 和 Vue 3 的过渡动画有较大差别,详见 Vue 2 动画Vue 3 动画
  • 符号 @v-on: 的缩写。

看看效果:

  • 点击“删除用户”后弹出“确定”按钮,注意是带有抖动动画的。
  • 点击“确定”按钮后此用户永久删除,并登出并跳转回首页。

课后作业

看起来我们的博客逐渐有模有样了,但还是有很多不完美的地方。请尝试优化以下功能:

  • 用户登出在多个地方都用到了,可抽象为独立模块。
  • 未登录用户通过输入 url 的方式还是可以到达其他用户的用户中心页面(虽然不能进行危险操作),请优化使用户中心页面仅本人可查看。
  • 每当页面刷新时,页眉都会向后台发送请求确认登录状态。是否可以利用缓存,减轻后端压力?