在实现用户资料的删除之前,先解决上一章的遗留问题:更新用户资料信息后,右上角欢迎词依然显示旧的用户名,必须强制刷新页面后才显示更新后的用户名。
有的读者不理解,更新资料时已经通过 $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 虽然能够解决我们的问题,但总感觉有点别扭:为什么我要持有 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>
删除本身没什么好说的,与用户更新的实现方式大同小异。需要注意的倒是另外的小知识点:
- 确认删除按钮的出现是带有动画的(横向抖动)。上面代码的一半内容都是样式,定义了按钮的外观、动画和关键帧。Vue 2 和 Vue 3 的过渡动画有较大差别,详见 Vue 2 动画 和 Vue 3 动画。
- 符号
@
是v-on:
的缩写。
看看效果:
- 点击“删除用户”后弹出“确定”按钮,注意是带有抖动动画的。
- 点击“确定”按钮后此用户永久删除,并登出并跳转回首页。
看起来我们的博客逐渐有模有样了,但还是有很多不完美的地方。请尝试优化以下功能:
- 用户登出在多个地方都用到了,可抽象为独立模块。
- 未登录用户通过输入 url 的方式还是可以到达其他用户的用户中心页面(虽然不能进行危险操作),请优化使用户中心页面仅本人可查看。
- 每当页面刷新时,页眉都会向后台发送请求确认登录状态。是否可以利用缓存,减轻后端压力?