TagDown 扩展程序开发—— Vue 组件间数据传递
TagDown 是一款开源的书签管理插件, 您可以使用扩展程序浏览、新增、修改书签,它也支持以不同方式导出书签。
除了常见的书签管理功能,还具有以下特点:
支持 ???? 新增书签,并附加额外的信息,例如
tags
、groups
等支持 ???? 导出任意书签为
json
文档以 ???? 树图的形式浏览层级结构的书签数据
一键打开多个书签,支持在 ???? 标签组内打开书签
弹出页面开发
在进行弹出页面开发中,主要需要解决的是 Vue 组件间数据传递问题。
为了便于代码的复用和维护,将页面分为多个 Vue 组件,例如在弹出页面的浏览状态中,将网页的父级组件 BrowserPage
中主要有 4 大子组件:
BrowserHeader.vue
BrowserMenu.vue
和BrowserMenuPin.vue
BrowserGrid.vue
和BrowserTree.vue
,以及BrowserPin.vue
BrowserFooter
父子组件间数据传递
在 Vue 中最常见的组件之间的数据传递方式是使用 prop
和 emit
实现父子组件间传值:
通过 prop 向子组件传递数据。prop 是在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个 property。
通过 emit 向父组件传递数据。当组件需要修改父层控管的数据(这些数据通过 props 传递进来)时,基于单向数据流原则不能在组件内进行修改,而是需要通过
$emit('eventName', value)
向外抛出自定义的事件,通知父层进行数据修改。
???? 对于输入型的组件,例如在项目中组件 BrowserFooter
包含一个 <input>
元素,可以允许用户输入标签组的名称。可以在组件上使用 v-model
,以在父子组件传值的同时实现数据的双向绑定,即数据可以从父层传递给子组件(一般作为变量的初始值),同时可以接收子组件返回的值,以修改绑定的变量。
在父层使用组件时,通过 v-model
实现数据的双向绑定。
<!-- 使用组件,为了简洁的演示,省略显示其他属性的绑定 --> <BrowserFooter v-model:multi-on-group="multiOnGroup" v-model:new-group-name="newGroupName" /> 复制代码
在子组件的表单元素中,绑定相应的属性,同时监听相应的事件:
对于
<input type="checkbox">
复选框,绑定的属性是checked
,需要监听的事件是change
对于
<input type="text">
文本输入框,绑定的属性是value
,需要监听的事件是input
当属性值变更时,需要抛出事件的格式以 update:
为前缀,而属性值通过读取事件对象的相应属性 $event.target
获得
<template> <!-- 省略其他模板内容 --> <!-- 单个复选框 --> <!-- 为了简洁的演示,省略显示其他属性的绑定 --> <input id="group" type="checkbox" :checked="multiOnGroup" @change="$emit('update:multiOnGroup', $event.target.checked)" > <label for="group" class="text-xs text-gray-500" >在组内打开</label> <!-- 省略其他模板内容 --> <!-- 文本输入框 --> <!-- 为了简洁的演示,省略显示其他属性的绑定 --> <input type="text" placeholder="输入 group 名称" :value="newGroupName" @input="$emit('update:newGroupName', $event.target.value)" > <!-- 省略其他模板内容 --> </template> <script> export default { props: { // ... multiOnGroup: Boolean, newGroupName: { type: String, default: 'new', }, }, emits: [ // ... 'update:multiOnGroup', 'update:newGroupName', ], // ... } </script> 复制代码
祖孙组件间数据传递
用 provide
和 inject
实现从祖先组件向其子孙后代组件传值:
父组件用
provide
选项或(在组合式 API 中)方法来提供数据,可以提供响应式数据,例如 ref 对象子组件用
inject
选项或(在组合式 API 中)方法来接收这个数据
在项目中多个组件中都需要使用 IndexedDB 数据库,在弹出页的 Vue 入口文件 src/popup/main.js
中使用框架 Dexie.js 创建一个数据库实例,再通过 provide
的方式供其他子组件使用。
import Dexie from 'dexie'; import { createApp } from 'vue'; import App from './App.vue'; import '@/styles/tailwind.css'; const app = createApp(App); const db = new Dexie('tagdown'); db.version(1).stores({ bookmark: 'id, *tags, *groups', share: 'id, share', star: 'id, star', }); db.open().then((database) => { app.provide('db', database); app.mount('#app'); }).catch((err) => { // Error occurred console.log(err); }); 复制代码
然后在组件,例如 BrowserPage.vue
中通过 const db = inject('db');
来接收该数据库实例。
???? 为了便于代码维护,当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的修改限制在定义 provide 的组件内。如果真的需要在注入数据的组件内部更新 inject 的数据,建议同时 provide 一个方法来负责改变值,这样可以将数据值的设定和数据修改的逻辑代码都依然集中放在祖父组件中,便于后续跟踪和维护。
在祖先组件 src/popup/App.vue
中根据响应式变量 Page
控制弹出页面的状态是 "browser"
还是 "edit"
,而控制该变量切换的按钮是在后代组件 BrowserFooter.vue
中.
因此在祖先组件提供修改该变量的方法
<script> import { ref, provide } from 'vue'; // ... export default { // ... // provide page state and change the page state function provide('page', page); const changePage = (value) => { page.value = value; }; provide('changePage', changePage); // ... } </script> 复制代码
在后代组件接收该方法,并绑定到按钮上
<template> <!-- 省略其他模板代码 --> <!-- 为了简洁的演示,省略显示其他属性的绑定 --> <button title="add bookmark" @click="changePage('edit')" > <!-- 省略其他模板代码 --> </template> <script> import { ref, watch, inject } from 'vue'; // ... export default { // ... const changePage = inject('changePage'); // ... return { // ... changePage } } </script> 复制代码
兄弟组件间信息传递
对于在层级结构中处于「平行」关系,如果需要进行数据传递,可以考虑使用 Vue 官方推荐的数据状态管理插件 Vuex。
在项目中有一个需求是通过点击组件 BrowserMenu
的按钮,控制兄弟组件 BrowserGrid
的文件夹展开/折叠。
把兄弟组件的父组件作为信息传递的「桥梁」,在父组件监听其中一个子组件抛出的事件,然后接收到自定义事件时,通过模板引用来调用另一个子组件的方法,从而实现兄弟组件信息传递。
将父组件 BrowserPage
作为「桥梁」,连接两个「平行」关系子组件。
<template> <!-- 省略其他模板代码 --> <!-- menu --> <!-- 为了简洁的演示,省略显示其他属性的绑定 --> <div> <component :is="browserMenuComponent" @unfold-all="unfoldAllHandler" @fold-all="foldAllHandler" /> </div> <!-- 省略其他模板代码 --> <!-- 为了简洁的演示,省略显示其他属性的绑定 --> <BrowserGrid ref="grid" /> </template> <script> import { ref, computed, watch, inject, } from 'vue'; export default { // ... setup() { // unfold or fold all folder const grid = ref(null); // 引用 BrowserGrid 组件 const unfoldAllHandler = () => { // ... grid.value.unfoldAll(); // 调用组件中的方法 unfoldAll() // ... }; const foldAllHandler = () => { // ... grid.value.foldAll(); // 调用组件中的方法 foldAll() // ... }; // ... return { // ... grid, unfoldAllHandler, foldAllHandler, } } } </script>
作者:Benbinbin
链接:https://juejin.cn/post/7018540193040826382