Django-Vue搭建个人博客:资料删除与组件通信
5281 views, 2021/03/27 updated Go to Comments
在实现用户资料的删除之前,先解决上一章的遗留问题:更新用户资料信息后,右上角欢迎词依然显示旧的用户名,必须强制刷新页面后才显示更新后的用户名。
有的读者不理解,更新资料时已经通过 $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 的方式还是可以到达其他用户的用户中心页面(虽然不能进行危险操作),请优化使用户中心页面仅本人可查看。
- 每当页面刷新时,页眉都会向后台发送请求确认登录状态。是否可以利用缓存,减轻后端压力?