资料删除与通信组件
在实现用户资料的删除之前,先解决上一章的遗留问题: 更新用户资料信息后,右上角欢迎词依然显示旧的用户名,必须强制刷新页面后才显示更新后的用户名。
有的读者不理解,更新资料时已经通过 $router.push() 刷新过页面了,为什么路径、表单数据都更新了,唯独欢迎词没有更新?原因在于 Vue 太高效了。因为 $router 跳转的是同一个页面,那么 Vue 就只会重新渲染此页面发生变化的组件,而那些 Vue 觉得没变化的组件就不再重新渲染。很显然 Vue 觉得页眉里的数据没发生变化 ,页眉的生命周期钩子 mounted() 没执行,欢迎词也就未更新了。
我们用两种方式来解决此小问题。
组件通信
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 了),这个参数会传递到子组件中使用。
然后修改 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 将会成为自然而然的选择。
关于数据通信和数据管理的讨论暂时就到这里了,继续了解请阅读官方文档吧。
访问子组件
Props 虽然能够解决我们的问题,但总感觉有点别扭:为什么我要持有 welcomeName 和 username 两个状态?这两货不应该是同一个东西吗?
幸好,还有一种更简单的方法来处理此问题: 用 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>
删除本身没什么好说的,与用户更新的实现方式大同小异。需要注意的倒是另外的小知识点:
看看效果:

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